From c09e6b82dcbc447eed26bbf615c22f5fc77ff122 Mon Sep 17 00:00:00 2001 From: heshupeng <hsp@bronet.cn> Date: Mon, 28 Dec 2020 14:16:05 +0800 Subject: [PATCH] 微信支付宝整合插件 --- addons/epay/.addonrc | 1 + addons/epay/Epay.php | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/certs/alipayCertPublicKey.crt | 0 addons/epay/certs/alipayRootCert.crt | 0 addons/epay/certs/apiclient_cert.pem | 0 addons/epay/certs/apiclient_key.pem | 0 addons/epay/certs/appCertPublicKey.crt | 0 addons/epay/config.html | 366 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/config.php | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/controller/Api.php | 243 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/controller/Index.php | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/info.ini | 10 ++++++++++ addons/epay/library/Collection.php | 18 ++++++++++++++++++ addons/epay/library/OrderException.php | 16 ++++++++++++++++ addons/epay/library/QRCode.php | 1856 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/RedirectResponse.php | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Response.php | 26 ++++++++++++++++++++++++++ addons/epay/library/Service.php | 301 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Wechat.php | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Contracts/GatewayApplicationInterface.php | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Contracts/GatewayInterface.php | 20 ++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events.php | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events/ApiRequested.php | 31 +++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events/ApiRequesting.php | 31 +++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events/Event.php | 40 ++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events/MethodCalled.php | 33 +++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events/PayStarted.php | 31 +++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events/PayStarting.php | 23 +++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events/RequestReceived.php | 25 +++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Events/SignFailed.php | 25 +++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Exceptions/BusinessException.php | 19 +++++++++++++++++++ addons/epay/library/Yansongda/Pay/Exceptions/Exception.php | 44 ++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Exceptions/GatewayException.php | 20 ++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Exceptions/InvalidArgumentException.php | 19 +++++++++++++++++++ addons/epay/library/Yansongda/Pay/Exceptions/InvalidConfigException.php | 19 +++++++++++++++++++ addons/epay/library/Yansongda/Pay/Exceptions/InvalidGatewayException.php | 19 +++++++++++++++++++ addons/epay/library/Yansongda/Pay/Exceptions/InvalidSignException.php | 19 +++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay.php | 422 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/AppGateway.php | 38 ++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/Gateway.php | 40 ++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/MiniGateway.php | 46 ++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/PosGateway.php | 47 +++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/RefundGateway.php | 21 +++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/ScanGateway.php | 41 +++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/Support.php | 452 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/TransferGateway.php | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/WapGateway.php | 26 ++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Alipay/WebGateway.php | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat.php | 366 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/AppGateway.php | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/Gateway.php | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/GroupRedpackGateway.php | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/MiniappGateway.php | 35 +++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/MpGateway.php | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/PosGateway.php | 44 ++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/RedpackGateway.php | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/RefundGateway.php | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/ScanGateway.php | 44 ++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/Support.php | 449 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/TransferGateway.php | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/WapGateway.php | 47 +++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Gateways/Wechat/WebGateway.php | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/LICENSE | 20 ++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Listeners/KernelLogSubscriber.php | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Log.php | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Pay/Pay.php | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Arr.php | 605 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Collection.php | 363 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Config.php | 7 +++++++ addons/epay/library/Yansongda/Supports/LICENSE | 20 ++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Log.php | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Logger.php | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Logger/StdoutHandler.php | 36 ++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Str.php | 570 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Traits/Accessable.php | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Traits/Arrayable.php | 32 ++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Traits/HasHttpRequest.php | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Traits/Serializable.php | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/library/Yansongda/Supports/Traits/ShouldThrottle.php | 147 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/view/api/alipay.html | 47 +++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/view/api/wechat.html | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/view/index/index.html | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ addons/epay/view/layout/default.html | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ application/admin/controller/Epay.php | 39 +++++++++++++++++++++++++++++++++++++++ application/extra/addons.php | 9 +++++---- public/assets/addons/epay/css/common.css | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public/assets/addons/epay/css/epay.css | 20 ++++++++++++++++++++ public/assets/addons/epay/images/alipay.png | Bin 0 -> 3651 bytes public/assets/addons/epay/images/expired.png | Bin 0 -> 4731 bytes public/assets/addons/epay/images/logo-alipay.png | Bin 0 -> 1682 bytes public/assets/addons/epay/images/logo-wechat.png | Bin 0 -> 1773 bytes public/assets/addons/epay/images/paid.png | Bin 0 -> 2294 bytes public/assets/addons/epay/images/scan.png | Bin 0 -> 922 bytes public/assets/addons/epay/images/screenshot-alipay.png | Bin 0 -> 66111 bytes public/assets/addons/epay/images/screenshot-wechat.png | Bin 0 -> 21909 bytes public/assets/addons/epay/images/wechat.png | Bin 0 -> 22720 bytes public/assets/addons/epay/js/common.js | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public/assets/addons/epay/less/common.less | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public/assets/addons/epay/less/epay.less | 28 ++++++++++++++++++++++++++++ 99 files changed, 10442 insertions(+), 4 deletions(-) create mode 100644 addons/epay/.addonrc create mode 100644 addons/epay/Epay.php create mode 100644 addons/epay/certs/alipayCertPublicKey.crt create mode 100644 addons/epay/certs/alipayRootCert.crt create mode 100644 addons/epay/certs/apiclient_cert.pem create mode 100644 addons/epay/certs/apiclient_key.pem create mode 100644 addons/epay/certs/appCertPublicKey.crt create mode 100644 addons/epay/config.html create mode 100644 addons/epay/config.php create mode 100644 addons/epay/controller/Api.php create mode 100644 addons/epay/controller/Index.php create mode 100644 addons/epay/info.ini create mode 100644 addons/epay/library/Collection.php create mode 100644 addons/epay/library/OrderException.php create mode 100644 addons/epay/library/QRCode.php create mode 100644 addons/epay/library/RedirectResponse.php create mode 100644 addons/epay/library/Response.php create mode 100644 addons/epay/library/Service.php create mode 100644 addons/epay/library/Wechat.php create mode 100644 addons/epay/library/Yansongda/Pay/Contracts/GatewayApplicationInterface.php create mode 100644 addons/epay/library/Yansongda/Pay/Contracts/GatewayInterface.php create mode 100644 addons/epay/library/Yansongda/Pay/Events.php create mode 100644 addons/epay/library/Yansongda/Pay/Events/ApiRequested.php create mode 100644 addons/epay/library/Yansongda/Pay/Events/ApiRequesting.php create mode 100644 addons/epay/library/Yansongda/Pay/Events/Event.php create mode 100644 addons/epay/library/Yansongda/Pay/Events/MethodCalled.php create mode 100644 addons/epay/library/Yansongda/Pay/Events/PayStarted.php create mode 100644 addons/epay/library/Yansongda/Pay/Events/PayStarting.php create mode 100644 addons/epay/library/Yansongda/Pay/Events/RequestReceived.php create mode 100644 addons/epay/library/Yansongda/Pay/Events/SignFailed.php create mode 100644 addons/epay/library/Yansongda/Pay/Exceptions/BusinessException.php create mode 100644 addons/epay/library/Yansongda/Pay/Exceptions/Exception.php create mode 100644 addons/epay/library/Yansongda/Pay/Exceptions/GatewayException.php create mode 100644 addons/epay/library/Yansongda/Pay/Exceptions/InvalidArgumentException.php create mode 100644 addons/epay/library/Yansongda/Pay/Exceptions/InvalidConfigException.php create mode 100644 addons/epay/library/Yansongda/Pay/Exceptions/InvalidGatewayException.php create mode 100644 addons/epay/library/Yansongda/Pay/Exceptions/InvalidSignException.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/AppGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/Gateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/MiniGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/PosGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/RefundGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/ScanGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/Support.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/TransferGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/WapGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Alipay/WebGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/AppGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/Gateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/GroupRedpackGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/MiniappGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/MpGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/PosGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/RedpackGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/RefundGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/ScanGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/Support.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/TransferGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/WapGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/Gateways/Wechat/WebGateway.php create mode 100644 addons/epay/library/Yansongda/Pay/LICENSE create mode 100644 addons/epay/library/Yansongda/Pay/Listeners/KernelLogSubscriber.php create mode 100644 addons/epay/library/Yansongda/Pay/Log.php create mode 100644 addons/epay/library/Yansongda/Pay/Pay.php create mode 100644 addons/epay/library/Yansongda/Supports/Arr.php create mode 100644 addons/epay/library/Yansongda/Supports/Collection.php create mode 100644 addons/epay/library/Yansongda/Supports/Config.php create mode 100644 addons/epay/library/Yansongda/Supports/LICENSE create mode 100644 addons/epay/library/Yansongda/Supports/Log.php create mode 100644 addons/epay/library/Yansongda/Supports/Logger.php create mode 100644 addons/epay/library/Yansongda/Supports/Logger/StdoutHandler.php create mode 100644 addons/epay/library/Yansongda/Supports/Str.php create mode 100644 addons/epay/library/Yansongda/Supports/Traits/Accessable.php create mode 100644 addons/epay/library/Yansongda/Supports/Traits/Arrayable.php create mode 100644 addons/epay/library/Yansongda/Supports/Traits/HasHttpRequest.php create mode 100644 addons/epay/library/Yansongda/Supports/Traits/Serializable.php create mode 100644 addons/epay/library/Yansongda/Supports/Traits/ShouldThrottle.php create mode 100644 addons/epay/view/api/alipay.html create mode 100644 addons/epay/view/api/wechat.html create mode 100644 addons/epay/view/index/index.html create mode 100644 addons/epay/view/layout/default.html create mode 100644 application/admin/controller/Epay.php create mode 100644 public/assets/addons/epay/css/common.css create mode 100644 public/assets/addons/epay/css/epay.css create mode 100644 public/assets/addons/epay/images/alipay.png create mode 100644 public/assets/addons/epay/images/expired.png create mode 100644 public/assets/addons/epay/images/logo-alipay.png create mode 100644 public/assets/addons/epay/images/logo-wechat.png create mode 100644 public/assets/addons/epay/images/paid.png create mode 100644 public/assets/addons/epay/images/scan.png create mode 100644 public/assets/addons/epay/images/screenshot-alipay.png create mode 100644 public/assets/addons/epay/images/screenshot-wechat.png create mode 100644 public/assets/addons/epay/images/wechat.png create mode 100644 public/assets/addons/epay/js/common.js create mode 100644 public/assets/addons/epay/less/common.less create mode 100644 public/assets/addons/epay/less/epay.less diff --git a/addons/epay/.addonrc b/addons/epay/.addonrc new file mode 100644 index 0000000..aaa3db5 --- /dev/null +++ b/addons/epay/.addonrc @@ -0,0 +1 @@ +{"license":"regular","licenseto":"10789","licensekey":"1TqXEBWr8Oblow6m IZ3EErd8gmOw\/2j0GfnHmQ==","files":["application\\admin\\controller\\Epay.php","public\\assets\\addons\\epay\\css\\common.css","public\\assets\\addons\\epay\\css\\epay.css","public\\assets\\addons\\epay\\images\\alipay.png","public\\assets\\addons\\epay\\images\\expired.png","public\\assets\\addons\\epay\\images\\logo-alipay.png","public\\assets\\addons\\epay\\images\\logo-wechat.png","public\\assets\\addons\\epay\\images\\paid.png","public\\assets\\addons\\epay\\images\\scan.png","public\\assets\\addons\\epay\\images\\screenshot-alipay.png","public\\assets\\addons\\epay\\images\\screenshot-wechat.png","public\\assets\\addons\\epay\\images\\wechat.png","public\\assets\\addons\\epay\\js\\common.js","public\\assets\\addons\\epay\\less\\common.less","public\\assets\\addons\\epay\\less\\epay.less"]} \ No newline at end of file diff --git a/addons/epay/Epay.php b/addons/epay/Epay.php new file mode 100644 index 0000000..9763bed --- /dev/null +++ b/addons/epay/Epay.php @@ -0,0 +1,69 @@ +<?php + +namespace addons\epay; + +use think\Addons; +use think\Config; +use think\Loader; + +/** + * 微信支付宝整合插件 + */ +class Epay extends Addons +{ + + /** + * 插件安装方法 + * @return bool + */ + public function install() + { + + return true; + } + + /** + * 插件卸载方法 + * @return bool + */ + public function uninstall() + { + + return true; + } + + /** + * 插件启用方法 + * @return bool + */ + public function enable() + { + + return true; + } + + /** + * 插件禁用方法 + * @return bool + */ + public function disable() + { + + return true; + } + + /** + * 添加命名空间 + */ + public function appInit() + { + //添加命名空间 + if (!class_exists('\Yansongda\Pay\Pay')) { + Loader::addNamespace('Yansongda\Pay', ADDON_PATH . 'epay' . DS . 'library' . DS . 'Yansongda' . DS . 'Pay' . DS); + } + if (!class_exists('\Yansongda\Supports\Logger')) { + Loader::addNamespace('Yansongda\Supports', ADDON_PATH . 'epay' . DS . 'library' . DS . 'Yansongda' . DS . 'Supports' . DS); + } + } + +} diff --git a/addons/epay/certs/alipayCertPublicKey.crt b/addons/epay/certs/alipayCertPublicKey.crt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/epay/certs/alipayCertPublicKey.crt diff --git a/addons/epay/certs/alipayRootCert.crt b/addons/epay/certs/alipayRootCert.crt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/epay/certs/alipayRootCert.crt diff --git a/addons/epay/certs/apiclient_cert.pem b/addons/epay/certs/apiclient_cert.pem new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/epay/certs/apiclient_cert.pem diff --git a/addons/epay/certs/apiclient_key.pem b/addons/epay/certs/apiclient_key.pem new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/epay/certs/apiclient_key.pem diff --git a/addons/epay/certs/appCertPublicKey.crt b/addons/epay/certs/appCertPublicKey.crt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/addons/epay/certs/appCertPublicKey.crt diff --git a/addons/epay/config.html b/addons/epay/config.html new file mode 100644 index 0000000..aaf45e5 --- /dev/null +++ b/addons/epay/config.html @@ -0,0 +1,366 @@ +<form id="config-form" class="edit-form form-horizontal" role="form" data-toggle="validator" method="POST" action=""> + + <div class="panel panel-default panel-intro"> + <div class="panel-heading"> + <ul class="nav nav-tabs nav-group"> + <li class="active"><a href="#wechat" data-toggle="tab">微信支付</a></li> + <li><a href="#alipay" data-toggle="tab">支付宝</a></li> + </ul> + </div> + + <div class="panel-body"> + <div id="myTabContent" class="tab-content"> + {foreach $addon.config as $item} + {if $item.name=='wechat'} + <div class="tab-pane fade active in" id="wechat"> + <table class="table table-striped table-config"> + <tbody> + <tr> + <td width="15%">APP appid</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][appid]" value="{$item.value.appid|default=''}" class="form-control" data-rule="" data-tip="APP应用中支付时使用"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>公众号的app_id</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][app_id]" value="{$item.value.app_id|default=''}" class="form-control" data-rule="" data-tip="公众号支付必须"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>公众号的app_secret</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][app_secret]" value="{$item.value.app_secret|default=''}" class="form-control" data-rule="" data-tip="仅在需要获取Openid时使用,一般情况下为空"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>小程序的app_id</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][miniapp_id]" value="{$item.value.miniapp_id|default=''}" class="form-control" data-rule="" data-tip="仅在小程序支付时使用"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>微信支付商户号ID</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][mch_id]" value="{$item.value.mch_id|default=''}" class="form-control" data-rule="" data-tip=""/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>微信支付商户的密钥</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][key]" value="{$item.value.key|default=''}" class="form-control" data-rule="" data-tip=""/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>支付模式</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + {:Form::radios('row[wechat][mode]',['normal'=>'正式环境','dev'=>'沙箱环境','service'=>'服务商模式'],$item.value.mode??'normal')} + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}"> + <td>子商户商户号ID</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][sub_mch_id]" value="{$item.value.sub_mch_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}"> + <td>子商户 APP appid</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][sub_appid]" value="{$item.value.sub_appid|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}"> + <td>子商户公众号的appid</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][sub_app_id]" value="{$item.value.sub_app_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}"> + <td>子商户小程序的appid</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][sub_miniapp_id]" value="{$item.value.sub_miniapp_id|default=''}" class="form-control" data-rule="" data-tip="如果未用到子商户,请勿填写"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>回调通知地址</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[wechat][notify_url]" value="{$item.value.notify_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>微信支付API证书cert</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <div class="input-group"> + <input id="c-cert_client" class="form-control" size="50" name="row[wechat][cert_client]" type="text" value="{$item.value.cert_client|htmlentities}" data-tip="可选, 仅在退款、红包等情况时需要用到"> + <div class="input-group-addon no-border no-padding"> + <span><button type="button" id="faupload-cert_client" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"cert_client"}' data-mimetype="pem" data-input-id="c-cert_client" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span> + </div> + <span class="msg-box n-right" for="c-cert_client"></span> + </div> + <div style="margin-top:5px;"><a href="https://pay.weixin.qq.com" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付API证书?</a></div> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>微信支付API证书key</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <div class="input-group"> + <input id="c-cert_key" class="form-control" size="50" name="row[wechat][cert_key]" type="text" value="{$item.value.cert_key|htmlentities}" data-tip="可选, 仅在退款、红包等情况时需要用到"> + <div class="input-group-addon no-border no-padding"> + <span><button type="button" id="faupload-cert_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"cert_key"}' data-mimetype="pem" data-input-id="c-cert_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span> + </div> + <span class="msg-box n-right" for="c-cert_key"></span> + </div> + <div style="margin-top:5px;"><a href="https://pay.weixin.qq.com" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付API证书?</a></div> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + + <tr> + <td>记录日志</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + {:Form::radios('row[wechat][log]',['1'=>'开启','0'=>'关闭'],$item.value.log)} + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + {elseif $item.name=='alipay'} + <div class="tab-pane fade" id="alipay"> + <table class="table table-striped table-config"> + <tbody> + <tr> + <td width="15%">应用ID(app_id)</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[alipay][app_id]" value="{$item.value.app_id|default=''}" class="form-control" data-rule="" data-tip=""/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>支付模式</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + {:Form::radios('row[alipay][mode]',['normal'=>'正式环境','dev'=>'沙箱环境'],$item.value.mode??'normal')} + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>回调通知地址</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[alipay][notify_url]" value="{$item.value.notify_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>支付跳转地址</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[alipay][return_url]" value="{$item.value.return_url|default=''}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>应用私钥(private_key)</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <input type="text" name="row[alipay][private_key]" value="{$item.value.private_key|default=''}" class="form-control" data-rule="" /> + <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/207/201602469554" target="_blank"><i class="fa fa-question-circle"></i> 如何获取应用私钥?</a></div> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>支付宝公钥路径(ali_public_key)</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <div class="input-group"> + <input id="c-ali_public_key" class="form-control" size="50" name="row[alipay][ali_public_key]" type="text" value="{$item.value.ali_public_key|htmlentities|default=''}" placeholder="公钥请直接粘贴,公钥证书请点击右侧的上传"> + <div class="input-group-addon no-border no-padding"> + <span><button type="button" id="faupload-ali_public_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"ali_public_key"}' data-mimetype="crt" data-input-id="c-ali_public_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span> + </div> + <span class="msg-box n-right" for="c-ali_public_key"></span> + </div> + <div style="margin-top:5px;"><a href="javascript:" data-toggle="tooltip" data-title="如果要使用转账、提现功能,则必须使用公钥证书"> <i class="fa fa-info-circle"></i> 公钥和公钥证书说明</a> <a href="https://opensupport.alipay.com/support/helpcenter/207/201602471154" target="_blank"><i class="fa fa-question-circle"></i> 如何获取支付宝公钥证书?</a></div> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>应用公钥证书路径(app_cert_public_key)</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <div class="input-group"> + <input id="c-app_cert_public_key" class="form-control" size="50" name="row[alipay][app_cert_public_key]" type="text" value="{$item.value.app_cert_public_key|htmlentities|default=''}"> + <div class="input-group-addon no-border no-padding"> + <span><button type="button" id="faupload-app_cert_public_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"app_cert_public_key"}' data-mimetype="crt" data-input-id="c-app_cert_public_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span> + </div> + <span class="msg-box n-right" for="c-app_cert_public_key"></span> + </div> + <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/207/201602469554" target="_blank"><i class="fa fa-question-circle"></i> 如何获取应用公钥证书?</a></div> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + <tr> + <td>支付宝根证书路径(alipay_root_cert)</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + <div class="input-group"> + <input id="c-alipay_root_cert" class="form-control" size="50" name="row[alipay][alipay_root_cert]" type="text" value="{$item.value.alipay_root_cert|htmlentities|default=''}"> + <div class="input-group-addon no-border no-padding"> + <span><button type="button" id="faupload-alipay_root_cert" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"alipay_root_cert"}' data-mimetype="crt" data-input-id="c-alipay_root_cert" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span> + </div> + <span class="msg-box n-right" for="c-alipay_root_cert"></span> + </div> + <div style="margin-top:5px;"><a href="https://opensupport.alipay.com/support/helpcenter/207/201602469554" target="_blank"><i class="fa fa-question-circle"></i> 如何获取支付宝证书?</a></div> + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + + <tr> + <td>记录日志</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + {:Form::radios('row[alipay][log]',['1'=>'开启','0'=>'关闭'],$item.value.log)} + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + + <tr> + <td>PC端使用扫码支付</td> + <td> + <div class="row"> + <div class="col-sm-8 col-xs-12"> + {:Form::radios('row[alipay][scanpay]',['1'=>'开启','0'=>'关闭'],$item.value.scanpay??0)} + </div> + <div class="col-sm-4"></div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + {/if} + {/foreach} + <div class="form-group layer-footer"> + <label class="control-label col-xs-12 col-sm-2"></label> + <div class="col-xs-12 col-sm-8"> + <button type="submit" class="btn btn-success btn-embossed disabled">{:__('OK')}</button> + <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button> + </div> + </div> + </div> + </div> + </div> +</form> +<script> + document.querySelectorAll("input[name='row[wechat][mode]']").forEach(function (i, j) { + i.addEventListener("click", function () { + document.querySelectorAll("#wechat table tr[data-type]").forEach(function (m, n) { + m.classList.add("hidden"); + }); + document.querySelectorAll("#wechat table tr[data-type='" + this.value + "']").forEach(function (m, n) { + m.classList.remove("hidden"); + }); + }); + }); +</script> diff --git a/addons/epay/config.php b/addons/epay/config.php new file mode 100644 index 0000000..e51d3a5 --- /dev/null +++ b/addons/epay/config.php @@ -0,0 +1,67 @@ +<?php + +return [ + [ + 'name' => 'wechat', + 'title' => '微信', + 'type' => 'array', + 'content' => [], + 'value' => [ + 'appid' => '', + 'app_id' => '', + 'app_secret' => '', + 'miniapp_id' => '', + 'mch_id' => '', + 'key' => '', + 'mode' => 'normal', + 'sub_mch_id' => '', + 'sub_appid' => '', + 'sub_app_id' => '', + 'sub_miniapp_id' => '', + 'notify_url' => '/addons/epay/api/notifyx/type/wechat', + 'cert_client' => '/addons/epay/certs/apiclient_cert.pem', + 'cert_key' => '/addons/epay/certs/apiclient_key.pem', + 'log' => '1', + ], + 'rule' => '', + 'msg' => '', + 'tip' => '微信参数配置', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => 'alipay', + 'title' => '支付宝', + 'type' => 'array', + 'content' => [], + 'value' => [ + 'app_id' => '', + 'mode' => 'normal', + 'notify_url' => '/addons/epay/api/notifyx/type/alipay', + 'return_url' => '/addons/epay/api/returnx/type/alipay', + 'private_key' => '', + 'ali_public_key' => '', + 'app_cert_public_key' => '', + 'alipay_root_cert' => '', + 'log' => '1', + 'scanpay' => '0', + ], + 'rule' => 'required', + 'msg' => '', + 'tip' => '支付宝参数配置', + 'ok' => '', + 'extend' => '', + ], + [ + 'name' => '__tips__', + 'title' => '温馨提示', + 'type' => 'array', + 'content' => [], + 'value' => '请注意微信支付证书路径位于/addons/epay/certs目录下,请替换成你自己的证书<br>appid:APP的appid<br>app_id:公众号的appid<br>app_secret:公众号的secret<br>miniapp_id:小程序ID<br>mch_id:微信商户ID<br>key:微信商户支付的密钥', + 'rule' => '', + 'msg' => '', + 'tip' => '微信参数配置', + 'ok' => '', + 'extend' => '', + ], +]; diff --git a/addons/epay/controller/Api.php b/addons/epay/controller/Api.php new file mode 100644 index 0000000..2645210 --- /dev/null +++ b/addons/epay/controller/Api.php @@ -0,0 +1,243 @@ +<?php + +namespace addons\epay\controller; + +use addons\epay\library\QRCode; +use addons\epay\library\Service; +use addons\epay\library\Wechat; +use addons\third\model\Third; +use app\common\library\Auth; +use think\addons\Controller; +use think\Response; +use think\Session; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Pay; + +/** + * API接口控制器 + * + * @package addons\epay\controller + */ +class Api extends Controller +{ + + protected $layout = 'default'; + protected $config = []; + + /** + * 默认方法 + */ + public function index() + { + return; + } + + /** + * 外部提交 + */ + public function submit() + { + $this->request->filter('trim'); + $out_trade_no = $this->request->request("out_trade_no"); + $title = $this->request->request("title"); + $amount = $this->request->request('amount'); + $type = $this->request->request('type'); + $method = $this->request->request('method', 'web'); + $openid = $this->request->request('openid', ''); + $auth_code = $this->request->request('auth_code', ''); + $notifyurl = $this->request->request('notifyurl', ''); + $returnurl = $this->request->request('returnurl', ''); + + if (!$amount || $amount < 0) { + $this->error("支付金额必须大于0"); + } + + if (!$type || !in_array($type, ['alipay', 'wechat'])) { + $this->error("支付类型错误"); + } + + $params = [ + 'type' => $type, + 'out_trade_no' => $out_trade_no, + 'title' => $title, + 'amount' => $amount, + 'method' => $method, + 'openid' => $openid, + 'auth_code' => $auth_code, + 'notifyurl' => $notifyurl, + 'returnurl' => $returnurl, + ]; + return Service::submitOrder($params); + } + + /** + * 微信支付(公众号支付&PC扫码支付) + * @return string + */ + public function wechat() + { + $config = Service::getConfig('wechat'); + + $isWechat = stripos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false; + $isMobile = $this->request->isMobile(); + $this->view->assign("isWechat", $isWechat); + $this->view->assign("isMobile", $isMobile); + + //发起PC支付(Scan支付)(PC扫码模式) + if ($this->request->isAjax()) { + $pay = Pay::wechat($config); + $orderid = $this->request->post("orderid"); + try { + $result = $pay->find($orderid); + if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') { + $this->success("", "", ['status' => $result['trade_state']]); + } else { + $this->error("查询失败"); + } + } catch (GatewayException $e) { + $this->error("查询失败"); + } + } + + $orderData = Session::get("wechatorderdata"); + if (!$orderData) { + $this->error("请求参数错误"); + } + if ($isWechat) { + //发起公众号(jsapi支付),openid必须 + + //如果没有openid,则自动去获取openid + if (!isset($orderData['openid']) || !$orderData['openid']) { + $orderData['openid'] = Service::getOpenid(); + } + + $orderData['method'] = 'mp'; + $type = 'jsapi'; + $payData = Service::submitOrder($orderData); + if (!isset($payData['paySign'])) { + $this->error("创建订单失败,请返回重试", ""); + } + } else { + $orderData['method'] = 'scan'; + $type = 'pc'; + $payData = Service::submitOrder($orderData); + if (!isset($payData['code_url'])) { + $this->error("创建订单失败,请返回重试", ""); + } + } + $this->view->assign("orderData", $orderData); + $this->view->assign("payData", $payData); + $this->view->assign("type", $type); + + $this->view->assign("title", "微信支付"); + return $this->view->fetch(); + } + + /** + * 支付宝支付(PC扫码支付) + * @return string + */ + public function alipay() + { + $config = Service::getConfig('alipay'); + + $isWechat = stripos($this->request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false; + $isMobile = $this->request->isMobile(); + $this->view->assign("isWechat", $isWechat); + $this->view->assign("isMobile", $isMobile); + + if ($this->request->isAjax()) { + $orderid = $this->request->post("orderid"); + $pay = Pay::alipay($config); + try { + $result = $pay->find($orderid); + if ($result['code'] == '10000' && $result['trade_status'] == 'TRADE_SUCCESS') { + $this->success("", "", ['status' => $result['trade_status']]); + } else { + $this->error("查询失败"); + } + } catch (GatewayException $e) { + $this->error("查询失败"); + } + } + + //发起PC支付(Scan支付)(PC扫码模式) + $orderData = Session::get("alipayorderdata"); + if (!$orderData) { + $this->error("请求参数错误"); + } + + $orderData['method'] = 'scan'; + $payData = Service::submitOrder($orderData); + if (!isset($payData['qr_code'])) { + $this->error("创建订单失败,请返回重试"); + } + + $type = 'pc'; + $this->view->assign("orderData", $orderData); + $this->view->assign("payData", $payData); + $this->view->assign("type", $type); + $this->view->assign("title", "支付宝支付"); + return $this->view->fetch(); + } + + /** + * 支付成功回调 + */ + public function notifyx() + { + $type = $this->request->param('type'); + if (!Service::checkNotify($type)) { + echo '签名错误'; + return; + } + + //你可以在这里你的业务处理逻辑,比如处理你的订单状态、给会员加余额等等功能 + //下面这句必须要执行,且在此之前不能有任何输出 + echo "success"; + return; + } + + /** + * 支付成功返回 + */ + public function returnx() + { + $type = $this->request->param('type'); + if (Service::checkReturn($type)) { + echo '签名错误'; + return; + } + + //你可以在这里定义你的提示信息,但切记不可在此编写逻辑 + $this->success("恭喜你!支付成功!", addon_url("epay/index/index")); + + return; + } + + /** + * 生成二维码 + */ + public function qrcode() + { + $text = $this->request->get('text', 'hello world'); + + //如果有安装二维码插件,则调用插件的生成方法 + if (class_exists("\addons\qrcode\library\Service") && get_addon_info('qrcode')['state']) { + $qrCode = \addons\qrcode\library\Service::qrcode(['text' => $text]); + $response = Response::create()->header("Content-Type", "image/png"); + + header('Content-Type: ' . $qrCode->getContentType()); + $response->content($qrCode->writeString()); + return $response; + } else { + $qr = QRCode::getMinimumQRCode($text); + $im = $qr->createImage(8, 5); + header("Content-type: image/png"); + imagepng($im); + imagedestroy($im); + return; + } + } + +} diff --git a/addons/epay/controller/Index.php b/addons/epay/controller/Index.php new file mode 100644 index 0000000..b152edd --- /dev/null +++ b/addons/epay/controller/Index.php @@ -0,0 +1,111 @@ +<?php + +namespace addons\epay\controller; + +use addons\epay\library\Service; +use fast\Random; +use think\addons\Controller; +use Exception; + +/** + * 微信支付宝插件首页 + * + * 此控制器仅用于开发展示说明和体验,建议自行添加一个新的控制器进行处理返回和回调事件,同时删除此控制器文件 + * + * Class Index + * @package addons\epay\controller + */ +class Index extends Controller +{ + + protected $layout = 'default'; + + protected $config = []; + + public function _initialize() + { + parent::_initialize(); + if (!config("app_debug")) { + $this->error("仅在开发环境下查看"); + } + } + + public function index() + { + $this->view->assign("title", "微信支付宝整合插件"); + return $this->view->fetch(); + } + + /** + * 体验,仅供开发测试 + */ + public function experience() + { + $amount = $this->request->request('amount'); + $type = $this->request->request('type'); + $method = $this->request->request('method'); + + if (!$amount || $amount < 0) { + $this->error("支付金额必须大于0"); + } + + if (!$type || !in_array($type, ['alipay', 'wechat'])) { + $this->error("支付类型不能为空"); + } + + //订单号 + $out_trade_no = date("YmdHis") . mt_rand(100000, 999999); + + //订单标题 + $title = '测试订单'; + + //回调链接 + $notifyurl = $this->request->root(true) . '/addons/epay/index/notifyx/paytype/' . $type; + $returnurl = $this->request->root(true) . '/addons/epay/index/returnx/paytype/' . $type . '/out_trade_no/' . $out_trade_no; + + $response = Service::submitOrder($amount, $out_trade_no, $type, $title, $notifyurl, $returnurl, $method); + + return $response; + } + + /** + * 支付成功,仅供开发测试 + */ + public function notifyx() + { + $paytype = $this->request->param('paytype'); + $pay = Service::checkNotify($paytype); + if (!$pay) { + echo '签名错误'; + return; + } + $data = $pay->verify(); + try { + $payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100; + $out_trade_no = $data['out_trade_no']; + + //你可以在此编写订单逻辑 + } catch (Exception $e) { + } + echo $pay->success(); + } + + /** + * 支付返回,仅供开发测试 + */ + public function returnx() + { + $paytype = $this->request->param('paytype'); + $out_trade_no = $this->request->param('out_trade_no'); + $pay = Service::checkReturn($paytype); + if (!$pay) { + $this->error('签名错误', ''); + } + + //你可以在这里通过out_trade_no去验证订单状态 + //但是不可以在此编写订单逻辑!!! + + $this->success("请返回网站查看支付结果", addon_url("epay/index/index")); + } + +} diff --git a/addons/epay/info.ini b/addons/epay/info.ini new file mode 100644 index 0000000..1a6a641 --- /dev/null +++ b/addons/epay/info.ini @@ -0,0 +1,10 @@ +name = epay +title = 微信支付宝整合 +intro = 可用于快速整合微信、支付宝支付功能 +author = FastAdmin +website = https://www.fastadmin.net +version = 1.2.3 +state = 1 +url = /addons/epay +license = regular +licenseto = 10789 diff --git a/addons/epay/library/Collection.php b/addons/epay/library/Collection.php new file mode 100644 index 0000000..8b5743e --- /dev/null +++ b/addons/epay/library/Collection.php @@ -0,0 +1,18 @@ +<?php + +namespace addons\epay\library; + +class Collection extends \Yansongda\Supports\Collection +{ + + /** + * 创建 Collection 实例 + * @access public + * @param array $items 数据 + * @return static + */ + public static function make($items = []) + { + return new static($items); + } +} diff --git a/addons/epay/library/OrderException.php b/addons/epay/library/OrderException.php new file mode 100644 index 0000000..96dc800 --- /dev/null +++ b/addons/epay/library/OrderException.php @@ -0,0 +1,16 @@ +<?php + +namespace addons\epay\library; + +use think\Exception; + +class OrderException extends Exception +{ + public function __construct($message = "", $code = 0, $data = []) + { + $this->message = $message; + $this->code = $code; + $this->data = $data; + } + +} diff --git a/addons/epay/library/QRCode.php b/addons/epay/library/QRCode.php new file mode 100644 index 0000000..f9d8582 --- /dev/null +++ b/addons/epay/library/QRCode.php @@ -0,0 +1,1856 @@ +<?php + +namespace addons\epay\library; + +//--------------------------------------------------------------- +// QRCode for PHP5 +// +// Copyright (c) 2009 Kazuhiko Arase +// +// URL: http://www.d-project.com/ +// +// Licensed under the MIT license: +// http://www.opensource.org/licenses/mit-license.php +// +// The word "QR Code" is registered trademark of +// DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/faqpatent-e.html +// +//--------------------------------------------------------------------- + +//--------------------------------------------------------------- +// QRCode +//--------------------------------------------------------------- + +define("QR_PAD0", 0xEC); +define("QR_PAD1", 0x11); + +class QRCode +{ + + var $typeNumber; + + var $modules; + + var $moduleCount; + + var $errorCorrectLevel; + + var $qrDataList; + + function __construct() + { + $this->typeNumber = 1; + $this->errorCorrectLevel = QR_ERROR_CORRECT_LEVEL_H; + $this->qrDataList = array(); + } + + function getTypeNumber() + { + return $this->typeNumber; + } + + function setTypeNumber($typeNumber) + { + $this->typeNumber = $typeNumber; + } + + function getErrorCorrectLevel() + { + return $this->errorCorrectLevel; + } + + function setErrorCorrectLevel($errorCorrectLevel) + { + $this->errorCorrectLevel = $errorCorrectLevel; + } + + function addData($data, $mode = 0) + { + + if ($mode == 0) { + $mode = QRUtil::getMode($data); + } + + switch ($mode) { + + case QR_MODE_NUMBER : + $this->addDataImpl(new QRNumber($data)); + break; + + case QR_MODE_ALPHA_NUM : + $this->addDataImpl(new QRAlphaNum($data)); + break; + + case QR_MODE_8BIT_BYTE : + $this->addDataImpl(new QR8BitByte($data)); + break; + + case QR_MODE_KANJI : + $this->addDataImpl(new QRKanji($data)); + break; + + default : + trigger_error("mode:$mode", E_USER_ERROR); + } + } + + function clearData() + { + $this->qrDataList = array(); + } + + function addDataImpl($qrData) + { + $this->qrDataList[] = $qrData; + } + + function getDataCount() + { + return count($this->qrDataList); + } + + function getData($index) + { + return $this->qrDataList[$index]; + } + + function isDark($row, $col) + { + if ($this->modules[$row][$col] !== null) { + return $this->modules[$row][$col]; + } else { + return false; + } + } + + function getModuleCount() + { + return $this->moduleCount; + } + + // used for converting fg/bg colors (e.g. #0000ff = 0x0000FF) + // added 2015.07.27 ~ DoktorJ + function hex2rgb($hex = 0x0) + { + return array( + 'r' => floor($hex / 65536), + 'g' => floor($hex / 256) % 256, + 'b' => $hex % 256 + ); + } + + function make() + { + $this->makeImpl(false, $this->getBestMaskPattern()); + } + + function getBestMaskPattern() + { + + $minLostPoint = 0; + $pattern = 0; + + for ($i = 0; $i < 8; $i++) { + + $this->makeImpl(true, $i); + + $lostPoint = QRUtil::getLostPoint($this); + + if ($i == 0 || $minLostPoint > $lostPoint) { + $minLostPoint = $lostPoint; + $pattern = $i; + } + } + + return $pattern; + } + + function createNullArray($length) + { + $nullArray = array(); + for ($i = 0; $i < $length; $i++) { + $nullArray[] = null; + } + return $nullArray; + } + + function makeImpl($test, $maskPattern) + { + + $this->moduleCount = $this->typeNumber * 4 + 17; + + $this->modules = array(); + for ($i = 0; $i < $this->moduleCount; $i++) { + $this->modules[] = QRCode::createNullArray($this->moduleCount); + } + + $this->setupPositionProbePattern(0, 0); + $this->setupPositionProbePattern($this->moduleCount - 7, 0); + $this->setupPositionProbePattern(0, $this->moduleCount - 7); + + $this->setupPositionAdjustPattern(); + $this->setupTimingPattern(); + + $this->setupTypeInfo($test, $maskPattern); + + if ($this->typeNumber >= 7) { + $this->setupTypeNumber($test); + } + + $dataArray = $this->qrDataList; + + $data = QRCode::createData($this->typeNumber, $this->errorCorrectLevel, $dataArray); + + $this->mapData($data, $maskPattern); + } + + function mapData(&$data, $maskPattern) + { + + $inc = -1; + $row = $this->moduleCount - 1; + $bitIndex = 7; + $byteIndex = 0; + + for ($col = $this->moduleCount - 1; $col > 0; $col -= 2) { + + if ($col == 6) $col--; + + while (true) { + + for ($c = 0; $c < 2; $c++) { + + if ($this->modules[$row][$col - $c] === null) { + + $dark = false; + + if ($byteIndex < count($data)) { + $dark = ((($data[$byteIndex] >> $bitIndex) & 1) == 1); + } + + if (QRUtil::getMask($maskPattern, $row, $col - $c)) { + $dark = !$dark; + } + + $this->modules[$row][$col - $c] = $dark; + $bitIndex--; + + if ($bitIndex == -1) { + $byteIndex++; + $bitIndex = 7; + } + } + } + + $row += $inc; + + if ($row < 0 || $this->moduleCount <= $row) { + $row -= $inc; + $inc = -$inc; + break; + } + } + } + } + + function setupPositionAdjustPattern() + { + + $pos = QRUtil::getPatternPosition($this->typeNumber); + + for ($i = 0; $i < count($pos); $i++) { + + for ($j = 0; $j < count($pos); $j++) { + + $row = $pos[$i]; + $col = $pos[$j]; + + if ($this->modules[$row][$col] !== null) { + continue; + } + + for ($r = -2; $r <= 2; $r++) { + + for ($c = -2; $c <= 2; $c++) { + $this->modules[$row + $r][$col + $c] = + $r == -2 || $r == 2 || $c == -2 || $c == 2 || ($r == 0 && $c == 0); + } + } + } + } + } + + function setupPositionProbePattern($row, $col) + { + + for ($r = -1; $r <= 7; $r++) { + + for ($c = -1; $c <= 7; $c++) { + + if ($row + $r <= -1 || $this->moduleCount <= $row + $r + || $col + $c <= -1 || $this->moduleCount <= $col + $c) { + continue; + } + + $this->modules[$row + $r][$col + $c] = + (0 <= $r && $r <= 6 && ($c == 0 || $c == 6)) + || (0 <= $c && $c <= 6 && ($r == 0 || $r == 6)) + || (2 <= $r && $r <= 4 && 2 <= $c && $c <= 4); + } + } + } + + function setupTimingPattern() + { + + for ($i = 8; $i < $this->moduleCount - 8; $i++) { + + if ($this->modules[$i][6] !== null || $this->modules[6][$i] !== null) { + continue; + } + + $this->modules[$i][6] = ($i % 2 == 0); + $this->modules[6][$i] = ($i % 2 == 0); + } + } + + function setupTypeNumber($test) + { + + $bits = QRUtil::getBCHTypeNumber($this->typeNumber); + + for ($i = 0; $i < 18; $i++) { + $mod = (!$test && (($bits >> $i) & 1) == 1); + $this->modules[(int)floor($i / 3)][$i % 3 + $this->moduleCount - 8 - 3] = $mod; + $this->modules[$i % 3 + $this->moduleCount - 8 - 3][floor($i / 3)] = $mod; + } + } + + function setupTypeInfo($test, $maskPattern) + { + + $data = ($this->errorCorrectLevel << 3) | $maskPattern; + $bits = QRUtil::getBCHTypeInfo($data); + + for ($i = 0; $i < 15; $i++) { + + $mod = (!$test && (($bits >> $i) & 1) == 1); + + if ($i < 6) { + $this->modules[$i][8] = $mod; + } else if ($i < 8) { + $this->modules[$i + 1][8] = $mod; + } else { + $this->modules[$this->moduleCount - 15 + $i][8] = $mod; + } + + if ($i < 8) { + $this->modules[8][$this->moduleCount - $i - 1] = $mod; + } else if ($i < 9) { + $this->modules[8][15 - $i - 1 + 1] = $mod; + } else { + $this->modules[8][15 - $i - 1] = $mod; + } + } + + $this->modules[$this->moduleCount - 8][8] = !$test; + } + + function createData($typeNumber, $errorCorrectLevel, $dataArray) + { + + $rsBlocks = QRRSBlock::getRSBlocks($typeNumber, $errorCorrectLevel); + + $buffer = new QRBitBuffer(); + + for ($i = 0; $i < count($dataArray); $i++) { + /** @var \QRData $data */ + $data = $dataArray[$i]; + $buffer->put($data->getMode(), 4); + $buffer->put($data->getLength(), $data->getLengthInBits($typeNumber)); + $data->write($buffer); + } + + $totalDataCount = 0; + for ($i = 0; $i < count($rsBlocks); $i++) { + $totalDataCount += $rsBlocks[$i]->getDataCount(); + } + + if ($buffer->getLengthInBits() > $totalDataCount * 8) { + trigger_error("code length overflow. (" + . $buffer->getLengthInBits() + . ">" + . $totalDataCount * 8 + . ")", E_USER_ERROR); + } + + // end code. + if ($buffer->getLengthInBits() + 4 <= $totalDataCount * 8) { + $buffer->put(0, 4); + } + + // padding + while ($buffer->getLengthInBits() % 8 != 0) { + $buffer->putBit(false); + } + + // padding + while (true) { + + if ($buffer->getLengthInBits() >= $totalDataCount * 8) { + break; + } + $buffer->put(QR_PAD0, 8); + + if ($buffer->getLengthInBits() >= $totalDataCount * 8) { + break; + } + $buffer->put(QR_PAD1, 8); + } + + return QRCode::createBytes($buffer, $rsBlocks); + } + + /** + * @param \QRBitBuffer $buffer + * @param \QRRSBlock[] $rsBlocks + * + * @return array + */ + function createBytes(&$buffer, &$rsBlocks) + { + + $offset = 0; + + $maxDcCount = 0; + $maxEcCount = 0; + + $dcdata = QRCode::createNullArray(count($rsBlocks)); + $ecdata = QRCode::createNullArray(count($rsBlocks)); + + $rsBlockCount = count($rsBlocks); + for ($r = 0; $r < $rsBlockCount; $r++) { + + $dcCount = $rsBlocks[$r]->getDataCount(); + $ecCount = $rsBlocks[$r]->getTotalCount() - $dcCount; + + $maxDcCount = max($maxDcCount, $dcCount); + $maxEcCount = max($maxEcCount, $ecCount); + + $dcdata[$r] = QRCode::createNullArray($dcCount); + $dcDataCount = count($dcdata[$r]); + for ($i = 0; $i < $dcDataCount; $i++) { + $bdata = $buffer->getBuffer(); + $dcdata[$r][$i] = 0xff & $bdata[$i + $offset]; + } + $offset += $dcCount; + + $rsPoly = QRUtil::getErrorCorrectPolynomial($ecCount); + $rawPoly = new QRPolynomial($dcdata[$r], $rsPoly->getLength() - 1); + + $modPoly = $rawPoly->mod($rsPoly); + $ecdata[$r] = QRCode::createNullArray($rsPoly->getLength() - 1); + + $ecDataCount = count($ecdata[$r]); + for ($i = 0; $i < $ecDataCount; $i++) { + $modIndex = $i + $modPoly->getLength() - count($ecdata[$r]); + $ecdata[$r][$i] = ($modIndex >= 0) ? $modPoly->get($modIndex) : 0; + } + } + + $totalCodeCount = 0; + for ($i = 0; $i < $rsBlockCount; $i++) { + $totalCodeCount += $rsBlocks[$i]->getTotalCount(); + } + + $data = QRCode::createNullArray($totalCodeCount); + + $index = 0; + + for ($i = 0; $i < $maxDcCount; $i++) { + for ($r = 0; $r < $rsBlockCount; $r++) { + if ($i < count($dcdata[$r])) { + $data[$index++] = $dcdata[$r][$i]; + } + } + } + + for ($i = 0; $i < $maxEcCount; $i++) { + for ($r = 0; $r < $rsBlockCount; $r++) { + if ($i < count($ecdata[$r])) { + $data[$index++] = $ecdata[$r][$i]; + } + } + } + + return $data; + } + + static function getMinimumQRCode($data, $errorCorrectLevel = QR_ERROR_CORRECT_LEVEL_L) + { + + $mode = QRUtil::getMode($data); + + $qr = new QRCode(); + $qr->setErrorCorrectLevel($errorCorrectLevel); + $qr->addData($data, $mode); + + $qrData = $qr->getData(0); + $length = $qrData->getLength(); + + for ($typeNumber = 1; $typeNumber <= 40; $typeNumber++) { + if ($length <= QRUtil::getMaxLength($typeNumber, $mode, $errorCorrectLevel)) { + $qr->setTypeNumber($typeNumber); + break; + } + } + + $qr->make(); + + return $qr; + } + + // added $fg (foreground), $bg (background), and $bgtrans (use transparent bg) parameters + // also added some simple error checking on parameters + // updated 2015.07.27 ~ DoktorJ + function createImage($size = 2, $margin = 2, $fg = 0x000000, $bg = 0xFFFFFF, $bgtrans = false) + { + + // size/margin EC + if (!is_numeric($size)) $size = 2; + if (!is_numeric($margin)) $margin = 2; + if ($size < 1) $size = 1; + if ($margin < 0) $margin = 0; + + $image_size = $this->getModuleCount() * $size + $margin * 2; + + $image = imagecreatetruecolor($image_size, $image_size); + + // fg/bg EC + if ($fg < 0 || $fg > 0xFFFFFF) $fg = 0x0; + if ($bg < 0 || $bg > 0xFFFFFF) $bg = 0xFFFFFF; + + // convert hexadecimal RGB to arrays for imagecolorallocate + $fgrgb = $this->hex2rgb($fg); + $bgrgb = $this->hex2rgb($bg); + + // replace $black and $white with $fgc and $bgc + $fgc = imagecolorallocate($image, $fgrgb['r'], $fgrgb['g'], $fgrgb['b']); + $bgc = imagecolorallocate($image, $bgrgb['r'], $bgrgb['g'], $bgrgb['b']); + if ($bgtrans) imagecolortransparent($image, $bgc); + + // update $white to $bgc + imagefilledrectangle($image, 0, 0, $image_size, $image_size, $bgc); + + for ($r = 0; $r < $this->getModuleCount(); $r++) { + for ($c = 0; $c < $this->getModuleCount(); $c++) { + if ($this->isDark($r, $c)) { + + // update $black to $fgc + imagefilledrectangle($image, + $margin + $c * $size, + $margin + $r * $size, + $margin + ($c + 1) * $size - 1, + $margin + ($r + 1) * $size - 1, + $fgc); + } + } + } + + return $image; + } + + function printHTML($size = "2px") + { + + $style = "border-style:none;border-collapse:collapse;margin:0px;padding:0px;"; + + print("<table style='$style'>"); + + for ($r = 0; $r < $this->getModuleCount(); $r++) { + + print("<tr style='$style'>"); + + for ($c = 0; $c < $this->getModuleCount(); $c++) { + $color = $this->isDark($r, $c) ? "#000000" : "#ffffff"; + print("<td style='$style;width:$size;height:$size;background-color:$color'></td>"); + } + + print("</tr>"); + } + + print("</table>"); + } +} + +//--------------------------------------------------------------- +// QRUtil +//--------------------------------------------------------------- + +define("QR_G15", (1 << 10) | (1 << 8) | (1 << 5) + | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0)); + +define("QR_G18", (1 << 12) | (1 << 11) | (1 << 10) + | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0)); + +define("QR_G15_MASK", (1 << 14) | (1 << 12) | (1 << 10) + | (1 << 4) | (1 << 1)); + +class QRUtil +{ + + static $QR_MAX_LENGTH = array( + array(array(41, 25, 17, 10), array(34, 20, 14, 8), array(27, 16, 11, 7), array(17, 10, 7, 4)), + array(array(77, 47, 32, 20), array(63, 38, 26, 16), array(48, 29, 20, 12), array(34, 20, 14, 8)), + array(array(127, 77, 53, 32), array(101, 61, 42, 26), array(77, 47, 32, 20), array(58, 35, 24, 15)), + array(array(187, 114, 78, 48), array(149, 90, 62, 38), array(111, 67, 46, 28), array(82, 50, 34, 21)), + array(array(255, 154, 106, 65), array(202, 122, 84, 52), array(144, 87, 60, 37), array(106, 64, 44, 27)), + array(array(322, 195, 134, 82), array(255, 154, 106, 65), array(178, 108, 74, 45), array(139, 84, 58, 36)), + array(array(370, 224, 154, 95), array(293, 178, 122, 75), array(207, 125, 86, 53), array(154, 93, 64, 39)), + array(array(461, 279, 192, 118), array(365, 221, 152, 93), array(259, 157, 108, 66), array(202, 122, 84, 52)), + array(array(552, 335, 230, 141), array(432, 262, 180, 111), array(312, 189, 130, 80), array(235, 143, 98, 60)), + array(array(652, 395, 271, 167), array(513, 311, 213, 131), array(364, 221, 151, 93), array(288, 174, 119, 74)) + ); + + static $QR_PATTERN_POSITION_TABLE = array( + array(), + array(6, 18), + array(6, 22), + array(6, 26), + array(6, 30), + array(6, 34), + array(6, 22, 38), + array(6, 24, 42), + array(6, 26, 46), + array(6, 28, 50), + array(6, 30, 54), + array(6, 32, 58), + array(6, 34, 62), + array(6, 26, 46, 66), + array(6, 26, 48, 70), + array(6, 26, 50, 74), + array(6, 30, 54, 78), + array(6, 30, 56, 82), + array(6, 30, 58, 86), + array(6, 34, 62, 90), + array(6, 28, 50, 72, 94), + array(6, 26, 50, 74, 98), + array(6, 30, 54, 78, 102), + array(6, 28, 54, 80, 106), + array(6, 32, 58, 84, 110), + array(6, 30, 58, 86, 114), + array(6, 34, 62, 90, 118), + array(6, 26, 50, 74, 98, 122), + array(6, 30, 54, 78, 102, 126), + array(6, 26, 52, 78, 104, 130), + array(6, 30, 56, 82, 108, 134), + array(6, 34, 60, 86, 112, 138), + array(6, 30, 58, 86, 114, 142), + array(6, 34, 62, 90, 118, 146), + array(6, 30, 54, 78, 102, 126, 150), + array(6, 24, 50, 76, 102, 128, 154), + array(6, 28, 54, 80, 106, 132, 158), + array(6, 32, 58, 84, 110, 136, 162), + array(6, 26, 54, 82, 110, 138, 166), + array(6, 30, 58, 86, 114, 142, 170) + ); + + static function getPatternPosition($typeNumber) + { + return self::$QR_PATTERN_POSITION_TABLE[$typeNumber - 1]; + } + + static function getMaxLength($typeNumber, $mode, $errorCorrectLevel) + { + + $t = $typeNumber - 1; + $e = 0; + $m = 0; + + switch ($errorCorrectLevel) { + case QR_ERROR_CORRECT_LEVEL_L : + $e = 0; + break; + case QR_ERROR_CORRECT_LEVEL_M : + $e = 1; + break; + case QR_ERROR_CORRECT_LEVEL_Q : + $e = 2; + break; + case QR_ERROR_CORRECT_LEVEL_H : + $e = 3; + break; + default : + trigger_error("e:$errorCorrectLevel", E_USER_ERROR); + } + + switch ($mode) { + case QR_MODE_NUMBER : + $m = 0; + break; + case QR_MODE_ALPHA_NUM : + $m = 1; + break; + case QR_MODE_8BIT_BYTE : + $m = 2; + break; + case QR_MODE_KANJI : + $m = 3; + break; + default : + trigger_error("m:$mode", E_USER_ERROR); + } + + return self::$QR_MAX_LENGTH[$t][$e][$m]; + } + + static function getErrorCorrectPolynomial($errorCorrectLength) + { + + $a = new QRPolynomial(array(1)); + + for ($i = 0; $i < $errorCorrectLength; $i++) { + $a = $a->multiply(new QRPolynomial(array(1, QRMath::gexp($i)))); + } + + return $a; + } + + static function getMask($maskPattern, $i, $j) + { + + switch ($maskPattern) { + + case QR_MASK_PATTERN000 : + return ($i + $j) % 2 == 0; + case QR_MASK_PATTERN001 : + return $i % 2 == 0; + case QR_MASK_PATTERN010 : + return $j % 3 == 0; + case QR_MASK_PATTERN011 : + return ($i + $j) % 3 == 0; + case QR_MASK_PATTERN100 : + return (floor($i / 2) + floor($j / 3)) % 2 == 0; + case QR_MASK_PATTERN101 : + return ($i * $j) % 2 + ($i * $j) % 3 == 0; + case QR_MASK_PATTERN110 : + return (($i * $j) % 2 + ($i * $j) % 3) % 2 == 0; + case QR_MASK_PATTERN111 : + return (($i * $j) % 3 + ($i + $j) % 2) % 2 == 0; + + default : + trigger_error("mask:$maskPattern", E_USER_ERROR); + } + } + + /** + * @param \QRCode $qrCode + * + * @return float|int + */ + static function getLostPoint($qrCode) + { + + $moduleCount = $qrCode->getModuleCount(); + + $lostPoint = 0; + + + // LEVEL1 + + for ($row = 0; $row < $moduleCount; $row++) { + + for ($col = 0; $col < $moduleCount; $col++) { + + $sameCount = 0; + $dark = $qrCode->isDark($row, $col); + + for ($r = -1; $r <= 1; $r++) { + + if ($row + $r < 0 || $moduleCount <= $row + $r) { + continue; + } + + for ($c = -1; $c <= 1; $c++) { + + if (($col + $c < 0 || $moduleCount <= $col + $c) || ($r == 0 && $c == 0)) { + continue; + } + + if ($dark == $qrCode->isDark($row + $r, $col + $c)) { + $sameCount++; + } + } + } + + if ($sameCount > 5) { + $lostPoint += (3 + $sameCount - 5); + } + } + } + + // LEVEL2 + + for ($row = 0; $row < $moduleCount - 1; $row++) { + for ($col = 0; $col < $moduleCount - 1; $col++) { + $count = 0; + if ($qrCode->isDark($row, $col)) $count++; + if ($qrCode->isDark($row + 1, $col)) $count++; + if ($qrCode->isDark($row, $col + 1)) $count++; + if ($qrCode->isDark($row + 1, $col + 1)) $count++; + if ($count == 0 || $count == 4) { + $lostPoint += 3; + } + } + } + + // LEVEL3 + + for ($row = 0; $row < $moduleCount; $row++) { + for ($col = 0; $col < $moduleCount - 6; $col++) { + if ($qrCode->isDark($row, $col) + && !$qrCode->isDark($row, $col + 1) + && $qrCode->isDark($row, $col + 2) + && $qrCode->isDark($row, $col + 3) + && $qrCode->isDark($row, $col + 4) + && !$qrCode->isDark($row, $col + 5) + && $qrCode->isDark($row, $col + 6)) { + $lostPoint += 40; + } + } + } + + for ($col = 0; $col < $moduleCount; $col++) { + for ($row = 0; $row < $moduleCount - 6; $row++) { + if ($qrCode->isDark($row, $col) + && !$qrCode->isDark($row + 1, $col) + && $qrCode->isDark($row + 2, $col) + && $qrCode->isDark($row + 3, $col) + && $qrCode->isDark($row + 4, $col) + && !$qrCode->isDark($row + 5, $col) + && $qrCode->isDark($row + 6, $col)) { + $lostPoint += 40; + } + } + } + + // LEVEL4 + + $darkCount = 0; + + for ($col = 0; $col < $moduleCount; $col++) { + for ($row = 0; $row < $moduleCount; $row++) { + if ($qrCode->isDark($row, $col)) { + $darkCount++; + } + } + } + + $ratio = abs(100 * $darkCount / $moduleCount / $moduleCount - 50) / 5; + $lostPoint += $ratio * 10; + + return $lostPoint; + } + + static function getMode($s) + { + if (QRUtil::isAlphaNum($s)) { + if (QRUtil::isNumber($s)) { + return QR_MODE_NUMBER; + } + return QR_MODE_ALPHA_NUM; + } else if (QRUtil::isKanji($s)) { + return QR_MODE_KANJI; + } else { + return QR_MODE_8BIT_BYTE; + } + } + + static function isNumber($s) + { + for ($i = 0; $i < strlen($s); $i++) { + $c = ord($s[$i]); + if (!(QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9'))) { + return false; + } + } + return true; + } + + static function isAlphaNum($s) + { + for ($i = 0; $i < strlen($s); $i++) { + $c = ord($s[$i]); + if (!(QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9')) + && !(QRUtil::toCharCode('A') <= $c && $c <= QRUtil::toCharCode('Z')) + && strpos(" $%*+-./:", $s[$i]) === false) { + return false; + } + } + return true; + } + + static function isKanji($s) + { + + $data = $s; + + $i = 0; + + while ($i + 1 < strlen($data)) { + + $c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1])); + + if (!(0x8140 <= $c && $c <= 0x9FFC) && !(0xE040 <= $c && $c <= 0xEBBF)) { + return false; + } + + $i += 2; + } + + if ($i < strlen($data)) { + return false; + } + + return true; + } + + static function toCharCode($s) + { + return ord($s[0]); + } + + static function getBCHTypeInfo($data) + { + $d = $data << 10; + while (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G15) >= 0) { + $d ^= (QR_G15 << (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G15))); + } + return (($data << 10) | $d) ^ QR_G15_MASK; + } + + static function getBCHTypeNumber($data) + { + $d = $data << 12; + while (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G18) >= 0) { + $d ^= (QR_G18 << (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G18))); + } + return ($data << 12) | $d; + } + + static function getBCHDigit($data) + { + + $digit = 0; + + while ($data != 0) { + $digit++; + $data >>= 1; + } + + return $digit; + } +} + +//--------------------------------------------------------------- +// QRRSBlock +//--------------------------------------------------------------- + +class QRRSBlock +{ + + var $totalCount; + var $dataCount; + + static $QR_RS_BLOCK_TABLE = array( + + // L + // M + // Q + // H + + // 1 + array(1, 26, 19), + array(1, 26, 16), + array(1, 26, 13), + array(1, 26, 9), + + // 2 + array(1, 44, 34), + array(1, 44, 28), + array(1, 44, 22), + array(1, 44, 16), + + // 3 + array(1, 70, 55), + array(1, 70, 44), + array(2, 35, 17), + array(2, 35, 13), + + // 4 + array(1, 100, 80), + array(2, 50, 32), + array(2, 50, 24), + array(4, 25, 9), + + // 5 + array(1, 134, 108), + array(2, 67, 43), + array(2, 33, 15, 2, 34, 16), + array(2, 33, 11, 2, 34, 12), + + // 6 + array(2, 86, 68), + array(4, 43, 27), + array(4, 43, 19), + array(4, 43, 15), + + // 7 + array(2, 98, 78), + array(4, 49, 31), + array(2, 32, 14, 4, 33, 15), + array(4, 39, 13, 1, 40, 14), + + // 8 + array(2, 121, 97), + array(2, 60, 38, 2, 61, 39), + array(4, 40, 18, 2, 41, 19), + array(4, 40, 14, 2, 41, 15), + + // 9 + array(2, 146, 116), + array(3, 58, 36, 2, 59, 37), + array(4, 36, 16, 4, 37, 17), + array(4, 36, 12, 4, 37, 13), + + // 10 + array(2, 86, 68, 2, 87, 69), + array(4, 69, 43, 1, 70, 44), + array(6, 43, 19, 2, 44, 20), + array(6, 43, 15, 2, 44, 16), + + // 11 + array(4, 101, 81), + array(1, 80, 50, 4, 81, 51), + array(4, 50, 22, 4, 51, 23), + array(3, 36, 12, 8, 37, 13), + + // 12 + array(2, 116, 92, 2, 117, 93), + array(6, 58, 36, 2, 59, 37), + array(4, 46, 20, 6, 47, 21), + array(7, 42, 14, 4, 43, 15), + + // 13 + array(4, 133, 107), + array(8, 59, 37, 1, 60, 38), + array(8, 44, 20, 4, 45, 21), + array(12, 33, 11, 4, 34, 12), + + // 14 + array(3, 145, 115, 1, 146, 116), + array(4, 64, 40, 5, 65, 41), + array(11, 36, 16, 5, 37, 17), + array(11, 36, 12, 5, 37, 13), + + // 15 + array(5, 109, 87, 1, 110, 88), + array(5, 65, 41, 5, 66, 42), + array(5, 54, 24, 7, 55, 25), + array(11, 36, 12, 7, 37, 13), + + // 16 + array(5, 122, 98, 1, 123, 99), + array(7, 73, 45, 3, 74, 46), + array(15, 43, 19, 2, 44, 20), + array(3, 45, 15, 13, 46, 16), + + // 17 + array(1, 135, 107, 5, 136, 108), + array(10, 74, 46, 1, 75, 47), + array(1, 50, 22, 15, 51, 23), + array(2, 42, 14, 17, 43, 15), + + // 18 + array(5, 150, 120, 1, 151, 121), + array(9, 69, 43, 4, 70, 44), + array(17, 50, 22, 1, 51, 23), + array(2, 42, 14, 19, 43, 15), + + // 19 + array(3, 141, 113, 4, 142, 114), + array(3, 70, 44, 11, 71, 45), + array(17, 47, 21, 4, 48, 22), + array(9, 39, 13, 16, 40, 14), + + // 20 + array(3, 135, 107, 5, 136, 108), + array(3, 67, 41, 13, 68, 42), + array(15, 54, 24, 5, 55, 25), + array(15, 43, 15, 10, 44, 16), + + // 21 + array(4, 144, 116, 4, 145, 117), + array(17, 68, 42), + array(17, 50, 22, 6, 51, 23), + array(19, 46, 16, 6, 47, 17), + + // 22 + array(2, 139, 111, 7, 140, 112), + array(17, 74, 46), + array(7, 54, 24, 16, 55, 25), + array(34, 37, 13), + + // 23 + array(4, 151, 121, 5, 152, 122), + array(4, 75, 47, 14, 76, 48), + array(11, 54, 24, 14, 55, 25), + array(16, 45, 15, 14, 46, 16), + + // 24 + array(6, 147, 117, 4, 148, 118), + array(6, 73, 45, 14, 74, 46), + array(11, 54, 24, 16, 55, 25), + array(30, 46, 16, 2, 47, 17), + + // 25 + array(8, 132, 106, 4, 133, 107), + array(8, 75, 47, 13, 76, 48), + array(7, 54, 24, 22, 55, 25), + array(22, 45, 15, 13, 46, 16), + + // 26 + array(10, 142, 114, 2, 143, 115), + array(19, 74, 46, 4, 75, 47), + array(28, 50, 22, 6, 51, 23), + array(33, 46, 16, 4, 47, 17), + + // 27 + array(8, 152, 122, 4, 153, 123), + array(22, 73, 45, 3, 74, 46), + array(8, 53, 23, 26, 54, 24), + array(12, 45, 15, 28, 46, 16), + + // 28 + array(3, 147, 117, 10, 148, 118), + array(3, 73, 45, 23, 74, 46), + array(4, 54, 24, 31, 55, 25), + array(11, 45, 15, 31, 46, 16), + + // 29 + array(7, 146, 116, 7, 147, 117), + array(21, 73, 45, 7, 74, 46), + array(1, 53, 23, 37, 54, 24), + array(19, 45, 15, 26, 46, 16), + + // 30 + array(5, 145, 115, 10, 146, 116), + array(19, 75, 47, 10, 76, 48), + array(15, 54, 24, 25, 55, 25), + array(23, 45, 15, 25, 46, 16), + + // 31 + array(13, 145, 115, 3, 146, 116), + array(2, 74, 46, 29, 75, 47), + array(42, 54, 24, 1, 55, 25), + array(23, 45, 15, 28, 46, 16), + + // 32 + array(17, 145, 115), + array(10, 74, 46, 23, 75, 47), + array(10, 54, 24, 35, 55, 25), + array(19, 45, 15, 35, 46, 16), + + // 33 + array(17, 145, 115, 1, 146, 116), + array(14, 74, 46, 21, 75, 47), + array(29, 54, 24, 19, 55, 25), + array(11, 45, 15, 46, 46, 16), + + // 34 + array(13, 145, 115, 6, 146, 116), + array(14, 74, 46, 23, 75, 47), + array(44, 54, 24, 7, 55, 25), + array(59, 46, 16, 1, 47, 17), + + // 35 + array(12, 151, 121, 7, 152, 122), + array(12, 75, 47, 26, 76, 48), + array(39, 54, 24, 14, 55, 25), + array(22, 45, 15, 41, 46, 16), + + // 36 + array(6, 151, 121, 14, 152, 122), + array(6, 75, 47, 34, 76, 48), + array(46, 54, 24, 10, 55, 25), + array(2, 45, 15, 64, 46, 16), + + // 37 + array(17, 152, 122, 4, 153, 123), + array(29, 74, 46, 14, 75, 47), + array(49, 54, 24, 10, 55, 25), + array(24, 45, 15, 46, 46, 16), + + // 38 + array(4, 152, 122, 18, 153, 123), + array(13, 74, 46, 32, 75, 47), + array(48, 54, 24, 14, 55, 25), + array(42, 45, 15, 32, 46, 16), + + // 39 + array(20, 147, 117, 4, 148, 118), + array(40, 75, 47, 7, 76, 48), + array(43, 54, 24, 22, 55, 25), + array(10, 45, 15, 67, 46, 16), + + // 40 + array(19, 148, 118, 6, 149, 119), + array(18, 75, 47, 31, 76, 48), + array(34, 54, 24, 34, 55, 25), + array(20, 45, 15, 61, 46, 16) + + ); + + function __construct($totalCount, $dataCount) + { + $this->totalCount = $totalCount; + $this->dataCount = $dataCount; + } + + function getDataCount() + { + return $this->dataCount; + } + + function getTotalCount() + { + return $this->totalCount; + } + + static function getRSBlocks($typeNumber, $errorCorrectLevel) + { + + $rsBlock = QRRSBlock::getRsBlockTable($typeNumber, $errorCorrectLevel); + $length = count($rsBlock) / 3; + + $list = array(); + + for ($i = 0; $i < $length; $i++) { + + $count = $rsBlock[$i * 3 + 0]; + $totalCount = $rsBlock[$i * 3 + 1]; + $dataCount = $rsBlock[$i * 3 + 2]; + + for ($j = 0; $j < $count; $j++) { + $list[] = new QRRSBlock($totalCount, $dataCount); + } + } + + return $list; + } + + static function getRsBlockTable($typeNumber, $errorCorrectLevel) + { + + switch ($errorCorrectLevel) { + case QR_ERROR_CORRECT_LEVEL_L : + return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 0]; + case QR_ERROR_CORRECT_LEVEL_M : + return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 1]; + case QR_ERROR_CORRECT_LEVEL_Q : + return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 2]; + case QR_ERROR_CORRECT_LEVEL_H : + return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 3]; + default : + trigger_error("tn:$typeNumber/ecl:$errorCorrectLevel", E_USER_ERROR); + } + } +} + +//--------------------------------------------------------------- +// QRNumber +//--------------------------------------------------------------- + +class QRNumber extends QRData +{ + + function __construct($data) + { + parent::__construct(QR_MODE_NUMBER, $data); + } + + function write(&$buffer) + { + + $data = $this->getData(); + + $i = 0; + + while ($i + 2 < strlen($data)) { + $num = QRNumber::parseInt(substr($data, $i, 3)); + $buffer->put($num, 10); + $i += 3; + } + + if ($i < strlen($data)) { + + if (strlen($data) - $i == 1) { + $num = QRNumber::parseInt(substr($data, $i, $i + 1)); + $buffer->put($num, 4); + } else if (strlen($data) - $i == 2) { + $num = QRNumber::parseInt(substr($data, $i, $i + 2)); + $buffer->put($num, 7); + } + } + } + + static function parseInt($s) + { + + $num = 0; + for ($i = 0; $i < strlen($s); $i++) { + $num = $num * 10 + QRNumber::parseIntAt(ord($s[$i])); + } + return $num; + } + + static function parseIntAt($c) + { + + if (QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9')) { + return $c - QRUtil::toCharCode('0'); + } + + trigger_error("illegal char : $c", E_USER_ERROR); + } +} + +//--------------------------------------------------------------- +// QRKanji +//--------------------------------------------------------------- + +class QRKanji extends QRData +{ + + function __construct($data) + { + parent::__construct(QR_MODE_KANJI, $data); + } + + function write(&$buffer) + { + + $data = $this->getData(); + + $i = 0; + + while ($i + 1 < strlen($data)) { + + $c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1])); + + if (0x8140 <= $c && $c <= 0x9FFC) { + $c -= 0x8140; + } else if (0xE040 <= $c && $c <= 0xEBBF) { + $c -= 0xC140; + } else { + trigger_error("illegal char at " . ($i + 1) . "/$c", E_USER_ERROR); + } + + $c = (($c >> 8) & 0xff) * 0xC0 + ($c & 0xff); + + $buffer->put($c, 13); + + $i += 2; + } + + if ($i < strlen($data)) { + trigger_error("illegal char at " . ($i + 1), E_USER_ERROR); + } + } + + function getLength() + { + return floor(strlen($this->getData()) / 2); + } +} + +//--------------------------------------------------------------- +// QRAlphaNum +//--------------------------------------------------------------- + +class QRAlphaNum extends QRData +{ + + function __construct($data) + { + parent::__construct(QR_MODE_ALPHA_NUM, $data); + } + + function write(&$buffer) + { + + $i = 0; + $c = $this->getData(); + + while ($i + 1 < strlen($c)) { + $buffer->put(QRAlphaNum::getCode(ord($c[$i])) * 45 + + QRAlphaNum::getCode(ord($c[$i + 1])), 11); + $i += 2; + } + + if ($i < strlen($c)) { + $buffer->put(QRAlphaNum::getCode(ord($c[$i])), 6); + } + } + + static function getCode($c) + { + + if (QRUtil::toCharCode('0') <= $c + && $c <= QRUtil::toCharCode('9')) { + return $c - QRUtil::toCharCode('0'); + } else if (QRUtil::toCharCode('A') <= $c + && $c <= QRUtil::toCharCode('Z')) { + return $c - QRUtil::toCharCode('A') + 10; + } else { + switch ($c) { + case QRUtil::toCharCode(' ') : + return 36; + case QRUtil::toCharCode('$') : + return 37; + case QRUtil::toCharCode('%') : + return 38; + case QRUtil::toCharCode('*') : + return 39; + case QRUtil::toCharCode('+') : + return 40; + case QRUtil::toCharCode('-') : + return 41; + case QRUtil::toCharCode('.') : + return 42; + case QRUtil::toCharCode('/') : + return 43; + case QRUtil::toCharCode(':') : + return 44; + default : + trigger_error("illegal char : $c", E_USER_ERROR); + } + } + + } +} + +//--------------------------------------------------------------- +// QR8BitByte +//--------------------------------------------------------------- + +class QR8BitByte extends QRData +{ + + function __construct($data) + { + parent::__construct(QR_MODE_8BIT_BYTE, $data); + } + + function write(&$buffer) + { + + $data = $this->getData(); + for ($i = 0; $i < strlen($data); $i++) { + $buffer->put(ord($data[$i]), 8); + } + } + +} + +//--------------------------------------------------------------- +// QRData +//--------------------------------------------------------------- + +abstract class QRData +{ + + var $mode; + + var $data; + + function __construct($mode, $data) + { + $this->mode = $mode; + $this->data = $data; + } + + function getMode() + { + return $this->mode; + } + + function getData() + { + return $this->data; + } + + /** + * @return int + */ + function getLength() + { + return strlen($this->getData()); + } + + /** + * @param \QRBitBuffer $buffer + */ + abstract function write(&$buffer); + + function getLengthInBits($type) + { + + if (1 <= $type && $type < 10) { + + // 1 - 9 + + switch ($this->mode) { + case QR_MODE_NUMBER : + return 10; + case QR_MODE_ALPHA_NUM : + return 9; + case QR_MODE_8BIT_BYTE : + return 8; + case QR_MODE_KANJI : + return 8; + default : + trigger_error("mode:$this->mode", E_USER_ERROR); + } + + } else if ($type < 27) { + + // 10 - 26 + + switch ($this->mode) { + case QR_MODE_NUMBER : + return 12; + case QR_MODE_ALPHA_NUM : + return 11; + case QR_MODE_8BIT_BYTE : + return 16; + case QR_MODE_KANJI : + return 10; + default : + trigger_error("mode:$this->mode", E_USER_ERROR); + } + + } else if ($type < 41) { + + // 27 - 40 + + switch ($this->mode) { + case QR_MODE_NUMBER : + return 14; + case QR_MODE_ALPHA_NUM : + return 13; + case QR_MODE_8BIT_BYTE : + return 16; + case QR_MODE_KANJI : + return 12; + default : + trigger_error("mode:$this->mode", E_USER_ERROR); + } + + } else { + trigger_error("mode:$this->mode", E_USER_ERROR); + } + } + +} + +//--------------------------------------------------------------- +// QRMath +//--------------------------------------------------------------- + +class QRMath +{ + + static $QR_MATH_EXP_TABLE = null; + static $QR_MATH_LOG_TABLE = null; + + static function init() + { + + self::$QR_MATH_EXP_TABLE = QRMath::createNumArray(256); + + for ($i = 0; $i < 8; $i++) { + self::$QR_MATH_EXP_TABLE[$i] = 1 << $i; + } + + for ($i = 8; $i < 256; $i++) { + self::$QR_MATH_EXP_TABLE[$i] = self::$QR_MATH_EXP_TABLE[$i - 4] + ^ self::$QR_MATH_EXP_TABLE[$i - 5] + ^ self::$QR_MATH_EXP_TABLE[$i - 6] + ^ self::$QR_MATH_EXP_TABLE[$i - 8]; + } + + self::$QR_MATH_LOG_TABLE = QRMath::createNumArray(256); + + for ($i = 0; $i < 255; $i++) { + self::$QR_MATH_LOG_TABLE[self::$QR_MATH_EXP_TABLE[$i]] = $i; + } + } + + static function createNumArray($length) + { + $num_array = array(); + for ($i = 0; $i < $length; $i++) { + $num_array[] = 0; + } + return $num_array; + } + + static function glog($n) + { + + if ($n < 1) { + trigger_error("log($n)", E_USER_ERROR); + } + + return self::$QR_MATH_LOG_TABLE[$n]; + } + + static function gexp($n) + { + + while ($n < 0) { + $n += 255; + } + + while ($n >= 256) { + $n -= 255; + } + + return self::$QR_MATH_EXP_TABLE[$n]; + } +} + +// init static table +QRMath::init(); + +//--------------------------------------------------------------- +// QRPolynomial +//--------------------------------------------------------------- + +class QRPolynomial +{ + + var $num; + + function __construct($num, $shift = 0) + { + + $offset = 0; + + while ($offset < count($num) && $num[$offset] == 0) { + $offset++; + } + + $this->num = QRMath::createNumArray(count($num) - $offset + $shift); + for ($i = 0; $i < count($num) - $offset; $i++) { + $this->num[$i] = $num[$i + $offset]; + } + } + + function get($index) + { + return $this->num[$index]; + } + + function getLength() + { + return count($this->num); + } + + // PHP5 + function __toString() + { + return $this->toString(); + } + + function toString() + { + + $buffer = ""; + + for ($i = 0; $i < $this->getLength(); $i++) { + if ($i > 0) { + $buffer .= ","; + } + $buffer .= $this->get($i); + } + + return $buffer; + } + + function toLogString() + { + + $buffer = ""; + + for ($i = 0; $i < $this->getLength(); $i++) { + if ($i > 0) { + $buffer .= ","; + } + $buffer .= QRMath::glog($this->get($i)); + } + + return $buffer; + } + + /** + * @param \QRPolynomial $e + * + * @return \QRPolynomial + */ + function multiply($e) + { + + $num = QRMath::createNumArray($this->getLength() + $e->getLength() - 1); + + for ($i = 0; $i < $this->getLength(); $i++) { + $vi = QRMath::glog($this->get($i)); + + for ($j = 0; $j < $e->getLength(); $j++) { + $num[$i + $j] ^= QRMath::gexp($vi + QRMath::glog($e->get($j))); + } + } + + return new QRPolynomial($num); + } + + /** + * @param \QRPolynomial $e + * + * @return $this|\QRPolynomial + */ + function mod($e) + { + + if ($this->getLength() - $e->getLength() < 0) { + return $this; + } + + $ratio = QRMath::glog($this->get(0)) - QRMath::glog($e->get(0)); + + $num = QRMath::createNumArray($this->getLength()); + for ($i = 0; $i < $this->getLength(); $i++) { + $num[$i] = $this->get($i); + } + + for ($i = 0; $i < $e->getLength(); $i++) { + $num[$i] ^= QRMath::gexp(QRMath::glog($e->get($i)) + $ratio); + } + + $newPolynomial = new QRPolynomial($num); + return $newPolynomial->mod($e); + } +} + +//--------------------------------------------------------------- +// Mode +//--------------------------------------------------------------- + +define("QR_MODE_NUMBER", 1 << 0); +define("QR_MODE_ALPHA_NUM", 1 << 1); +define("QR_MODE_8BIT_BYTE", 1 << 2); +define("QR_MODE_KANJI", 1 << 3); + +//--------------------------------------------------------------- +// MaskPattern +//--------------------------------------------------------------- + +define("QR_MASK_PATTERN000", 0); +define("QR_MASK_PATTERN001", 1); +define("QR_MASK_PATTERN010", 2); +define("QR_MASK_PATTERN011", 3); +define("QR_MASK_PATTERN100", 4); +define("QR_MASK_PATTERN101", 5); +define("QR_MASK_PATTERN110", 6); +define("QR_MASK_PATTERN111", 7); + +//--------------------------------------------------------------- +// ErrorCorrectLevel + +// 7%. +define("QR_ERROR_CORRECT_LEVEL_L", 1); +// 15%. +define("QR_ERROR_CORRECT_LEVEL_M", 0); +// 25%. +define("QR_ERROR_CORRECT_LEVEL_Q", 3); +// 30%. +define("QR_ERROR_CORRECT_LEVEL_H", 2); + + +//--------------------------------------------------------------- +// QRBitBuffer +//--------------------------------------------------------------- + +class QRBitBuffer +{ + + var $buffer; + var $length; + + function __construct() + { + $this->buffer = array(); + $this->length = 0; + } + + function getBuffer() + { + return $this->buffer; + } + + function getLengthInBits() + { + return $this->length; + } + + function __toString() + { + $buffer = ""; + for ($i = 0; $i < $this->getLengthInBits(); $i++) { + $buffer .= $this->get($i) ? '1' : '0'; + } + return $buffer; + } + + function get($index) + { + $bufIndex = (int)floor($index / 8); + return (($this->buffer[$bufIndex] >> (7 - $index % 8)) & 1) == 1; + } + + function put($num, $length) + { + + for ($i = 0; $i < $length; $i++) { + $this->putBit((($num >> ($length - $i - 1)) & 1) == 1); + } + } + + function putBit($bit) + { + + $bufIndex = (int)floor($this->length / 8); + if (count($this->buffer) <= $bufIndex) { + $this->buffer[] = 0; + } + + if ($bit) { + $this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8)); + } + + $this->length++; + } +} + diff --git a/addons/epay/library/RedirectResponse.php b/addons/epay/library/RedirectResponse.php new file mode 100644 index 0000000..e5df8c9 --- /dev/null +++ b/addons/epay/library/RedirectResponse.php @@ -0,0 +1,59 @@ +<?php + +namespace addons\epay\library; + +class RedirectResponse extends \Symfony\Component\HttpFoundation\RedirectResponse implements \JsonSerializable, \Serializable +{ + public function __toString() + { + return $this->getContent(); + } + + public function setTargetUrl($url) + { + if ('' === ($url ?? '')) { + throw new \InvalidArgumentException('无法跳转到空页面'); + } + + $this->targetUrl = $url; + + $this->setContent( + sprintf('<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8" /> + <meta http-equiv="refresh" content="0;url=\'%1$s\'" /> + + <title>正在跳转支付 %1$s</title> + </head> + <body> + <div id="redirect" style="display:none;">正在跳转支付 <a href="%1$s">%1$s</a></div> + <script type="text/javascript"> + setTimeout(function(){ + location.href="%1$s"; + document.getElementById("redirect").style.display = "block"; + }, 1000); + </script> + </body> +</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8'))); + + $this->headers->set('Location', $url); + + return $this; + } + + public function jsonSerialize() + { + return $this->getContent(); + } + + public function serialize() + { + return serialize($this->content); + } + + public function unserialize($serialized) + { + return $this->content = unserialize($serialized); + } +} diff --git a/addons/epay/library/Response.php b/addons/epay/library/Response.php new file mode 100644 index 0000000..f1e926b --- /dev/null +++ b/addons/epay/library/Response.php @@ -0,0 +1,26 @@ +<?php + +namespace addons\epay\library; + +class Response extends \Symfony\Component\HttpFoundation\Response implements \JsonSerializable, \Serializable +{ + public function __toString() + { + return $this->getContent(); + } + + public function jsonSerialize() + { + return $this->getContent(); + } + + public function serialize() + { + return serialize($this->content); + } + + public function unserialize($serialized) + { + return $this->content = unserialize($serialized); + } +} diff --git a/addons/epay/library/Service.php b/addons/epay/library/Service.php new file mode 100644 index 0000000..5715769 --- /dev/null +++ b/addons/epay/library/Service.php @@ -0,0 +1,301 @@ +<?php + +namespace addons\epay\library; + +use addons\third\model\Third; +use app\common\library\Auth; +use Exception; +use think\Session; +use Yansongda\Pay\Pay; +use Yansongda\Supports\Str; + +/** + * 订单服务类 + * + * @package addons\epay\library + */ +class Service +{ + + /** + * 提交订单 + * @param array|float $amount 订单金额 + * @param string $orderid 订单号 + * @param string $type 支付类型,可选alipay或wechat + * @param string $title 订单标题 + * @param string $notifyurl 通知回调URL + * @param string $returnurl 跳转返回URL + * @param string $method 支付方法 + * @return Response|RedirectResponse|Collection + * @throws Exception + */ + public static function submitOrder($amount, $orderid = null, $type = null, $title = null, $notifyurl = null, $returnurl = null, $method = null, $openid = '') + { + if (!is_array($amount)) { + $params = [ + 'amount' => $amount, + 'orderid' => $orderid, + 'type' => $type, + 'title' => $title, + 'notifyurl' => $notifyurl, + 'returnurl' => $returnurl, + 'method' => $method, + 'openid' => $openid, + ]; + } else { + $params = $amount; + } + $type = isset($params['type']) && in_array($params['type'], ['alipay', 'wechat']) ? $params['type'] : 'wechat'; + $method = isset($params['method']) ? $params['method'] : 'web'; + $orderid = isset($params['orderid']) ? $params['orderid'] : date("YmdHis") . mt_rand(100000, 999999); + $amount = isset($params['amount']) ? $params['amount'] : 1; + $title = isset($params['title']) ? $params['title'] : "支付"; + $auth_code = isset($params['auth_code']) ? $params['auth_code'] : ''; + $openid = isset($params['openid']) ? $params['openid'] : ''; + + $request = request(); + $notifyurl = isset($params['notifyurl']) ? $params['notifyurl'] : $request->root(true) . '/addons/epay/index/' . $type . 'notify'; + $returnurl = isset($params['returnurl']) ? $params['returnurl'] : $request->root(true) . '/addons/epay/index/' . $type . 'return/out_trade_no/' . $orderid; + $html = ''; + $config = Service::getConfig($type); + $config['notify_url'] = $notifyurl; + $config['return_url'] = $returnurl; + $isWechat = strpos($request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false; + + $result = null; + if ($type == 'alipay') { + //如果是PC支付,判断当前环境,进行跳转 + if ($method == 'web') { + //如果是微信环境或后台配置PC使用扫码支付 + if ($isWechat || $config['scanpay']) { + Session::set("alipayorderdata", $params); + $url = addon_url('epay/api/alipay', [], true, true); + return RedirectResponse::create($url); + } elseif ($request->isMobile()) { + $method = 'wap'; + } + } + //创建支付对象 + $pay = Pay::alipay($config); + $params = [ + 'out_trade_no' => $orderid,//你的订单号 + 'total_amount' => $amount,//单位元 + 'subject' => $title, + ]; + + switch ($method) { + case 'web': + //电脑支付 + $result = $pay->web($params); + break; + case 'wap': + //手机网页支付 + $result = $pay->wap($params); + break; + case 'app': + //APP支付 + $result = $pay->app($params); + break; + case 'scan': + //扫码支付 + $result = $pay->scan($params); + break; + case 'pos': + //刷卡支付必须要有auth_code + $params['auth_code'] = $auth_code; + $result = $pay->pos($params); + break; + default: + } + } else { + //如果是PC支付,判断当前环境,进行跳转 + if ($method == 'web') { + //如果是移动端,但不是微信环境 + if ($request->isMobile() && !$isWechat) { + $method = 'wap'; + } else { + Session::set("wechatorderdata", $params); + $url = addon_url('epay/api/wechat', [], true, true); + return RedirectResponse::create($url); + } + } + + //创建支付对象 + $pay = Pay::wechat($config); + $params = [ + 'out_trade_no' => $orderid,//你的订单号 + 'body' => $title, + 'total_fee' => $amount * 100, //单位分 + ]; + switch ($method) { + //case 'web': + // //电脑支付,跳转到自定义展示页面(FastAdmin独有) + // $result = $pay->web($params); + // break; + case 'mp': + //公众号支付 + //公众号支付必须有openid + $params['openid'] = $openid; + $result = $pay->mp($params); + break; + case 'wap': + //手机网页支付,跳转 + $params['spbill_create_ip'] = $request->ip(0, false); + $result = $pay->wap($params); + break; + case 'app': + //APP支付,直接返回字符串 + $result = $pay->app($params); + break; + case 'scan': + //扫码支付,直接返回字符串 + $result = $pay->scan($params); + break; + case 'pos': + //刷卡支付,直接返回字符串 + //刷卡支付必须要有auth_code + $params['auth_code'] = $auth_code; + $result = $pay->pos($params); + break; + case 'miniapp': + //小程序支付,直接返回字符串 + //小程序支付必须要有openid + $params['openid'] = $openid; + $result = $pay->miniapp($params); + break; + default: + } + } + + //使用重写的Response类、RedirectResponse、Collection类 + if ($result instanceof \Symfony\Component\HttpFoundation\RedirectResponse) { + $result = RedirectResponse::create($result->getTargetUrl()); + } elseif ($result instanceof \Symfony\Component\HttpFoundation\Response) { + $result = Response::create($result->getContent()); + } elseif ($result instanceof \Yansongda\Supports\Collection) { + $result = Collection::make($result->all()); + } + + return $result; + } + + /** + * 验证回调是否成功 + * @param string $type 支付类型 + * @param array $config 配置信息 + * @return bool|\Yansongda\Pay\Gateways\Alipay|\Yansongda\Pay\Gateways\Wechat + */ + public static function checkNotify($type, $config = []) + { + $type = strtolower($type); + if (!in_array($type, ['wechat', 'alipay'])) { + return false; + } + try { + $config = self::getConfig($type); + $pay = $type == 'wechat' ? Pay::wechat($config) : Pay::alipay($config); + $data = $pay->verify(); + + if ($type == 'alipay') { + if (in_array($data['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED'])) { + return $pay; + } + } else { + return $pay; + } + } catch (Exception $e) { + return false; + } + + return false; + } + + /** + * 验证返回是否成功,请勿用于判断是否支付成功的逻辑验证 + * 已弃用 + * + * @param string $type 支付类型 + * @param array $config 配置信息 + * @return bool + * @deprecated 已弃用,请勿用于逻辑验证 + */ + public static function checkReturn($type, $config = []) + { + //由于PC及移动端无法获取请求的参数信息,取消return验证,均返回true + return true; + } + + /** + * 获取配置 + * @param string $type 支付类型 + * @return array|mixed + */ + public static function getConfig($type = 'wechat') + { + $config = get_addon_config('epay'); + $config = isset($config[$type]) ? $config[$type] : $config['wechat']; + if ($config['log']) { + $config['log'] = [ + 'file' => LOG_PATH . 'epaylogs' . DS . $type . '-' . date("Y-m-d") . '.log', + 'level' => 'debug' + ]; + } + if (isset($config['cert_client']) && substr($config['cert_client'], 0, 8) == '/addons/') { + $config['cert_client'] = ROOT_PATH . str_replace('/', DS, substr($config['cert_client'], 1)); + } + if (isset($config['cert_key']) && substr($config['cert_key'], 0, 8) == '/addons/') { + $config['cert_key'] = ROOT_PATH . str_replace('/', DS, substr($config['cert_key'], 1)); + } + if (isset($config['app_cert_public_key']) && substr($config['app_cert_public_key'], 0, 8) == '/addons/') { + $config['app_cert_public_key'] = ROOT_PATH . str_replace('/', DS, substr($config['app_cert_public_key'], 1)); + } + if (isset($config['alipay_root_cert']) && substr($config['alipay_root_cert'], 0, 8) == '/addons/') { + $config['alipay_root_cert'] = ROOT_PATH . str_replace('/', DS, substr($config['alipay_root_cert'], 1)); + } + if (isset($config['ali_public_key']) && (Str::endsWith($config['ali_public_key'], '.crt') || Str::endsWith($config['ali_public_key'], '.pem'))) { + $config['ali_public_key'] = ROOT_PATH . str_replace('/', DS, substr($config['ali_public_key'], 1)); + } + // 可选 + $config['http'] = [ + 'timeout' => 10, + 'connect_timeout' => 10, + // 更多配置项请参考 [Guzzle](https://guzzle-cn.readthedocs.io/zh_CN/latest/request-options.html) + ]; + + $config['notify_url'] = empty($config['notify_url']) ? addon_url('epay/api/notifyx', [], false) . '/type/' . $type : $config['notify_url']; + $config['notify_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['notify_url']) ? request()->root(true) . $config['notify_url'] : $config['notify_url']; + $config['return_url'] = empty($config['return_url']) ? addon_url('epay/api/returnx', [], false) . '/type/' . $type : $config['return_url']; + $config['return_url'] = !preg_match("/^(http:\/\/|https:\/\/)/i", $config['return_url']) ? request()->root(true) . $config['return_url'] : $config['return_url']; + return $config; + } + + /** + * 获取微信Openid + * + * @return mixed|string + */ + public static function getOpenid() + { + $config = self::getConfig('wechat'); + $openid = ''; + $auth = Auth::instance(); + if ($auth->isLogin()) { + $third = get_addon_info('third'); + if ($third && $third['state']) { + $thirdInfo = Third::where('user_id', $auth->id)->where('platform', 'wechat')->where('apptype', 'mp')->find(); + $openid = $thirdInfo ? $thirdInfo['openid'] : ''; + } + } + if (!$openid) { + $openid = Session::get("openid"); + + //如果未传openid,则去读取openid + if (!$openid) { + $wechat = new Wechat($config['app_id'], $config['app_secret']); + $openid = $wechat->getOpenid(); + } + } + return $openid; + } + +} diff --git a/addons/epay/library/Wechat.php b/addons/epay/library/Wechat.php new file mode 100644 index 0000000..1ecd037 --- /dev/null +++ b/addons/epay/library/Wechat.php @@ -0,0 +1,107 @@ +<?php + +namespace addons\epay\library; + +use fast\Http; +use think\Cache; +use think\Session; + +/** + * 微信授权 + * + */ +class Wechat +{ + private $app_id = ''; + private $app_secret = ''; + private $scope = 'snsapi_userinfo'; + + public function __construct($app_id, $app_secret) + { + $this->app_id = $app_id; + $this->app_secret = $app_secret; + } + + /** + * 获取微信授权链接 + * + * @return string + */ + public function getAuthorizeUrl() + { + $redirect_uri = addon_url('epay/api/wechat', [], true, true); + $redirect_uri = urlencode($redirect_uri); + $state = \fast\Random::alnum(); + Session::set('state', $state); + return "https://open.weixin.qq.com/connect/oauth2/authorize?appid={$this->app_id}&redirect_uri={$redirect_uri}&response_type=code&scope={$this->scope}&state={$state}#wechat_redirect"; + } + + /** + * 获取微信openid + * + * @return mixed|string + */ + public function getOpenid() + { + $openid = Session::get('openid'); + if (!$openid) { + if (!isset($_GET['code'])) { + $url = $this->getAuthorizeUrl(); + + Header("Location: $url"); + exit(); + } else { + $state = Session::get('state'); + if ($state == $_GET['state']) { + $code = $_GET['code']; + $token = $this->getAccessToken($code); + $openid = isset($token['openid']) ? $token['openid'] : ''; + if ($openid) { + Session::set("openid", $openid); + } + } + } + } + return $openid; + } + + /** + * 获取授权token网页授权 + * + * @param string $code + * @return mixed|string + */ + public function getAccessToken($code = '') + { + $params = [ + 'appid' => $this->app_id, + 'secret' => $this->app_secret, + 'code' => $code, + 'grant_type' => 'authorization_code' + ]; + $ret = Http::sendRequest('https://api.weixin.qq.com/sns/oauth2/access_token', $params, 'GET'); + if ($ret['ret']) { + $ar = json_decode($ret['msg'], true); + return $ar; + } + return []; + } + + public function getJsticket($code = '') + { + $jsticket = Session::get('jsticket'); + if (!$jsticket) { + $token = $this->getAccessToken($code); + $params = [ + 'access_token' => 'token', + 'type' => 'jsapi', + ]; + $ret = Http::sendRequest('https://api.weixin.qq.com/cgi-bin/ticket/getticket', $params, 'GET'); + if ($ret['ret']) { + $ar = json_decode($ret['msg'], true); + return $ar; + } + } + return $jsticket; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Contracts/GatewayApplicationInterface.php b/addons/epay/library/Yansongda/Pay/Contracts/GatewayApplicationInterface.php new file mode 100644 index 0000000..b4e296a --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Contracts/GatewayApplicationInterface.php @@ -0,0 +1,83 @@ +<?php + +namespace Yansongda\Pay\Contracts; + +use Symfony\Component\HttpFoundation\Response; +use Yansongda\Supports\Collection; + +interface GatewayApplicationInterface +{ + /** + * To pay. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $gateway + * @param array $params + * + * @return Collection|Response + */ + public function pay($gateway, $params); + + /** + * Query an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $order + * + * @return Collection + */ + public function find($order, string $type); + + /** + * Refund an order. + * + * @author yansongda <me@yansongda.cn> + * + * @return Collection + */ + public function refund(array $order); + + /** + * Cancel an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $order + * + * @return Collection + */ + public function cancel($order); + + /** + * Close an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $order + * + * @return Collection + */ + public function close($order); + + /** + * Verify a request. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array|null $content + * + * @return Collection + */ + public function verify($content, bool $refund); + + /** + * Echo success to server. + * + * @author yansongda <me@yansongda.cn> + * + * @return Response + */ + public function success(); +} diff --git a/addons/epay/library/Yansongda/Pay/Contracts/GatewayInterface.php b/addons/epay/library/Yansongda/Pay/Contracts/GatewayInterface.php new file mode 100644 index 0000000..8286763 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Contracts/GatewayInterface.php @@ -0,0 +1,20 @@ +<?php + +namespace Yansongda\Pay\Contracts; + +use Symfony\Component\HttpFoundation\Response; +use Yansongda\Supports\Collection; + +interface GatewayInterface +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @return Collection|Response + */ + public function pay($endpoint, array $payload); +} diff --git a/addons/epay/library/Yansongda/Pay/Events.php b/addons/epay/library/Yansongda/Pay/Events.php new file mode 100644 index 0000000..a747305 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events.php @@ -0,0 +1,98 @@ +<?php + +namespace Yansongda\Pay; + +use Exception; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * @author yansongda <me@yansongda.cn> + * + * @method static Event dispatch(Event $event) Dispatches an event to all registered listeners + * @method static array getListeners($eventName = null) Gets the listeners of a specific event or all listeners sorted by descending priority. + * @method static int|void getListenerPriority($eventName, $listener) Gets the listener priority for a specific event. + * @method static bool hasListeners($eventName = null) Checks whether an event has any registered listeners. + * @method static void addListener($eventName, $listener, $priority = 0) Adds an event listener that listens on the specified events. + * @method static removeListener($eventName, $listener) Removes an event listener from the specified events. + * @method static void addSubscriber(EventSubscriberInterface $subscriber) Adds an event subscriber. + * @method static void removeSubscriber(EventSubscriberInterface $subscriber) + */ +class Events +{ + /** + * dispatcher. + * + * @var EventDispatcher + */ + protected static $dispatcher; + + /** + * Forward call. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $args + * + * @throws Exception + * + * @return mixed + */ + public static function __callStatic($method, $args) + { + return call_user_func_array([self::getDispatcher(), $method], $args); + } + + /** + * Forward call. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $args + * + * @throws Exception + * + * @return mixed + */ + public function __call($method, $args) + { + return call_user_func_array([self::getDispatcher(), $method], $args); + } + + /** + * setDispatcher. + * + * @author yansongda <me@yansongda.cn> + */ + public static function setDispatcher(EventDispatcher $dispatcher) + { + self::$dispatcher = $dispatcher; + } + + /** + * getDispatcher. + * + * @author yansongda <me@yansongda.cn> + */ + public static function getDispatcher(): EventDispatcher + { + if (self::$dispatcher) { + return self::$dispatcher; + } + + return self::$dispatcher = self::createDispatcher(); + } + + /** + * createDispatcher. + * + * @author yansongda <me@yansongda.cn> + */ + public static function createDispatcher(): EventDispatcher + { + return new EventDispatcher(); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Events/ApiRequested.php b/addons/epay/library/Yansongda/Pay/Events/ApiRequested.php new file mode 100644 index 0000000..845ac87 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events/ApiRequested.php @@ -0,0 +1,31 @@ +<?php + +namespace Yansongda\Pay\Events; + +class ApiRequested extends Event +{ + /** + * Endpoint. + * + * @var string + */ + public $endpoint; + + /** + * Result. + * + * @var array + */ + public $result; + + /** + * Bootstrap. + */ + public function __construct(string $driver, string $gateway, string $endpoint, array $result) + { + $this->endpoint = $endpoint; + $this->result = $result; + + parent::__construct($driver, $gateway); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Events/ApiRequesting.php b/addons/epay/library/Yansongda/Pay/Events/ApiRequesting.php new file mode 100644 index 0000000..14c9fdd --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events/ApiRequesting.php @@ -0,0 +1,31 @@ +<?php + +namespace Yansongda\Pay\Events; + +class ApiRequesting extends Event +{ + /** + * Endpoint. + * + * @var string + */ + public $endpoint; + + /** + * Payload. + * + * @var array + */ + public $payload; + + /** + * Bootstrap. + */ + public function __construct(string $driver, string $gateway, string $endpoint, array $payload) + { + $this->endpoint = $endpoint; + $this->payload = $payload; + + parent::__construct($driver, $gateway); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Events/Event.php b/addons/epay/library/Yansongda/Pay/Events/Event.php new file mode 100644 index 0000000..1ef883c --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events/Event.php @@ -0,0 +1,40 @@ +<?php + +namespace Yansongda\Pay\Events; + +use Symfony\Contracts\EventDispatcher\Event as SymfonyEvent; + +class Event extends SymfonyEvent +{ + /** + * Driver. + * + * @var string + */ + public $driver; + + /** + * Method. + * + * @var string + */ + public $gateway; + + /** + * Extra attributes. + * + * @var mixed + */ + public $attributes; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + */ + public function __construct(string $driver, string $gateway) + { + $this->driver = $driver; + $this->gateway = $gateway; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Events/MethodCalled.php b/addons/epay/library/Yansongda/Pay/Events/MethodCalled.php new file mode 100644 index 0000000..d199f23 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events/MethodCalled.php @@ -0,0 +1,33 @@ +<?php + +namespace Yansongda\Pay\Events; + +class MethodCalled extends Event +{ + /** + * endpoint. + * + * @var string + */ + public $endpoint; + + /** + * payload. + * + * @var array + */ + public $payload; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + */ + public function __construct(string $driver, string $gateway, string $endpoint, array $payload = []) + { + $this->endpoint = $endpoint; + $this->payload = $payload; + + parent::__construct($driver, $gateway); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Events/PayStarted.php b/addons/epay/library/Yansongda/Pay/Events/PayStarted.php new file mode 100644 index 0000000..845d93c --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events/PayStarted.php @@ -0,0 +1,31 @@ +<?php + +namespace Yansongda\Pay\Events; + +class PayStarted extends Event +{ + /** + * Endpoint. + * + * @var string + */ + public $endpoint; + + /** + * Payload. + * + * @var array + */ + public $payload; + + /** + * Bootstrap. + */ + public function __construct(string $driver, string $gateway, string $endpoint, array $payload) + { + $this->endpoint = $endpoint; + $this->payload = $payload; + + parent::__construct($driver, $gateway); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Events/PayStarting.php b/addons/epay/library/Yansongda/Pay/Events/PayStarting.php new file mode 100644 index 0000000..ad73d9d --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events/PayStarting.php @@ -0,0 +1,23 @@ +<?php + +namespace Yansongda\Pay\Events; + +class PayStarting extends Event +{ + /** + * Params. + * + * @var array + */ + public $params; + + /** + * Bootstrap. + */ + public function __construct(string $driver, string $gateway, array $params) + { + $this->params = $params; + + parent::__construct($driver, $gateway); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Events/RequestReceived.php b/addons/epay/library/Yansongda/Pay/Events/RequestReceived.php new file mode 100644 index 0000000..3620b12 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events/RequestReceived.php @@ -0,0 +1,25 @@ +<?php + +namespace Yansongda\Pay\Events; + +class RequestReceived extends Event +{ + /** + * Received data. + * + * @var array + */ + public $data; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + */ + public function __construct(string $driver, string $gateway, array $data) + { + $this->data = $data; + + parent::__construct($driver, $gateway); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Events/SignFailed.php b/addons/epay/library/Yansongda/Pay/Events/SignFailed.php new file mode 100644 index 0000000..4e45932 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Events/SignFailed.php @@ -0,0 +1,25 @@ +<?php + +namespace Yansongda\Pay\Events; + +class SignFailed extends Event +{ + /** + * Received data. + * + * @var array + */ + public $data; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + */ + public function __construct(string $driver, string $gateway, array $data) + { + $this->data = $data; + + parent::__construct($driver, $gateway); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Exceptions/BusinessException.php b/addons/epay/library/Yansongda/Pay/Exceptions/BusinessException.php new file mode 100644 index 0000000..8e94a4f --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Exceptions/BusinessException.php @@ -0,0 +1,19 @@ +<?php + +namespace Yansongda\Pay\Exceptions; + +class BusinessException extends GatewayException +{ + /** + * Bootstrap. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $message + * @param array|string $raw + */ + public function __construct($message, $raw = []) + { + parent::__construct('ERROR_BUSINESS: '.$message, $raw, self::ERROR_BUSINESS); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Exceptions/Exception.php b/addons/epay/library/Yansongda/Pay/Exceptions/Exception.php new file mode 100644 index 0000000..9d007ad --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Exceptions/Exception.php @@ -0,0 +1,44 @@ +<?php + +namespace Yansongda\Pay\Exceptions; + +class Exception extends \Exception +{ + const UNKNOWN_ERROR = 9999; + + const INVALID_GATEWAY = 1; + + const INVALID_CONFIG = 2; + + const INVALID_ARGUMENT = 3; + + const ERROR_GATEWAY = 4; + + const INVALID_SIGN = 5; + + const ERROR_BUSINESS = 6; + + /** + * Raw error info. + * + * @var array + */ + public $raw; + + /** + * Bootstrap. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $message + * @param array|string $raw + * @param int|string $code + */ + public function __construct($message = '', $raw = [], $code = self::UNKNOWN_ERROR) + { + $message = '' === $message ? 'Unknown Error' : $message; + $this->raw = is_array($raw) ? $raw : [$raw]; + + parent::__construct($message, intval($code)); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Exceptions/GatewayException.php b/addons/epay/library/Yansongda/Pay/Exceptions/GatewayException.php new file mode 100644 index 0000000..e844801 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Exceptions/GatewayException.php @@ -0,0 +1,20 @@ +<?php + +namespace Yansongda\Pay\Exceptions; + +class GatewayException extends Exception +{ + /** + * Bootstrap. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $message + * @param array|string $raw + * @param int $code + */ + public function __construct($message, $raw = [], $code = self::ERROR_GATEWAY) + { + parent::__construct('ERROR_GATEWAY: '.$message, $raw, $code); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Exceptions/InvalidArgumentException.php b/addons/epay/library/Yansongda/Pay/Exceptions/InvalidArgumentException.php new file mode 100644 index 0000000..2ecb16b --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Exceptions/InvalidArgumentException.php @@ -0,0 +1,19 @@ +<?php + +namespace Yansongda\Pay\Exceptions; + +class InvalidArgumentException extends Exception +{ + /** + * Bootstrap. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $message + * @param array|string $raw + */ + public function __construct($message, $raw = []) + { + parent::__construct('INVALID_ARGUMENT: '.$message, $raw, self::INVALID_ARGUMENT); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Exceptions/InvalidConfigException.php b/addons/epay/library/Yansongda/Pay/Exceptions/InvalidConfigException.php new file mode 100644 index 0000000..5719310 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Exceptions/InvalidConfigException.php @@ -0,0 +1,19 @@ +<?php + +namespace Yansongda\Pay\Exceptions; + +class InvalidConfigException extends Exception +{ + /** + * Bootstrap. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $message + * @param array|string $raw + */ + public function __construct($message, $raw = []) + { + parent::__construct('INVALID_CONFIG: '.$message, $raw, self::INVALID_CONFIG); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Exceptions/InvalidGatewayException.php b/addons/epay/library/Yansongda/Pay/Exceptions/InvalidGatewayException.php new file mode 100644 index 0000000..3f4067d --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Exceptions/InvalidGatewayException.php @@ -0,0 +1,19 @@ +<?php + +namespace Yansongda\Pay\Exceptions; + +class InvalidGatewayException extends Exception +{ + /** + * Bootstrap. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $message + * @param array|string $raw + */ + public function __construct($message, $raw = []) + { + parent::__construct('INVALID_GATEWAY: '.$message, $raw, self::INVALID_GATEWAY); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Exceptions/InvalidSignException.php b/addons/epay/library/Yansongda/Pay/Exceptions/InvalidSignException.php new file mode 100644 index 0000000..135c8a5 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Exceptions/InvalidSignException.php @@ -0,0 +1,19 @@ +<?php + +namespace Yansongda\Pay\Exceptions; + +class InvalidSignException extends Exception +{ + /** + * Bootstrap. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $message + * @param array|string $raw + */ + public function __construct($message, $raw = []) + { + parent::__construct('INVALID_SIGN: '.$message, $raw, self::INVALID_SIGN); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay.php new file mode 100644 index 0000000..aa94ca4 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay.php @@ -0,0 +1,422 @@ +<?php + +namespace Yansongda\Pay\Gateways; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Yansongda\Pay\Contracts\GatewayApplicationInterface; +use Yansongda\Pay\Contracts\GatewayInterface; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidConfigException; +use Yansongda\Pay\Exceptions\InvalidGatewayException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Alipay\Support; +use Yansongda\Supports\Collection; +use Yansongda\Supports\Config; +use Yansongda\Supports\Str; + +/** + * @method Response app(array $config) APP 支付 + * @method Collection pos(array $config) 刷卡支付 + * @method Collection scan(array $config) 扫码支付 + * @method Collection transfer(array $config) 帐户转账 + * @method Response wap(array $config) 手机网站支付 + * @method Response web(array $config) 电脑支付 + * @method Collection mini(array $config) 小程序支付 + */ +class Alipay implements GatewayApplicationInterface +{ + /** + * Const mode_normal. + */ + const MODE_NORMAL = 'normal'; + + /** + * Const mode_dev. + */ + const MODE_DEV = 'dev'; + + /** + * Const mode_service. + */ + const MODE_SERVICE = 'service'; + + /** + * Const url. + */ + const URL = [ + self::MODE_NORMAL => 'https://openapi.alipay.com/gateway.do?charset=utf-8', + self::MODE_DEV => 'https://openapi.alipaydev.com/gateway.do?charset=utf-8', + ]; + + /** + * Alipay payload. + * + * @var array + */ + protected $payload; + + /** + * Alipay gateway. + * + * @var string + */ + protected $gateway; + + /** + * extends. + * + * @var array + */ + protected $extends; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + * + * @throws \Exception + */ + public function __construct(Config $config) + { + $this->gateway = Support::create($config)->getBaseUri(); + $this->payload = [ + 'app_id' => $config->get('app_id'), + 'method' => '', + 'format' => 'JSON', + 'charset' => 'utf-8', + 'sign_type' => 'RSA2', + 'version' => '1.0', + 'return_url' => $config->get('return_url'), + 'notify_url' => $config->get('notify_url'), + 'timestamp' => date('Y-m-d H:i:s'), + 'sign' => '', + 'biz_content' => '', + 'app_auth_token' => $config->get('app_auth_token'), + ]; + + if ($config->get('app_cert_public_key') && $config->get('alipay_root_cert')) { + $this->payload['app_cert_sn'] = Support::getCertSN($config->get('app_cert_public_key')); + $this->payload['alipay_root_cert_sn'] = Support::getRootCertSN($config->get('alipay_root_cert')); + } + } + + /** + * Magic pay. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $params + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws InvalidGatewayException + * @throws InvalidSignException + * + * @return Response|Collection + */ + public function __call($method, $params) + { + if (isset($this->extends[$method])) { + return $this->makeExtend($method, ...$params); + } + + return $this->pay($method, ...$params); + } + + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $gateway + * @param array $params + * + * @throws InvalidGatewayException + * + * @return Response|Collection + */ + public function pay($gateway, $params = []) + { + Events::dispatch(new Events\PayStarting('Alipay', $gateway, $params)); + + $this->payload['return_url'] = $params['return_url'] ?? $this->payload['return_url']; + $this->payload['notify_url'] = $params['notify_url'] ?? $this->payload['notify_url']; + + unset($params['return_url'], $params['notify_url']); + + $this->payload['biz_content'] = json_encode($params); + + $gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway'; + + if (class_exists($gateway)) { + return $this->makePay($gateway); + } + + throw new InvalidGatewayException("Pay Gateway [{$gateway}] not exists"); + } + + /** + * Verify sign. + * + * @author yansongda <me@yansongda.cn> + * + * @param array|null $data + * + * @throws InvalidSignException + * @throws InvalidConfigException + */ + public function verify($data = null, bool $refund = false): Collection + { + if (is_null($data)) { + $request = Request::createFromGlobals(); + + $data = $request->request->count() > 0 ? $request->request->all() : $request->query->all(); + } + + if (isset($data['fund_bill_list'])) { + $data['fund_bill_list'] = htmlspecialchars_decode($data['fund_bill_list']); + } + + Events::dispatch(new Events\RequestReceived('Alipay', '', $data)); + + if (Support::verifySign($data)) { + return new Collection($data); + } + + Events::dispatch(new Events\SignFailed('Alipay', '', $data)); + + throw new InvalidSignException('Alipay Sign Verify FAILED', $data); + } + + /** + * Query an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $order + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public function find($order, string $type = 'wap'): Collection + { + $gateway = get_class($this).'\\'.Str::studly($type).'Gateway'; + + if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) { + throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method"); + } + + $config = call_user_func([new $gateway(), 'find'], $order); + + $this->payload['method'] = $config['method']; + $this->payload['biz_content'] = $config['biz_content']; + $this->payload['sign'] = Support::generateSign($this->payload); + + Events::dispatch(new Events\MethodCalled('Alipay', 'Find', $this->gateway, $this->payload)); + + return Support::requestApi($this->payload); + } + + /** + * Refund an order. + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public function refund(array $order): Collection + { + $this->payload['method'] = 'alipay.trade.refund'; + $this->payload['biz_content'] = json_encode($order); + $this->payload['sign'] = Support::generateSign($this->payload); + + Events::dispatch(new Events\MethodCalled('Alipay', 'Refund', $this->gateway, $this->payload)); + + return Support::requestApi($this->payload); + } + + /** + * Cancel an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param array|string $order + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public function cancel($order): Collection + { + $this->payload['method'] = 'alipay.trade.cancel'; + $this->payload['biz_content'] = json_encode(is_array($order) ? $order : ['out_trade_no' => $order]); + $this->payload['sign'] = Support::generateSign($this->payload); + + Events::dispatch(new Events\MethodCalled('Alipay', 'Cancel', $this->gateway, $this->payload)); + + return Support::requestApi($this->payload); + } + + /** + * Close an order. + * + * @param string|array $order + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public function close($order): Collection + { + $this->payload['method'] = 'alipay.trade.close'; + $this->payload['biz_content'] = json_encode(is_array($order) ? $order : ['out_trade_no' => $order]); + $this->payload['sign'] = Support::generateSign($this->payload); + + Events::dispatch(new Events\MethodCalled('Alipay', 'Close', $this->gateway, $this->payload)); + + return Support::requestApi($this->payload); + } + + /** + * Download bill. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $bill + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public function download($bill): string + { + $this->payload['method'] = 'alipay.data.dataservice.bill.downloadurl.query'; + $this->payload['biz_content'] = json_encode(is_array($bill) ? $bill : ['bill_type' => 'trade', 'bill_date' => $bill]); + $this->payload['sign'] = Support::generateSign($this->payload); + + Events::dispatch(new Events\MethodCalled('Alipay', 'Download', $this->gateway, $this->payload)); + + $result = Support::requestApi($this->payload); + + return ($result instanceof Collection) ? $result->get('bill_download_url') : ''; + } + + /** + * Reply success to alipay. + * + * @author yansongda <me@yansongda.cn> + */ + public function success(): Response + { + Events::dispatch(new Events\MethodCalled('Alipay', 'Success', $this->gateway)); + + return new Response('success'); + } + + /** + * extend. + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + * @throws InvalidArgumentException + */ + public function extend(string $method, callable $function, bool $now = true): ?Collection + { + if (!$now && !method_exists($this, $method)) { + $this->extends[$method] = $function; + + return null; + } + + $customize = $function($this->payload); + + if (!is_array($customize) && !($customize instanceof Collection)) { + throw new InvalidArgumentException('Return Type Must Be Array Or Collection'); + } + + Events::dispatch(new Events\MethodCalled('Alipay', 'extend', $this->gateway, $customize)); + + if (is_array($customize)) { + $this->payload = $customize; + $this->payload['sign'] = Support::generateSign($this->payload); + + return Support::requestApi($this->payload); + } + + return $customize; + } + + /** + * Make pay gateway. + * + * @author yansongda <me@yansongda.cn> + * + * @throws InvalidGatewayException + * + * @return Response|Collection + */ + protected function makePay(string $gateway) + { + $app = new $gateway(); + + if ($app instanceof GatewayInterface) { + return $app->pay($this->gateway, array_filter($this->payload, function ($value) { + return '' !== $value && !is_null($value); + })); + } + + throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface"); + } + + /** + * makeExtend. + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + protected function makeExtend(string $method, array ...$params): Collection + { + $params = count($params) >= 1 ? $params[0] : $params; + + $function = $this->extends[$method]; + + $customize = $function($this->payload, $params); + + if (!is_array($customize) && !($customize instanceof Collection)) { + throw new InvalidArgumentException('Return Type Must Be Array Or Collection'); + } + + Events::dispatch(new Events\MethodCalled( + 'Alipay', + 'extend - '.$method, + $this->gateway, + is_array($customize) ? $customize : $customize->toArray() + )); + + if (is_array($customize)) { + $this->payload = $customize; + $this->payload['sign'] = Support::generateSign($this->payload); + + return Support::requestApi($this->payload); + } + + return $customize; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/AppGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/AppGateway.php new file mode 100644 index 0000000..860b44b --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/AppGateway.php @@ -0,0 +1,38 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +use Symfony\Component\HttpFoundation\Response; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidConfigException; +use Yansongda\Pay\Gateways\Alipay; + +class AppGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws InvalidConfigException + * @throws InvalidArgumentException + */ + public function pay($endpoint, array $payload): Response + { + $payload['method'] = 'alipay.trade.app.pay'; + + $biz_array = json_decode($payload['biz_content'], true); + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) { + $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid]; + } + $payload['biz_content'] = json_encode(array_merge($biz_array, ['product_code' => 'QUICK_MSECURITY_PAY'])); + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Alipay', 'App', $endpoint, $payload)); + + return new Response(http_build_query($payload)); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/Gateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/Gateway.php new file mode 100644 index 0000000..5f57cc3 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/Gateway.php @@ -0,0 +1,40 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +use Yansongda\Pay\Contracts\GatewayInterface; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Supports\Collection; + +abstract class Gateway implements GatewayInterface +{ + /** + * Mode. + * + * @var string + */ + protected $mode; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + * + * @throws InvalidArgumentException + */ + public function __construct() + { + $this->mode = Support::getInstance()->mode; + } + + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @return Collection + */ + abstract public function pay($endpoint, array $payload); +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/MiniGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/MiniGateway.php new file mode 100644 index 0000000..0198194 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/MiniGateway.php @@ -0,0 +1,46 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidConfigException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Alipay; +use Yansongda\Supports\Collection; + +class MiniGateway extends Gateway +{ + /** + * Pay an order. + * + * @author xiaozan <i@xiaozan.me> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws InvalidSignException + * + * @see https://docs.alipay.com/mini/introduce/pay + */ + public function pay($endpoint, array $payload): Collection + { + $biz_array = json_decode($payload['biz_content'], true); + if (empty($biz_array['buyer_id'])) { + throw new InvalidArgumentException('buyer_id required'); + } + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) { + $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid]; + } + $payload['biz_content'] = json_encode($biz_array); + $payload['method'] = 'alipay.trade.create'; + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Alipay', 'Mini', $endpoint, $payload)); + + return Support::requestApi($payload); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/PosGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/PosGateway.php new file mode 100644 index 0000000..de04761 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/PosGateway.php @@ -0,0 +1,47 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidConfigException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Alipay; +use Yansongda\Supports\Collection; + +class PosGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws InvalidArgumentException + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + $payload['method'] = 'alipay.trade.pay'; + $biz_array = json_decode($payload['biz_content'], true); + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) { + $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid]; + } + $payload['biz_content'] = json_encode(array_merge( + $biz_array, + [ + 'product_code' => 'FACE_TO_FACE_PAYMENT', + 'scene' => 'bar_code', + ] + )); + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Alipay', 'Pos', $endpoint, $payload)); + + return Support::requestApi($payload); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/RefundGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/RefundGateway.php new file mode 100644 index 0000000..abbb71f --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/RefundGateway.php @@ -0,0 +1,21 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +class RefundGateway +{ + /** + * Find. + * + * @author yansongda <me@yansongda.cn> + * + * @param $order + */ + public function find($order): array + { + return [ + 'method' => 'alipay.trade.fastpay.refund.query', + 'biz_content' => json_encode(is_array($order) ? $order : ['out_trade_no' => $order]), + ]; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/ScanGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/ScanGateway.php new file mode 100644 index 0000000..9d6cbc3 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/ScanGateway.php @@ -0,0 +1,41 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidConfigException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Alipay; +use Yansongda\Supports\Collection; + +class ScanGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + $payload['method'] = 'alipay.trade.precreate'; + $biz_array = json_decode($payload['biz_content'], true); + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) { + $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid]; + } + $payload['biz_content'] = json_encode(array_merge($biz_array, ['product_code' => ''])); + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Alipay', 'Scan', $endpoint, $payload)); + + return Support::requestApi($payload); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/Support.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/Support.php new file mode 100644 index 0000000..289bb94 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/Support.php @@ -0,0 +1,452 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +use Exception; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidConfigException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Alipay; +use Yansongda\Pay\Log; +use Yansongda\Supports\Arr; +use Yansongda\Supports\Collection; +use Yansongda\Supports\Config; +use Yansongda\Supports\Str; +use Yansongda\Supports\Traits\HasHttpRequest; + +/** + * @author yansongda <me@yansongda.cn> + * + * @property string app_id alipay app_id + * @property string ali_public_key + * @property string private_key + * @property array http http options + * @property string mode current mode + * @property array log log options + * @property string pid ali pid + */ +class Support +{ + use HasHttpRequest; + + /** + * Alipay gateway. + * + * @var string + */ + protected $baseUri; + + /** + * Config. + * + * @var Config + */ + protected $config; + + /** + * Instance. + * + * @var Support + */ + private static $instance; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + */ + private function __construct(Config $config) + { + $this->baseUri = Alipay::URL[$config->get('mode', Alipay::MODE_NORMAL)]; + $this->config = $config; + + $this->setHttpOptions(); + } + + /** + * __get. + * + * @author yansongda <me@yansongda.cn> + * + * @param $key + * + * @return mixed|Config|null + */ + public function __get($key) + { + return $this->getConfig($key); + } + + /** + * create. + * + * @author yansongda <me@yansongda.cn> + * + * @return Support + */ + public static function create(Config $config) + { + if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) { + self::$instance = new self($config); + } + + return self::$instance; + } + + /** + * getInstance. + * + * @author yansongda <me@yansongda.cn> + * + * @throws InvalidArgumentException + * + * @return Support + */ + public static function getInstance() + { + if (is_null(self::$instance)) { + throw new InvalidArgumentException('You Should [Create] First Before Using'); + } + + return self::$instance; + } + + /** + * clear. + * + * @author yansongda <me@yansongda.cn> + */ + public function clear() + { + self::$instance = null; + } + + /** + * Get Alipay API result. + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public static function requestApi(array $data): Collection + { + Events::dispatch(new Events\ApiRequesting('Alipay', '', self::$instance->getBaseUri(), $data)); + + $data = array_filter($data, function ($value) { + return ('' == $value || is_null($value)) ? false : true; + }); + + $result = json_decode(self::$instance->post('', $data), true); + + Events::dispatch(new Events\ApiRequested('Alipay', '', self::$instance->getBaseUri(), $result)); + + return self::processingApiResult($data, $result); + } + + /** + * Generate sign. + * + * @author yansongda <me@yansongda.cn> + * + * @throws InvalidConfigException + */ + public static function generateSign(array $params): string + { + $privateKey = self::$instance->private_key; + + if (is_null($privateKey)) { + throw new InvalidConfigException('Missing Alipay Config -- [private_key]'); + } + + if (Str::endsWith($privateKey, '.pem')) { + $privateKey = openssl_pkey_get_private( + Str::startsWith($privateKey, 'file://') ? $privateKey : 'file://'.$privateKey + ); + } else { + $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n". + wordwrap($privateKey, 64, "\n", true). + "\n-----END RSA PRIVATE KEY-----"; + } + + openssl_sign(self::getSignContent($params), $sign, $privateKey, OPENSSL_ALGO_SHA256); + + $sign = base64_encode($sign); + + Log::debug('Alipay Generate Sign', [$params, $sign]); + + if (is_resource($privateKey)) { + openssl_free_key($privateKey); + } + + return $sign; + } + + /** + * Verify sign. + * + * @author yansongda <me@yansonga.cn> + * + * @param bool $sync + * @param string|null $sign + * + * @throws InvalidConfigException + */ + public static function verifySign(array $data, $sync = false, $sign = null): bool + { + $publicKey = self::$instance->ali_public_key; + + if (is_null($publicKey)) { + throw new InvalidConfigException('Missing Alipay Config -- [ali_public_key]'); + } + + if (Str::endsWith($publicKey, '.crt')) { + $publicKey = file_get_contents($publicKey); + } elseif (Str::endsWith($publicKey, '.pem')) { + $publicKey = openssl_pkey_get_public( + Str::startsWith($publicKey, 'file://') ? $publicKey : 'file://'.$publicKey + ); + } else { + $publicKey = "-----BEGIN PUBLIC KEY-----\n". + wordwrap($publicKey, 64, "\n", true). + "\n-----END PUBLIC KEY-----"; + } + + $sign = $sign ?? $data['sign']; + + $toVerify = $sync ? json_encode($data, JSON_UNESCAPED_UNICODE) : self::getSignContent($data, true); + + $isVerify = 1 === openssl_verify($toVerify, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256); + + if (is_resource($publicKey)) { + openssl_free_key($publicKey); + } + + return $isVerify; + } + + /** + * Get signContent that is to be signed. + * + * @author yansongda <me@yansongda.cn> + * + * @param bool $verify + */ + public static function getSignContent(array $data, $verify = false): string + { + ksort($data); + + $stringToBeSigned = ''; + foreach ($data as $k => $v) { + if ($verify && 'sign' != $k && 'sign_type' != $k) { + $stringToBeSigned .= $k.'='.$v.'&'; + } + if (!$verify && '' !== $v && !is_null($v) && 'sign' != $k && '@' != substr($v, 0, 1)) { + $stringToBeSigned .= $k.'='.$v.'&'; + } + } + + Log::debug('Alipay Generate Sign Content Before Trim', [$data, $stringToBeSigned]); + + return trim($stringToBeSigned, '&'); + } + + /** + * Convert encoding. + * + * @author yansongda <me@yansonga.cn> + * + * @param string|array $data + * @param string $to + * @param string $from + */ + public static function encoding($data, $to = 'utf-8', $from = 'gb2312'): array + { + return Arr::encoding((array) $data, $to, $from); + } + + /** + * Get service config. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|null $key + * @param mixed|null $default + * + * @return mixed|null + */ + public function getConfig($key = null, $default = null) + { + if (is_null($key)) { + return $this->config->all(); + } + + if ($this->config->has($key)) { + return $this->config[$key]; + } + + return $default; + } + + /** + * Get Base Uri. + * + * @author yansongda <me@yansongda.cn> + * + * @return string + */ + public function getBaseUri() + { + return $this->baseUri; + } + + /** + * 生成应用证书SN. + * + * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html + * + * @param $certPath + * + * @throws /Exception + */ + public static function getCertSN($certPath): string + { + if (!is_file($certPath)) { + throw new Exception('unknown certPath -- [getCertSN]'); + } + $x509data = file_get_contents($certPath); + if (false === $x509data) { + throw new Exception('Alipay CertSN Error -- [getCertSN]'); + } + openssl_x509_read($x509data); + $certdata = openssl_x509_parse($x509data); + if (empty($certdata)) { + throw new Exception('Alipay openssl_x509_parse Error -- [getCertSN]'); + } + $issuer_arr = []; + foreach ($certdata['issuer'] as $key => $val) { + $issuer_arr[] = $key.'='.$val; + } + $issuer = implode(',', array_reverse($issuer_arr)); + Log::debug('getCertSN:', [$certPath, $issuer, $certdata['serialNumber']]); + + return md5($issuer.$certdata['serialNumber']); + } + + /** + * 生成支付宝根证书SN. + * + * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html + * + * @param $certPath + * + * @return string + * + * @throws /Exception + */ + public static function getRootCertSN($certPath) + { + if (!is_file($certPath)) { + throw new Exception('unknown certPath -- [getRootCertSN]'); + } + $x509data = file_get_contents($certPath); + if (false === $x509data) { + throw new Exception('Alipay CertSN Error -- [getRootCertSN]'); + } + $kCertificateEnd = '-----END CERTIFICATE-----'; + $certStrList = explode($kCertificateEnd, $x509data); + $md5_arr = []; + foreach ($certStrList as $one) { + if (!empty(trim($one))) { + $_x509data = $one.$kCertificateEnd; + openssl_x509_read($_x509data); + $_certdata = openssl_x509_parse($_x509data); + if (in_array($_certdata['signatureTypeSN'], ['RSA-SHA256', 'RSA-SHA1'])) { + $issuer_arr = []; + foreach ($_certdata['issuer'] as $key => $val) { + $issuer_arr[] = $key.'='.$val; + } + $_issuer = implode(',', array_reverse($issuer_arr)); + if (0 === strpos($_certdata['serialNumber'], '0x')) { + $serialNumber = self::bchexdec($_certdata['serialNumber']); + } else { + $serialNumber = $_certdata['serialNumber']; + } + $md5_arr[] = md5($_issuer.$serialNumber); + Log::debug('getRootCertSN Sub:', [$certPath, $_issuer, $serialNumber]); + } + } + } + + return implode('_', $md5_arr); + } + + /** + * processingApiResult. + * + * @author yansongda <me@yansongda.cn> + * + * @param $data + * @param $result + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + protected static function processingApiResult($data, $result): Collection + { + $method = str_replace('.', '_', $data['method']).'_response'; + + if (!isset($result['sign']) || '10000' != $result[$method]['code']) { + throw new GatewayException('Get Alipay API Error:'.$result[$method]['msg'].(isset($result[$method]['sub_code']) ? (' - '.$result[$method]['sub_code']) : ''), $result); + } + + if (self::verifySign($result[$method], true, $result['sign'])) { + return new Collection($result[$method]); + } + + Events::dispatch(new Events\SignFailed('Alipay', '', $result)); + + throw new InvalidSignException('Alipay Sign Verify FAILED', $result); + } + + /** + * Set Http options. + * + * @author yansongda <me@yansongda.cn> + */ + protected function setHttpOptions(): self + { + if ($this->config->has('http') && is_array($this->config->get('http'))) { + $this->config->forget('http.base_uri'); + $this->httpOptions = $this->config->get('http'); + } + + return $this; + } + + /** + * 0x转高精度数字. + * + * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html + * + * @param $hex + * + * @return int|string + */ + private static function bchexdec($hex) + { + $dec = 0; + $len = strlen($hex); + for ($i = 1; $i <= $len; ++$i) { + if (ctype_xdigit($hex[$i - 1])) { + $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i)))); + } + } + + return str_replace('.00', '', $dec); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/TransferGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/TransferGateway.php new file mode 100644 index 0000000..1015ac3 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/TransferGateway.php @@ -0,0 +1,49 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +use Yansongda\Pay\Contracts\GatewayInterface; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidConfigException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Supports\Collection; + +class TransferGateway implements GatewayInterface +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidConfigException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + $payload['method'] = 'alipay.fund.trans.uni.transfer'; + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Alipay', 'Transfer', $endpoint, $payload)); + + return Support::requestApi($payload); + } + + /** + * Find. + * + * @author yansongda <me@yansongda.cn> + * + * @param $order + */ + public function find($order): array + { + return [ + 'method' => 'alipay.fund.trans.order.query', + 'biz_content' => json_encode(is_array($order) ? $order : ['out_biz_no' => $order]), + ]; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/WapGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/WapGateway.php new file mode 100644 index 0000000..dda8c65 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/WapGateway.php @@ -0,0 +1,26 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +class WapGateway extends WebGateway +{ + /** + * Get method config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getMethod(): string + { + return 'alipay.trade.wap.pay'; + } + + /** + * Get productCode config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getProductCode(): string + { + return 'QUICK_WAP_WAY'; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Alipay/WebGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/WebGateway.php new file mode 100644 index 0000000..37a0cd9 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Alipay/WebGateway.php @@ -0,0 +1,104 @@ +<?php + +namespace Yansongda\Pay\Gateways\Alipay; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidConfigException; +use Yansongda\Pay\Gateways\Alipay; + +class WebGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws InvalidConfigException + * @throws InvalidArgumentException + */ + public function pay($endpoint, array $payload): Response + { + $biz_array = json_decode($payload['biz_content'], true); + $biz_array['product_code'] = $this->getProductCode(); + + $method = $biz_array['http_method'] ?? 'POST'; + + unset($biz_array['http_method']); + if ((Alipay::MODE_SERVICE === $this->mode) && (!empty(Support::getInstance()->pid))) { + $biz_array['extend_params'] = is_array($biz_array['extend_params']) ? array_merge(['sys_service_provider_id' => Support::getInstance()->pid], $biz_array['extend_params']) : ['sys_service_provider_id' => Support::getInstance()->pid]; + } + $payload['method'] = $this->getMethod(); + $payload['biz_content'] = json_encode($biz_array); + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Alipay', 'Web/Wap', $endpoint, $payload)); + + return $this->buildPayHtml($endpoint, $payload, $method); + } + + /** + * Find. + * + * @author yansongda <me@yansongda.cn> + * + * @param $order + */ + public function find($order): array + { + return [ + 'method' => 'alipay.trade.query', + 'biz_content' => json_encode(is_array($order) ? $order : ['out_trade_no' => $order]), + ]; + } + + /** + * Build Html response. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * @param array $payload + * @param string $method + */ + protected function buildPayHtml($endpoint, $payload, $method = 'POST'): Response + { + if ('GET' === strtoupper($method)) { + return new RedirectResponse($endpoint.'&'.http_build_query($payload)); + } + + $sHtml = "<form id='alipay_submit' name='alipay_submit' action='".$endpoint."' method='".$method."'>"; + foreach ($payload as $key => $val) { + $val = str_replace("'", ''', $val); + $sHtml .= "<input type='hidden' name='".$key."' value='".$val."'/>"; + } + $sHtml .= "<input type='submit' value='ok' style='display:none;'></form>"; + $sHtml .= "<script>document.forms['alipay_submit'].submit();</script>"; + + return new Response($sHtml); + } + + /** + * Get method config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getMethod(): string + { + return 'alipay.trade.page.pay'; + } + + /** + * Get productCode config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getProductCode(): string + { + return 'FAST_INSTANT_TRADE_PAY'; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat.php new file mode 100644 index 0000000..92d87af --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat.php @@ -0,0 +1,366 @@ +<?php + +namespace Yansongda\Pay\Gateways; + +use Exception; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Yansongda\Pay\Contracts\GatewayApplicationInterface; +use Yansongda\Pay\Contracts\GatewayInterface; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidGatewayException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Wechat\Support; +use Yansongda\Pay\Log; +use Yansongda\Supports\Collection; +use Yansongda\Supports\Config; +use Yansongda\Supports\Str; + +/** + * @method Response app(array $config) APP 支付 + * @method Collection groupRedpack(array $config) 分裂红包 + * @method Collection miniapp(array $config) 小程序支付 + * @method Collection mp(array $config) 公众号支付 + * @method Collection pos(array $config) 刷卡支付 + * @method Collection redpack(array $config) 普通红包 + * @method Collection scan(array $config) 扫码支付 + * @method Collection transfer(array $config) 企业付款 + * @method RedirectResponse web(array $config) Web 扫码支付 + * @method RedirectResponse wap(array $config) H5 支付 + */ +class Wechat implements GatewayApplicationInterface +{ + /** + * 普通模式. + */ + const MODE_NORMAL = 'normal'; + + /** + * 沙箱模式. + */ + const MODE_DEV = 'dev'; + + /** + * 香港钱包 API. + */ + const MODE_HK = 'hk'; + + /** + * 境外 API. + */ + const MODE_US = 'us'; + + /** + * 服务商模式. + */ + const MODE_SERVICE = 'service'; + + /** + * Const url. + */ + const URL = [ + self::MODE_NORMAL => 'https://api.mch.weixin.qq.com/', + self::MODE_DEV => 'https://api.mch.weixin.qq.com/sandboxnew/', + self::MODE_HK => 'https://apihk.mch.weixin.qq.com/', + self::MODE_SERVICE => 'https://api.mch.weixin.qq.com/', + self::MODE_US => 'https://apius.mch.weixin.qq.com/', + ]; + + /** + * Wechat payload. + * + * @var array + */ + protected $payload; + + /** + * Wechat gateway. + * + * @var string + */ + protected $gateway; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + * + * @throws Exception + */ + public function __construct(Config $config) + { + $this->gateway = Support::create($config)->getBaseUri(); + $this->payload = [ + 'appid' => $config->get('app_id', ''), + 'mch_id' => $config->get('mch_id', ''), + 'nonce_str' => Str::random(), + 'notify_url' => $config->get('notify_url', ''), + 'sign' => '', + 'trade_type' => '', + 'spbill_create_ip' => Request::createFromGlobals()->getClientIp(), + ]; + + if ($config->get('mode', self::MODE_NORMAL) === static::MODE_SERVICE) { + $this->payload = array_merge($this->payload, [ + 'sub_mch_id' => $config->get('sub_mch_id'), + 'sub_appid' => $config->get('sub_app_id', ''), + ]); + } + } + + /** + * Magic pay. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param string $params + * + * @throws InvalidGatewayException + * + * @return Response|Collection + */ + public function __call($method, $params) + { + return self::pay($method, ...$params); + } + + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $gateway + * @param array $params + * + * @throws InvalidGatewayException + * + * @return Response|Collection + */ + public function pay($gateway, $params = []) + { + Events::dispatch(new Events\PayStarting('Wechat', $gateway, $params)); + + $this->payload = array_merge($this->payload, $params); + + $gateway = get_class($this).'\\'.Str::studly($gateway).'Gateway'; + + if (class_exists($gateway)) { + return $this->makePay($gateway); + } + + throw new InvalidGatewayException("Pay Gateway [{$gateway}] Not Exists"); + } + + /** + * Verify data. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|null $content + * + * @throws InvalidSignException + * @throws InvalidArgumentException + */ + public function verify($content = null, bool $refund = false): Collection + { + $content = $content ?? Request::createFromGlobals()->getContent(); + + Events::dispatch(new Events\RequestReceived('Wechat', '', [$content])); + + $data = Support::fromXml($content); + if ($refund) { + $decrypt_data = Support::decryptRefundContents($data['req_info']); + $data = array_merge(Support::fromXml($decrypt_data), $data); + } + + Log::debug('Resolved The Received Wechat Request Data', $data); + + if ($refund || Support::generateSign($data) === $data['sign']) { + return new Collection($data); + } + + Events::dispatch(new Events\SignFailed('Wechat', '', $data)); + + throw new InvalidSignException('Wechat Sign Verify FAILED', $data); + } + + /** + * Query an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $order + * + * @throws GatewayException + * @throws InvalidSignException + * @throws InvalidArgumentException + */ + public function find($order, string $type = 'wap'): Collection + { + if ('wap' != $type) { + unset($this->payload['spbill_create_ip']); + } + + $gateway = get_class($this).'\\'.Str::studly($type).'Gateway'; + + if (!class_exists($gateway) || !is_callable([new $gateway(), 'find'])) { + throw new GatewayException("{$gateway} Done Not Exist Or Done Not Has FIND Method"); + } + + $config = call_user_func([new $gateway(), 'find'], $order); + + $this->payload = Support::filterPayload($this->payload, $config['order']); + + Events::dispatch(new Events\MethodCalled('Wechat', 'Find', $this->gateway, $this->payload)); + + return Support::requestApi( + $config['endpoint'], + $this->payload, + $config['cert'] + ); + } + + /** + * Refund an order. + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidSignException + * @throws InvalidArgumentException + */ + public function refund(array $order): Collection + { + $this->payload = Support::filterPayload($this->payload, $order, true); + + Events::dispatch(new Events\MethodCalled('Wechat', 'Refund', $this->gateway, $this->payload)); + + return Support::requestApi( + 'secapi/pay/refund', + $this->payload, + true + ); + } + + /** + * Cancel an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param array $order + * + * @throws GatewayException + * @throws InvalidSignException + * @throws InvalidArgumentException + */ + public function cancel($order): Collection + { + unset($this->payload['spbill_create_ip']); + + $this->payload = Support::filterPayload($this->payload, $order); + + Events::dispatch(new Events\MethodCalled('Wechat', 'Cancel', $this->gateway, $this->payload)); + + return Support::requestApi( + 'secapi/pay/reverse', + $this->payload, + true + ); + } + + /** + * Close an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $order + * + * @throws GatewayException + * @throws InvalidSignException + * @throws InvalidArgumentException + */ + public function close($order): Collection + { + unset($this->payload['spbill_create_ip']); + + $this->payload = Support::filterPayload($this->payload, $order); + + Events::dispatch(new Events\MethodCalled('Wechat', 'Close', $this->gateway, $this->payload)); + + return Support::requestApi('pay/closeorder', $this->payload); + } + + /** + * Echo success to server. + * + * @author yansongda <me@yansongda.cn> + * + * @throws InvalidArgumentException + */ + public function success(): Response + { + Events::dispatch(new Events\MethodCalled('Wechat', 'Success', $this->gateway)); + + return new Response( + Support::toXml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']), + 200, + ['Content-Type' => 'application/xml'] + ); + } + + /** + * Download the bill. + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidArgumentException + */ + public function download(array $params): string + { + unset($this->payload['spbill_create_ip']); + + $this->payload = Support::filterPayload($this->payload, $params, true); + + Events::dispatch(new Events\MethodCalled('Wechat', 'Download', $this->gateway, $this->payload)); + + $result = Support::getInstance()->post( + 'pay/downloadbill', + Support::getInstance()->toXml($this->payload) + ); + + if (is_array($result)) { + throw new GatewayException('Get Wechat API Error: '.$result['return_msg'], $result); + } + + return $result; + } + + /** + * Make pay gateway. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $gateway + * + * @throws InvalidGatewayException + * + * @return Response|Collection + */ + protected function makePay($gateway) + { + $app = new $gateway(); + + if ($app instanceof GatewayInterface) { + return $app->pay($this->gateway, array_filter($this->payload, function ($value) { + return '' !== $value && !is_null($value); + })); + } + + throw new InvalidGatewayException("Pay Gateway [{$gateway}] Must Be An Instance Of GatewayInterface"); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/AppGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/AppGateway.php new file mode 100644 index 0000000..b37b965 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/AppGateway.php @@ -0,0 +1,62 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Exception; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Wechat; +use Yansongda\Supports\Str; + +class AppGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + * @throws Exception + */ + public function pay($endpoint, array $payload): Response + { + $payload['appid'] = Support::getInstance()->appid; + $payload['trade_type'] = $this->getTradeType(); + + if (Wechat::MODE_SERVICE === $this->mode) { + $payload['sub_appid'] = Support::getInstance()->sub_appid; + } + + $pay_request = [ + 'appid' => Wechat::MODE_SERVICE === $this->mode ? $payload['sub_appid'] : $payload['appid'], + 'partnerid' => Wechat::MODE_SERVICE === $this->mode ? $payload['sub_mch_id'] : $payload['mch_id'], + 'prepayid' => $this->preOrder($payload)->get('prepay_id'), + 'timestamp' => strval(time()), + 'noncestr' => Str::random(), + 'package' => 'Sign=WXPay', + ]; + $pay_request['sign'] = Support::generateSign($pay_request); + + Events::dispatch(new Events\PayStarted('Wechat', 'App', $endpoint, $pay_request)); + + return new JsonResponse($pay_request); + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getTradeType(): string + { + return 'APP'; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/Gateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/Gateway.php new file mode 100644 index 0000000..439a908 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/Gateway.php @@ -0,0 +1,88 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Yansongda\Pay\Contracts\GatewayInterface; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Supports\Collection; + +abstract class Gateway implements GatewayInterface +{ + /** + * Mode. + * + * @var string + */ + protected $mode; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + * + * @throws InvalidArgumentException + */ + public function __construct() + { + $this->mode = Support::getInstance()->mode; + } + + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @return Collection + */ + abstract public function pay($endpoint, array $payload); + + /** + * Find. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $order + */ + public function find($order): array + { + return [ + 'endpoint' => 'pay/orderquery', + 'order' => is_array($order) ? $order : ['out_trade_no' => $order], + 'cert' => false, + ]; + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + * + * @return string + */ + abstract protected function getTradeType(); + + /** + * Schedule an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param array $payload + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + protected function preOrder($payload): Collection + { + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\MethodCalled('Wechat', 'PreOrder', '', $payload)); + + return Support::requestApi('pay/unifiedorder', $payload); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/GroupRedpackGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/GroupRedpackGateway.php new file mode 100644 index 0000000..de690d6 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/GroupRedpackGateway.php @@ -0,0 +1,57 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Wechat; +use Yansongda\Supports\Collection; + +class GroupRedpackGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + $payload['wxappid'] = $payload['appid']; + $payload['amt_type'] = 'ALL_RAND'; + + if (Wechat::MODE_SERVICE === $this->mode) { + $payload['msgappid'] = $payload['appid']; + } + + unset($payload['appid'], $payload['trade_type'], + $payload['notify_url'], $payload['spbill_create_ip']); + + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Wechat', 'Group Redpack', $endpoint, $payload)); + + return Support::requestApi( + 'mmpaymkttransfers/sendgroupredpack', + $payload, + true + ); + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getTradeType(): string + { + return ''; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/MiniappGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/MiniappGateway.php new file mode 100644 index 0000000..8878dd2 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/MiniappGateway.php @@ -0,0 +1,35 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Wechat; +use Yansongda\Supports\Collection; + +class MiniappGateway extends MpGateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + $payload['appid'] = Support::getInstance()->miniapp_id; + + if (Wechat::MODE_SERVICE === $this->mode) { + $payload['sub_appid'] = Support::getInstance()->sub_miniapp_id; + $this->payRequestUseSubAppId = true; + } + + return parent::pay($endpoint, $payload); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/MpGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/MpGateway.php new file mode 100644 index 0000000..1c7fd9d --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/MpGateway.php @@ -0,0 +1,59 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Exception; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Supports\Collection; +use Yansongda\Supports\Str; + +class MpGateway extends Gateway +{ + /** + * @var bool + */ + protected $payRequestUseSubAppId = false; + + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + * @throws Exception + */ + public function pay($endpoint, array $payload): Collection + { + $payload['trade_type'] = $this->getTradeType(); + + $pay_request = [ + 'appId' => !$this->payRequestUseSubAppId ? $payload['appid'] : $payload['sub_appid'], + 'timeStamp' => strval(time()), + 'nonceStr' => Str::random(), + 'package' => 'prepay_id='.$this->preOrder($payload)->get('prepay_id'), + 'signType' => 'MD5', + ]; + $pay_request['paySign'] = Support::generateSign($pay_request); + + Events::dispatch(new Events\PayStarted('Wechat', 'JSAPI', $endpoint, $pay_request)); + + return new Collection($pay_request); + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getTradeType(): string + { + return 'JSAPI'; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/PosGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/PosGateway.php new file mode 100644 index 0000000..01061b6 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/PosGateway.php @@ -0,0 +1,44 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Supports\Collection; + +class PosGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + unset($payload['trade_type'], $payload['notify_url']); + + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Wechat', 'Pos', $endpoint, $payload)); + + return Support::requestApi('pay/micropay', $payload); + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getTradeType(): string + { + return 'MICROPAY'; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/RedpackGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/RedpackGateway.php new file mode 100644 index 0000000..db7d06f --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/RedpackGateway.php @@ -0,0 +1,61 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Symfony\Component\HttpFoundation\Request; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Wechat; +use Yansongda\Supports\Collection; + +class RedpackGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + $payload['wxappid'] = $payload['appid']; + + if ('cli' !== php_sapi_name()) { + $payload['client_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR'); + } + + if (Wechat::MODE_SERVICE === $this->mode) { + $payload['msgappid'] = $payload['appid']; + } + + unset($payload['appid'], $payload['trade_type'], + $payload['notify_url'], $payload['spbill_create_ip']); + + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Wechat', 'Redpack', $endpoint, $payload)); + + return Support::requestApi( + 'mmpaymkttransfers/sendredpack', + $payload, + true + ); + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getTradeType(): string + { + return ''; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/RefundGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/RefundGateway.php new file mode 100644 index 0000000..e354cf5 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/RefundGateway.php @@ -0,0 +1,50 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Yansongda\Pay\Exceptions\InvalidArgumentException; + +class RefundGateway extends Gateway +{ + /** + * Find. + * + * @author yansongda <me@yansongda.cn> + * + * @param $order + */ + public function find($order): array + { + return [ + 'endpoint' => 'pay/refundquery', + 'order' => is_array($order) ? $order : ['out_trade_no' => $order], + 'cert' => false, + ]; + } + + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws InvalidArgumentException + */ + public function pay($endpoint, array $payload) + { + throw new InvalidArgumentException('Not Support Refund In Pay'); + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + * + * @throws InvalidArgumentException + */ + protected function getTradeType() + { + throw new InvalidArgumentException('Not Support Refund In Pay'); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/ScanGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/ScanGateway.php new file mode 100644 index 0000000..1338665 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/ScanGateway.php @@ -0,0 +1,44 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Symfony\Component\HttpFoundation\Request; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Supports\Collection; + +class ScanGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR'); + $payload['trade_type'] = $this->getTradeType(); + + Events::dispatch(new Events\PayStarted('Wechat', 'Scan', $endpoint, $payload)); + + return $this->preOrder($payload); + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getTradeType(): string + { + return 'NATIVE'; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/Support.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/Support.php new file mode 100644 index 0000000..6b8f54f --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/Support.php @@ -0,0 +1,449 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Exception; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\BusinessException; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Wechat; +use Yansongda\Pay\Log; +use Yansongda\Supports\Collection; +use Yansongda\Supports\Config; +use Yansongda\Supports\Str; +use Yansongda\Supports\Traits\HasHttpRequest; + +/** + * @author yansongda <me@yansongda.cn> + * + * @property string appid + * @property string app_id + * @property string miniapp_id + * @property string sub_appid + * @property string sub_app_id + * @property string sub_miniapp_id + * @property string mch_id + * @property string sub_mch_id + * @property string key + * @property string return_url + * @property string cert_client + * @property string cert_key + * @property array log + * @property array http + * @property string mode + */ +class Support +{ + use HasHttpRequest; + + /** + * Wechat gateway. + * + * @var string + */ + protected $baseUri; + + /** + * Config. + * + * @var Config + */ + protected $config; + + /** + * Instance. + * + * @var Support + */ + private static $instance; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + */ + private function __construct(Config $config) + { + $this->baseUri = Wechat::URL[$config->get('mode', Wechat::MODE_NORMAL)]; + $this->config = $config; + + $this->setHttpOptions(); + } + + /** + * __get. + * + * @author yansongda <me@yansongda.cn> + * + * @param $key + * + * @return mixed|Config|null + */ + public function __get($key) + { + return $this->getConfig($key); + } + + /** + * create. + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + * + * @return Support + */ + public static function create(Config $config) + { + if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) { + self::$instance = new self($config); + + self::setDevKey(); + } + + return self::$instance; + } + + /** + * getInstance. + * + * @author yansongda <me@yansongda.cn> + * + * @throws InvalidArgumentException + * + * @return Support + */ + public static function getInstance() + { + if (is_null(self::$instance)) { + throw new InvalidArgumentException('You Should [Create] First Before Using'); + } + + return self::$instance; + } + + /** + * clear. + * + * @author yansongda <me@yansongda.cn> + */ + public static function clear() + { + self::$instance = null; + } + + /** + * Request wechat api. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * @param array $data + * @param bool $cert + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + public static function requestApi($endpoint, $data, $cert = false): Collection + { + Events::dispatch(new Events\ApiRequesting('Wechat', '', self::$instance->getBaseUri().$endpoint, $data)); + + $result = self::$instance->post( + $endpoint, + self::toXml($data), + $cert ? [ + 'cert' => self::$instance->cert_client, + 'ssl_key' => self::$instance->cert_key, + ] : [] + ); + $result = is_array($result) ? $result : self::fromXml($result); + + Events::dispatch(new Events\ApiRequested('Wechat', '', self::$instance->getBaseUri().$endpoint, $result)); + + return self::processingApiResult($endpoint, $result); + } + + /** + * Filter payload. + * + * @author yansongda <me@yansongda.cn> + * + * @param array $payload + * @param array|string $params + * @param bool $preserve_notify_url + * + * @throws InvalidArgumentException + */ + public static function filterPayload($payload, $params, $preserve_notify_url = false): array + { + $type = self::getTypeName($params['type'] ?? ''); + + $payload = array_merge( + $payload, + is_array($params) ? $params : ['out_trade_no' => $params] + ); + $payload['appid'] = self::$instance->getConfig($type, ''); + + if (Wechat::MODE_SERVICE === self::$instance->getConfig('mode', Wechat::MODE_NORMAL)) { + $payload['sub_appid'] = self::$instance->getConfig('sub_'.$type, ''); + } + + unset($payload['trade_type'], $payload['type']); + if (!$preserve_notify_url) { + unset($payload['notify_url']); + } + + $payload['sign'] = self::generateSign($payload); + + return $payload; + } + + /** + * Generate wechat sign. + * + * @author yansongda <me@yansongda.cn> + * + * @param array $data + * + * @throws InvalidArgumentException + */ + public static function generateSign($data): string + { + $key = self::$instance->key; + + if (is_null($key)) { + throw new InvalidArgumentException('Missing Wechat Config -- [key]'); + } + + ksort($data); + + $string = md5(self::getSignContent($data).'&key='.$key); + + Log::debug('Wechat Generate Sign Before UPPER', [$data, $string]); + + return strtoupper($string); + } + + /** + * Generate sign content. + * + * @author yansongda <me@yansongda.cn> + * + * @param array $data + */ + public static function getSignContent($data): string + { + $buff = ''; + + foreach ($data as $k => $v) { + $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : ''; + } + + Log::debug('Wechat Generate Sign Content Before Trim', [$data, $buff]); + + return trim($buff, '&'); + } + + /** + * Decrypt refund contents. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $contents + */ + public static function decryptRefundContents($contents): string + { + return openssl_decrypt( + base64_decode($contents), + 'AES-256-ECB', + md5(self::$instance->key), + OPENSSL_RAW_DATA + ); + } + + /** + * Convert array to xml. + * + * @author yansongda <me@yansongda.cn> + * + * @param array $data + * + * @throws InvalidArgumentException + */ + public static function toXml($data): string + { + if (!is_array($data) || count($data) <= 0) { + throw new InvalidArgumentException('Convert To Xml Error! Invalid Array!'); + } + + $xml = '<xml>'; + foreach ($data as $key => $val) { + $xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' : + '<'.$key.'><![CDATA['.$val.']]></'.$key.'>'; + } + $xml .= '</xml>'; + + return $xml; + } + + /** + * Convert xml to array. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $xml + * + * @throws InvalidArgumentException + */ + public static function fromXml($xml): array + { + if (!$xml) { + throw new InvalidArgumentException('Convert To Array Error! Invalid Xml!'); + } + + libxml_disable_entity_loader(true); + + return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true); + } + + /** + * Get service config. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|null $key + * @param mixed|null $default + * + * @return mixed|null + */ + public function getConfig($key = null, $default = null) + { + if (is_null($key)) { + return $this->config->all(); + } + + if ($this->config->has($key)) { + return $this->config[$key]; + } + + return $default; + } + + /** + * Get app id according to param type. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $type + */ + public static function getTypeName($type = ''): string + { + switch ($type) { + case '': + $type = 'app_id'; + break; + case 'app': + $type = 'appid'; + break; + default: + $type = $type.'_id'; + } + + return $type; + } + + /** + * Get Base Uri. + * + * @author yansongda <me@yansongda.cn> + * + * @return string + */ + public function getBaseUri() + { + return $this->baseUri; + } + + /** + * processingApiResult. + * + * @author yansongda <me@yansongda.cn> + * + * @param $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + * + * @return Collection + */ + protected static function processingApiResult($endpoint, array $result) + { + if (!isset($result['return_code']) || 'SUCCESS' != $result['return_code']) { + throw new GatewayException('Get Wechat API Error:'.($result['return_msg'] ?? $result['retmsg'] ?? ''), $result); + } + + if (isset($result['result_code']) && 'SUCCESS' != $result['result_code']) { + throw new BusinessException('Wechat Business Error: '.$result['err_code'].' - '.$result['err_code_des'], $result); + } + + if ('pay/getsignkey' === $endpoint || + false !== strpos($endpoint, 'mmpaymkttransfers') || + self::generateSign($result) === $result['sign']) { + return new Collection($result); + } + + Events::dispatch(new Events\SignFailed('Wechat', '', $result)); + + throw new InvalidSignException('Wechat Sign Verify FAILED', $result); + } + + /** + * setDevKey. + * + * @author yansongda <me@yansongda.cn> + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + * @throws Exception + * + * @return Support + */ + private static function setDevKey() + { + if (Wechat::MODE_DEV == self::$instance->mode) { + $data = [ + 'mch_id' => self::$instance->mch_id, + 'nonce_str' => Str::random(), + ]; + $data['sign'] = self::generateSign($data); + + $result = self::requestApi('pay/getsignkey', $data); + + self::$instance->config->set('key', $result['sandbox_signkey']); + } + + return self::$instance; + } + + /** + * Set Http options. + * + * @author yansongda <me@yansongda.cn> + */ + private function setHttpOptions(): self + { + if ($this->config->has('http') && is_array($this->config->get('http'))) { + $this->config->forget('http.base_uri'); + $this->httpOptions = $this->config->get('http'); + } + + return $this; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/TransferGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/TransferGateway.php new file mode 100644 index 0000000..bbd97b3 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/TransferGateway.php @@ -0,0 +1,80 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Symfony\Component\HttpFoundation\Request; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Pay\Gateways\Wechat; +use Yansongda\Supports\Collection; + +class TransferGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): Collection + { + if (Wechat::MODE_SERVICE === $this->mode) { + unset($payload['sub_mch_id'], $payload['sub_appid']); + } + + $type = Support::getTypeName($payload['type'] ?? ''); + + $payload['mch_appid'] = Support::getInstance()->getConfig($type, ''); + $payload['mchid'] = $payload['mch_id']; + + if ('cli' !== php_sapi_name() && !isset($payload['spbill_create_ip'])) { + $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR'); + } + + unset($payload['appid'], $payload['mch_id'], $payload['trade_type'], + $payload['notify_url'], $payload['type']); + + $payload['sign'] = Support::generateSign($payload); + + Events::dispatch(new Events\PayStarted('Wechat', 'Transfer', $endpoint, $payload)); + + return Support::requestApi( + 'mmpaymkttransfers/promotion/transfers', + $payload, + true + ); + } + + /** + * Find. + * + * @author yansongda <me@yansongda.cn> + * + * @param $order + */ + public function find($order): array + { + return [ + 'endpoint' => 'mmpaymkttransfers/gettransferinfo', + 'order' => is_array($order) ? $order : ['partner_trade_no' => $order], + 'cert' => true, + ]; + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getTradeType(): string + { + return ''; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/WapGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/WapGateway.php new file mode 100644 index 0000000..0c0c920 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/WapGateway.php @@ -0,0 +1,47 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; + +class WapGateway extends Gateway +{ + /** + * Pay an order. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $endpoint + * + * @throws GatewayException + * @throws InvalidArgumentException + * @throws InvalidSignException + */ + public function pay($endpoint, array $payload): RedirectResponse + { + $payload['trade_type'] = $this->getTradeType(); + + Events::dispatch(new Events\PayStarted('Wechat', 'Wap', $endpoint, $payload)); + + $mweb_url = $this->preOrder($payload)->get('mweb_url'); + + $url = is_null(Support::getInstance()->return_url) ? $mweb_url : $mweb_url. + '&redirect_url='.urlencode(Support::getInstance()->return_url); + + return new RedirectResponse($url); + } + + /** + * Get trade type config. + * + * @author yansongda <me@yansongda.cn> + */ + protected function getTradeType(): string + { + return 'MWEB'; + } +} diff --git a/addons/epay/library/Yansongda/Pay/Gateways/Wechat/WebGateway.php b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/WebGateway.php new file mode 100644 index 0000000..6501d14 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Gateways/Wechat/WebGateway.php @@ -0,0 +1,86 @@ +<?php + +namespace Yansongda\Pay\Gateways\Wechat; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; +use Yansongda\Pay\Events; +use Yansongda\Pay\Exceptions\GatewayException; +use Yansongda\Pay\Exceptions\InvalidArgumentException; +use Yansongda\Pay\Exceptions\InvalidSignException; +use Yansongda\Supports\Collection; + +class WebGateway extends Gateway +{ + /** + * Pay an order. + * + * @param string $endpoint + * @param array $payload + * + * @author yansongda <me@yansongda.cn> + * + */ + public function pay($endpoint, array $payload): Response + { + $payload['spbill_create_ip'] = Request::createFromGlobals()->server->get('SERVER_ADDR'); + $payload['trade_type'] = $this->getTradeType(); + + $code_url = $this->preOrder($payload)['code_url']; + $params = [ + 'body' => $payload['body'], + 'code_url' => $code_url, + 'out_trade_no' => $payload['out_trade_no'], + 'return_url' => Support::getInstance()->return_url, + 'total_fee' => $payload['total_fee'], + ]; + + $params['sign'] = md5(implode('', $params) . Support::getInstance()->app_id); + $endpoint = addon_url("epay/api/wechat"); + + Events::dispatch(new Events\PayStarted('Wechat', 'Web/Wap', $endpoint, $payload)); + + return $this->buildPayHtml($endpoint, $params); + } + + /** + * Build Html response. + * + * @param string $endpoint + * @param array $payload + * @param string $method + * + * @return Response + * @author yansongda <me@yansongda.cn> + * + */ + protected function buildPayHtml($endpoint, $payload, $method = 'POST'): Response + { + if (strtoupper($method) === 'GET') { + return RedirectResponse::create($endpoint . '?' . http_build_query($payload)); + } + + $sHtml = "<form id='wechat_submit' name='wechat_submit' action='" . $endpoint . "' method='" . $method . "'>"; + foreach ($payload as $key => $val) { + $val = str_replace("'", ''', $val); + $sHtml .= "<input type='hidden' name='" . $key . "' value='" . $val . "'/>"; + } + $sHtml .= "<input type='submit' value='ok' style='display:none;'></form>"; + $sHtml .= "<script>document.forms['wechat_submit'].submit();</script>"; + + return Response::create($sHtml); + } + + /** + * Get trade type config. + * + * @return string + * @author yansongda <me@yansongda.cn> + * + */ + protected function getTradeType(): string + { + return 'NATIVE'; + } +} diff --git a/addons/epay/library/Yansongda/Pay/LICENSE b/addons/epay/library/Yansongda/Pay/LICENSE new file mode 100644 index 0000000..6ceb153 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017 yansongda <me@yansongda.cn> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/addons/epay/library/Yansongda/Pay/Listeners/KernelLogSubscriber.php b/addons/epay/library/Yansongda/Pay/Listeners/KernelLogSubscriber.php new file mode 100644 index 0000000..2250de3 --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Listeners/KernelLogSubscriber.php @@ -0,0 +1,114 @@ +<?php + +namespace Yansongda\Pay\Listeners; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Yansongda\Pay\Events; +use Yansongda\Pay\Log; + +class KernelLogSubscriber implements EventSubscriberInterface +{ + /** + * Returns an array of event names this subscriber wants to listen to. + * + * The array keys are event names and the value can be: + * + * * The method name to call (priority defaults to 0) + * * An array composed of the method name to call and the priority + * * An array of arrays composed of the method names to call and respective + * priorities, or 0 if unset + * + * For instance: + * + * * array('eventName' => 'methodName') + * * array('eventName' => array('methodName', $priority)) + * * array('eventName' => array(array('methodName1', $priority), array('methodName2'))) + * + * @return array The event names to listen to + */ + public static function getSubscribedEvents() + { + return [ + Events\PayStarting::class => ['writePayStartingLog', 256], + Events\PayStarted::class => ['writePayStartedLog', 256], + Events\ApiRequesting::class => ['writeApiRequestingLog', 256], + Events\ApiRequested::class => ['writeApiRequestedLog', 256], + Events\SignFailed::class => ['writeSignFailedLog', 256], + Events\RequestReceived::class => ['writeRequestReceivedLog', 256], + Events\MethodCalled::class => ['writeMethodCalledLog', 256], + ]; + } + + /** + * writePayStartingLog. + * + * @author yansongda <me@yansongda.cn> + */ + public function writePayStartingLog(Events\PayStarting $event) + { + Log::debug("Starting To {$event->driver}", [$event->gateway, $event->params]); + } + + /** + * writePayStartedLog. + * + * @author yansongda <me@yansongda.cn> + */ + public function writePayStartedLog(Events\PayStarted $event) + { + Log::info( + "{$event->driver} {$event->gateway} Has Started", + [$event->endpoint, $event->payload] + ); + } + + /** + * writeApiRequestingLog. + * + * @author yansongda <me@yansongda.cn> + */ + public function writeApiRequestingLog(Events\ApiRequesting $event) + { + Log::debug("Requesting To {$event->driver} Api", [$event->endpoint, $event->payload]); + } + + /** + * writeApiRequestedLog. + * + * @author yansongda <me@yansongda.cn> + */ + public function writeApiRequestedLog(Events\ApiRequested $event) + { + Log::debug("Result Of {$event->driver} Api", $event->result); + } + + /** + * writeSignFailedLog. + * + * @author yansongda <me@yansongda.cn> + */ + public function writeSignFailedLog(Events\SignFailed $event) + { + Log::warning("{$event->driver} Sign Verify FAILED", $event->data); + } + + /** + * writeRequestReceivedLog. + * + * @author yansongda <me@yansongda.cn> + */ + public function writeRequestReceivedLog(Events\RequestReceived $event) + { + Log::info("Received {$event->driver} Request", $event->data); + } + + /** + * writeMethodCalledLog. + * + * @author yansongda <me@yansongda.cn> + */ + public function writeMethodCalledLog(Events\MethodCalled $event) + { + Log::info("{$event->driver} {$event->gateway} Method Has Called", [$event->endpoint, $event->payload]); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Log.php b/addons/epay/library/Yansongda/Pay/Log.php new file mode 100644 index 0000000..c1f8c3e --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Log.php @@ -0,0 +1,49 @@ +<?php + +namespace Yansongda\Pay; + +use Yansongda\Supports\Log as BaseLog; + +/** + * @method static void emergency($message, array $context = array()) + * @method static void alert($message, array $context = array()) + * @method static void critical($message, array $context = array()) + * @method static void error($message, array $context = array()) + * @method static void warning($message, array $context = array()) + * @method static void notice($message, array $context = array()) + * @method static void info($message, array $context = array()) + * @method static void debug($message, array $context = array()) + * @method static void log($message, array $context = array()) + */ +class Log +{ + /** + * Forward call. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $args + * + * @return mixed + */ + public static function __callStatic($method, $args) + { + return forward_static_call_array([BaseLog::class, $method], $args); + } + + /** + * Forward call. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $args + * + * @return mixed + */ + public function __call($method, $args) + { + return call_user_func_array([BaseLog::class, $method], $args); + } +} diff --git a/addons/epay/library/Yansongda/Pay/Pay.php b/addons/epay/library/Yansongda/Pay/Pay.php new file mode 100644 index 0000000..eb33e3d --- /dev/null +++ b/addons/epay/library/Yansongda/Pay/Pay.php @@ -0,0 +1,131 @@ +<?php + +namespace Yansongda\Pay; + +use Exception; +use Yansongda\Pay\Contracts\GatewayApplicationInterface; +use Yansongda\Pay\Exceptions\InvalidGatewayException; +use Yansongda\Pay\Gateways\Alipay; +use Yansongda\Pay\Gateways\Wechat; +use Yansongda\Pay\Listeners\KernelLogSubscriber; +use Yansongda\Supports\Config; +use Yansongda\Supports\Log; +use Yansongda\Supports\Logger; +use Yansongda\Supports\Str; + +/** + * @method static Alipay alipay(array $config) 支付宝 + * @method static Wechat wechat(array $config) 微信 + */ +class Pay +{ + /** + * Config. + * + * @var Config + */ + protected $config; + + /** + * Bootstrap. + * + * @author yansongda <me@yansongda.cn> + * + * @throws Exception + */ + public function __construct(array $config) + { + $this->config = new Config($config); + + $this->registerLogService(); + $this->registerEventService(); + } + + /** + * Magic static call. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $params + * + * @throws InvalidGatewayException + * @throws Exception + */ + public static function __callStatic($method, $params): GatewayApplicationInterface + { + $app = new self(...$params); + + return $app->create($method); + } + + /** + * Create a instance. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * + * @throws InvalidGatewayException + */ + protected function create($method): GatewayApplicationInterface + { + $gateway = __NAMESPACE__.'\\Gateways\\'.Str::studly($method); + + if (class_exists($gateway)) { + return self::make($gateway); + } + + throw new InvalidGatewayException("Gateway [{$method}] Not Exists"); + } + + /** + * Make a gateway. + * + * @author yansongda <me@yansonga.cn> + * + * @param string $gateway + * + * @throws InvalidGatewayException + */ + protected function make($gateway): GatewayApplicationInterface + { + $app = new $gateway($this->config); + + if ($app instanceof GatewayApplicationInterface) { + return $app; + } + + throw new InvalidGatewayException("Gateway [{$gateway}] Must Be An Instance Of GatewayApplicationInterface"); + } + + /** + * Register log service. + * + * @author yansongda <me@yansongda.cn> + * + * @throws Exception + */ + protected function registerLogService() + { + $config = $this->config->get('log'); + $config['identify'] = 'yansongda.pay'; + + $logger = new Logger(); + $logger->setConfig($config); + + Log::setInstance($logger); + } + + /** + * Register event service. + * + * @author yansongda <me@yansongda.cn> + */ + protected function registerEventService() + { + Events::setDispatcher(Events::createDispatcher()); + + Events::addSubscriber(new KernelLogSubscriber()); + } +} diff --git a/addons/epay/library/Yansongda/Supports/Arr.php b/addons/epay/library/Yansongda/Supports/Arr.php new file mode 100644 index 0000000..9f30ae4 --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Arr.php @@ -0,0 +1,605 @@ +<?php + +namespace Yansongda\Supports; + +use ArrayAccess; + +/** + * Array helper from Illuminate\Support\Arr. + */ +class Arr +{ + /** + * Determine whether the given value is array accessible. + * + * @param mixed $value + */ + public static function accessible($value): bool + { + return is_array($value) || $value instanceof ArrayAccess; + } + + /** + * Add an element to an array using "dot" notation if it doesn't exist. + * + * @param mixed $value + */ + public static function add(array $array, string $key, $value): array + { + if (is_null(static::get($array, $key))) { + static::set($array, $key, $value); + } + + return $array; + } + + /** + * Build a new array using a callback. + */ + public static function build(array $array, callable $callback): array + { + $results = []; + + foreach ($array as $key => $value) { + [$innerKey, $innerValue] = call_user_func($callback, $key, $value); + $results[$innerKey] = $innerValue; + } + + return $results; + } + + /** + * Divide an array into two arrays. One with keys and the other with values. + */ + public static function divide(array $array): array + { + return [ + array_keys($array), + array_values($array), + ]; + } + + /** + * Flatten a multi-dimensional associative array with dots. + */ + public static function dot(array $array, string $prepend = ''): array + { + $results = []; + + foreach ($array as $key => $value) { + if (is_array($value)) { + $results = array_merge($results, static::dot($value, $prepend.$key.'.')); + } else { + $results[$prepend.$key] = $value; + } + } + + return $results; + } + + /** + * Get all of the given array except for a specified array of items. + * + * @param array|string $keys + */ + public static function except(array $array, $keys): array + { + return array_diff_key($array, array_flip((array) $keys)); + } + + /** + * access array. + * + * if not array access, return original. + * + * @author yansongda <me@yansongda.cn> + * + * @param mixed $data + * + * @return mixed + */ + public static function access($data) + { + if (!self::accessible($data) && + !(is_object($data) && method_exists($data, 'toArray'))) { + return $data; + } + + return is_object($data) ? $data->toArray() : $data; + } + + /** + * Determine if the given key exists in the provided array. + * + * @param \ArrayAccess|array $array + * @param string|int $key + * + * @return bool + */ + public static function exists($array, $key) + { + $array = self::access($array); + + if ($array instanceof ArrayAccess) { + return $array->offsetExists($key); + } + + return array_key_exists($key, $array); + } + + /** + * Check if an item or items exist in an array using "dot" notation. + * + * @param \ArrayAccess|array $array + * @param string|array $keys + * + * @return bool + */ + public static function has($array, $keys) + { + $array = self::access($array); + + $keys = (array) $keys; + + if (!$array || $keys === []) { + return false; + } + + foreach ($keys as $key) { + $subKeyArray = $array; + + if (static::exists($array, $key)) { + continue; + } + + foreach (explode('.', $key) as $segment) { + if (static::accessible($subKeyArray) && static::exists($subKeyArray, $segment)) { + $subKeyArray = $subKeyArray[$segment]; + } else { + return false; + } + } + } + + return true; + } + + /** + * Determine if any of the keys exist in an array using "dot" notation. + * + * @param \ArrayAccess|array $array + * @param string|array $keys + * + * @return bool + */ + public static function hasAny($array, $keys) + { + $array = self::access($array); + + if (is_null($keys)) { + return false; + } + + $keys = (array) $keys; + + if (!$array) { + return false; + } + + if ($keys === []) { + return false; + } + + foreach ($keys as $key) { + if (static::has($array, $key)) { + return true; + } + } + + return false; + } + + /** + * Fetch a flattened array of a nested array element. + */ + public static function fetch(array $array, string $key): array + { + $results = []; + + foreach (explode('.', $key) as $segment) { + $results = []; + foreach ($array as $value) { + $value = (array) $value; + $results[] = $value[$segment]; + } + $array = array_values($results); + } + + return array_values($results); + } + + /** + * Return the first element in an array passing a given truth test. + * + * @param mixed $default + * + * @return mixed + */ + public static function first(array $array, callable $callback, $default = null) + { + foreach ($array as $key => $value) { + if (call_user_func($callback, $key, $value)) { + return $value; + } + } + + return $default; + } + + /** + * Return the last element in an array passing a given truth test. + * + * @param mixed $default + * + * @return mixed + */ + public static function last(array $array, callable $callback, $default = null) + { + return static::first(array_reverse($array), $callback, $default); + } + + /** + * Flatten a multi-dimensional array into a single level. + */ + public static function flatten(array $array): array + { + $return = []; + array_walk_recursive( + $array, + function ($x) use (&$return) { + $return[] = $x; + } + ); + + return $return; + } + + /** + * Remove one or many array items from a given array using "dot" notation. + * + * @param array $array + * @param array|string $keys + */ + public static function forget(&$array, $keys) + { + $original = &$array; + + $keys = (array) $keys; + + if (0 === count($keys)) { + return; + } + + foreach ($keys as $key) { + // if the exact key exists in the top-level, remove it + if (static::exists($array, $key)) { + unset($array[$key]); + + continue; + } + + $parts = explode('.', $key); + + // clean up before each pass + $array = &$original; + + while (count($parts) > 1) { + $part = array_shift($parts); + + if (isset($array[$part]) && is_array($array[$part])) { + $array = &$array[$part]; + } else { + continue 2; + } + } + + unset($array[array_shift($parts)]); + } + } + + /** + * Get an item from an array using "dot" notation. + * + * @param mixed $default + * + * @return mixed + */ + public static function get(array $array, string $key, $default = null) + { + if (is_null($key)) { + return $array; + } + + if (isset($array[$key])) { + return $array[$key]; + } + + foreach (explode('.', $key) as $segment) { + if (!is_array($array) || !array_key_exists($segment, $array)) { + return $default; + } + $array = $array[$segment]; + } + + return $array; + } + + /** + * Get a subset of the items from the given array. + * + * @param array|string $keys + */ + public static function only(array $array, $keys): array + { + return array_intersect_key($array, array_flip((array) $keys)); + } + + /** + * Pluck an array of values from an array. + * + * @param string $key + */ + public static function pluck(array $array, string $value, string $key = null): array + { + $results = []; + + foreach ($array as $item) { + $itemValue = is_object($item) ? $item->{$value} : $item[$value]; + // If the key is "null", we will just append the value to the array and keep + // looping. Otherwise we will key the array using the value of the key we + // received from the developer. Then we'll return the final array form. + if (is_null($key)) { + $results[] = $itemValue; + } else { + $itemKey = is_object($item) ? $item->{$key} : $item[$key]; + $results[$itemKey] = $itemValue; + } + } + + return $results; + } + + /** + * Push an item onto the beginning of an array. + * + * @param mixed $value + * @param mixed $key + * + * @return array + */ + public static function prepend(array $array, $value, $key = null) + { + if (is_null($key)) { + array_unshift($array, $value); + } else { + $array = [$key => $value] + $array; + } + + return $array; + } + + /** + * Get a value from the array, and remove it. + * + * @param mixed $default + * + * @return mixed + */ + public static function pull(array &$array, string $key, $default = null) + { + $value = static::get($array, $key, $default); + + static::forget($array, $key); + + return $value; + } + + /** + * Get one or a specified number of random values from an array. + * + * @param array $array + * @param int|null $number + * + * @return mixed + * + * @throws \InvalidArgumentException + */ + public static function random(array $array, $number = null) + { + $requested = is_null($number) ? 1 : $number; + + $count = count($array); + + $number = $requested > $count ? $count : $requested; + + if (is_null($number)) { + return $array[array_rand($array)]; + } + + if (0 === (int) $number) { + return []; + } + + $keys = array_rand($array, $number); + + $results = []; + + foreach ((array) $keys as $key) { + $results[] = $array[$key]; + } + + return $results; + } + + /** + * Set an array item to a given value using "dot" notation. + * + * If no key is given to the method, the entire array will be replaced. + * + * @param mixed $value + */ + public static function set(array &$array, string $key, $value): array + { + if (is_null($key)) { + return $array = $value; + } + + $keys = explode('.', $key); + + while (count($keys) > 1) { + $key = array_shift($keys); + // If the key doesn't exist at this depth, we will just create an empty array + // to hold the next value, allowing us to create the arrays to hold final + // values at the correct depth. Then we'll keep digging into the array. + if (!isset($array[$key]) || !is_array($array[$key])) { + $array[$key] = []; + } + $array = &$array[$key]; + } + $array[array_shift($keys)] = $value; + + return $array; + } + + /** + * Sort the array using the given Closure. + */ + public static function sort(array $array, callable $callback): array + { + $results = []; + + foreach ($array as $key => $value) { + $results[$key] = $callback($value); + } + + return $results; + } + + /** + * Shuffle the given array and return the result. + * + * @param array $array + * @param int|null $seed + * + * @return array + */ + public static function shuffle(array $array, $seed = null): array + { + if (is_null($seed)) { + shuffle($array); + } else { + mt_srand($seed); + shuffle($array); + mt_srand(); + } + + return $array; + } + + /** + * Convert the array into a query string. + */ + public static function query(array $array): string + { + return http_build_query($array, null, '&', PHP_QUERY_RFC3986); + } + + /** + * Filter the array using the given callback. + */ + public static function where(array $array, ?callable $callback = null): array + { + return array_filter($array, $callback ?? function ($value) use ($callback) { + if (static::accessible($value)) { + $value = static::where($value, $callback); + } + + if (is_array($value) && 0 === count($value)) { + $value = null; + } + + return '' !== $value && !is_null($value); + }, ARRAY_FILTER_USE_BOTH); + } + + /** + * Convert encoding. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $from_encoding + */ + public static function encoding(array $array, string $to_encoding, $from_encoding = 'gb2312'): array + { + $encoded = []; + + foreach ($array as $key => $value) { + $encoded[$key] = is_array($value) ? self::encoding($value, $to_encoding, $from_encoding) : + mb_convert_encoding($value, $to_encoding, $from_encoding); + } + + return $encoded; + } + + /** + * camelCaseKey. + * + * @author yansongda <me@yansongda.cn> + * + * @param mixed $data + * + * @return mixed + */ + public static function camelCaseKey($data) + { + if (!self::accessible($data) && + !(is_object($data) && method_exists($data, 'toArray'))) { + return $data; + } + + $result = []; + $data = self::access($data); + + foreach ($data as $key => $value) { + $result[is_string($key) ? Str::camel($key) : $key] = self::camelCaseKey($value); + } + + return $result; + } + + /** + * snakeCaseKey. + * + * @author yansongda <me@yansongda.cn> + * + * @param mixed $data + * + * @return mixed + */ + public static function snakeCaseKey($data) + { + if (!self::accessible($data) && + !(is_object($data) && method_exists($data, 'toArray'))) { + return $data; + } + + $data = self::access($data); + $result = []; + + foreach ($data as $key => $value) { + $result[is_string($key) ? Str::snake($key) : $key] = self::snakeCaseKey($value); + } + + return $result; + } +} diff --git a/addons/epay/library/Yansongda/Supports/Collection.php b/addons/epay/library/Yansongda/Supports/Collection.php new file mode 100644 index 0000000..ba3a70b --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Collection.php @@ -0,0 +1,363 @@ +<?php + +namespace Yansongda\Supports; + +use ArrayAccess; +use ArrayIterator; +use Countable; +use IteratorAggregate; +use JsonSerializable; +use Serializable; + +class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable, Serializable +{ + /** + * The collection data. + * + * @var array + */ + protected $items = []; + + /** + * set data. + * + * @param mixed $items + */ + public function __construct(array $items = []) + { + foreach ($items as $key => $value) { + $this->set($key, $value); + } + } + + /** + * To string. + */ + public function __toString(): string + { + return $this->toJson(); + } + + /** + * Get a data by key. + * + * @return mixed + */ + public function __get(string $key) + { + return $this->get($key); + } + + /** + * Assigns a value to the specified data. + * + * @param mixed $value + */ + public function __set(string $key, $value) + { + $this->set($key, $value); + } + + /** + * Whether or not an data exists by key. + */ + public function __isset(string $key): bool + { + return $this->has($key); + } + + /** + * Unsets an data by key. + */ + public function __unset(string $key) + { + $this->forget($key); + } + + /** + * Return all items. + */ + public function all(): array + { + return $this->items; + } + + /** + * Return specific items. + */ + public function only(array $keys): array + { + $return = []; + + foreach ($keys as $key) { + $value = $this->get($key); + + if (!is_null($value)) { + $return[$key] = $value; + } + } + + return $return; + } + + /** + * Get all items except for those with the specified keys. + * + * @param mixed $keys + * + * @return static + */ + public function except($keys) + { + $keys = is_array($keys) ? $keys : func_get_args(); + + return new static(Arr::except($this->items, $keys)); + } + + /** + * Merge data. + * + * @param Collection|array $items + */ + public function merge($items): array + { + foreach ($items as $key => $value) { + $this->set($key, $value); + } + + return $this->all(); + } + + /** + * To determine Whether the specified element exists. + */ + public function has(string $key): bool + { + return !is_null(Arr::get($this->items, $key)); + } + + /** + * Retrieve the first item. + * + * @return mixed + */ + public function first() + { + return reset($this->items); + } + + /** + * Retrieve the last item. + * + * @return mixed + */ + public function last() + { + $end = end($this->items); + + reset($this->items); + + return $end; + } + + /** + * add the item value. + * + * @param mixed $value + */ + public function add(string $key, $value) + { + Arr::set($this->items, $key, $value); + } + + /** + * Set the item value. + * + * @param mixed $value + */ + public function set(string $key, $value) + { + Arr::set($this->items, $key, $value); + } + + /** + * Retrieve item from Collection. + * + * @param string $key + * @param mixed $default + * + * @return mixed + */ + public function get(?string $key = null, $default = null) + { + return Arr::get($this->items, $key, $default); + } + + /** + * Remove item form Collection. + */ + public function forget(string $key) + { + Arr::forget($this->items, $key); + } + + /** + * Build to array. + */ + public function toArray(): array + { + return $this->all(); + } + + /** + * Build to json. + */ + public function toJson(int $option = JSON_UNESCAPED_UNICODE): string + { + return json_encode($this->all(), $option); + } + + /** + * (PHP 5 >= 5.4.0)<br/> + * Specify data which should be serialized to JSON. + * + * @see http://php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed data which can be serialized by <b>json_encode</b>, + * which is a value of any type other than a resource + */ + public function jsonSerialize() + { + return $this->items; + } + + /** + * (PHP 5 >= 5.1.0)<br/> + * String representation of object. + * + * @see http://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or null + */ + public function serialize() + { + return serialize($this->items); + } + + /** + * (PHP 5 >= 5.0.0)<br/> + * Retrieve an external iterator. + * + * @see http://php.net/manual/en/iteratoraggregate.getiterator.php + * + * @return ArrayIterator An instance of an object implementing <b>Iterator</b> or + * <b>ArrayIterator</b> + */ + public function getIterator() + { + return new ArrayIterator($this->items); + } + + /** + * (PHP 5 >= 5.1.0)<br/> + * Count elements of an object. + * + * @see http://php.net/manual/en/countable.count.php + * + * @return int The custom count as an integer. + * </p> + * <p> + * The return value is cast to an integer + */ + public function count() + { + return count($this->items); + } + + /** + * (PHP 5 >= 5.1.0)<br/> + * Constructs the object. + * + * @see http://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized <p> + * The string representation of the object. + * </p> + * + * @return mixed|void + */ + public function unserialize($serialized) + { + return $this->items = unserialize($serialized); + } + + /** + * (PHP 5 >= 5.0.0)<br/> + * Whether a offset exists. + * + * @see http://php.net/manual/en/arrayaccess.offsetexists.php + * + * @param mixed $offset <p> + * An offset to check for. + * </p> + * + * @return bool true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned + */ + public function offsetExists($offset) + { + return $this->has($offset); + } + + /** + * (PHP 5 >= 5.0.0)<br/> + * Offset to unset. + * + * @see http://php.net/manual/en/arrayaccess.offsetunset.php + * + * @param mixed $offset <p> + * The offset to unset. + * </p> + */ + public function offsetUnset($offset) + { + if ($this->offsetExists($offset)) { + $this->forget($offset); + } + } + + /** + * (PHP 5 >= 5.0.0)<br/> + * Offset to retrieve. + * + * @see http://php.net/manual/en/arrayaccess.offsetget.php + * + * @param mixed $offset <p> + * The offset to retrieve. + * </p> + * + * @return mixed Can return all value types + */ + public function offsetGet($offset) + { + return $this->offsetExists($offset) ? $this->get($offset) : null; + } + + /** + * (PHP 5 >= 5.0.0)<br/> + * Offset to set. + * + * @see http://php.net/manual/en/arrayaccess.offsetset.php + * + * @param mixed $offset <p> + * The offset to assign the value to. + * </p> + * @param mixed $value <p> + * The value to set. + * </p> + */ + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } +} diff --git a/addons/epay/library/Yansongda/Supports/Config.php b/addons/epay/library/Yansongda/Supports/Config.php new file mode 100644 index 0000000..a660bad --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Config.php @@ -0,0 +1,7 @@ +<?php + +namespace Yansongda\Supports; + +class Config extends Collection +{ +} diff --git a/addons/epay/library/Yansongda/Supports/LICENSE b/addons/epay/library/Yansongda/Supports/LICENSE new file mode 100644 index 0000000..6ceb153 --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2017 yansongda <me@yansongda.cn> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/addons/epay/library/Yansongda/Supports/Log.php b/addons/epay/library/Yansongda/Supports/Log.php new file mode 100644 index 0000000..4901a2d --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Log.php @@ -0,0 +1,91 @@ +<?php + +namespace Yansongda\Supports; + +/** + * @method static void emergency($message, array $context = array()) + * @method static void alert($message, array $context = array()) + * @method static void critical($message, array $context = array()) + * @method static void error($message, array $context = array()) + * @method static void warning($message, array $context = array()) + * @method static void notice($message, array $context = array()) + * @method static void info($message, array $context = array()) + * @method static void debug($message, array $context = array()) + * @method static void log($message, array $context = array()) + */ +class Log extends Logger +{ + /** + * instance. + * + * @var \Psr\Log\LoggerInterface + */ + private static $instance; + + /** + * Bootstrap. + */ + private function __construct() + { + } + + /** + * __call. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $args + * + * @throws \Exception + */ + public function __call($method, $args): void + { + call_user_func_array([self::getInstance(), $method], $args); + } + + /** + * __callStatic. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $args + * + * @throws \Exception + */ + public static function __callStatic($method, $args): void + { + forward_static_call_array([self::getInstance(), $method], $args); + } + + /** + * getInstance. + * + * @author yansongda <me@yansongda.cn> + * + * @return \Yansongda\Supports\Logger + */ + public static function getInstance(): Logger + { + if (is_null(self::$instance)) { + self::$instance = new Logger(); + } + + return self::$instance; + } + + /** + * setInstance. + * + * @author yansongda <me@yansongda.cn> + * + * @param \Yansongda\Supports\Logger $logger + * + * @throws \Exception + */ + public static function setInstance(Logger $logger): void + { + self::$instance = $logger; + } +} diff --git a/addons/epay/library/Yansongda/Supports/Logger.php b/addons/epay/library/Yansongda/Supports/Logger.php new file mode 100644 index 0000000..7232c59 --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Logger.php @@ -0,0 +1,240 @@ +<?php + +namespace Yansongda\Supports; + +use Exception; +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\AbstractHandler; +use Monolog\Handler\RotatingFileHandler; +use Monolog\Handler\StreamHandler; +use Monolog\Logger as BaseLogger; +use Psr\Log\LoggerInterface; + +/** + * @method void emergency($message, array $context = array()) + * @method void alert($message, array $context = array()) + * @method void critical($message, array $context = array()) + * @method void error($message, array $context = array()) + * @method void warning($message, array $context = array()) + * @method void notice($message, array $context = array()) + * @method void info($message, array $context = array()) + * @method void debug($message, array $context = array()) + * @method void log($message, array $context = array()) + */ +class Logger +{ + /** + * Logger instance. + * + * @var LoggerInterface + */ + protected $logger; + + /** + * formatter. + * + * @var \Monolog\Formatter\FormatterInterface + */ + protected $formatter; + + /** + * handler. + * + * @var AbstractHandler + */ + protected $handler; + + /** + * config. + * + * @var array + */ + protected $config = [ + 'file' => null, + 'identify' => 'yansongda.supports', + 'level' => BaseLogger::DEBUG, + 'type' => 'daily', + 'max_files' => 30, + ]; + + /** + * Forward call. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $method + * @param array $args + * + * @throws Exception + */ + public function __call($method, $args): void + { + call_user_func_array([$this->getLogger(), $method], $args); + } + + /** + * Set logger. + * + * @author yansongda <me@yansongda.cn> + */ + public function setLogger(LoggerInterface $logger): Logger + { + $this->logger = $logger; + + return $this; + } + + /** + * Return the logger instance. + * + * @author yansongda <me@yansongda.cn> + * + * @throws Exception + */ + public function getLogger(): LoggerInterface + { + if (is_null($this->logger)) { + $this->logger = $this->createLogger(); + } + + return $this->logger; + } + + /** + * Make a default log instance. + * + * @author yansongda <me@yansongda.cn> + * + * @throws Exception + */ + public function createLogger(): BaseLogger + { + $handler = $this->getHandler(); + + $handler->setFormatter($this->getFormatter()); + + $logger = new BaseLogger($this->config['identify']); + + $logger->pushHandler($handler); + + return $logger; + } + + /** + * setFormatter. + * + * @author yansongda <me@yansongda.cn> + * + * @return $this + */ + public function setFormatter(FormatterInterface $formatter): self + { + $this->formatter = $formatter; + + return $this; + } + + /** + * getFormatter. + * + * @author yansongda <me@yansongda.cn> + */ + public function getFormatter(): FormatterInterface + { + if (is_null($this->formatter)) { + $this->formatter = $this->createFormatter(); + } + + return $this->formatter; + } + + /** + * createFormatter. + * + * @author yansongda <me@yansongda.cn> + */ + public function createFormatter(): LineFormatter + { + return new LineFormatter( + "%datetime% > %channel%.%level_name% > %message% %context% %extra%\n\n", + null, + false, + true + ); + } + + /** + * setHandler. + * + * @author yansongda <me@yansongda.cn> + * + * @return $this + */ + public function setHandler(AbstractHandler $handler): self + { + $this->handler = $handler; + + return $this; + } + + /** + * getHandler. + * + * @author yansongda <me@yansongda.cn> + * + * @throws \Exception + */ + public function getHandler(): AbstractHandler + { + if (is_null($this->handler)) { + $this->handler = $this->createHandler(); + } + + return $this->handler; + } + + /** + * createHandler. + * + * @author yansongda <me@yansongda.cn> + * + * @throws \Exception + * + * @return \Monolog\Handler\RotatingFileHandler|\Monolog\Handler\StreamHandler + */ + public function createHandler(): AbstractHandler + { + $file = $this->config['file'] ?? sys_get_temp_dir().'/logs/'.$this->config['identify'].'.log'; + + if ('single' === $this->config['type']) { + return new StreamHandler($file, $this->config['level']); + } + + return new RotatingFileHandler($file, $this->config['max_files'], $this->config['level']); + } + + /** + * setConfig. + * + * @author yansongda <me@yansongda.cn> + * + * @return $this + */ + public function setConfig(array $config): self + { + $this->config = array_merge($this->config, $config); + + return $this; + } + + /** + * getConfig. + * + * @author yansongda <me@yansongda.cn> + */ + public function getConfig(): array + { + return $this->config; + } +} diff --git a/addons/epay/library/Yansongda/Supports/Logger/StdoutHandler.php b/addons/epay/library/Yansongda/Supports/Logger/StdoutHandler.php new file mode 100644 index 0000000..bba384f --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Logger/StdoutHandler.php @@ -0,0 +1,36 @@ +<?php + +namespace Yansongda\Supports\Logger; + +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; + +class StdoutHandler extends AbstractProcessingHandler +{ + /** + * @var OutputInterface + */ + private $output; + + /** + * Bootstrap. + * + * @param int $level + * @param bool $bubble + */ + public function __construct($level = Logger::DEBUG, $bubble = true, ?OutputInterface $output = null) + { + $this->output = $output ?? new ConsoleOutput(); + parent::__construct($level, $bubble); + } + + /** + * Writes the record down to the log of the implementing handler. + */ + protected function write(array $record): void + { + $this->output->writeln($record['formatted']); + } +} diff --git a/addons/epay/library/Yansongda/Supports/Str.php b/addons/epay/library/Yansongda/Supports/Str.php new file mode 100644 index 0000000..b540be6 --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Str.php @@ -0,0 +1,570 @@ +<?php + +namespace Yansongda\Supports; + +use Exception; + +/** + * modify from Illuminate\Support. + */ +class Str +{ + /** + * The cache of snake-cased words. + * + * @var array + */ + protected static $snakeCache = []; + + /** + * The cache of camel-cased words. + * + * @var array + */ + protected static $camelCache = []; + + /** + * The cache of studly-cased words. + * + * @var array + */ + protected static $studlyCache = []; + + /** + * Return the remainder of a string after a given value. + */ + public static function after(string $subject, string $search): string + { + return '' === $search ? $subject : array_reverse(explode($search, $subject, 2))[0]; + } + + /** + * Transliterate a UTF-8 value to ASCII. + */ + public static function ascii(string $value, string $language = 'en'): string + { + $languageSpecific = static::languageSpecificCharsArray($language); + + if (!is_null($languageSpecific)) { + $value = str_replace($languageSpecific[0], $languageSpecific[1], $value); + } + + foreach (static::charsArray() as $key => $val) { + $value = str_replace($val, $key, $value); + } + + return preg_replace('/[^\x20-\x7E]/u', '', $value); + } + + /** + * Get the portion of a string before a given value. + */ + public static function before(string $subject, string $search): string + { + return '' === $search ? $subject : explode($search, $subject)[0]; + } + + /** + * Convert a value to camel case. + */ + public static function camel(string $value): string + { + if (isset(static::$camelCache[$value])) { + return static::$camelCache[$value]; + } + + return static::$camelCache[$value] = lcfirst(static::studly($value)); + } + + /** + * Determine if a given string contains a given substring. + * + * @param string|array $needles + */ + public static function contains(string $haystack, $needles): bool + { + foreach ((array) $needles as $needle) { + if ('' !== $needle && false !== mb_strpos($haystack, $needle)) { + return true; + } + } + + return false; + } + + /** + * Determine if a given string ends with a given substring. + * + * @param string|array $needles + */ + public static function endsWith(string $haystack, $needles): bool + { + foreach ((array) $needles as $needle) { + if (substr($haystack, -strlen($needle)) === (string) $needle) { + return true; + } + } + + return false; + } + + /** + * Cap a string with a single instance of a given value. + */ + public static function finish(string $value, string $cap): string + { + $quoted = preg_quote($cap, '/'); + + return preg_replace('/(?:'.$quoted.')+$/u', '', $value).$cap; + } + + /** + * Determine if a given string matches a given pattern. + * + * @param string|array $pattern + */ + public static function is($pattern, string $value): bool + { + $patterns = is_array($pattern) ? $pattern : (array) $pattern; + + if (empty($patterns)) { + return false; + } + + foreach ($patterns as $pattern) { + // If the given value is an exact match we can of course return true right + // from the beginning. Otherwise, we will translate asterisks and do an + // actual pattern match against the two strings to see if they match. + if ($pattern == $value) { + return true; + } + + $pattern = preg_quote($pattern, '#'); + + // Asterisks are translated into zero-or-more regular expression wildcards + // to make it convenient to check if the strings starts with the given + // pattern such as "library/*", making any string check convenient. + $pattern = str_replace('\*', '.*', $pattern); + + if (1 === preg_match('#^'.$pattern.'\z#u', $value)) { + return true; + } + } + + return false; + } + + /** + * Convert a string to kebab case. + */ + public static function kebab(string $value): string + { + return static::snake($value, '-'); + } + + /** + * Return the length of the given string. + * + * @param string $encoding + */ + public static function length(string $value, ?string $encoding = null): int + { + if (null !== $encoding) { + return mb_strlen($value, $encoding); + } + + return mb_strlen($value); + } + + /** + * Limit the number of characters in a string. + */ + public static function limit(string $value, int $limit = 100, string $end = '...'): string + { + if (mb_strwidth($value, 'UTF-8') <= $limit) { + return $value; + } + + return rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8')).$end; + } + + /** + * Convert the given string to lower-case. + */ + public static function lower(string $value): string + { + return mb_strtolower($value, 'UTF-8'); + } + + /** + * Limit the number of words in a string. + */ + public static function words(string $value, int $words = 100, string $end = '...'): string + { + preg_match('/^\s*+(?:\S++\s*+){1,'.$words.'}/u', $value, $matches); + + if (!isset($matches[0]) || static::length($value) === static::length($matches[0])) { + return $value; + } + + return rtrim($matches[0]).$end; + } + + /** + * Parse a Class. + */ + public static function parseCallback(string $callback, ?string $default = null): array + { + return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default]; + } + + /** + * Generate a more truly "random" alpha-numeric string. + * + * @throws Exception + */ + public static function random(int $length = 16): string + { + $string = ''; + + while (($len = strlen($string)) < $length) { + $size = $length - $len; + + $bytes = function_exists('random_bytes') ? random_bytes($size) : mt_rand(); + + $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size); + } + + return $string; + } + + /** + * Replace a given value in the string sequentially with an array. + */ + public static function replaceArray(string $search, array $replace, string $subject): string + { + foreach ($replace as $value) { + $subject = static::replaceFirst($search, $value, $subject); + } + + return $subject; + } + + /** + * Replace the first occurrence of a given value in the string. + */ + public static function replaceFirst(string $search, string $replace, string $subject): string + { + if ('' == $search) { + return $subject; + } + + $position = strpos($subject, $search); + + if (false !== $position) { + return substr_replace($subject, $replace, $position, strlen($search)); + } + + return $subject; + } + + /** + * Replace the last occurrence of a given value in the string. + */ + public static function replaceLast(string $search, string $replace, string $subject): string + { + $position = strrpos($subject, $search); + + if (false !== $position) { + return substr_replace($subject, $replace, $position, strlen($search)); + } + + return $subject; + } + + /** + * Begin a string with a single instance of a given value. + */ + public static function start(string $value, string $prefix): string + { + $quoted = preg_quote($prefix, '/'); + + return $prefix.preg_replace('/^(?:'.$quoted.')+/u', '', $value); + } + + /** + * Convert the given string to upper-case. + */ + public static function upper(string $value): string + { + return mb_strtoupper($value, 'UTF-8'); + } + + /** + * Convert the given string to title case. + */ + public static function title(string $value): string + { + return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); + } + + /** + * Generate a URL friendly "slug" from a given string. + */ + public static function slug(string $title, string $separator = '-', string $language = 'en'): string + { + $title = static::ascii($title, $language); + + // Convert all dashes/underscores into separator + $flip = '-' == $separator ? '_' : '-'; + + $title = preg_replace('!['.preg_quote($flip).']+!u', $separator, $title); + + // Replace @ with the word 'at' + $title = str_replace('@', $separator.'at'.$separator, $title); + + // Remove all characters that are not the separator, letters, numbers, or whitespace. + $title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', mb_strtolower($title)); + + // Replace all separator characters and whitespace by a single separator + $title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title); + + return trim($title, $separator); + } + + /** + * Convert a string to snake case. + */ + public static function snake(string $value, string $delimiter = '_'): string + { + $key = $value; + + if (isset(static::$snakeCache[$key][$delimiter])) { + return static::$snakeCache[$key][$delimiter]; + } + + if (!ctype_lower($value)) { + $value = preg_replace('/\s+/u', '', ucwords($value)); + + $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value)); + } + + return static::$snakeCache[$key][$delimiter] = $value; + } + + /** + * Determine if a given string starts with a given substring. + * + * @param string|array $needles + */ + public static function startsWith(string $haystack, $needles): bool + { + foreach ((array) $needles as $needle) { + if ('' !== $needle && substr($haystack, 0, strlen($needle)) === (string) $needle) { + return true; + } + } + + return false; + } + + /** + * Convert a value to studly caps case. + */ + public static function studly(string $value): string + { + $key = $value; + + if (isset(static::$studlyCache[$key])) { + return static::$studlyCache[$key]; + } + + $value = ucwords(str_replace(['-', '_'], ' ', $value)); + + return static::$studlyCache[$key] = str_replace(' ', '', $value); + } + + /** + * Returns the portion of string specified by the start and length parameters. + */ + public static function substr(string $string, int $start, ?int $length = null): string + { + return mb_substr($string, $start, $length, 'UTF-8'); + } + + /** + * Make a string's first character uppercase. + */ + public static function ucfirst(string $string): string + { + return static::upper(static::substr($string, 0, 1)).static::substr($string, 1); + } + + /** + * Convert string's encoding. + * + * @author yansongda <me@yansonga.cn> + */ + public static function encoding(string $string, string $to = 'utf-8', string $from = 'gb2312'): string + { + return mb_convert_encoding($string, $to, $from); + } + + /** + * Returns the replacements for the ascii method. + * + * Note: Adapted from Stringy\Stringy. + * + * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt + */ + protected static function charsArray(): array + { + static $charsArray; + + if (isset($charsArray)) { + return $charsArray; + } + + return $charsArray = [ + '0' => ['°', '₀', '۰', '0'], + '1' => ['¹', '₁', '۱', '1'], + '2' => ['²', '₂', '۲', '2'], + '3' => ['³', '₃', '۳', '3'], + '4' => ['⁴', '₄', '۴', '٤', '4'], + '5' => ['⁵', '₅', '۵', '٥', '5'], + '6' => ['⁶', '₆', '۶', '٦', '6'], + '7' => ['⁷', '₇', '۷', '7'], + '8' => ['⁸', '₈', '۸', '8'], + '9' => ['⁹', '₉', '۹', '9'], + 'a' => ['à', 'á', 'ả', 'ã', 'ạ', 'ă', 'ắ', 'ằ', 'ẳ', 'ẵ', 'ặ', 'â', 'ấ', 'ầ', 'ẩ', 'ẫ', 'ậ', 'ā', 'ą', 'å', 'α', 'ά', 'ἀ', 'ἁ', 'ἂ', 'ἃ', 'ἄ', 'ἅ', 'ἆ', 'ἇ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ὰ', 'ά', 'ᾰ', 'ᾱ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'а', 'أ', 'အ', 'ာ', 'ါ', 'ǻ', 'ǎ', 'ª', 'ა', 'अ', 'ا', 'a', 'ä'], + 'b' => ['б', 'β', 'ب', 'ဗ', 'ბ', 'b'], + 'c' => ['ç', 'ć', 'č', 'ĉ', 'ċ', 'c'], + 'd' => ['ď', 'ð', 'đ', 'ƌ', 'ȡ', 'ɖ', 'ɗ', 'ᵭ', 'ᶁ', 'ᶑ', 'д', 'δ', 'د', 'ض', 'ဍ', 'ဒ', 'დ', 'd'], + 'e' => ['é', 'è', 'ẻ', 'ẽ', 'ẹ', 'ê', 'ế', 'ề', 'ể', 'ễ', 'ệ', 'ë', 'ē', 'ę', 'ě', 'ĕ', 'ė', 'ε', 'έ', 'ἐ', 'ἑ', 'ἒ', 'ἓ', 'ἔ', 'ἕ', 'ὲ', 'έ', 'е', 'ё', 'э', 'є', 'ə', 'ဧ', 'ေ', 'ဲ', 'ე', 'ए', 'إ', 'ئ', 'e'], + 'f' => ['ф', 'φ', 'ف', 'ƒ', 'ფ', 'f'], + 'g' => ['ĝ', 'ğ', 'ġ', 'ģ', 'г', 'ґ', 'γ', 'ဂ', 'გ', 'گ', 'g'], + 'h' => ['ĥ', 'ħ', 'η', 'ή', 'ح', 'ه', 'ဟ', 'ှ', 'ჰ', 'h'], + 'i' => ['í', 'ì', 'ỉ', 'ĩ', 'ị', 'î', 'ï', 'ī', 'ĭ', 'į', 'ı', 'ι', 'ί', 'ϊ', 'ΐ', 'ἰ', 'ἱ', 'ἲ', 'ἳ', 'ἴ', 'ἵ', 'ἶ', 'ἷ', 'ὶ', 'ί', 'ῐ', 'ῑ', 'ῒ', 'ΐ', 'ῖ', 'ῗ', 'і', 'ї', 'и', 'ဣ', 'ိ', 'ီ', 'ည်', 'ǐ', 'ი', 'इ', 'ی', 'i'], + 'j' => ['ĵ', 'ј', 'Ј', 'ჯ', 'ج', 'j'], + 'k' => ['ķ', 'ĸ', 'к', 'κ', 'Ķ', 'ق', 'ك', 'က', 'კ', 'ქ', 'ک', 'k'], + 'l' => ['ł', 'ľ', 'ĺ', 'ļ', 'ŀ', 'л', 'λ', 'ل', 'လ', 'ლ', 'l'], + 'm' => ['м', 'μ', 'م', 'မ', 'მ', 'm'], + 'n' => ['ñ', 'ń', 'ň', 'ņ', 'ʼn', 'ŋ', 'ν', 'н', 'ن', 'န', 'ნ', 'n'], + 'o' => ['ó', 'ò', 'ỏ', 'õ', 'ọ', 'ô', 'ố', 'ồ', 'ổ', 'ỗ', 'ộ', 'ơ', 'ớ', 'ờ', 'ở', 'ỡ', 'ợ', 'ø', 'ō', 'ő', 'ŏ', 'ο', 'ὀ', 'ὁ', 'ὂ', 'ὃ', 'ὄ', 'ὅ', 'ὸ', 'ό', 'о', 'و', 'θ', 'ို', 'ǒ', 'ǿ', 'º', 'ო', 'ओ', 'o', 'ö'], + 'p' => ['п', 'π', 'ပ', 'პ', 'پ', 'p'], + 'q' => ['ყ', 'q'], + 'r' => ['ŕ', 'ř', 'ŗ', 'р', 'ρ', 'ر', 'რ', 'r'], + 's' => ['ś', 'š', 'ş', 'с', 'σ', 'ș', 'ς', 'س', 'ص', 'စ', 'ſ', 'ს', 's'], + 't' => ['ť', 'ţ', 'т', 'τ', 'ț', 'ت', 'ط', 'ဋ', 'တ', 'ŧ', 'თ', 'ტ', 't'], + 'u' => ['ú', 'ù', 'ủ', 'ũ', 'ụ', 'ư', 'ứ', 'ừ', 'ử', 'ữ', 'ự', 'û', 'ū', 'ů', 'ű', 'ŭ', 'ų', 'µ', 'у', 'ဉ', 'ု', 'ူ', 'ǔ', 'ǖ', 'ǘ', 'ǚ', 'ǜ', 'უ', 'उ', 'u', 'ў', 'ü'], + 'v' => ['в', 'ვ', 'ϐ', 'v'], + 'w' => ['ŵ', 'ω', 'ώ', 'ဝ', 'ွ', 'w'], + 'x' => ['χ', 'ξ', 'x'], + 'y' => ['ý', 'ỳ', 'ỷ', 'ỹ', 'ỵ', 'ÿ', 'ŷ', 'й', 'ы', 'υ', 'ϋ', 'ύ', 'ΰ', 'ي', 'ယ', 'y'], + 'z' => ['ź', 'ž', 'ż', 'з', 'ζ', 'ز', 'ဇ', 'ზ', 'z'], + 'aa' => ['ع', 'आ', 'آ'], + 'ae' => ['æ', 'ǽ'], + 'ai' => ['ऐ'], + 'ch' => ['ч', 'ჩ', 'ჭ', 'چ'], + 'dj' => ['ђ', 'đ'], + 'dz' => ['џ', 'ძ'], + 'ei' => ['ऍ'], + 'gh' => ['غ', 'ღ'], + 'ii' => ['ई'], + 'ij' => ['ij'], + 'kh' => ['х', 'خ', 'ხ'], + 'lj' => ['љ'], + 'nj' => ['њ'], + 'oe' => ['ö', 'œ', 'ؤ'], + 'oi' => ['ऑ'], + 'oii' => ['ऒ'], + 'ps' => ['ψ'], + 'sh' => ['ш', 'შ', 'ش'], + 'shch' => ['щ'], + 'ss' => ['ß'], + 'sx' => ['ŝ'], + 'th' => ['þ', 'ϑ', 'ث', 'ذ', 'ظ'], + 'ts' => ['ц', 'ც', 'წ'], + 'ue' => ['ü'], + 'uu' => ['ऊ'], + 'ya' => ['я'], + 'yu' => ['ю'], + 'zh' => ['ж', 'ჟ', 'ژ'], + '(c)' => ['©'], + 'A' => ['Á', 'À', 'Ả', 'Ã', 'Ạ', 'Ă', 'Ắ', 'Ằ', 'Ẳ', 'Ẵ', 'Ặ', 'Â', 'Ấ', 'Ầ', 'Ẩ', 'Ẫ', 'Ậ', 'Å', 'Ā', 'Ą', 'Α', 'Ά', 'Ἀ', 'Ἁ', 'Ἂ', 'Ἃ', 'Ἄ', 'Ἅ', 'Ἆ', 'Ἇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'Ᾰ', 'Ᾱ', 'Ὰ', 'Ά', 'ᾼ', 'А', 'Ǻ', 'Ǎ', 'A', 'Ä'], + 'B' => ['Б', 'Β', 'ब', 'B'], + 'C' => ['Ç', 'Ć', 'Č', 'Ĉ', 'Ċ', 'C'], + 'D' => ['Ď', 'Ð', 'Đ', 'Ɖ', 'Ɗ', 'Ƌ', 'ᴅ', 'ᴆ', 'Д', 'Δ', 'D'], + 'E' => ['É', 'È', 'Ẻ', 'Ẽ', 'Ẹ', 'Ê', 'Ế', 'Ề', 'Ể', 'Ễ', 'Ệ', 'Ë', 'Ē', 'Ę', 'Ě', 'Ĕ', 'Ė', 'Ε', 'Έ', 'Ἐ', 'Ἑ', 'Ἒ', 'Ἓ', 'Ἔ', 'Ἕ', 'Έ', 'Ὲ', 'Е', 'Ё', 'Э', 'Є', 'Ə', 'E'], + 'F' => ['Ф', 'Φ', 'F'], + 'G' => ['Ğ', 'Ġ', 'Ģ', 'Г', 'Ґ', 'Γ', 'G'], + 'H' => ['Η', 'Ή', 'Ħ', 'H'], + 'I' => ['Í', 'Ì', 'Ỉ', 'Ĩ', 'Ị', 'Î', 'Ï', 'Ī', 'Ĭ', 'Į', 'İ', 'Ι', 'Ί', 'Ϊ', 'Ἰ', 'Ἱ', 'Ἳ', 'Ἴ', 'Ἵ', 'Ἶ', 'Ἷ', 'Ῐ', 'Ῑ', 'Ὶ', 'Ί', 'И', 'І', 'Ї', 'Ǐ', 'ϒ', 'I'], + 'J' => ['J'], + 'K' => ['К', 'Κ', 'K'], + 'L' => ['Ĺ', 'Ł', 'Л', 'Λ', 'Ļ', 'Ľ', 'Ŀ', 'ल', 'L'], + 'M' => ['М', 'Μ', 'M'], + 'N' => ['Ń', 'Ñ', 'Ň', 'Ņ', 'Ŋ', 'Н', 'Ν', 'N'], + 'O' => ['Ó', 'Ò', 'Ỏ', 'Õ', 'Ọ', 'Ô', 'Ố', 'Ồ', 'Ổ', 'Ỗ', 'Ộ', 'Ơ', 'Ớ', 'Ờ', 'Ở', 'Ỡ', 'Ợ', 'Ø', 'Ō', 'Ő', 'Ŏ', 'Ο', 'Ό', 'Ὀ', 'Ὁ', 'Ὂ', 'Ὃ', 'Ὄ', 'Ὅ', 'Ὸ', 'Ό', 'О', 'Θ', 'Ө', 'Ǒ', 'Ǿ', 'O', 'Ö'], + 'P' => ['П', 'Π', 'P'], + 'Q' => ['Q'], + 'R' => ['Ř', 'Ŕ', 'Р', 'Ρ', 'Ŗ', 'R'], + 'S' => ['Ş', 'Ŝ', 'Ș', 'Š', 'Ś', 'С', 'Σ', 'S'], + 'T' => ['Ť', 'Ţ', 'Ŧ', 'Ț', 'Т', 'Τ', 'T'], + 'U' => ['Ú', 'Ù', 'Ủ', 'Ũ', 'Ụ', 'Ư', 'Ứ', 'Ừ', 'Ử', 'Ữ', 'Ự', 'Û', 'Ū', 'Ů', 'Ű', 'Ŭ', 'Ų', 'У', 'Ǔ', 'Ǖ', 'Ǘ', 'Ǚ', 'Ǜ', 'U', 'Ў', 'Ü'], + 'V' => ['В', 'V'], + 'W' => ['Ω', 'Ώ', 'Ŵ', 'W'], + 'X' => ['Χ', 'Ξ', 'X'], + 'Y' => ['Ý', 'Ỳ', 'Ỷ', 'Ỹ', 'Ỵ', 'Ÿ', 'Ῠ', 'Ῡ', 'Ὺ', 'Ύ', 'Ы', 'Й', 'Υ', 'Ϋ', 'Ŷ', 'Y'], + 'Z' => ['Ź', 'Ž', 'Ż', 'З', 'Ζ', 'Z'], + 'AE' => ['Æ', 'Ǽ'], + 'Ch' => ['Ч'], + 'Dj' => ['Ђ'], + 'Dz' => ['Џ'], + 'Gx' => ['Ĝ'], + 'Hx' => ['Ĥ'], + 'Ij' => ['IJ'], + 'Jx' => ['Ĵ'], + 'Kh' => ['Х'], + 'Lj' => ['Љ'], + 'Nj' => ['Њ'], + 'Oe' => ['Œ'], + 'Ps' => ['Ψ'], + 'Sh' => ['Ш'], + 'Shch' => ['Щ'], + 'Ss' => ['ẞ'], + 'Th' => ['Þ'], + 'Ts' => ['Ц'], + 'Ya' => ['Я'], + 'Yu' => ['Ю'], + 'Zh' => ['Ж'], + ' ' => ["\xC2\xA0", "\xE2\x80\x80", "\xE2\x80\x81", "\xE2\x80\x82", "\xE2\x80\x83", "\xE2\x80\x84", "\xE2\x80\x85", "\xE2\x80\x86", "\xE2\x80\x87", "\xE2\x80\x88", "\xE2\x80\x89", "\xE2\x80\x8A", "\xE2\x80\xAF", "\xE2\x81\x9F", "\xE3\x80\x80", "\xEF\xBE\xA0"], + ]; + } + + /** + * Returns the language specific replacements for the ascii method. + * + * Note: Adapted from Stringy\Stringy. + * + * @see https://github.com/danielstjules/Stringy/blob/3.1.0/LICENSE.txt + */ + protected static function languageSpecificCharsArray(string $language): ?array + { + static $languageSpecific; + if (!isset($languageSpecific)) { + $languageSpecific = [ + 'bg' => [ + ['х', 'Х', 'щ', 'Щ', 'ъ', 'Ъ', 'ь', 'Ь'], + ['h', 'H', 'sht', 'SHT', 'a', 'А', 'y', 'Y'], + ], + 'de' => [ + ['ä', 'ö', 'ü', 'Ä', 'Ö', 'Ü'], + ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], + ], + ]; + } + + return isset($languageSpecific[$language]) ? $languageSpecific[$language] : null; + } +} diff --git a/addons/epay/library/Yansongda/Supports/Traits/Accessable.php b/addons/epay/library/Yansongda/Supports/Traits/Accessable.php new file mode 100644 index 0000000..a477561 --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Traits/Accessable.php @@ -0,0 +1,142 @@ +<?php + +declare(strict_types=1); + +namespace Yansongda\Supports\Traits; + +trait Accessable +{ + /** + * __get. + * + * @author yansongda <me@yansongda.cn> + * + * @return mixed + */ + public function __get(string $key) + { + return $this->get($key); + } + + /** + * __set. + * + * @author yansongda <me@yansongda.cn> + * + * @param mixed $value + * + * @return mixed + */ + public function __set(string $key, $value) + { + return $this->set($key, $value); + } + + /** + * get. + * + * @author yansongda <me@yansongda.cn> + * + * @param mixed $default + * + * @return mixed + */ + public function get(?string $key = null, $default = null) + { + if (is_null($key)) { + return method_exists($this, 'toArray') ? $this->toArray() : $default; + } + + $method = 'get'; + foreach (explode('_', $key) as $item) { + $method .= ucfirst($item); + } + + if (method_exists($this, $method)) { + return $this->{$method}(); + } + + return $default; + } + + /** + * set. + * + * @author yansongda <me@yansongda.cn> + * + * @param mixed $value + * + * @return $this + */ + public function set(string $key, $value) + { + $method = 'set'; + foreach (explode('_', $key) as $item) { + $method .= ucfirst($item); + } + + if (method_exists($this, $method)) { + return $this->{$method}($value); + } + + return $this; + } + + /** + * Whether a offset exists. + * + * @see https://php.net/manual/en/arrayaccess.offsetexists.php + * + * @param mixed $offset an offset to check for + * + * @return bool true on success or false on failure. + * + * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($offset) + { + return !is_null($this->get($offset)); + } + + /** + * Offset to retrieve. + * + * @see https://php.net/manual/en/arrayaccess.offsetget.php + * + * @param mixed $offset the offset to retrieve + * + * @return mixed can return all value types + */ + public function offsetGet($offset) + { + return $this->get($offset); + } + + /** + * Offset to set. + * + * @see https://php.net/manual/en/arrayaccess.offsetset.php + * + * @param mixed $offset the offset to assign the value to + * @param mixed $value the value to set + * + * @return void + */ + public function offsetSet($offset, $value) + { + $this->set($offset, $value); + } + + /** + * Offset to unset. + * + * @see https://php.net/manual/en/arrayaccess.offsetunset.php + * + * @param mixed $offset the offset to unset + * + * @return void + */ + public function offsetUnset($offset) + { + } +} diff --git a/addons/epay/library/Yansongda/Supports/Traits/Arrayable.php b/addons/epay/library/Yansongda/Supports/Traits/Arrayable.php new file mode 100644 index 0000000..eb73c6c --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Traits/Arrayable.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +namespace Yansongda\Supports\Traits; + +use ReflectionClass; +use Yansongda\Supports\Str; + +trait Arrayable +{ + /** + * toArray. + * + * @author yansongda <me@yansongda.cn> + * + * @throws \ReflectionException + */ + public function toArray(): array + { + $result = []; + + foreach ((new ReflectionClass($this))->getProperties() as $item) { + $k = $item->getName(); + $method = 'get'.Str::studly($k); + + $result[Str::snake($k)] = method_exists($this, $method) ? $this->{$method}() : $this->{$k}; + } + + return $result; + } +} diff --git a/addons/epay/library/Yansongda/Supports/Traits/HasHttpRequest.php b/addons/epay/library/Yansongda/Supports/Traits/HasHttpRequest.php new file mode 100644 index 0000000..6e98d24 --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Traits/HasHttpRequest.php @@ -0,0 +1,229 @@ +<?php + +namespace Yansongda\Supports\Traits; + +use GuzzleHttp\Client; +use Psr\Http\Message\ResponseInterface; + +/** + * Trait HasHttpRequest. + * + * @property string $baseUri + * @property float $timeout + * @property float $connectTimeout + */ +trait HasHttpRequest +{ + /** + * Http client. + * + * @var Client|null + */ + protected $httpClient = null; + + /** + * Http client options. + * + * @var array + */ + protected $httpOptions = []; + + /** + * Send a GET request. + * + * @author yansongda <me@yansongda.cn> + * + * @return array|string + */ + public function get(string $endpoint, array $query = [], array $headers = []) + { + return $this->request('get', $endpoint, [ + 'headers' => $headers, + 'query' => $query, + ]); + } + + /** + * Send a POST request. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|array $data + * + * @return array|string + */ + public function post(string $endpoint, $data, array $options = []) + { + if (!is_array($data)) { + $options['body'] = $data; + } else { + $options['form_params'] = $data; + } + + return $this->request('post', $endpoint, $options); + } + + /** + * Send request. + * + * @author yansongda <me@yansongda.cn> + * + * @return array|string + */ + public function request(string $method, string $endpoint, array $options = []) + { + return $this->unwrapResponse($this->getHttpClient()->{$method}($endpoint, $options)); + } + + /** + * Set http client. + * + * @author yansongda <me@yansongda.cn> + * + * @return $this + */ + public function setHttpClient(Client $client): self + { + $this->httpClient = $client; + + return $this; + } + + /** + * Return http client. + */ + public function getHttpClient(): Client + { + if (is_null($this->httpClient)) { + $this->httpClient = $this->getDefaultHttpClient(); + } + + return $this->httpClient; + } + + /** + * Get default http client. + * + * @author yansongda <me@yansongda.cn> + */ + public function getDefaultHttpClient(): Client + { + return new Client($this->getOptions()); + } + + /** + * setBaseUri. + * + * @author yansongda <me@yansongda.cn> + * + * @return $this + */ + public function setBaseUri(string $url): self + { + if (property_exists($this, 'baseUri')) { + $parsedUrl = parse_url($url); + + $this->baseUri = ($parsedUrl['scheme'] ?? 'http').'://'. + $parsedUrl['host'].(isset($parsedUrl['port']) ? (':'.$parsedUrl['port']) : ''); + } + + return $this; + } + + /** + * getBaseUri. + * + * @author yansongda <me@yansongda.cn> + */ + public function getBaseUri(): string + { + return property_exists($this, 'baseUri') ? $this->baseUri : ''; + } + + public function getTimeout(): float + { + return property_exists($this, 'timeout') ? $this->timeout : 5.0; + } + + public function setTimeout(float $timeout): self + { + if (property_exists($this, 'timeout')) { + $this->timeout = $timeout; + } + + return $this; + } + + public function getConnectTimeout(): float + { + return property_exists($this, 'connectTimeout') ? $this->connectTimeout : 3.0; + } + + public function setConnectTimeout(float $connectTimeout): self + { + if (property_exists($this, 'connectTimeout')) { + $this->connectTimeout = $connectTimeout; + } + + return $this; + } + + /** + * Get default options. + * + * @author yansongda <me@yansongda.cn> + */ + public function getOptions(): array + { + return array_merge([ + 'base_uri' => $this->getBaseUri(), + 'timeout' => $this->getTimeout(), + 'connect_timeout' => $this->getConnectTimeout(), + ], $this->getHttpOptions()); + } + + /** + * setOptions. + * + * @author yansongda <me@yansongda.cn> + * + * @return $this + */ + public function setOptions(array $options): self + { + return $this->setHttpOptions($options); + } + + public function getHttpOptions(): array + { + return $this->httpOptions; + } + + public function setHttpOptions(array $httpOptions): self + { + $this->httpOptions = $httpOptions; + + return $this; + } + + /** + * Convert response. + * + * @author yansongda <me@yansongda.cn> + * + * @return array|string + */ + public function unwrapResponse(ResponseInterface $response) + { + $contentType = $response->getHeaderLine('Content-Type'); + $contents = $response->getBody()->getContents(); + + if (false !== stripos($contentType, 'json') || stripos($contentType, 'javascript')) { + return json_decode($contents, true); + } elseif (false !== stripos($contentType, 'xml')) { + return json_decode(json_encode(simplexml_load_string($contents, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true); + } + + return $contents; + } +} diff --git a/addons/epay/library/Yansongda/Supports/Traits/Serializable.php b/addons/epay/library/Yansongda/Supports/Traits/Serializable.php new file mode 100644 index 0000000..5e2010b --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Traits/Serializable.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Yansongda\Supports\Traits; + +use RuntimeException; + +trait Serializable +{ + /** + * toJson. + * + * @author yansongda <me@yansongda.cn> + * + * @return string + */ + public function toJson() + { + return $this->serialize(); + } + + /** + * Specify data which should be serialized to JSON. + * + * @see https://php.net/manual/en/jsonserializable.jsonserialize.php + * + * @return mixed data which can be serialized by <b>json_encode</b>, + * which is a value of any type other than a resource + * + * @since 5.4.0 + */ + public function jsonSerialize() + { + if (method_exists($this, 'toArray')) { + return $this->toArray(); + } + + return []; + } + + /** + * String representation of object. + * + * @see https://php.net/manual/en/serializable.serialize.php + * + * @return string the string representation of the object or null + * + * @since 5.1.0 + */ + public function serialize() + { + if (method_exists($this, 'toArray')) { + return json_encode($this->toArray()); + } + + return json_encode([]); + } + + /** + * Constructs the object. + * + * @see https://php.net/manual/en/serializable.unserialize.php + * + * @param string $serialized <p> + * The string representation of the object. + * </p> + * + * @since 5.1.0 + */ + public function unserialize($serialized) + { + $data = json_decode($serialized, true); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new RuntimeException('Invalid Json Format'); + } + + foreach ($data as $key => $item) { + if (method_exists($this, 'set')) { + $this->set($key, $item); + } + } + } +} diff --git a/addons/epay/library/Yansongda/Supports/Traits/ShouldThrottle.php b/addons/epay/library/Yansongda/Supports/Traits/ShouldThrottle.php new file mode 100644 index 0000000..cbdeb71 --- /dev/null +++ b/addons/epay/library/Yansongda/Supports/Traits/ShouldThrottle.php @@ -0,0 +1,147 @@ +<?php + +namespace Yansongda\Supports\Traits; + +use Predis\Client; + +/** + * Trait ShouldThrottle. + * + * @property Client $redis + */ +trait ShouldThrottle +{ + /** + * _throttle. + * + * @var array + */ + protected $_throttle = [ + 'limit' => 60, + 'period' => 60, + 'count' => 0, + 'reset_time' => 0, + ]; + + /** + * isThrottled. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $key + * @param int $limit + * @param int $period + * @param bool $auto_add + * + * @return bool + */ + public function isThrottled($key, $limit = 60, $period = 60, $auto_add = false) + { + if (-1 === $limit) { + return false; + } + + $now = microtime(true) * 1000; + + $this->redis->zremrangebyscore($key, 0, $now - $period * 1000); + + $this->_throttle = [ + 'limit' => $limit, + 'period' => $period, + 'count' => $this->getThrottleCounts($key, $period), + 'reset_time' => $this->getThrottleResetTime($key, $now), + ]; + + if ($this->_throttle['count'] < $limit) { + if ($auto_add) { + $this->throttleAdd($key, $period); + } + + return false; + } + + return true; + } + + /** + * 限流 + 1. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $key + * @param int $period + */ + public function throttleAdd($key, $period = 60) + { + $now = microtime(true) * 1000; + + $this->redis->zadd($key, [$now => $now]); + $this->redis->expire($key, $period * 2); + } + + /** + * getResetTime. + * + * @author yansongda <me@yansongda.cn> + * + * @param $key + * @param $now + * + * @return int + */ + public function getThrottleResetTime($key, $now) + { + $data = $this->redis->zrangebyscore( + $key, + $now - $this->_throttle['period'] * 1000, + $now, + ['limit' => [0, 1]] + ); + + if (0 === count($data)) { + return $this->_throttle['reset_time'] = time() + $this->_throttle['period']; + } + + return intval($data[0] / 1000) + $this->_throttle['period']; + } + + /** + * 获取限流相关信息. + * + * @author yansongda <me@yansongda.cn> + * + * @param string|null $key + * @param mixed|null $default + * + * @return array|null + */ + public function getThrottleInfo($key = null, $default = null) + { + if (is_null($key)) { + return $this->_throttle; + } + + if (isset($this->_throttle[$key])) { + return $this->_throttle[$key]; + } + + return $default; + } + + /** + * 获取已使用次数. + * + * @author yansongda <me@yansongda.cn> + * + * @param string $key + * @param int $period + * + * @return string + */ + public function getThrottleCounts($key, $period = 60) + { + $now = microtime(true) * 1000; + + return $this->redis->zcount($key, $now - $period * 1000, $now); + } +} diff --git a/addons/epay/view/api/alipay.html b/addons/epay/view/api/alipay.html new file mode 100644 index 0000000..b2e2b7e --- /dev/null +++ b/addons/epay/view/api/alipay.html @@ -0,0 +1,47 @@ +<div class="container"> + <h2 class="scanpay-title"> + <img src="__ADDON__/images/logo-alipay.png" alt="" height="32" class="pull-left" style="margin-right:5px;"> 支付宝支付 + <div class="scanpay-time"> + 请在 <span>60</span> 秒内完成支付 + </div> + </h2> + + <div class="scanpay scanpay-alipay"> + <div class="row"> + <div class="col-xs-12 col-sm-12"> + <div class="row"> + <div class="col-xs-12 col-sm-5"> + <div class="scanpay-body"> + <div class="scanpay-order clearfix"> + <p>订单标题:<em>{$orderData.title}</em></p> + <p>订单编号:<em>{$orderData.orderid}</em></p> + <p>订单价格:<em class="scanpay-price">¥{$orderData.amount}</em> 元</p> + </div> + <div class="scanpay-qrcode"> + <img src="{:addon_url('epay/api/qrcode',[],false)}?text={$payData.qr_code}"> + <div class="expired hidden"></div> + <div class="paid hidden"></div> + </div> + <div class="scanpay-tips"> + <p>请使用支付宝扫一扫<br>扫描二维码支付</p> + </div> + </div> + </div> + <div class="col-sm-1"></div> + <div class="col-sm-6 hidden-xs"> + <div class="scanpay-screenshot"> + <img src="__ADDON__/images/screenshot-alipay.png" class="img-responsive" alt=""/> + </div> + </div> + </div> + </div> + </div> + </div> + +</div> + +<!--@formatter:off--> +<script> + var queryParams = {"paytype":"alipay", "orderid":"{$orderData.orderid}", "returnurl":"{$orderData.returnurl}"}; +</script> +<!--@formatter:on--> diff --git a/addons/epay/view/api/wechat.html b/addons/epay/view/api/wechat.html new file mode 100644 index 0000000..cda1611 --- /dev/null +++ b/addons/epay/view/api/wechat.html @@ -0,0 +1,90 @@ +{if $type=='jsapi'} +<div class="container"> + <div class="row" style="margin-top:20px;"> + <div class="col-xs-12"> + <button type="button" class="btn btn-success btn-lg btn-block">正在发起微信支付</button> + <button type="button" class="btn btn-default btn-lg btn-block" onclick="location.href='{$orderData.returnurl}'">如果页面未自动跳转</button> + </div> + </div> +</div> +<!--@formatter:off--> +<script> + function onBridgeReady() { + WeixinJSBridge.invoke('getBrandWCPayRequest', {$payData|json_encode}, function(res) { + if (res.err_msg == "get_brand_wcpay_request:ok") { + layer.msg('支付成功!'); + } else if (res.err_msg == "get_brand_wcpay_request:cancel") { + layer.msg('您取消了支付'); + } else if (res.err_msg == "get_brand_wcpay_request:fail") { + layer.msg('支付失败'); + }else{ + layer.msg(typeof res.err_msg!='undefined' ? res.err_msg : (typeof res.errMsg !=='undefined' ? res.errMsg : "未知支付状态")); + } + setTimeout(function () { + location.href = '{$orderData.returnurl}'; + }, 1500); + }); + } + + if (typeof WeixinJSBridge == "undefined") { + if (document.addEventListener) { + document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false); + } else if (document.attachEvent) { + document.attachEvent('WeixinJSBridgeReady', onBridgeReady); + document.attachEvent('onWeixinJSBridgeReady', onBridgeReady); + } + } else { + onBridgeReady(); + } +</script> +<!--@formatter:on--> + +{elseif $type=='pc' /} +<div class="container"> + <h2 class="scanpay-title"> + <img src="__ADDON__/images/logo-wechat.png" alt="" height="32" class="pull-left" style="margin-right:5px;"> 微信支付 + <div class="scanpay-time"> + 请在 <span>60</span> 秒内完成支付 + </div> + </h2> + + <div class="scanpay scanpay-wechat"> + <div class="row"> + <div class="col-xs-12 col-sm-12"> + <div class="row"> + <div class="col-xs-12 col-sm-5"> + <div class="scanpay-body"> + <div class="scanpay-order clearfix"> + <p>订单标题:<em>{$orderData.title}</em></p> + <p>订单编号:<em>{$orderData.orderid}</em></p> + <p>订单价格:<em class="scanpay-price">¥{$orderData.amount}</em> 元</p> + </div> + <div class="scanpay-qrcode"> + <img src="{:addon_url('epay/api/qrcode',[],false)}?text={$payData.code_url}"> + <div class="expired hidden"></div> + <div class="paid hidden"></div> + </div> + <div class="scanpay-tips"> + <p>请使用微信扫一扫<br>扫描二维码支付</p> + </div> + </div> + </div> + <div class="col-sm-1"></div> + <div class="col-sm-6 hidden-xs"> + <div class="scanpay-screenshot"> + <img src="__ADDON__/images/screenshot-wechat.png" class="img-responsive" alt=""/> + </div> + </div> + </div> + </div> + </div> + </div> + +</div> + +<!--@formatter:off--> +<script> + var queryParams = {"paytype":"wechat", "orderid":"{$orderData.orderid}", "returnurl":"{$orderData.returnurl}"}; +</script> +<!--@formatter:on--> +{/if} diff --git a/addons/epay/view/index/index.html b/addons/epay/view/index/index.html new file mode 100644 index 0000000..e369d16 --- /dev/null +++ b/addons/epay/view/index/index.html @@ -0,0 +1,92 @@ +<!-- Header Carousel --> +<header id="myCarousel" class="carousel slide"> + <!-- Indicators --> + <ol class="carousel-indicators"> + <li data-target="#myCarousel" data-slide-to="0" class="active"></li> + <li data-target="#myCarousel" data-slide-to="1"></li> + <li data-target="#myCarousel" data-slide-to="2"></li> + <li data-target="#myCarousel" data-slide-to="3"></li> + </ol> + + <!-- Wrapper for slides --> + <div class="carousel-inner"> + <div class="item active"> + <a href="javascript:" target="_blank"> + <div class="fill" + style="background-image:url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzOTMiIGhlaWdodD0iMjI2Ij48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSJyZ2IoNjMsIDE0MywgODUpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iI2RkZCIgZmlsbC1vcGFjaXR5PSIwLjE0MTMzMzMzMzMzMzMzIiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLCAtMzcuODMzMzMzMzMzMzMzKSByb3RhdGUoMTgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjZGRkIiBmaWxsLW9wYWNpdHk9IjAuMTQxMzMzMzMzMzMzMzMiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsIDE4OS4xNjY2NjY2NjY2Nykgcm90YXRlKDE4MCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iIzIyMiIgZmlsbC1vcGFjaXR5PSIwLjA2MzMzMzMzMzMzMzMzMyIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuNTI5MjU1NTUzMDIzLCAtMzcuODMzMzMzMzMzMzMzKSByb3RhdGUoMCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iIzIyMiIgZmlsbC1vcGFjaXR5PSIwLjA2MzMzMzMzMzMzMzMzMyIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuNTI5MjU1NTUzMDIzLCAxODkuMTY2NjY2NjY2NjcpIHJvdGF0ZSgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDgwNjY2NjY2NjY2NjY3IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMzEuMDU4NTExMTA2MDUsIC0zNy44MzMzMzMzMzMzMzMpIHJvdGF0ZSgxODAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiMyMjIiIGZpbGwtb3BhY2l0eT0iMC4wODA2NjY2NjY2NjY2NjciIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEzMS4wNTg1MTExMDYwNSwgMTg5LjE2NjY2NjY2NjY3KSByb3RhdGUoMTgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDQ2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOTYuNTg3NzY2NjU5MDcsIC0zNy44MzMzMzMzMzMzMzMpIHJvdGF0ZSgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDQ2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOTYuNTg3NzY2NjU5MDcsIDE4OS4xNjY2NjY2NjY2Nykgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiNkZGQiIGZpbGwtb3BhY2l0eT0iMC4wODkzMzMzMzMzMzMzMzMiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2Mi4xMTcwMjIyMTIwOSwgLTM3LjgzMzMzMzMzMzMzMykgcm90YXRlKDE4MCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iI2RkZCIgZmlsbC1vcGFjaXR5PSIwLjA4OTMzMzMzMzMzMzMzMyIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYyLjExNzAyMjIxMjA5LCAxODkuMTY2NjY2NjY2NjcpIHJvdGF0ZSgxODAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiNkZGQiIGZpbGwtb3BhY2l0eT0iMC4xNDEzMzMzMzMzMzMzMyIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzI3LjY0NjI3Nzc2NTExLCAtMzcuODMzMzMzMzMzMzMzKSByb3RhdGUoMCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iI2RkZCIgZmlsbC1vcGFjaXR5PSIwLjE0MTMzMzMzMzMzMzMzIiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMjcuNjQ2Mjc3NzY1MTEsIDE4OS4xNjY2NjY2NjY2Nykgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiNkZGQiIGZpbGwtb3BhY2l0eT0iMC4wMiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwgMCkgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiMyMjIiIGZpbGwtb3BhY2l0eT0iMC4wMjg2NjY2NjY2NjY2NjciIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDY1LjUyOTI1NTU1MzAyMywgMCkgcm90YXRlKDE4MCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iIzIyMiIgZmlsbC1vcGFjaXR5PSIwLjExNTMzMzMzMzMzMzMzIiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMzEuMDU4NTExMTA2MDUsIDApIHJvdGF0ZSgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDgwNjY2NjY2NjY2NjY3IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOTYuNTg3NzY2NjU5MDcsIDApIHJvdGF0ZSgxODAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiMyMjIiIGZpbGwtb3BhY2l0eT0iMC4wODA2NjY2NjY2NjY2NjciIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2Mi4xMTcwMjIyMTIwOSwgMCkgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiNkZGQiIGZpbGwtb3BhY2l0eT0iMC4wMiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzI3LjY0NjI3Nzc2NTExLCAwKSByb3RhdGUoMTgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDgwNjY2NjY2NjY2NjY3IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLCAzNy44MzMzMzMzMzMzMzMpIHJvdGF0ZSgxODAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiNkZGQiIGZpbGwtb3BhY2l0eT0iMC4wMiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMpIHJvdGF0ZSgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDk4IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMzEuMDU4NTExMTA2MDUsIDM3LjgzMzMzMzMzMzMzMykgcm90YXRlKDE4MCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iIzIyMiIgZmlsbC1vcGFjaXR5PSIwLjE1IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOTYuNTg3NzY2NjU5MDcsIDM3LjgzMzMzMzMzMzMzMykgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiMyMjIiIGZpbGwtb3BhY2l0eT0iMC4wMjg2NjY2NjY2NjY2NjciIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2Mi4xMTcwMjIyMTIwOSwgMzcuODMzMzMzMzMzMzMzKSByb3RhdGUoMTgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDQ2IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMjcuNjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiNkZGQiIGZpbGwtb3BhY2l0eT0iMC4wMzczMzMzMzMzMzMzMzMiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsIDc1LjY2NjY2NjY2NjY2Nykgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiNkZGQiIGZpbGwtb3BhY2l0eT0iMC4wMiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjUuNTI5MjU1NTUzMDIzLCA3NS42NjY2NjY2NjY2NjcpIHJvdGF0ZSgxODAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiMyMjIiIGZpbGwtb3BhY2l0eT0iMC4xNSIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTMxLjA1ODUxMTEwNjA1LCA3NS42NjY2NjY2NjY2NjcpIHJvdGF0ZSgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjZGRkIiBmaWxsLW9wYWNpdHk9IjAuMDIiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE5Ni41ODc3NjY2NTkwNywgNzUuNjY2NjY2NjY2NjY3KSByb3RhdGUoMTgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDk4IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNjIuMTE3MDIyMjEyMDksIDc1LjY2NjY2NjY2NjY2Nykgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiMyMjIiIGZpbGwtb3BhY2l0eT0iMC4wNjMzMzMzMzMzMzMzMzMiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMyNy42NDYyNzc3NjUxMSwgNzUuNjY2NjY2NjY2NjY3KSByb3RhdGUoMTgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjZGRkIiBmaWxsLW9wYWNpdHk9IjAuMTI0IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLCAxMTMuNSkgcm90YXRlKDE4MCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iIzIyMiIgZmlsbC1vcGFjaXR5PSIwLjE1IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NS41MjkyNTU1NTMwMjMsIDExMy41KSByb3RhdGUoMCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iI2RkZCIgZmlsbC1vcGFjaXR5PSIwLjEyNCIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTMxLjA1ODUxMTEwNjA1LCAxMTMuNSkgcm90YXRlKDE4MCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iI2RkZCIgZmlsbC1vcGFjaXR5PSIwLjEwNjY2NjY2NjY2NjY3IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxOTYuNTg3NzY2NjU5MDcsIDExMy41KSByb3RhdGUoMCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iI2RkZCIgZmlsbC1vcGFjaXR5PSIwLjEyNCIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYyLjExNzAyMjIxMjA5LCAxMTMuNSkgcm90YXRlKDE4MCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iIzIyMiIgZmlsbC1vcGFjaXR5PSIwLjEzMjY2NjY2NjY2NjY3IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMjcuNjQ2Mjc3NzY1MTEsIDExMy41KSByb3RhdGUoMCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iI2RkZCIgZmlsbC1vcGFjaXR5PSIwLjEwNjY2NjY2NjY2NjY3IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLCAxNTEuMzMzMzMzMzMzMzMpIHJvdGF0ZSgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDYzMzMzMzMzMzMzMzMzIiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2NS41MjkyNTU1NTMwMjMsIDE1MS4zMzMzMzMzMzMzMykgcm90YXRlKDE4MCwgMzIuNzY0NjI3Nzc2NTExLCAzNy44MzMzMzMzMzMzMzMpIi8+PHBvbHlsaW5lIHBvaW50cz0iMCwgMCwgNjUuNTI5MjU1NTUzMDIzLCAzNy44MzMzMzMzMzMzMzMsIDAsIDc1LjY2NjY2NjY2NjY2NywgMCwgMCIgZmlsbD0iIzIyMiIgZmlsbC1vcGFjaXR5PSIwLjA4MDY2NjY2NjY2NjY2NyIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utb3BhY2l0eT0iMC4wMiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTMxLjA1ODUxMTEwNjA1LCAxNTEuMzMzMzMzMzMzMzMpIHJvdGF0ZSgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjZGRkIiBmaWxsLW9wYWNpdHk9IjAuMDIiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE5Ni41ODc3NjY2NTkwNywgMTUxLjMzMzMzMzMzMzMzKSByb3RhdGUoMTgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48cG9seWxpbmUgcG9pbnRzPSIwLCAwLCA2NS41MjkyNTU1NTMwMjMsIDM3LjgzMzMzMzMzMzMzMywgMCwgNzUuNjY2NjY2NjY2NjY3LCAwLCAwIiBmaWxsPSIjMjIyIiBmaWxsLW9wYWNpdHk9IjAuMDk4IiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIwLjAyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNjIuMTE3MDIyMjEyMDksIDE1MS4zMzMzMzMzMzMzMykgcm90YXRlKDAsIDMyLjc2NDYyNzc3NjUxMSwgMzcuODMzMzMzMzMzMzMzKSIvPjxwb2x5bGluZSBwb2ludHM9IjAsIDAsIDY1LjUyOTI1NTU1MzAyMywgMzcuODMzMzMzMzMzMzMzLCAwLCA3NS42NjY2NjY2NjY2NjcsIDAsIDAiIGZpbGw9IiNkZGQiIGZpbGwtb3BhY2l0eT0iMC4xMjQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLW9wYWNpdHk9IjAuMDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMyNy42NDYyNzc3NjUxMSwgMTUxLjMzMzMzMzMzMzMzKSByb3RhdGUoMTgwLCAzMi43NjQ2Mjc3NzY1MTEsIDM3LjgzMzMzMzMzMzMzMykiLz48L3N2Zz4=');"></div> + <div class="carousel-body"> + <div class="container"> + <h1 class="display-1 text-white">微信支付宝整合</h1> + <h2 class="display-4 text-white">微信支付定整合示例</h2> + </div> + </div> + </a> + </div> + </div> +</header> + +<!-- Page Content --> +<div class="container"> + + <div class="row"> + <div class="col-lg-12"> + <h2 class="page-header"> + 开始接入 + </h2> + </div> + <div class="col-md-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h4><i class="fa fa-fw fa-check"></i> 准备工作</h4> + </div> + <div class="panel-body"> + <p><a href="https://b.alipay.com/" target="_blank">申请支付宝相应的支付产品,并获取相关配置</a></p> + <p><a href="https://pay.weixin.qq.com" target="_blank">申请微信相应的支付产品,并获取相关配置</a></p> + <p>插件管理中配置相应的微信或支付宝参数</p> + </div> + </div> + </div> + <div class="col-md-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h4><i class="fa fa-fw fa-gift"></i> 开发工作</h4> + </div> + <div class="panel-body"> + <p>在你的PHP代码中调用相关代码进行支付,请参考控制器代码</p> + </div> + </div> + </div> + <div class="col-md-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h4><i class="fa fa-fw fa-compass"></i> 立即体验</h4> + </div> + <div class="panel-body"> + <p>请选择对应的支付金额和支付方式</p> + <p> + <span class="input-group"> + <input type="number" name="amount" step="0.01" value="1.01" + class="form-control" placeholder="请输入一个随机金额"/> + <span class="input-group-addon" style="padding:0;width:100px;"> + <select class="form-control" name="method" id="method" style="border:none;height: 32px;"> + <option value="web">PC网页支付</option> + <option value="wap">H5手机网页支付</option> + <option value="app">APP支付</option> + <option value="scan">扫码支付</option> + <option value="mp">公众号支付(不支持支付宝)</option> + <option value="miniapp">小程序支付(不支持支付宝)</option> + </select> + </span> + </span> + </p> + <button data-type="alipay" class="btn btn-info btn-experience"><i class="fa fa-money"></i> 支付宝支付</button> + <button data-type="wechat" class="btn btn-success btn-experience"><i class="fa fa-wechat"></i> 微信支付</button> + </div> + </div> + </div> + </div> + <!-- /.row --> + + <hr> + +</div> diff --git a/addons/epay/view/layout/default.html b/addons/epay/view/layout/default.html new file mode 100644 index 0000000..90aeff0 --- /dev/null +++ b/addons/epay/view/layout/default.html @@ -0,0 +1,103 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content=""> + <meta name="author" content=""> + + <title>{$title} - {$site.name}</title> + + <link href="__CDN__/assets/libs/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet"> + <link href="__ADDON__/css/common.css" rel="stylesheet"> + <link href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css" rel="stylesheet"> + + <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries --> + <!--[if lt IE 9]> + <script src="https://cdn.staticfile.org/html5shiv/3.7.3/html5shiv.min.js"></script> + <script src="https://cdn.staticfile.org/respond.js/1.4.2/respond.min.js"></script> + <![endif]--> + +</head> + +<body> + +<!-- Navigation --> +<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> + <div class="container"> + <!-- Brand and toggle get grouped for better mobile display --> + <div class="navbar-header"> + <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> + <span class="sr-only">Toggle navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + <a class="navbar-brand" href="{:addon_url('epay/index/index')}">{$site.name}</a> + </div> + <!-- Collect the nav links, forms, and other content for toggling --> + <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> + <ul class="nav navbar-nav navbar-right"> + <li> + </li> + {if $user} + <li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown">欢迎你! {$user.nickname}<b class="caret"></b></a> + <ul class="dropdown-menu"> + <li> + <a href="{:url('index/user/index')}">会员中心</a> + </li> + <li> + <a href="{:url('index/user/profile')}">个人资料</a> + </li> + <li> + <a href="{:url('index/user/logout')}">退出登录</a> + </li> + </ul> + </li> + {else /} + <li class="dropdown"> + <a href="{:url('index/user/index')}" class="dropdown-toggle" data-toggle="dropdown">会员中心 <b class="caret"></b></a> + <ul class="dropdown-menu"> + <li> + <a href="{:url('index/user/login')}">登录</a> + </li> + <li> + <a href="{:url('index/user/register')}">注册</a> + </li> + </ul> + </li> + {/if} + </ul> + </div> + <!-- /.navbar-collapse --> + </div> + <!-- /.container --> +</nav> + +{__CONTENT__} + +<div class="container"> + <!-- Footer --> + <footer> + <div class="row"> + <div class="col-lg-12"> + <hr> + <p>Copyright © {$site.name} 2017-2020</p> + </div> + </div> + </footer> + +</div> +<!-- /.container --> + +<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script> +<script src="__CDN__/assets/libs/bootstrap/dist/js/bootstrap.min.js"></script> +<script src="__CDN__/assets/libs/fastadmin-layer/dist/layer.js"></script> +<script src="__ADDON__/js/common.js"></script> + +</body> +</html> diff --git a/application/admin/controller/Epay.php b/application/admin/controller/Epay.php new file mode 100644 index 0000000..5fe0a5b --- /dev/null +++ b/application/admin/controller/Epay.php @@ -0,0 +1,39 @@ +<?php + +namespace app\admin\controller; + +use app\common\controller\Backend; +use think\Config; + +class Epay extends Backend +{ + protected $noNeedRight = ['upload']; + + /** + * 上传本地证书 + * @return void + */ + public function upload() + { + Config::set('default_return_type', 'json'); + + $certname = $this->request->post('certname', ''); + $certPathArr = [ + 'cert_client' => '/addons/epay/certs/apiclient_cert.pem', //微信支付api + 'cert_key' => '/addons/epay/certs/apiclient_key.pem', //微信支付api + 'app_cert_public_key' => '/addons/epay/certs/appCertPublicKey.crt',//应用公钥证书路径 + 'alipay_root_cert' => '/addons/epay/certs/alipayRootCert.crt', //支付宝根证书路径 + 'ali_public_key' => '/addons/epay/certs/alipayCertPublicKey.crt', //支付宝公钥证书路径 + ]; + if (!isset($certPathArr[$certname])) { + $this->error("证书错误"); + } + $url = $certPathArr[$certname]; + $file = $this->request->file('file'); + if (!$file) { + $this->error("未上传文件"); + } + $file->move(dirname(ROOT_PATH . $url), basename(ROOT_PATH . $url), true); + $this->success(__('上传成功'), '', ['url' => $url]); + } +} diff --git a/application/extra/addons.php b/application/extra/addons.php index 1eaea32..b3c7651 100644 --- a/application/extra/addons.php +++ b/application/extra/addons.php @@ -3,6 +3,11 @@ return [ 'autoload' => false, 'hooks' => [ + 'app_init' => [ + 'epay', + 'qiniu', + 'shopro', + ], 'config_init' => [ 'nkeditor', ], @@ -12,10 +17,6 @@ return [ 'upload_delete' => [ 'qiniu', ], - 'app_init' => [ - 'qiniu', - 'shopro', - ], 'upgrade' => [ 'shopro', ], diff --git a/public/assets/addons/epay/css/common.css b/public/assets/addons/epay/css/common.css new file mode 100644 index 0000000..8353679 --- /dev/null +++ b/public/assets/addons/epay/css/common.css @@ -0,0 +1,180 @@ +/*! + * Start Bootstrap - Modern Business (http://startbootstrap.com/) + * Copyright 2013-2016 Start Bootstrap + * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE) + */ +/* Global Styles */ +html, +body { + height: 100%; +} +body { + padding-top: 50px; + /* Required padding for .navbar-fixed-top. Remove if using .navbar-static-top. Change if height of navigation changes. */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; +} +.img-addon { + margin-bottom: 10px; + width: 100%; +} +.img-hover:hover { + opacity: 0.8; +} +.display-1 { + font-size: 44px; +} +.display-4 { + font-size: 24px; + line-height: 32px; +} +/* Home Page Carousel */ +header.carousel { + height: 50%; +} +header.carousel .item, +header.carousel .item.active, +header.carousel .carousel-inner { + height: 100%; +} +header.carousel .fill { + width: 100%; + height: 100%; +} +.error-404 { + font-size: 100px; +} +/* Pricing Page Styles */ +.price { + display: block; + font-size: 50px; + line-height: 50px; +} +.price sup { + top: -20px; + left: 2px; + font-size: 20px; +} +.period { + display: block; + font-style: italic; +} +/* Footer Styles */ +/* Responsive Styles */ +@media (max-width: 991px) { + .customer-img, + .img-related { + margin-bottom: 30px; + } +} +@media (max-width: 767px) { + .img-addon { + margin-bottom: 15px; + } + header.carousel .carousel { + height: 70%; + } +} +.carousel-body { + position: absolute; + width: 100%; + top: 25%; + text-align: center; + color: #fff; +} +.addonlist a > p { + margin-bottom: 15px; +} +/* PC扫码支付 */ +.scanpay { + margin-top: 20px; +} +.scanpay-title { + margin: 30px 0 15px 0; + padding-bottom: 15px; + border-bottom: 1px solid #eee; + position: relative; +} +.scanpay-qrcode { + margin-bottom: 20px; + position: relative; +} +.scanpay-qrcode img { + width: 100%; + border: 1px solid #eee; +} +.scanpay-qrcode .expired { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + opacity: .95; + background: #fff url(../images/expired.png) center center no-repeat; +} +.scanpay-qrcode .paid { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + opacity: .95; + background: #fff url(../images/paid.png) center center no-repeat; +} +.scanpay-screenshot { + padding: 0; +} +.scanpay-screenshot img { + width: 100%; +} +.scanpay-tips { + height: 60px; + padding: 8px 0 8px 125px; + background: #00c800 url(../images/scan.png) 50px 12px no-repeat; + background-size: 36px 36px; +} +.scanpay-tips p { + margin: 0; + font-size: 14px; + line-height: 22px; + color: #fff; + font-weight: 700; +} +.scanpay-time { + font-size: 14px; + margin-bottom: 15px; + position: absolute; + top: 15px; + right: 10px; + font-weight: normal; + display: none; +} +.scanpay-time span { + color: red; +} +.scanpay-order { + margin-bottom: 5px; +} +.scanpay-order em { + font-style: normal; + color: #666; +} +.scanpay-order em.scanpay-price { + color: #ff3333; + font-weight: bold; +} +.scanpay-alipay .scanpay-tips { + background-color: #4290e8; +} +@media (max-width: 767px) { + .scanpay { + margin-top: 20px; + } +} +@media (max-height: 855px) and (min-width: 767px) { + .scanpay { + width: calc(130vh); + min-width: 760px; + } +} diff --git a/public/assets/addons/epay/css/epay.css b/public/assets/addons/epay/css/epay.css new file mode 100644 index 0000000..dccff65 --- /dev/null +++ b/public/assets/addons/epay/css/epay.css @@ -0,0 +1,20 @@ +@import url("../../../css/bootstrap.min.css"); +@import url("../../../libs/font-awesome/css/font-awesome.min.css"); +html, +body { + height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 400; + overflow-x: hidden; + overflow-y: auto; + background: #f4f6f8; + font-size: 14px; + color: #616161; +} +.container { + max-width: 850px; + margin: 0 auto; + padding: 50px; +} diff --git a/public/assets/addons/epay/images/alipay.png b/public/assets/addons/epay/images/alipay.png new file mode 100644 index 0000000..8831c35 Binary files /dev/null and b/public/assets/addons/epay/images/alipay.png differ diff --git a/public/assets/addons/epay/images/expired.png b/public/assets/addons/epay/images/expired.png new file mode 100644 index 0000000..febcae1 Binary files /dev/null and b/public/assets/addons/epay/images/expired.png differ diff --git a/public/assets/addons/epay/images/logo-alipay.png b/public/assets/addons/epay/images/logo-alipay.png new file mode 100644 index 0000000..7a8f23c Binary files /dev/null and b/public/assets/addons/epay/images/logo-alipay.png differ diff --git a/public/assets/addons/epay/images/logo-wechat.png b/public/assets/addons/epay/images/logo-wechat.png new file mode 100644 index 0000000..3d65617 Binary files /dev/null and b/public/assets/addons/epay/images/logo-wechat.png differ diff --git a/public/assets/addons/epay/images/paid.png b/public/assets/addons/epay/images/paid.png new file mode 100644 index 0000000..4f04f12 Binary files /dev/null and b/public/assets/addons/epay/images/paid.png differ diff --git a/public/assets/addons/epay/images/scan.png b/public/assets/addons/epay/images/scan.png new file mode 100644 index 0000000..203727c Binary files /dev/null and b/public/assets/addons/epay/images/scan.png differ diff --git a/public/assets/addons/epay/images/screenshot-alipay.png b/public/assets/addons/epay/images/screenshot-alipay.png new file mode 100644 index 0000000..0bd2050 Binary files /dev/null and b/public/assets/addons/epay/images/screenshot-alipay.png differ diff --git a/public/assets/addons/epay/images/screenshot-wechat.png b/public/assets/addons/epay/images/screenshot-wechat.png new file mode 100644 index 0000000..093cc6f Binary files /dev/null and b/public/assets/addons/epay/images/screenshot-wechat.png differ diff --git a/public/assets/addons/epay/images/wechat.png b/public/assets/addons/epay/images/wechat.png new file mode 100644 index 0000000..6737169 Binary files /dev/null and b/public/assets/addons/epay/images/wechat.png differ diff --git a/public/assets/addons/epay/js/common.js b/public/assets/addons/epay/js/common.js new file mode 100644 index 0000000..ab513ee --- /dev/null +++ b/public/assets/addons/epay/js/common.js @@ -0,0 +1,65 @@ +$(function () { + + if ($('.carousel').length > 0) { + $('.carousel').carousel({ + interval: 5000 //changes the speed + }); + } + + if ($(".btn-experience").length > 0) { + $(".btn-experience").on("click", function () { + location.href = "/addons/epay/index/experience?amount=" + $("input[name=amount]").val() + "&type=" + $(this).data("type") + "&method=" + $("#method").val(); + }); + } + + var si, xhr; + if (typeof queryParams != 'undefined') { + var queryResult = function () { + xhr && xhr.abort(); + xhr = $.ajax({ + url: "", + type: "post", + data: queryParams, + dataType: 'json', + success: function (ret) { + if (ret.code == 1) { + var data = ret.data; + if (typeof data.status != 'undefined') { + var status = data.status; + if (status == 'SUCCESS' || status == 'TRADE_SUCCESS') { + $(".scanpay-qrcode .paid").removeClass("hidden"); + $(".scanpay-tips p").html("支付成功!<br><span>3</span>秒后将自动跳转..."); + + var sin = setInterval(function () { + $(".scanpay-tips p span").text(parseInt($(".scanpay-tips p span").text()) - 1); + }, 1000); + + setTimeout(function () { + clearInterval(sin); + location.href = queryParams.returnurl; + }, 3000); + + clearInterval(si); + } else if (status == 'REFUND' || status == 'TRADE_CLOSED') { + $(".scanpay-tips p").html("请求失败!<br>请返回重新发起支付"); + clearInterval(si); + } else if (status == 'NOTPAY' || status == 'TRADE_NOT_EXIST') { + } else if (status == 'CLOSED' || status == 'TRADE_CLOSED') { + $(".scanpay-tips p").html("订单已关闭!<br>请返回重新发起支付"); + clearInterval(si); + } else if (status == 'USERPAYING' || status == 'WAIT_BUYER_PAY') { + } else if (status == 'PAYERROR') { + clearInterval(si); + } + } + } + } + }); + }; + si = setInterval(function () { + queryResult(); + }, 3000); + queryResult(); + } + +}); diff --git a/public/assets/addons/epay/less/common.less b/public/assets/addons/epay/less/common.less new file mode 100644 index 0000000..381056b --- /dev/null +++ b/public/assets/addons/epay/less/common.less @@ -0,0 +1,229 @@ +/*! + * Start Bootstrap - Modern Business (http://startbootstrap.com/) + * Copyright 2013-2016 Start Bootstrap + * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap/blob/gh-pages/LICENSE) + */ + +/* Global Styles */ + +html, +body { + height: 100%; +} + +body { + padding-top: 50px; /* Required padding for .navbar-fixed-top. Remove if using .navbar-static-top. Change if height of navigation changes. */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; + +} + +.img-addon { + margin-bottom: 10px; + width: 100%; +} + +.img-hover:hover { + opacity: 0.8; +} + +.display-1 { + font-size: 44px; +} + +.display-4 { + font-size: 24px; + line-height: 32px; +} + +/* Home Page Carousel */ + +header.carousel { + height: 50%; +} + +header.carousel .item, +header.carousel .item.active, +header.carousel .carousel-inner { + height: 100%; +} + +header.carousel .fill { + width: 100%; + height: 100%; +} + +.error-404 { + font-size: 100px; +} + +/* Pricing Page Styles */ + +.price { + display: block; + font-size: 50px; + line-height: 50px; +} + +.price sup { + top: -20px; + left: 2px; + font-size: 20px; +} + +.period { + display: block; + font-style: italic; +} + +/* Footer Styles */ + +footer { +} + +/* Responsive Styles */ + +@media (max-width: 991px) { + .customer-img, + .img-related { + margin-bottom: 30px; + } +} + +@media (max-width: 767px) { + .img-addon { + margin-bottom: 15px; + } + + header.carousel .carousel { + height: 70%; + } +} + +.carousel-body { + position: absolute; + width: 100%; + top: 25%; + text-align: center; + color: #fff; +} + +.addonlist a > p { + margin-bottom: 15px; +} + +/* PC扫码支付 */ + +.scanpay { + margin-top: 20px; +} +.scanpay-title { + margin: 30px 0 15px 0; + padding-bottom: 15px; + border-bottom: 1px solid #eee; + position: relative; +} + +.scanpay-qrcode { + margin-bottom: 20px; + position: relative; + + img { + width: 100%; + border: 1px solid #eee; + } + + .expired { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + opacity: .95; + background: #fff url(../images/expired.png) center center no-repeat; + } + + .paid { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + opacity: .95; + background: #fff url(../images/paid.png) center center no-repeat; + } +} + + +.scanpay-screenshot { + padding: 0; + + img { + width: 100%; + } +} + +.scanpay-tips { + height: 60px; + padding: 8px 0 8px 125px; + background: #00c800 url(../images/scan.png) 50px 12px no-repeat; + background-size: 36px 36px; + + p { + margin: 0; + font-size: 14px; + line-height: 22px; + color: #fff; + font-weight: 700 + } +} + +.scanpay-time { + font-size: 14px; + margin-bottom: 15px; + position: absolute; + top: 15px; + right: 10px; + font-weight: normal; + display: none; + + span { + color: red; + } + +} + +.scanpay-order { + margin-bottom: 5px; + + em { + font-style: normal; + color: #666; + + &.scanpay-price { + color: #ff3333; + font-weight: bold; + } + } +} + +.scanpay-alipay { + .scanpay-tips { + background-color: #4290e8; + } +} + + +@media (max-width: 767px) { + .scanpay { + margin-top: 20px; + } +} + +@media (max-height: 855px) and (min-width: 767px) { + .scanpay { + width: calc(~ '130vh'); + min-width: 760px; + } +} diff --git a/public/assets/addons/epay/less/epay.less b/public/assets/addons/epay/less/epay.less new file mode 100644 index 0000000..028eca1 --- /dev/null +++ b/public/assets/addons/epay/less/epay.less @@ -0,0 +1,28 @@ +@import (reference) "../../../../public/assets/less/bootstrap-less/mixins.less"; +@import (reference) "../../../../public/assets/less/bootstrap-less/variables.less"; +@import (reference) "../../../../public/assets/less/fastadmin/mixins.less"; +@import (reference) "../../../../public/assets/less/fastadmin/variables.less"; +@import "../../../../public/assets/less/lesshat.less"; +@import url("../../../css/bootstrap.min.css"); +@import url("../../../libs/font-awesome/css/font-awesome.min.css"); + +html, +body { + height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 400; + overflow-x: hidden; + overflow-y: auto; + background: #f4f6f8; + font-size: 14px; + color: #616161; + +} + +.container { + max-width: 850px; + margin: 0 auto; + padding:50px; +} -- libgit2 0.24.0