Răsfoiți Sursa

版本构建

bingqishi 5 ore în urmă
comite
6789991722
100 a modificat fișierele cu 5789 adăugiri și 0 ștergeri
  1. 139 0
      Gruntfile.js
  2. 191 0
      LICENSE
  3. 93 0
      README.md
  4. BIN
      addons/.DS_Store
  5. 1 0
      addons/.gitkeep
  6. 1 0
      addons/.htaccess
  7. 1 0
      addons/address/.addonrc
  8. 32 0
      addons/address/Address.php
  9. 34 0
      addons/address/bootstrap.js
  10. 132 0
      addons/address/config.php
  11. 64 0
      addons/address/controller/Index.php
  12. 10 0
      addons/address/info.ini
  13. 258 0
      addons/address/view/index/amap.html
  14. 243 0
      addons/address/view/index/baidu.html
  15. 132 0
      addons/address/view/index/index.html
  16. 291 0
      addons/address/view/index/tencent.html
  17. 1 0
      addons/epay/.addonrc
  18. 112 0
      addons/epay/Epay.php
  19. 0 0
      addons/epay/certs/alipayCertPublicKey.crt
  20. 0 0
      addons/epay/certs/alipayRootCert.crt
  21. 0 0
      addons/epay/certs/apiclient_cert.pem
  22. 0 0
      addons/epay/certs/apiclient_key.pem
  23. 0 0
      addons/epay/certs/appCertPublicKey.crt
  24. 0 0
      addons/epay/certs/public_key.pem
  25. 529 0
      addons/epay/config.html
  26. 72 0
      addons/epay/config.php
  27. 234 0
      addons/epay/controller/Api.php
  28. 131 0
      addons/epay/controller/Index.php
  29. 10 0
      addons/epay/info.ini
  30. 18 0
      addons/epay/library/Collection.php
  31. 16 0
      addons/epay/library/OrderException.php
  32. 63 0
      addons/epay/library/RedirectResponse.php
  33. 46 0
      addons/epay/library/Response.php
  34. 494 0
      addons/epay/library/Service.php
  35. 110 0
      addons/epay/library/Wechat.php
  36. 2 0
      addons/epay/library/hyperf/context/.gitattributes
  37. 21 0
      addons/epay/library/hyperf/context/LICENSE
  38. 40 0
      addons/epay/library/hyperf/context/composer.json
  39. 112 0
      addons/epay/library/hyperf/context/src/Context.php
  40. 2 0
      addons/epay/library/hyperf/contract/.gitattributes
  41. 21 0
      addons/epay/library/hyperf/contract/LICENSE
  42. 33 0
      addons/epay/library/hyperf/contract/composer.json
  43. 16 0
      addons/epay/library/hyperf/contract/src/ApplicationInterface.php
  44. 22 0
      addons/epay/library/hyperf/contract/src/Castable.php
  45. 33 0
      addons/epay/library/hyperf/contract/src/CastsAttributes.php
  46. 24 0
      addons/epay/library/hyperf/contract/src/CastsInboundAttributes.php
  47. 17 0
      addons/epay/library/hyperf/contract/src/CompressInterface.php
  48. 41 0
      addons/epay/library/hyperf/contract/src/ConfigInterface.php
  49. 40 0
      addons/epay/library/hyperf/contract/src/ConnectionInterface.php
  50. 53 0
      addons/epay/library/hyperf/contract/src/ContainerInterface.php
  51. 17 0
      addons/epay/library/hyperf/contract/src/DispatcherInterface.php
  52. 25 0
      addons/epay/library/hyperf/contract/src/FrequencyInterface.php
  53. 17 0
      addons/epay/library/hyperf/contract/src/IdGeneratorInterface.php
  54. 30 0
      addons/epay/library/hyperf/contract/src/LengthAwarePaginatorInterface.php
  55. 17 0
      addons/epay/library/hyperf/contract/src/MiddlewareInitializerInterface.php
  56. 32 0
      addons/epay/library/hyperf/contract/src/NormalizerInterface.php
  57. 23 0
      addons/epay/library/hyperf/contract/src/OnCloseInterface.php
  58. 20 0
      addons/epay/library/hyperf/contract/src/OnHandShakeInterface.php
  59. 24 0
      addons/epay/library/hyperf/contract/src/OnMessageInterface.php
  60. 24 0
      addons/epay/library/hyperf/contract/src/OnOpenInterface.php
  61. 24 0
      addons/epay/library/hyperf/contract/src/OnPacketInterface.php
  62. 23 0
      addons/epay/library/hyperf/contract/src/OnReceiveInterface.php
  63. 21 0
      addons/epay/library/hyperf/contract/src/OnRequestInterface.php
  64. 19 0
      addons/epay/library/hyperf/contract/src/PackerInterface.php
  65. 95 0
      addons/epay/library/hyperf/contract/src/PaginatorInterface.php
  66. 30 0
      addons/epay/library/hyperf/contract/src/PoolInterface.php
  67. 27 0
      addons/epay/library/hyperf/contract/src/PoolOptionInterface.php
  68. 36 0
      addons/epay/library/hyperf/contract/src/ProcessInterface.php
  69. 22 0
      addons/epay/library/hyperf/contract/src/ResponseEmitterInterface.php
  70. 158 0
      addons/epay/library/hyperf/contract/src/SessionInterface.php
  71. 18 0
      addons/epay/library/hyperf/contract/src/StdoutLoggerInterface.php
  72. 20 0
      addons/epay/library/hyperf/contract/src/Synchronized.php
  73. 37 0
      addons/epay/library/hyperf/contract/src/TranslatorInterface.php
  74. 35 0
      addons/epay/library/hyperf/contract/src/TranslatorLoaderInterface.php
  75. 17 0
      addons/epay/library/hyperf/contract/src/UnCompressInterface.php
  76. 60 0
      addons/epay/library/hyperf/contract/src/ValidatorInterface.php
  77. 4 0
      addons/epay/library/hyperf/engine/.gitattributes
  78. 4 0
      addons/epay/library/hyperf/engine/.gitignore
  79. 89 0
      addons/epay/library/hyperf/engine/.php-cs-fixer.php
  80. 6 0
      addons/epay/library/hyperf/engine/.phpstorm.meta.php
  81. 46 0
      addons/epay/library/hyperf/engine/Dockerfile
  82. 7 0
      addons/epay/library/hyperf/engine/README.md
  83. 49 0
      addons/epay/library/hyperf/engine/composer.json
  84. 15 0
      addons/epay/library/hyperf/engine/phpunit.xml
  85. 142 0
      addons/epay/library/hyperf/engine/src/Channel.php
  86. 25 0
      addons/epay/library/hyperf/engine/src/Constant.php
  87. 134 0
      addons/epay/library/hyperf/engine/src/Contract/ChannelInterface.php
  88. 70 0
      addons/epay/library/hyperf/engine/src/Contract/CoroutineInterface.php
  89. 24 0
      addons/epay/library/hyperf/engine/src/Contract/Http/ClientInterface.php
  90. 23 0
      addons/epay/library/hyperf/engine/src/Contract/WebSocket/WebSocketInterface.php
  91. 100 0
      addons/epay/library/hyperf/engine/src/Coroutine.php
  92. 16 0
      addons/epay/library/hyperf/engine/src/Exception/CoroutineDestroyedException.php
  93. 16 0
      addons/epay/library/hyperf/engine/src/Exception/HttpClientException.php
  94. 16 0
      addons/epay/library/hyperf/engine/src/Exception/RunningInNonCoroutineException.php
  95. 16 0
      addons/epay/library/hyperf/engine/src/Exception/RuntimeException.php
  96. 20 0
      addons/epay/library/hyperf/engine/src/Extension.php
  97. 76 0
      addons/epay/library/hyperf/engine/src/Http/Client.php
  98. 22 0
      addons/epay/library/hyperf/engine/src/Http/FdGetter.php
  99. 47 0
      addons/epay/library/hyperf/engine/src/Http/RawResponse.php
  100. 16 0
      addons/epay/library/hyperf/engine/src/Socket.php

+ 139 - 0
Gruntfile.js

@@ -0,0 +1,139 @@
+module.exports = function (grunt) {
+
+    grunt.initConfig({
+        pkg: grunt.file.readJSON('package.json'),
+        copy: {
+            main: {
+                files: []
+            }
+        }
+    });
+
+    var build = function (module, type, callback) {
+        var config = {
+            compile: {
+                options: type === 'js' ? {
+                    optimizeCss: "standard",
+                    optimize: "uglify",   //可使用uglify|closure|none
+                    preserveLicenseComments: true,
+                    removeCombined: false,
+                    baseUrl: "./public/assets/js/",    //JS文件所在的基础目录
+                    name: "require-" + module, //来源文件,不包含后缀
+                    out: "./public/assets/js/require-" + module + ".min.js"  //目标文件
+                } : {
+                    optimizeCss: "default",
+                    optimize: "uglify",   //可使用uglify|closure|none
+                    cssIn: "./public/assets/css/" + module + ".css",    //CSS文件所在的基础目录
+                    out: "./public/assets/css/" + module + ".min.css"  //目标文件
+                }
+            }
+        };
+
+
+        var content = grunt.file.read("./public/assets/js/require-" + module + ".js"),
+            pattern = /^require\.config\(\{[\r\n]?[\n]?(.*?)[\r\n]?[\n]?}\);/is;
+
+        var matches = content.match(pattern);
+        if (matches) {
+            if (type === 'js') {
+                var data = matches[1].replaceAll(/(urlArgs|baseUrl):(.*)\n/gi, '');
+                const parse = require('parse-config-file'), fs = require('fs');
+                require('jsonminify');
+
+                data = JSON.minify("{\n" + data + "\n}");
+                let options = parse(data);
+                options.paths.tableexport = "empty:";
+                Object.assign(config.compile.options, options);
+            }
+            let requirejs = require("./application/admin/command/Min/r");
+
+            try {
+                requirejs.optimize(config.compile.options, function (buildResponse) {
+                    // var contents = require('fs').readFileSync(config.compile.options.out, 'utf8');
+                    callback();
+                }, function (err) {
+                    console.error(err);
+                    callback();
+                });
+            } catch (err) {
+                console.error(err);
+                callback();
+            }
+        }
+    };
+
+    // 加载 "copy" 插件
+    grunt.loadNpmTasks('grunt-contrib-copy');
+
+    grunt.registerTask('frontend:js', 'build frontend js', function () {
+        var done = this.async();
+        build('frontend', 'js', done);
+    });
+
+    grunt.registerTask('backend:js', 'build backend js', function () {
+        var done = this.async();
+        build('backend', 'js', done);
+    });
+
+    grunt.registerTask('frontend:css', 'build frontend css', function () {
+        var done = this.async();
+        build('frontend', 'css', done);
+    });
+
+    grunt.registerTask('backend:css', 'build frontend css', function () {
+        var done = this.async();
+        build('backend', 'css', done);
+    });
+
+    // 注册部署JS和CSS任务
+    grunt.registerTask('deploy', 'deploy', function () {
+        const fs = require('fs');
+        const path = require("path")
+        const nodeModulesDir = path.resolve(__dirname, "./node_modules");
+
+        const getAllFiles = function (dirPath, arrayOfFiles) {
+            files = fs.readdirSync(dirPath)
+
+            arrayOfFiles = arrayOfFiles || []
+
+            files.forEach(function (file) {
+                if (fs.statSync(dirPath + "/" + file).isDirectory()) {
+                    arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles)
+                } else {
+                    arrayOfFiles.push(path.join(__dirname, dirPath, "/", file))
+                }
+            });
+
+            return arrayOfFiles
+        };
+        const mainPackage = grunt.config.get('pkg');
+        let dists = mainPackage.dists || [];
+        let files = [];
+
+        // 兼容旧版本bower使用的目录
+        let specialKey = {
+            'fastadmin-bootstraptable': 'bootstrap-table',
+            'sortablejs': 'Sortable',
+            'tableexport.jquery.plugin': 'tableExport.jquery.plugin',
+        };
+        Object.keys(dists).forEach(key => {
+            let src = ["**/*LICENSE*", "**/*license*"];
+            src = src.concat(Array.isArray(dists[key]) ? dists[key] : [dists[key]]);
+            files.push({expand: true, cwd: nodeModulesDir + "/" + key, src: src, dest: 'public/assets/libs/' + (specialKey[key] || key) + "/"});
+        });
+
+        // 兼容bower历史路径文件
+        files = [...files,
+            {src: nodeModulesDir + "/toastr/build/toastr.min.css", dest: "public/assets/libs/toastr/toastr.min.css"},
+            {src: nodeModulesDir + "/bootstrap-slider/dist/css/bootstrap-slider.css", dest: "public/assets/libs/bootstrap-slider/slider.css"},
+            {expand: true, cwd: nodeModulesDir + "/bootstrap-slider/dist", src: ["*.js"], dest: "public/assets/libs/bootstrap-slider/"}
+        ]
+
+        grunt.config.set('copy.main.files', files);
+        grunt.task.run("copy:main");
+    });
+
+    // 注册默认任务
+    grunt.registerTask('default', ['deploy', 'frontend:js', 'backend:js', 'frontend:css', 'backend:css']);
+
+};

+ 191 - 0
LICENSE

@@ -0,0 +1,191 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, "control" means (i) the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+"Object" form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+2. Grant of Copyright License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution.
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+If the Work includes a "NOTICE" text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+5. Submission of Contributions.
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+6. Trademarks.
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty.
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+8. Limitation of Liability.
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability.
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets "{}" replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same "printed page" as the copyright notice for easier identification within
+third-party archives.
+
+   Copyright 2017 Karson
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 93 - 0
README.md

@@ -0,0 +1,93 @@
+FastAdmin是一款基于ThinkPHP+Bootstrap的极速后台开发框架。
+
+
+## 主要特性
+
+* 基于`Auth`验证的权限管理系统
+    * 支持无限级父子级权限继承,父级的管理员可任意增删改子级管理员及权限设置
+    * 支持单管理员多角色
+    * 支持管理子级数据或个人数据
+* 强大的一键生成功能
+    * 一键生成CRUD,包括控制器、模型、视图、JS、语言包、菜单、回收站等
+    * 一键压缩打包JS和CSS文件,一键CDN静态资源部署
+    * 一键生成控制器菜单和规则
+    * 一键生成API接口文档
+* 完善的前端功能组件开发
+    * 基于`AdminLTE`二次开发
+    * 基于`Bootstrap`开发,自适应手机、平板、PC
+    * 基于`RequireJS`进行JS模块管理,按需加载
+    * 基于`Less`进行样式开发
+* 强大的插件扩展功能,在线安装卸载升级插件
+* 通用的会员模块和API模块
+* 共用同一账号体系的Web端会员中心权限验证和API接口会员权限验证
+* 二级域名部署支持,同时域名支持绑定到应用插件
+* 多语言支持,服务端及客户端支持
+* 支持大文件分片上传、剪切板粘贴上传、拖拽上传,进度条显示,图片上传前压缩
+* 支持表格固定列、固定表头、跨页选择、Excel导出、模板渲染等功能
+* 强大的第三方应用模块支持([CMS](https://www.fastadmin.net/store/cms.html)、[CRM](https://www.fastadmin.net/store/facrm.html)、[企业网站管理系统](https://www.fastadmin.net/store/ldcms.html)、[知识库文档系统](https://www.fastadmin.net/store/knowbase.html)、[在线投票系统](https://www.fastadmin.net/store/vote.html)、[B2C商城](https://www.fastadmin.net/store/shopro.html)、[B2B2C商城](https://www.fastadmin.net/store/wanlshop.html))
+* 整合第三方短信接口(阿里云、腾讯云短信)
+* 无缝整合第三方云存储(七牛云、阿里云OSS、腾讯云存储、又拍云)功能,支持云储存分片上传
+* 第三方富文本编辑器支持(Summernote、百度编辑器)
+* 第三方登录(QQ、微信、微博)整合
+* 第三方支付(微信、支付宝)无缝整合,微信支持PC端扫码支付
+* 丰富的插件应用市场
+
+## 安装使用
+
+https://doc.fastadmin.net
+
+## 在线演示
+
+https://demo.fastadmin.net
+
+用户名:admin
+
+密 码:123456
+
+提 示:演示站数据无法进行修改,请下载源码安装体验全部功能
+
+## 界面截图
+![控制台](https://images.gitee.com/uploads/images/2020/0929/202947_8db2d281_10933.gif "控制台")
+
+## 问题反馈
+
+在使用中有任何问题,请使用以下联系方式联系我们
+
+问答社区: https://ask.fastadmin.net
+
+Github: https://github.com/fastadminnet/fastadmin
+
+Gitee: https://gitee.com/fastadminnet/fastadmin
+
+## 特别鸣谢
+
+感谢以下的项目,排名不分先后
+
+ThinkPHP:http://www.thinkphp.cn
+
+AdminLTE:https://adminlte.io
+
+Bootstrap:http://getbootstrap.com
+
+jQuery:http://jquery.com
+
+Bootstrap-table:https://github.com/wenzhixin/bootstrap-table
+
+Nice-validator: https://validator.niceue.com
+
+SelectPage: https://github.com/TerryZ/SelectPage
+
+Layer: https://layuion.com/layer/
+
+DropzoneJS: https://www.dropzonejs.com
+
+
+## 版权信息
+
+FastAdmin遵循Apache2开源协议发布,并提供免费使用。
+
+本项目包含的第三方源码和二进制文件之版权信息另行标注。
+
+版权所有Copyright © 2017-2024 by FastAdmin (https://www.fastadmin.net)
+
+All rights reserved。

BIN
addons/.DS_Store


+ 1 - 0
addons/.gitkeep

@@ -0,0 +1 @@
+

+ 1 - 0
addons/.htaccess

@@ -0,0 +1 @@
+deny from all

+ 1 - 0
addons/address/.addonrc

@@ -0,0 +1 @@
+{"files":["public\/assets\/addons\/address\/js\/jquery.autocomplete.js","public\/assets\/addons\/address\/js\/gcoord.min.js"],"license":"regular","licenseto":"1170","licensekey":"NLH4tXOocgzVRjkJ TxH1dhwuN4a8UfATl313Qg==","domains":["yanglao.com"],"licensecodes":[],"validations":["b3df24e8513782a968400fe06d2a184d"]}

+ 32 - 0
addons/address/Address.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace addons\address;
+
+use think\Addons;
+
+/**
+ * 地址选择
+ * @author [MiniLing] <[laozheyouxiang@163.com]>
+ */
+class Address extends Addons
+{
+
+    /**
+     * 插件安装方法
+     * @return bool
+     */
+    public function install()
+    {
+        return true;
+    }
+
+    /**
+     * 插件卸载方法
+     * @return bool
+     */
+    public function uninstall()
+    {
+        return true;
+    }
+
+}

+ 34 - 0
addons/address/bootstrap.js

@@ -0,0 +1,34 @@
+require([], function () {
+    //绑定data-toggle=addresspicker属性点击事件
+
+    $(document).on('click', "[data-toggle='addresspicker']", function () {
+        var that = this;
+        var callback = $(that).data('callback');
+        var input_id = $(that).data("input-id") ? $(that).data("input-id") : "";
+        var lat_id = $(that).data("lat-id") ? $(that).data("lat-id") : "";
+        var lng_id = $(that).data("lng-id") ? $(that).data("lng-id") : "";
+        var zoom_id = $(that).data("zoom-id") ? $(that).data("zoom-id") : "";
+        var lat = lat_id ? $("#" + lat_id).val() : '';
+        var lng = lng_id ? $("#" + lng_id).val() : '';
+        var zoom = zoom_id ? $("#" + zoom_id).val() : '';
+        var url = "/addons/address/index/select";
+        url += (lat && lng) ? '?lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") + (zoom ? "&zoom=" + zoom : "") : '';
+        Fast.api.open(url, '位置选择', {
+            callback: function (res) {
+                input_id && $("#" + input_id).val(res.address).trigger("change");
+                lat_id && $("#" + lat_id).val(res.lat).trigger("change");
+                lng_id && $("#" + lng_id).val(res.lng).trigger("change");
+                zoom_id && $("#" + zoom_id).val(res.zoom).trigger("change");
+
+                try {
+                    //执行回调函数
+                    if (typeof callback === 'function') {
+                        callback.call(that, res);
+                    }
+                } catch (e) {
+
+                }
+            }
+        });
+    });
+});

+ 132 - 0
addons/address/config.php

@@ -0,0 +1,132 @@
+<?php
+
+return [
+    [
+        'name' => 'maptype',
+        'title' => '默认地图类型',
+        'type' => 'radio',
+        'content' => [
+            'baidu' => '百度地图',
+            'amap' => '高德地图',
+            'tencent' => '腾讯地图',
+        ],
+        'value' => 'amap',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'zoom',
+        'title' => '默认缩放级别',
+        'type' => 'string',
+        'content' => [],
+        'value' => '11',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'lat',
+        'title' => '默认Lat',
+        'type' => 'string',
+        'content' => [],
+        'value' => '39.919990',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'lng',
+        'title' => '默认Lng',
+        'type' => 'string',
+        'content' => [],
+        'value' => '116.456270',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'baidukey',
+        'title' => '百度地图KEY',
+        'type' => 'string',
+        'content' => [],
+        'value' => '',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'amapkey',
+        'title' => '高德地图KEY',
+        'type' => 'string',
+        'content' => [],
+        'value' => '31c75f22f389726d794ff19969c5b927',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'amapsecurityjscode',
+        'title' => '高德地图安全密钥',
+        'type' => 'string',
+        'content' => [],
+        'value' => 'f86bb3f38a6e2bfff25aa784a45eed9b',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'tencentkey',
+        'title' => '腾讯地图KEY',
+        'type' => 'string',
+        'content' => [],
+        'value' => '',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => 'coordtype',
+        'title' => '坐标系类型',
+        'type' => 'select',
+        'content' => [
+            'DEFAULT' => '默认(使用所选地图默认坐标系)',
+            'GCJ02' => 'GCJ-02',
+            'BD09' => 'BD-09',
+        ],
+        'value' => 'DEFAULT',
+        'rule' => 'required',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => '',
+    ],
+    [
+        'name' => '__tips__',
+        'title' => '温馨提示',
+        'type' => '',
+        'content' => [],
+        'value' => '请先申请对应地图的Key,配置后再使用',
+        'rule' => '',
+        'msg' => '',
+        'tip' => '',
+        'ok' => '',
+        'extend' => 'alert-danger-light',
+    ],
+];

+ 64 - 0
addons/address/controller/Index.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace addons\address\controller;
+
+use think\addons\Controller;
+use think\Config;
+use think\Hook;
+
+class Index extends Controller
+{
+
+    // 首页
+    public function index()
+    {
+        // 语言检测
+        $lang = $this->request->langset();
+        $lang = preg_match("/^([a-zA-Z\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn';
+
+        $site = Config::get("site");
+
+        // 配置信息
+        $config = [
+            'site'           => array_intersect_key($site, array_flip(['name', 'cdnurl', 'version', 'timezone', 'languages'])),
+            'upload'         => null,
+            'modulename'     => 'addons',
+            'controllername' => 'index',
+            'actionname'     => 'index',
+            'jsname'         => 'addons/address',
+            'moduleurl'      => '',
+            'language'       => $lang
+        ];
+        $config = array_merge($config, Config::get("view_replace_str"));
+
+        // 配置信息后
+        Hook::listen("config_init", $config);
+        // 加载当前控制器语言包
+        $this->view->assign('site', $site);
+        $this->view->assign('config', $config);
+
+        return $this->view->fetch();
+    }
+
+    // 选择地址
+    public function select()
+    {
+        $config = get_addon_config('address');
+        $zoom = (int)$this->request->get('zoom', $config['zoom']);
+        $lng = (float)$this->request->get('lng');
+        $lat = (float)$this->request->get('lat');
+        $address = $this->request->get('address');
+        $lng = $lng ?: $config['lng'];
+        $lat = $lat ?: $config['lat'];
+        $this->view->assign('zoom', $zoom);
+        $this->view->assign('lng', $lng);
+        $this->view->assign('lat', $lat);
+        $this->view->assign('address', $address);
+        $maptype = $config['maptype'];
+        if (!isset($config[$maptype . 'key']) || !$config[$maptype . 'key']) {
+            $this->error("请在配置中配置对应类型地图的密钥");
+        }
+        return $this->view->fetch('index/' . $maptype);
+    }
+
+}

+ 10 - 0
addons/address/info.ini

@@ -0,0 +1,10 @@
+name = address
+title = 地址位置选择插件
+intro = 地图位置选择插件,可返回地址和经纬度
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.1.8
+state = 1
+url = /addons/address
+license = regular
+licenseto = 1170

+ 258 - 0
addons/address/view/index/amap.html

@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
+    <title>地址选择器</title>
+    <link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
+    <link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
+    <style type="text/css">
+        body {
+            margin: 0;
+            padding: 0;
+        }
+
+        #container {
+            position: absolute;
+            left: 0;
+            top: 0;
+            right: 0;
+            bottom: 0;
+        }
+
+        .confirm {
+            position: absolute;
+            bottom: 30px;
+            right: 4%;
+            z-index: 99;
+            height: 50px;
+            width: 50px;
+            line-height: 50px;
+            font-size: 15px;
+            text-align: center;
+            background-color: white;
+            background: #1ABC9C;
+            color: white;
+            border: none;
+            cursor: pointer;
+            border-radius: 50%;
+        }
+
+        .search {
+            position: absolute;
+            width: 400px;
+            top: 0;
+            left: 50%;
+            padding: 5px;
+            margin-left: -200px;
+        }
+
+        .amap-marker-label {
+            border: 0;
+            background-color: transparent;
+        }
+
+        .info {
+            padding: .75rem 1.25rem;
+            margin-bottom: 1rem;
+            border-radius: .25rem;
+            position: fixed;
+            top: 2rem;
+            background-color: white;
+            width: auto;
+            min-width: 22rem;
+            border-width: 0;
+            left: 1.8rem;
+            box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
+        }
+    </style>
+</head>
+<body>
+<div class="search">
+    <div class="input-group">
+        <input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
+        <span class="input-group-btn">
+            <button type="submit" name="search" id="search-btn" class="btn btn-success">
+                <i class="fa fa-search"></i>
+            </button>
+        </span>
+    </div>
+</div>
+<div class="confirm">确定</div>
+<div id="container"></div>
+<script type="text/javascript">
+    window._AMapSecurityConfig = {
+        securityJsCode: "{$config.amapsecurityjscode|default=''}",
+    }
+</script>
+<script type="text/javascript" src="//webapi.amap.com/maps?v=1.4.11&key={$config.amapkey|default=''}&plugin=AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch,AMap.Geocoder"></script>
+<!-- UI组件库 1.0 -->
+<script src="//webapi.amap.com/ui/1.0/main.js?v=1.0.11"></script>
+<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
+<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
+<script type="text/javascript">
+    $(function () {
+        var as, map, geocoder, address, fromtype, totype;
+        address = "{$address|htmlentities}";
+        var lng = Number("{$lng}");
+        var lat = Number("{$lat}");
+        fromtype = "GCJ02";
+        totype = "{$config.coordtype|default='DEFAULT'}"
+        totype = totype === 'DEFAULT' ? "GCJ02" : totype;
+
+        if (lng && lat && fromtype !== totype) {
+            var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
+            lng = result[0] || lng;
+            lat = result[1] || lat;
+        }
+
+        var init = function () {
+            AMapUI.loadUI(['misc/PositionPicker', 'misc/PoiPicker'], function (PositionPicker, PoiPicker) {
+                //加载PositionPicker,loadUI的路径参数为模块名中 'ui/' 之后的部分
+                map = new AMap.Map('container', {
+                    zoom: parseInt('{$zoom}'),
+                    center: [lng, lat]
+                });
+                geocoder = new AMap.Geocoder({
+                    radius: 1000 //范围,默认:500
+                });
+                var positionPicker = new PositionPicker({
+                    mode: 'dragMarker',//设定为拖拽地图模式,可选'dragMap'、'dragMarker',默认为'dragMap'
+                    map: map//依赖地图对象
+                });
+                //输入提示
+                var autoOptions = {
+                    input: "place"
+                };
+
+                var relocation = function (lnglat, addr) {
+                    lng = lnglat.lng;
+                    lat = lnglat.lat;
+                    map.panTo([lng, lat]);
+                    positionPicker.start(lnglat);
+                    if (addr) {
+                        // var label = '<div class="info">地址:' + addr + '<br>经度:' + lng + '<br>纬度:' + lat + '</div>';
+                        var label = '<div class="info">地址:' + addr + '</div>';
+                        positionPicker.marker.setLabel({
+                            content: label //显示内容
+                        });
+                    } else {
+                        geocoder.getAddress(lng + ',' + lat, function (status, result) {
+                            if (status === 'complete' && result.regeocode) {
+                                var address = result.regeocode.formattedAddress;
+                                // var label = '<div class="info">地址:' + address + '<br>经度:' + lng + '<br>纬度:' + lat + '</div>';
+                                var label = '<div class="info">地址:' + address + '</div>';
+                                positionPicker.marker.setLabel({
+                                    content: label //显示内容
+                                });
+                            } else {
+                                console.log(JSON.stringify(result));
+                            }
+                        });
+                    }
+                };
+                var auto = new AMap.Autocomplete(autoOptions);
+
+                //构造地点查询类
+                var placeSearch = new AMap.PlaceSearch({
+                    map: map
+                });
+                //注册监听,当选中某条记录时会触发
+                AMap.event.addListener(auto, "select", function (e) {
+                    placeSearch.setCity(e.poi.adcode);
+                    placeSearch.search(e.poi.name, function (status, result) {
+                        $(map.getAllOverlays("marker")).each(function (i, j) {
+                            j.on("click", function () {
+                                relocation(j.De.position);
+                            });
+                        });
+                    });  //关键字查询查询
+                });
+                AMap.event.addListener(map, 'click', function (e) {
+                    relocation(e.lnglat);
+                });
+
+                //加载工具条
+                var tool = new AMap.ToolBar();
+                map.addControl(tool);
+
+                var poiPicker = new PoiPicker({
+                    input: 'place',
+                    placeSearchOptions: {
+                        map: map,
+                        pageSize: 6 //关联搜索分页
+                    }
+                });
+                poiPicker.on('poiPicked', function (poiResult) {
+                    poiPicker.hideSearchResults();
+                    $('.poi .nearpoi').text(poiResult.item.name);
+                    $('.address .info').text(poiResult.item.address);
+                    $('#address').val(poiResult.item.address);
+                    $("#place").val(poiResult.item.name);
+
+                    relocation(poiResult.item.location);
+                });
+
+                positionPicker.on('success', function (positionResult) {
+                    console.log(positionResult);
+                    as = positionResult.position;
+                    address = positionResult.address;
+                    lat = as.lat;
+                    lng = as.lng;
+                });
+                positionPicker.on('fail', function (positionResult) {
+                    address = '';
+                });
+                positionPicker.start();
+
+                if (address) {
+                    // 添加label
+                    var label = '<div class="info">地址:' + address + '</div>';
+                    positionPicker.marker.setLabel({
+                        content: label //显示内容
+                    });
+                }
+
+                //点击搜索按钮
+                $(document).on('click', '#search-btn', function () {
+                    if ($("#place").val() == '')
+                        return;
+                    placeSearch.search($("#place").val(), function (status, result) {
+                        $(map.getAllOverlays("marker")).each(function (i, j) {
+                            j.on("click", function () {
+                                relocation(j.De.position);
+                            });
+                        });
+                    });
+                });
+            });
+        };
+
+        //点击确定后执行回调赋值
+        var close = function (data) {
+            var index = parent.Layer.getFrameIndex(window.name);
+            var callback = parent.$("#layui-layer" + index).data("callback");
+            //再执行关闭
+            parent.Layer.close(index);
+            //再调用回传函数
+            if (typeof callback === 'function') {
+                callback.call(undefined, data);
+            }
+        };
+
+        //点击搜索按钮
+        $(document).on('click', '.confirm', function () {
+            var zoom = map.getZoom();
+            var data = {lat: lat, lng: lng, zoom: zoom, address: address};
+            if (fromtype !== totype) {
+                var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
+                data.lng = (result[0] || data.lng).toFixed(5);
+                data.lat = (result[1] || data.lat).toFixed(5);
+                console.log(data, result, fromtype, totype);
+            }
+            close(data);
+        });
+        init();
+    });
+</script>
+</body>
+</html>

+ 243 - 0
addons/address/view/index/baidu.html

@@ -0,0 +1,243 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
+    <title>地址选择器</title>
+    <link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
+    <link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
+    <style type="text/css">
+        body {
+            margin: 0;
+            padding: 0;
+        }
+
+        #container {
+            position: absolute;
+            left: 0;
+            top: 0;
+            right: 0;
+            bottom: 0;
+        }
+
+        .confirm {
+            position: absolute;
+            bottom: 30px;
+            right: 4%;
+            z-index: 99;
+            height: 50px;
+            width: 50px;
+            line-height: 50px;
+            font-size: 15px;
+            text-align: center;
+            background-color: white;
+            background: #1ABC9C;
+            color: white;
+            border: none;
+            cursor: pointer;
+            border-radius: 50%;
+        }
+
+        .search {
+            position: absolute;
+            width: 400px;
+            top: 0;
+            left: 50%;
+            padding: 5px;
+            margin-left: -200px;
+        }
+
+        label.BMapLabel {
+            max-width: inherit;
+            padding: .75rem 1.25rem;
+            margin-bottom: 1rem;
+            background-color: white;
+            width: auto;
+            min-width: 22rem;
+            border: none;
+            box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
+        }
+
+    </style>
+</head>
+<body>
+<div class="search">
+    <div class="input-group">
+        <input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
+        <div id="searchResultPanel" style="border:1px solid #C0C0C0;width:150px;height:auto; display:none;"></div>
+        <span class="input-group-btn">
+            <button type="button" name="search" id="search-btn" class="btn btn-success">
+                <i class="fa fa-search"></i>
+            </button>
+        </span>
+    </div>
+</div>
+<div class="confirm">确定</div>
+<div id="container"></div>
+<script type="text/javascript" src="//api.map.baidu.com/api?v=2.0&ak={$config.baidukey|default=''}"></script>
+<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
+<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
+<script type="text/javascript">
+    $(function () {
+        var map, marker, point, fromtype, totype;
+        var zoom = parseInt("{$zoom}");
+        var address = "{$address|htmlentities}";
+        var lng = Number("{$lng}");
+        var lat = Number("{$lat}");
+        fromtype = "BD09";
+        totype = "{$config.coordtype|default='DEFAULT'}"
+        totype = totype === 'DEFAULT' ? "BD09" : totype;
+
+        if (lng && lat && fromtype !== totype) {
+            var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
+            lng = result[0] || lng;
+            lat = result[1] || lat;
+        }
+
+        var geocoder = new BMap.Geocoder();
+
+        var addPointMarker = function (point, addr) {
+            deletePoint();
+            addPoint(point);
+
+            if (addr) {
+                addMarker(point, addr);
+            } else {
+                geocoder.getLocation(point, function (rs) {
+                    addMarker(point, rs.address);
+                });
+            }
+
+        };
+
+        var addPoint = function (point) {
+            lng = point.lng;
+            lat = point.lat;
+            marker = new BMap.Marker(point);
+            map.addOverlay(marker);
+            map.panTo(point);
+        };
+
+        var addMarker = function (point, addr) {
+            address = addr;
+            // var labelhtml = '<div class="info">地址:' + address + '<br>经度:' + point.lng + '<br>纬度:' + point.lat + '</div>';
+            var labelhtml = '<div class="info">地址:' + address + '</div>';
+            var label = new BMap.Label(labelhtml, {offset: new BMap.Size(16, 20)});
+            label.setStyle({
+                border: 'none',
+                padding: '.75rem 1.25rem'
+            });
+            marker.setLabel(label);
+        };
+
+        var deletePoint = function () {
+            var allOverlay = map.getOverlays();
+            for (var i = 0; i < allOverlay.length; i++) {
+                map.removeOverlay(allOverlay[i]);
+            }
+        };
+
+        var init = function () {
+            map = new BMap.Map("container"); // 创建地图实例
+            var point = new BMap.Point(lng, lat); // 创建点坐标
+            map.enableScrollWheelZoom(true); //开启鼠标滚轮缩放
+            map.centerAndZoom(point, zoom); // 初始化地图,设置中心点坐标和地图级别
+
+            var size = new BMap.Size(10, 20);
+            map.addControl(new BMap.CityListControl({
+                anchor: BMAP_ANCHOR_TOP_LEFT,
+                offset: size,
+            }));
+
+            if ("{$lng}" != '' && "{$lat}" != '') {
+                addPointMarker(point, address);
+            }
+
+            ac = new BMap.Autocomplete({"input": "place", "location": map}); //建立一个自动完成的对象
+            ac.addEventListener("onhighlight", function (e) {  //鼠标放在下拉列表上的事件
+                var str = "";
+                var _value = e.fromitem.value;
+                var value = "";
+                if (e.fromitem.index > -1) {
+                    value = _value.province + _value.city + _value.district + _value.street + _value.business;
+                }
+                str = "FromItem<br />index = " + e.fromitem.index + "<br />value = " + value;
+
+                value = "";
+                if (e.toitem.index > -1) {
+                    _value = e.toitem.value;
+                    value = _value.province + _value.city + _value.district + _value.street + _value.business;
+                }
+                str += "<br />ToItem<br />index = " + e.toitem.index + "<br />value = " + value;
+                $("#searchResultPanel").html(str);
+            });
+            ac.addEventListener("onconfirm", function (e) {    //鼠标点击下拉列表后的事件
+                var _value = e.item.value;
+                myValue = _value.province + _value.city + _value.district + _value.street + _value.business;
+                $("#searchResultPanel").html("onconfirm<br />index = " + e.item.index + "<br />myValue = " + myValue);
+                setPlace();
+            });
+
+            function setPlace(text) {
+                map.clearOverlays();    //清除地图上所有覆盖物
+                function myFun() {
+                    var results = local.getResults();
+                    var result = local.getResults().getPoi(0);
+                    var point = result.point;    //获取第一个智能搜索的结果
+                    map.centerAndZoom(point, 18);
+                    // map.addOverlay(new BMap.Marker(point));    //添加标注
+                    if (result.type != 0) {
+                        address = results.province + results.city + result.address;
+                    } else {
+                        address = result.address;
+                    }
+                    addPointMarker(point, address);
+                }
+
+                var local = new BMap.LocalSearch(map, { //智能搜索
+                    onSearchComplete: myFun
+                });
+                local.search(text || myValue);
+            }
+
+            map.addEventListener("click", function (e) {
+                //通过点击百度地图,可以获取到对应的point, 由point的lng、lat属性就可以获取对应的经度纬度
+                addPointMarker(e.point);
+            });
+
+            //点击搜索按钮
+            $(document).on('click', '#search-btn', function () {
+                if ($("#place").val() == '')
+                    return;
+                setPlace($("#place").val());
+            });
+        };
+
+        var close = function (data) {
+            var index = parent.Layer.getFrameIndex(window.name);
+            var callback = parent.$("#layui-layer" + index).data("callback");
+            //再执行关闭
+            parent.Layer.close(index);
+            //再调用回传函数
+            if (typeof callback === 'function') {
+                callback.call(undefined, data);
+            }
+        };
+
+        //点击确定后执行回调赋值
+        $(document).on('click', '.confirm', function () {
+            var zoom = map.getZoom();
+            var data = {lat: lat, lng: lng, zoom: zoom, address: address};
+            if (fromtype !== totype) {
+                var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
+                data.lng = (result[0] || data.lng).toFixed(5);
+                data.lat = (result[1] || data.lat).toFixed(5);
+                console.log(data, result, fromtype, totype);
+            }
+            close(data);
+        });
+
+        init();
+    });
+</script>
+</body>
+</html>

+ 132 - 0
addons/address/view/index/index.html

@@ -0,0 +1,132 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
+    <title>地图位置(经纬度)选择插件</title>
+    <link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
+    <link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
+</head>
+<body>
+<div class="container">
+
+    <div class="bs-docs-section clearfix">
+        <div class="row">
+            <div class="col-lg-12">
+                <div class="page-header">
+                    <h2>地图位置(经纬度)选择示例</h2>
+                </div>
+
+                <div class="bs-component">
+                    <form action="" method="post" role="form">
+                        <div class="form-group">
+                            <label for=""></label>
+                            <input type="text" class="form-control" name="" id="address" placeholder="地址">
+                        </div>
+                        <div class="form-group">
+                            <label for=""></label>
+                            <input type="text" class="form-control" name="" id="lng" placeholder="经度">
+                        </div>
+                        <div class="form-group">
+                            <label for=""></label>
+                            <input type="text" class="form-control" name="" id="lat" placeholder="纬度">
+                        </div>
+                        <div class="form-group">
+                            <label for=""></label>
+                            <input type="text" class="form-control" name="" id="zoom" placeholder="缩放">
+                        </div>
+
+                        <button type="button" class="btn btn-primary" data-toggle='addresspicker' data-input-id="address" data-lng-id="lng" data-lat-id="lat" data-zoom-id="zoom">点击选择</button>
+                    </form>
+                </div>
+
+                <div class="page-header">
+                    <h2 id="code">调用代码</h2>
+                </div>
+                <div class="bs-component">
+                        <textarea class="form-control" rows="17">
+                            <form action="" method="post" role="form">
+                                <div class="form-group">
+                                    <label for=""></label>
+                                    <input type="text" class="form-control" name="" id="address" placeholder="地址">
+                                </div>
+                                <div class="form-group">
+                                    <label for=""></label>
+                                    <input type="text" class="form-control" name="" id="lng" placeholder="经度">
+                                </div>
+                                <div class="form-group">
+                                    <label for=""></label>
+                                    <input type="text" class="form-control" name="" id="lat" placeholder="纬度">
+                                </div>
+                                <div class="form-group">
+                                    <label for=""></label>
+                                    <input type="text" class="form-control" name="" id="zoom" placeholder="缩放">
+                                </div>
+
+                                <button type="button" class="btn btn-primary" data-toggle='addresspicker' data-input-id="address" data-lng-id="lng" data-lat-id="lat" data-zoom-id="zoom">点击选择</button>
+                            </form>
+                        </textarea>
+                </div>
+                <div class="page-header">
+                    <h2>参数说明</h2>
+                </div>
+
+                <div class="bs-component" style="background:#fff;">
+                    <table class="table table-bordered">
+                        <thead>
+                        <tr>
+                            <th>参数</th>
+                            <th>释义</th>
+                        </tr>
+                        </thead>
+                        <tbody>
+                        <tr>
+                            <td>data-input-id</td>
+                            <td>填充地址的文本框ID</td>
+                        </tr>
+                        <tr>
+                            <td>data-lng-id</td>
+                            <td>填充经度的文本框ID</td>
+                        </tr>
+                        <tr>
+                            <td>data-lat-id</td>
+                            <td>填充纬度的文本框ID</td>
+                        </tr>
+                        <tr>
+                            <td>data-zoom-id</td>
+                            <td>填充缩放的文本框ID</td>
+                        </tr>
+                        </tbody>
+                    </table>
+                </div>
+
+            </div>
+        </div>
+    </div>
+
+</div>
+<!--@formatter:off-->
+<script type="text/javascript">
+    var require = {
+        config: {$config|json_encode}
+    };
+</script>
+<!--@formatter:on-->
+
+<script>
+    require.callback = function () {
+        define('addons/address', ['jquery', 'bootstrap', 'frontend', 'template'], function ($, undefined, Frontend, Template) {
+            var Controller = {
+                index: function () {
+                }
+            };
+            return Controller;
+        });
+        define('lang', function () {
+            return [];
+        });
+    }
+</script>
+
+<script src="__CDN__/assets/js/require.min.js" data-main="__CDN__/assets/js/require-frontend.min.js?v={$site.version}"></script>
+</body>
+</html>

+ 291 - 0
addons/address/view/index/tencent.html

@@ -0,0 +1,291 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
+    <title>地址选择器</title>
+    <link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
+    <link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
+    <style type="text/css">
+        body {
+            margin: 0;
+            padding: 0;
+        }
+
+        #container {
+            position: absolute;
+            left: 0;
+            top: 0;
+            right: 0;
+            bottom: 0;
+        }
+
+        .confirm {
+            position: absolute;
+            bottom: 30px;
+            right: 4%;
+            z-index: 99;
+            height: 50px;
+            width: 50px;
+            line-height: 50px;
+            font-size: 15px;
+            text-align: center;
+            background-color: white;
+            background: #1ABC9C;
+            color: white;
+            border: none;
+            cursor: pointer;
+            border-radius: 50%;
+        }
+
+        .search {
+            position: absolute;
+            width: 400px;
+            top: 0;
+            left: 50%;
+            padding: 5px;
+            margin-left: -200px;
+        }
+
+        .autocomplete-search {
+            text-align: left;
+            cursor: default;
+            background: #fff;
+            border-radius: 2px;
+            -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+            -moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+            background-clip: padding-box;
+            position: absolute;
+            display: none;
+            z-index: 1036;
+            max-height: 254px;
+            overflow: hidden;
+            overflow-y: auto;
+            box-sizing: border-box;
+        }
+
+        .autocomplete-search .autocomplete-suggestion {
+            padding: 5px;
+        }
+
+        .autocomplete-search .autocomplete-suggestion:hover {
+            background: #f0f0f0;
+        }
+    </style>
+</head>
+<body>
+<div class="search">
+    <div class="input-group">
+        <input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
+        <span class="input-group-btn">
+            <button type="button" name="search" id="search-btn" class="btn btn-success">
+                <i class="fa fa-search"></i>
+            </button>
+        </span>
+    </div>
+</div>
+<div class="confirm">确定</div>
+<div id="container"></div>
+
+<script charset="utf-8" src="//map.qq.com/api/js?v=2.exp&libraries=place&key={$config.tencentkey|default=''}"></script>
+<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
+<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
+<script src="__CDN__/assets/addons/address/js/jquery.autocomplete.js"></script>
+
+<script type="text/javascript">
+    $(function () {
+        var map, marker, geocoder, infoWin, searchService, keyword, address, fromtype, totype;
+        address = "{$address|htmlentities}";
+        var lng = Number("{$lng}");
+        var lat = Number("{$lat}");
+        fromtype = "GCJ02";
+        totype = "{$config.coordtype|default='DEFAULT'}"
+        totype = totype === 'DEFAULT' ? "GCJ02" : totype;
+
+        if (lng && lat && fromtype !== totype) {
+            var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
+            lng = result[0] || lng;
+            lat = result[1] || lat;
+        }
+
+        var init = function () {
+            var center = new qq.maps.LatLng(lat, lng);
+            map = new qq.maps.Map(document.getElementById('container'), {
+                center: center,
+                zoom: parseInt("{$config.zoom}")
+            });
+
+            //实例化信息窗口
+            infoWin = new qq.maps.InfoWindow({
+                map: map
+            });
+
+            geocoder = {
+                getAddress: function (latLng) {
+                    $.ajax({
+                        url: "https://apis.map.qq.com/ws/geocoder/v1/?location=" + latLng.lat + "," + latLng.lng + "&key={$config.tencentkey|default=''}&output=jsonp",
+                        dataType: "jsonp",
+                        type: 'GET',
+                        cache: true,
+                        crossDomain: true,
+                        success: function (ret) {
+                            console.log("getAddress:", ret)
+                            if (ret.status === 0) {
+                                var component = ret.result.address_component;
+                                if (ret.result.formatted_addresses && ret.result.formatted_addresses.recommend) {
+                                    var recommend = ret.result.formatted_addresses.recommend;
+                                    var standard_address = ret.result.formatted_addresses.standard_address;
+                                    var address = component.province !== component.city ? component.province + component.city : component.province;
+
+                                    address = address + (recommend.indexOf(component.district) === 0 ? '' : component.district) + recommend;
+                                } else {
+                                    address = ret.result.address;
+                                }
+                                showMarker(ret.result.location, address);
+                                showInfoWin(ret.result.location, address);
+                            }
+                        },
+                        error: function (e) {
+                            console.log(e, 'error')
+                        }
+                    });
+                }
+            };
+
+            //初始化marker
+            showMarker(center);
+            if (address) {
+                showInfoWin(center, address);
+            } else {
+                geocoder.getAddress(center);
+            }
+
+            var place = $("#place");
+            place.autoComplete({
+                minChars: 1,
+                cache: 0,
+                menuClass: 'autocomplete-search',
+                source: function (term, response) {
+                    try {
+                        xhr.abort();
+                    } catch (e) {
+                    }
+                    xhr = $.ajax({
+                        url: "https://apis.map.qq.com/ws/place/v1/suggestion?keyword=" + term + "&key={$config.tencentkey|default=''}&output=jsonp",
+                        dataType: "jsonp",
+                        type: 'GET',
+                        cache: true,
+                        success: function (ret) {
+                            if (ret.status === 0) {
+                                if(ret.data.length === 0){
+                                    $(".autocomplete-suggestions.autocomplete-search").html('');
+                                }
+                                response(ret.data);
+                            } else {
+                                console.log(ret);
+                            }
+                        }
+                    });
+                },
+                renderItem: function (item, search) {
+                    search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+                    var regexp = new RegExp("(" + search.replace(/[\,|\u3000|\uff0c]/, ' ').split(' ').join('|') + ")", "gi");
+                    return "<div class='autocomplete-suggestion' data-item='" + JSON.stringify(item) + "' data-title='" + item.title + "' data-val='" + item.title + "'>" + item.title.replace(regexp, "<b>$1</b>") + "</div>";
+                },
+                onSelect: function (e, term, sel) {
+                    e.preventDefault();
+                    var item = $(sel).data("item");
+                    //调用获取位置方法
+                    geocoder.getAddress(item.location);
+
+                    var position = new qq.maps.LatLng(item.location.lat, item.location.lng);
+                    map.setCenter(position);
+                }
+            });
+
+            //地图点击
+            qq.maps.event.addListener(map, 'click', function (event) {
+                    try {
+                        //调用获取位置方法
+                        geocoder.getAddress(event.latLng);
+                    } catch (e) {
+                        console.log(e);
+                    }
+                }
+            );
+        };
+
+        //显示info窗口
+        var showInfoWin = function (latLng, title) {
+            var position = new qq.maps.LatLng(latLng.lat, latLng.lng);
+            infoWin.open();
+            infoWin.setContent(title);
+            infoWin.setPosition(position);
+        };
+
+        //实例化marker和监听拖拽结束事件
+        var showMarker = function (latLng, title) {
+            console.log("showMarker", latLng, title)
+            var position = new qq.maps.LatLng(latLng.lat, latLng.lng);
+            marker && marker.setMap(null);
+            marker = new qq.maps.Marker({
+                map: map,
+                position: position,
+                draggable: true,
+                title: title || '拖动图标选择位置'
+            });
+
+            //监听拖拽结束
+            qq.maps.event.addListener(marker, 'dragend', function (event) {
+                //调用获取位置方法
+                geocoder.getAddress(event.latLng);
+            });
+        };
+
+        var close = function (data) {
+            var index = parent.Layer.getFrameIndex(window.name);
+            var callback = parent.$("#layui-layer" + index).data("callback");
+            //再执行关闭
+            parent.Layer.close(index);
+            //再调用回传函数
+            if (typeof callback === 'function') {
+                callback.call(undefined, data);
+            }
+        };
+
+        //点击确定后执行回调赋值
+        $(document).on('click', '.confirm', function () {
+            var zoom = map.getZoom();
+            var data = {lat: infoWin.position.lat.toFixed(5), lng: infoWin.position.lng.toFixed(5), zoom: zoom, address: infoWin.content};
+            if (fromtype !== totype) {
+                var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
+                data.lng = (result[0] || data.lng).toFixed(5);
+                data.lat = (result[1] || data.lat).toFixed(5);
+                console.log(data, result, fromtype, totype);
+            }
+
+            close(data);
+        });
+
+        //点击搜索按钮
+        $(document).on('click', '#search-btn', function () {
+            if ($("#place").val() === '')
+                return;
+            var first = $(".autocomplete-search > .autocomplete-suggestion:first");
+            if (!first.length) {
+                return;
+            }
+            var item = first.data("item");
+
+            //调用获取位置方法
+            geocoder.getAddress(item.location);
+
+            var position = new qq.maps.LatLng(item.location.lat, item.location.lng);
+            map.setCenter(position);
+        });
+
+        init();
+    });
+</script>
+</body>
+</html>

+ 1 - 0
addons/epay/.addonrc

@@ -0,0 +1 @@
+{"files":["application\/admin\/controller\/Epay.php","public\/assets\/addons\/epay\/css\/common.css","public\/assets\/addons\/epay\/less\/common.less","public\/assets\/addons\/epay\/images\/paid.png","public\/assets\/addons\/epay\/images\/logo-wechat.png","public\/assets\/addons\/epay\/images\/logo-alipay.png","public\/assets\/addons\/epay\/images\/screenshot-alipay.png","public\/assets\/addons\/epay\/images\/alipay.png","public\/assets\/addons\/epay\/images\/screenshot-wechat.png","public\/assets\/addons\/epay\/images\/wechat.png","public\/assets\/addons\/epay\/images\/scan.png","public\/assets\/addons\/epay\/images\/expired.png","public\/assets\/addons\/epay\/js\/jquery.qrcode.min.js","public\/assets\/addons\/epay\/js\/common.js"],"license":"regular","licenseto":"1170","licensekey":"wUbol3W1VIh0C2Pj oengTbETuRCt2anFoxwLmA==","domains":["yanglao.com"],"licensecodes":[],"validations":["b3df24e8513782a968400fe06d2a184d"]}

+ 112 - 0
addons/epay/Epay.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace addons\epay;
+
+use addons\epay\library\Service;
+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 epayConfigInit()
+    {
+        $this->actionBegin();
+    }
+
+    // 插件方法加载开始
+    public function addonActionBegin()
+    {
+        $this->actionBegin();
+    }
+
+    // 模块控制器方法加载开始
+    public function actionBegin()
+    {
+        //添加命名空间
+        if (!class_exists('\Yansongda\Pay\Pay')) {
+
+            //SDK版本
+            $version = Service::getSdkVersion();
+
+            $libraryDir = ADDON_PATH . 'epay' . DS . 'library' . DS;
+
+            if (PHP_VERSION_ID >= 80000 && $version == Service::SDK_VERSION_V3) {
+                $yansongdaDir = $libraryDir . $version . 'new' . DS . 'Yansongda' . DS;
+
+                // PHP 8.0+ 版本且为V3接口,需要加载 Artful 的命名空间
+                Loader::addNamespace('Yansongda\Artful', $yansongdaDir . 'Artful' . DS);
+                require_once $yansongdaDir . 'Artful' . DS . 'Functions.php';
+            } else {
+                $yansongdaDir = $libraryDir . $version . DS . 'Yansongda' . DS;
+            }
+
+            // 注册Pay命名空间
+            Loader::addNamespace('Yansongda\Pay', $yansongdaDir . 'Pay' . DS);
+
+            $checkArr = [
+                '\Hyperf\Context\Context'     => 'context',
+                '\Hyperf\Contract\Castable'   => 'contract',
+                '\Hyperf\Engine\Constant'     => 'engine',
+                '\Hyperf\Macroable\Macroable' => 'macroable',
+                '\Hyperf\Pimple\Container'    => 'pimple',
+                '\Hyperf\Utils\Arr'           => 'utils',
+            ];
+            foreach ($checkArr as $index => $item) {
+                if (!class_exists($index)) {
+                    Loader::addNamespace(substr($index, 1, strrpos($index, '\\') - 1), $libraryDir . 'hyperf' . DS . $item . DS . 'src' . DS);
+                }
+            }
+
+            if (!class_exists('\Yansongda\Supports\Logger')) {
+                Loader::addNamespace('Yansongda\Supports', $yansongdaDir . 'Supports' . DS);
+            }
+
+            // V3需载入辅助函数
+            if ($version == Service::SDK_VERSION_V3) {
+                require_once $yansongdaDir . 'Pay' . DS . 'Functions.php';
+            }
+        }
+    }
+}

+ 0 - 0
addons/epay/certs/alipayCertPublicKey.crt


+ 0 - 0
addons/epay/certs/alipayRootCert.crt


+ 0 - 0
addons/epay/certs/apiclient_cert.pem


+ 0 - 0
addons/epay/certs/apiclient_key.pem


+ 0 - 0
addons/epay/certs/appCertPublicKey.crt


+ 0 - 0
addons/epay/certs/public_key.pem


+ 529 - 0
addons/epay/config.html

@@ -0,0 +1,529 @@
+<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=='version'}
+                <input type="hidden" value="{$item.value|htmlentities}" name="row[version]"/>
+
+                {elseif $item.name=='wechat'/}
+                <div class="tab-pane fade active in" id="wechat">
+                    <table class="table table-striped table-config">
+                        <tbody>
+                        <tr>
+                            <td width="20%">APP的app_id</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=''|htmlentities}" class="form-control" data-rule="appid" 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=''|htmlentities}" class="form-control" data-rule="appid" 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=''|htmlentities}" class="form-control" data-rule="keysecret" data-tip="公众号中支付时使用"/>
+                                    </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=''|htmlentities}" class="form-control" data-rule="appid" 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="number" name="row[wechat][mch_id]" value="{$item.value.mch_id|default=''|htmlentities}" class="form-control" data-rule="mchid" data-tip=""/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>商户APIv2密钥</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=''|htmlentities}" class="form-control" data-rule="keysecret" data-tip="仅用于微信支付V2接口"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>V3配置</td>
+                            <td>
+                                <table class="table  table-config mb-0">
+                                    <tbody>
+                                    <!-- V3 特有字段 -->
+                                    <tr>
+                                        <td>商户APIv3密钥</td>
+                                        <td>
+                                            <div class="row">
+                                                <div class="col-sm-7 col-xs-12">
+                                                    <input type="text" name="row[wechat][key_v3]" value="{$item.value.key_v3|default=''|htmlentities}" class="form-control" data-rule="keysecret" data-tip=""/>
+                                                </div>
+                                                <div class="col-sm-4"></div>
+                                            </div>
+                                        </td>
+                                    </tr>
+                                    <tr>
+                                        <td>公钥ID</td>
+                                        <td>
+                                            <div class="row">
+                                                <div class="col-sm-7 col-xs-12">
+                                                    <input type="text" name="row[wechat][public_key_id]" value="{$item.value.public_key_id|default=''|htmlentities}" class="form-control" data-rule="public_key_id" data-tip=""/>
+                                                </div>
+                                                <div class="col-sm-4"></div>
+                                            </div>
+                                        </td>
+                                    </tr>
+                                    <tr>
+                                        <td>公钥证书路径</td>
+                                        <td>
+                                            <div class="row">
+                                                <div class="col-sm-7 col-xs-12">
+                                                    <div class="input-group">
+                                                        <input id="c-public_key" class="form-control" size="50" name="row[wechat][public_key]" type="text" value="{$item.value.public_key|default=''|htmlentities}" data-tip="可选, 仅在退款、红包等情况时需要用到">
+                                                        <div class="input-group-addon no-border no-padding">
+                                                            <span><button type="button" id="faupload-public_key" class="btn btn-danger faupload" data-url="epay/upload" data-multipart='{"certname":"public_key"}' data-mimetype="pem" data-input-id="c-public_key" data-multiple="false"><i class="fa fa-upload"></i> {:__('Upload')}</button></span>
+                                                        </div>
+                                                        <span class="msg-box n-right" for="c-public_key"></span>
+                                                    </div>
+                                                    <div style="margin-top:5px;"><a href="https://pay.weixin.qq.com/doc/v3/partner/4012925323" target="_blank"><i class="fa fa-question-circle"></i> 如何获取微信支付公钥证书?</a></div>
+                                                </div>
+                                                <div class="col-sm-4"></div>
+                                            </div>
+                                        </td>
+                                    </tr>
+                                    </tbody>
+                                </table>
+                            </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://kf.qq.com/faq/161222NneAJf161222U7fARv.html" 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://kf.qq.com/faq/161222NneAJf161222U7fARv.html" 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][mode]',['normal'=>'正式环境','dev'=>'沙箱环境','service'=>'服务商模式'],$item.value.mode??'normal')}
+                                        <div style="margin-top:5px;" data-type="dev" class="text-muted {if ($item.value.mode??'')!=='dev'}hidden{/if}">
+                                            <i class="fa fa-info-circle"></i> 沙箱环境:<a href="https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_1&index=2" target="_blank">微信支付验收指引</a>
+                                        </div>
+                                    </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=''|htmlentities}" class="form-control" data-rule="mchid" data-tip="如果未用到子商户,请勿填写"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
+                            <td>子商户APP的app_id</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=''|htmlentities}" class="form-control" data-rule="appid" data-tip="如果未用到子商户,请勿填写"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
+                            <td>子商户公众号的app_id</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=''|htmlentities}" class="form-control" data-rule="appid" data-tip="如果未用到子商户,请勿填写"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr data-type="service" class="{:$item.value.mode!='service'?'hidden':''}">
+                            <td>子商户小程序的app_id</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=''|htmlentities}" class="form-control" data-rule="appid" 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=''|htmlentities}" 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][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>支付模式</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-12 col-xs-12">
+                                        {:Form::radios('row[alipay][mode]',['normal'=>'正式环境','dev'=>'沙箱环境', 'service'=>'服务商模式'],$item.value.mode??'normal')}
+
+                                        <div style="margin-top:5px;" data-mode="dev" class="text-muted {if ($item.value.mode??'')!=='dev'}hidden{/if}">
+                                            <i class="fa fa-info-circle"></i> 如果使用沙箱环境,务必使用沙箱的app_id和沙箱配置,以及使用沙箱账号进行测试。<br>
+                                            沙箱环境:<a href="https://openhome.alipay.com/develop/sandbox/app" target="_blank">https://openhome.alipay.com/develop/sandbox/app</a>
+                                        </div>
+                                    </div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr class="text-muted {if ($item.value.mode??'')!=='service'}hidden{/if}" data-mode="service">
+                            <td width="20%">服务商ID(pid)</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="number" name="row[alipay][pid]" value="{$item.value.pid|default=''|htmlentities}" class="form-control" data-rule="pid" data-tip=""/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td width="20%">应用ID(app_id)</td>
+                            <td>
+                                <div class="row">
+                                    <div class="col-sm-8 col-xs-12">
+                                        <input type="number" name="row[alipay][app_id]" value="{$item.value.app_id|default=''|htmlentities}" class="form-control" data-rule="pid" 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][notify_url]" value="{$item.value.notify_url|default=''|htmlentities}" 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=''|htmlentities}" class="form-control" data-rule="" data-tip="请勿随意修改,实际以逻辑代码中请求的为准"/>
+                                    </div>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>签名方式</td>
+                            <td>
+                                <div>
+                                    <div class="radio">
+                                        <label for="row[alipay][signtype]-publickey"><input id="row[alipay][signtype]-publickey" name="row[alipay][signtype]" {if isset($item.value.signtype)&&$item.value.signtype=='publickey'}checked{/if} type="radio" value="publickey"> 普通公钥</label>
+                                        <label for="row[alipay][signtype]-cert"><input id="row[alipay][signtype]-cert" {if isset($item.value.signtype)&&$item.value.signtype=='cert'}checked{/if} name="row[alipay][signtype]" type="radio" value="cert"> 公钥证书</label>
+                                    </div>
+                                </div>
+                                <div style="margin:5px 0;" class="text-muted">
+                                    <i class="fa fa-info-circle"></i> 如果要使用转账、提现功能,则必须使用公钥证书
+                                </div>
+                                <div data-signtype="publickey" class="{if ($item.value.signtype??'')==='cert'}hidden{/if}">
+                                    <a href="https://opensupport.alipay.com/support/FAQ/65b9c843a8e10e054512d07dprod" target="_blank"><i class="fa fa-info-circle"></i> 如何生成支付宝公钥、应用私钥?</a>
+                                </div>
+                                <div data-signtype="cert" class="{if ($item.value.signtype??'')==='publickey'}hidden{/if}">
+                                    <a href="https://opensupport.alipay.com/support/FAQ/6718ab4563fae8044fe13dc7prod" target="_blank"><i class="fa fa-info-circle"></i> 支付宝公钥证书、应用公钥证书、支付宝根证书?</a>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>
+                                <span data-signtype="publickey" class="{if ($item.value.signtype??'')==='cert'}hidden{/if}">支付宝公钥</span>
+                                <span data-signtype="cert" class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}">支付宝公钥证书路径</span>
+                                (alipay_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|default=''|htmlentities}" placeholder="普通公钥请直接粘贴,公钥证书请点击上传">
+                                            <div class="input-group-addon no-border no-padding {if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}" data-signtype="cert">
+                                                <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>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}" data-signtype="cert">
+                            <td>
+                                <span data-signtype="publickey" class="{if ($item.value.signtype??'')==='cert'}hidden{/if}">应用公钥</span>
+                                <span data-signtype="cert" class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}">应用公钥证书路径</span>
+                                (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|default=''|htmlentities}">
+                                            <div class="input-group-addon no-border no-padding {if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}" data-signtype="cert">
+                                                <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>
+                                    <div class="col-sm-4"></div>
+                                </div>
+                            </td>
+                        </tr>
+                        <tr class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}" data-signtype="cert">
+                            <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|default=''|htmlentities}">
+                                            <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>
+                                    <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=''|htmlentities}" class="form-control" data-rule=""/>
+                                        <div style="margin-top:5px;">
+                                            <span data-signtype="publickey" class="{if ($item.value.signtype??'')==='cert'}hidden{/if}">
+                                                <a href="https://opensupport.alipay.com/support/FAQ/671860f40f6363044540b717prod" target="_blank"><i class="fa fa-question-circle"></i> 如何获取应用私钥?</a>
+                                            </span>
+                                            <span data-signtype="cert" class="{if ($item.value.signtype??'')==='publickey' || ($item.value.signtype??'')==''}hidden{/if}">
+                                                <a href="https://opensupport.alipay.com/support/FAQ/6718ab4563fae8044fe13dc7prod" target="_blank"><i class="fa fa-question-circle"></i> 如何获取应用私钥?</a>
+                                            </span>
+                                        </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-primary btn-embossed disabled">{:__('OK')}</button>
+                        <button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</form>
+<script>
+    require.callback = function () {
+        define('backend/addon', ['backend', 'form'], function (Backend, Form) {
+            var Controller = {
+                config: function () {
+                    $.validator.config({
+                        rules: {
+                            appid: function (element) {
+                                if (!element.value.toString().match(/^wx[a-zA-Z0-9]{16}$/)) {
+                                    return __('必须以wx开头,且长度为18位');
+                                }
+                            },
+                            keysecret: function (element) {
+                                if (!element.value.toString().match(/^[a-zA-Z0-9]{32}$/)) {
+                                    return __('长度为32位,且为字母数字');
+                                }
+                            },
+                            public_key_id: function (element) {
+                                if (!element.value.toString().match(/^PUB_KEY_ID_[0-9]{34}$/)) {
+                                    return __('必须以PUB_KEY_ID_开头,长度为45位');
+                                }
+                            },
+                            mchid: function (element) {
+                                if (!element.value.toString().match(/^[0-9]{8,32}$/)) {
+                                    return __('长度为至少8位,且为数字');
+                                }
+                            },
+                            pid: function (element) {
+                                if (!element.value.toString().match(/^[0-9]{16}$/)) {
+                                    return __('长度为16位,且为数字');
+                                }
+                            },
+                        }
+                    });
+
+                    $(document).on("click", ".nav-group li a[data-toggle='tab']", function () {
+                        if ($(this).attr("href") === "#all") {
+                            $(".tab-pane").addClass("active in");
+                        }
+                        return;
+                    });
+
+                    $(document).on("click", "input[name='row[wechat][mode]']", function () {
+                        $("#wechat [data-type]").addClass("hidden");
+                        $("#wechat [data-type='" + $(this).val() + "']").removeClass("hidden");
+                    });
+                    $(document).on("click", "input[name='row[alipay][mode]']", function () {
+                        $("#alipay [data-mode]").addClass("hidden");
+                        $("#alipay [data-mode='" + $(this).val() + "']").removeClass("hidden");
+                    });
+                    $(document).on("click", "input[name='row[alipay][signtype]']", function () {
+                        let value = $(this).val();
+                        $("#alipay [data-signtype]").addClass("hidden");
+                        $("#alipay [data-signtype='" + value + "']").removeClass("hidden");
+                    });
+
+                    Form.api.bindevent($("form[role=form]"), undefined, undefined, function () {
+                        let value = $("input[name='row[alipay][signtype]']:checked").val();
+                        // 如果选择了普通公钥,则需要清空未使用的应用公钥证书路径值和支付宝根证书路径值
+                        if (value === 'publickey') {
+                            $("#c-app_cert_public_key,#c-alipay_root_cert").val('');
+                        }
+                    });
+                }
+            };
+            return Controller;
+        });
+    };
+</script>

+ 72 - 0
addons/epay/config.php

@@ -0,0 +1,72 @@
+<?php
+
+return [
+    [
+        'name'    => 'version',
+        'title'   => 'API版本(请勿修改该值)',
+        'type'    => 'radio',
+        'content' => [],
+        'value'   => 'v2',
+        'rule'    => '',
+        'msg'     => '',
+        'tip'     => 'V2版本只支持微信支付V2密钥,V3版本只支持微信支付V3密钥,请勿修改该值!!!',
+        'ok'      => '',
+        'extend'  => '',
+    ],
+    [
+        'name'    => 'wechat',
+        'title'   => '微信',
+        'type'    => 'array',
+        'content' => [],
+        'value'   => [
+            'appid'          => '',
+            'app_id'         => '',
+            'app_secret'     => '',
+            'miniapp_id'     => '',
+            'mch_id'         => '',
+            'key'            => '',
+            'key_v3'         => '',
+            'public_key_id'  => '',
+            'public_key'     => '/addons/epay/certs/public_key.pem',
+            'cert_client'    => '/addons/epay/certs/apiclient_cert.pem',
+            'cert_key'       => '/addons/epay/certs/apiclient_key.pem',
+            'mode'           => 'normal',
+            'sub_mch_id'     => '',
+            'sub_appid'      => '',
+            'sub_app_id'     => '',
+            'sub_miniapp_id' => '',
+            'notify_url'     => '',
+            'log'            => '1',
+        ],
+        'rule'    => 'required',
+        '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'         => '',
+            'signtype'            => 'cert',
+            'pid'                 => '',
+            'ali_public_key'      => '',
+            'app_cert_public_key' => '',
+            'alipay_root_cert'    => '',
+            'log'                 => '1',
+            'scanpay'             => '0',
+        ],
+        'rule'    => 'required',
+        'msg'     => '',
+        'tip'     => '支付宝参数配置',
+        'ok'      => '',
+        'extend'  => '',
+    ]
+];

+ 234 - 0
addons/epay/controller/Api.php

@@ -0,0 +1,234 @@
+<?php
+
+namespace addons\epay\controller;
+
+use addons\epay\library\Service;
+use addons\epay\library\Wechat;
+use addons\third\model\Third;
+use app\common\library\Auth;
+use Exception;
+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', $this->request->request('paytype'));
+        $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扫码支付)
+     */
+    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 = Service::isVersionV3() ? $pay->find(['out_trade_no' => $orderid]) : $pay->find($orderid, 'scan');
+                $this->success("", "", ['status' => $result['trade_state'] ?? 'NOTPAY']);
+            } catch (GatewayException $e) {
+                $this->error("查询失败(1001)");
+            }
+        }
+
+        $orderData = Session::get("wechatorderdata");
+        if (!$orderData) {
+            $this->error("请求参数错误");
+        }
+        if ($isWechat && $isMobile) {
+            //发起公众号(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扫码支付)
+     */
+    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(['out_trade_no' => $orderid]);
+                if ($result['code'] == '10000' && $result['trade_status'] == 'TRADE_SUCCESS') {
+                    $this->success("", "", ['status' => $result['trade_status']]);
+                } else {
+                    $this->error("查询失败");
+                }
+            } catch (GatewayException $e) {
+                $this->error("查询失败(1001)");
+            }
+        }
+
+        //发起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()
+    {
+        $paytype = $this->request->param('paytype');
+        $pay = Service::checkNotify($paytype);
+        if (!$pay) {
+            return json(['code' => 'FAIL', 'message' => '失败'], 500, ['Content-Type' => 'application/json']);
+        }
+
+        // 获取回调数据,V3和V2的回调接收不同
+        $data = Service::isVersionV3() ? $pay->callback() : $pay->verify();
+
+        try {
+            //微信支付V3返回和V2不同
+            if (Service::isVersionV3() && $paytype === 'wechat') {
+                $data = $data['resource']['ciphertext'];
+                $data['total_fee'] = $data['amount']['total'];
+            }
+
+            \think\Log::record($data);
+            //获取支付金额、订单号
+            $payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
+            $out_trade_no = $data['out_trade_no'];
+
+            \think\Log::record("回调成功,订单号:{$out_trade_no},金额:{$payamount}");
+
+            //你可以在此编写订单逻辑
+        } catch (Exception $e) {
+            \think\Log::record("回调逻辑处理错误:" . $e->getMessage(), "error");
+        }
+
+        //下面这句必须要执行,且在此之前不能有任何输出
+        if (Service::isVersionV3()) {
+            return $pay->success()->getBody()->getContents();
+        } else {
+            return $pay->success()->send();
+        }
+    }
+
+    /**
+     * 支付成功返回
+     */
+    public function returnx()
+    {
+        $paytype = $this->request->param('paytype');
+        if (Service::checkReturn($paytype)) {
+            echo '签名错误';
+            return;
+        }
+
+        //你可以在这里定义你的提示信息,但切记不可在此编写逻辑
+        $this->success("恭喜你!支付成功!", addon_url("epay/index/index"));
+    }
+
+}

+ 131 - 0
addons/epay/controller/Index.php

@@ -0,0 +1,131 @@
+<?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->post('amount');
+        $type = $this->request->post('type');
+        $method = $this->request->post('method');
+        $openid = $this->request->post('openid', "");
+
+        if (!$amount || $amount < 0) {
+            $this->error("支付金额必须大于0");
+        }
+
+        if (!$type || !in_array($type, ['alipay', 'wechat'])) {
+            $this->error("支付类型不能为空");
+        }
+
+        if (in_array($method, ['miniapp', 'mp']) && !$openid) {
+            $this->error("openid不能为空");
+        }
+
+        //订单号
+        $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, $openid);
+
+        return $response;
+    }
+
+    /**
+     * 支付成功,仅供开发测试
+     */
+    public function notifyx()
+    {
+        $paytype = $this->request->param('paytype');
+        $pay = Service::checkNotify($paytype);
+        if (!$pay) {
+            return json(['code' => 'FAIL', 'message' => '失败'], 500, ['Content-Type' => 'application/json']);
+        }
+
+        // 获取回调数据,V3和V2的回调接收不同
+        $data = Service::isVersionV3() ? $pay->callback() : $pay->verify();
+
+        try {
+            //微信支付V3返回和V2不同
+            if (Service::isVersionV3() && $paytype === 'wechat') {
+                $data = $data['resource']['ciphertext'];
+                $data['total_fee'] = $data['amount']['total'];
+            }
+
+            \think\Log::record($data);
+            //获取支付金额、订单号
+            $payamount = $paytype == 'alipay' ? $data['total_amount'] : $data['total_fee'] / 100;
+            $out_trade_no = $data['out_trade_no'];
+
+            \think\Log::record("回调成功,订单号:{$out_trade_no},金额:{$payamount}");
+
+            //你可以在此编写订单逻辑
+        } catch (Exception $e) {
+            \think\Log::record("回调逻辑处理错误:" . $e->getMessage(), "error");
+        }
+
+        //下面这句必须要执行,且在此之前不能有任何输出
+        if (Service::isVersionV3()) {
+            return $pay->success()->getBody()->getContents();
+        } else {
+            return $pay->success()->send();
+        }
+    }
+
+    /**
+     * 支付返回,仅供开发测试
+     */
+    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('签名错误', '');
+        }
+
+        //你可以在这里定义你的提示信息,但切记不可在此编写逻辑
+        $this->success("请返回网站查看支付结果", addon_url("epay/index/index"));
+    }
+}

+ 10 - 0
addons/epay/info.ini

@@ -0,0 +1,10 @@
+name = epay
+title = 微信支付宝整合
+intro = 可用于快速整合微信支付、支付宝支付功能
+author = FastAdmin
+website = https://www.fastadmin.net
+version = 1.3.15
+state = 1
+url = /addons/epay
+license = regular
+licenseto = 1170

+ 18 - 0
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);
+    }
+}

+ 16 - 0
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;
+    }
+
+}

+ 63 - 0
addons/epay/library/RedirectResponse.php

@@ -0,0 +1,63 @@
+<?php
+
+namespace addons\epay\library;
+
+use Symfony\Component\HttpFoundation\RedirectResponse as BaseRedirectResponse;
+use JsonSerializable;
+
+class RedirectResponse extends BaseRedirectResponse implements JsonSerializable
+{
+    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(){
+                document.getElementById("redirect").style.display = "block";
+            }, 1000);
+        </script>
+    </body>
+</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));
+
+        $this->headers->set('Location', $url);
+
+        return $this;
+    }
+
+    #[\ReturnTypeWillChange]
+    public function jsonSerialize()
+    {
+        return $this->getContent();
+    }
+
+    // 使用 PHP 8 兼容的新序列化方式
+    public function __serialize(): array
+    {
+        return ['content' => $this->content];
+    }
+
+    public function __unserialize(array $data): void
+    {
+        $this->content = $data['content'] ?? '';
+    }
+}

+ 46 - 0
addons/epay/library/Response.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace addons\epay\library;
+
+class Response extends \Symfony\Component\HttpFoundation\Response implements \JsonSerializable, \Serializable
+{
+    public function __toString()
+    {
+        return $this->getContent();
+    }
+
+    public function jsonSerialize(): mixed
+    {
+        return $this->getContent();
+    }
+
+    public function serialize()
+    {
+        return serialize($this->content);
+    }
+
+    public function unserialize($data)
+    {
+        return $this->content = unserialize($data);
+    }
+
+    /**
+     * (PHP 8.1+) Magic method for serialization.
+     *
+     * @return mixed
+     */
+    public function __serialize()
+    {
+        return $this->content;
+    }
+
+    /**
+     * (PHP 8.1+) Magic method for unserialization.
+     *
+     * @param array $data
+     */
+    public function __unserialize(array $data): void
+    {
+        $this->content = $data;
+    }
+}

+ 494 - 0
addons/epay/library/Service.php

@@ -0,0 +1,494 @@
+<?php
+
+namespace addons\epay\library;
+
+use addons\third\model\Third;
+use app\common\library\Auth;
+use Exception;
+use think\Hook;
+use think\Session;
+use Yansongda\Pay\Pay;
+use Yansongda\Supports\Str;
+
+/**
+ * 订单服务类
+ *
+ * @package addons\epay\library
+ */
+class Service
+{
+
+    public const SDK_VERSION_V2 = 'v2';
+
+    public const SDK_VERSION_V3 = 'v3';
+
+    /**
+     * 提交订单
+     * @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    支付方法
+     * @param string      $openid    Openid
+     * @param array       $custom    自定义微信支付宝相关配置
+     * @return Response|RedirectResponse|Collection
+     * @throws Exception
+     */
+    public static function submitOrder($amount, $orderid = null, $type = null, $title = null, $notifyurl = null, $returnurl = null, $method = null, $openid = '', $custom = [])
+    {
+        $version = self::getSdkVersion();
+        $request = request();
+        $addonConfig = get_addon_config('epay');
+
+        if (!is_array($amount)) {
+            $params = [
+                'amount'    => $amount,
+                'orderid'   => $orderid,
+                'type'      => $type,
+                'title'     => $title,
+                'notifyurl' => $notifyurl,
+                'returnurl' => $returnurl,
+                'method'    => $method,
+                'openid'    => $openid,
+                'custom'    => $custom,
+            ];
+        } else {
+            $params = $amount;
+        }
+        $type = isset($params['type']) && in_array($params['type'], ['alipay', 'wechat']) ? $params['type'] : 'wechat';
+        $method = $params['method'] ?? 'web';
+        $orderid = $params['orderid'] ?? date("YmdHis") . mt_rand(100000, 999999);
+        $amount = $params['amount'] ?? 1;
+        $title = $params['title'] ?? "支付";
+        $auth_code = $params['auth_code'] ?? '';
+        $openid = $params['openid'] ?? '';
+
+        //自定义微信支付宝相关配置
+        $custom = $params['custom'] ?? [];
+
+        //未定义则使用默认回调和跳转
+        $notifyurl = !empty($params['notifyurl']) ? $params['notifyurl'] : $request->root(true) . '/addons/epay/index/notifyx/paytype/' . $type;
+        $returnurl = !empty($params['returnurl']) ? $params['returnurl'] : $request->root(true) . '/addons/epay/index/returnx/paytype/' . $type . '/out_trade_no/' . $orderid;
+
+        $html = '';
+        $config = Service::getConfig($type, array_merge($custom, ['notify_url' => $notifyurl, 'return_url' => $returnurl]));
+
+        //判断是否移动端或微信内浏览器
+        $isMobile = $request->isMobile();
+        $isWechat = strpos($request->server('HTTP_USER_AGENT'), 'MicroMessenger') !== false;
+
+        $result = null;
+        if ($type == 'alipay') {
+            //如果是PC支付,判断当前环境,进行跳转
+            if ($method == 'web') {
+                //如果是微信环境或后台配置PC使用扫码支付
+                if ($isWechat || $addonConfig['alipay']['scanpay']) {
+                    Session::set("alipayorderdata", $params);
+                    $url = addon_url('epay/api/alipay', [], true, true);
+                    return new RedirectResponse($url);
+                } elseif ($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;
+                case 'mini':
+                case 'miniapp':
+                    //小程序支付,直接返回字符串
+                    //小程序支付必须要有buyer_id或buyer_open_id
+                    if (is_numeric($openid) && strlen($openid) === 16) {
+                        $params['buyer_id'] = $openid;
+                    } else {
+                        $params['buyer_open_id'] = $openid;
+                    }
+                    $result = $pay->mini($params);
+                    break;
+                default:
+            }
+        } else {
+            //如果是PC支付,判断当前环境,进行跳转
+            if ($method == 'web') {
+                //如果是移动端,但不是微信环境
+                if ($isMobile && !$isWechat) {
+                    $method = 'wap';
+                } else {
+                    Session::set("wechatorderdata", $params);
+                    $url = addon_url('epay/api/wechat', [], true, true);
+                    return new RedirectResponse($url);
+                }
+            }
+
+            //单位分
+            $total_fee = function_exists('bcmul') ? bcmul($amount, 100) : $amount * 100;
+            $total_fee = (int)$total_fee;
+            $ip = $request->ip();
+            //微信服务商模式时需传递sub_openid参数
+            $openidName = $addonConfig['wechat']['mode'] == 'service' ? 'sub_openid' : 'openid';
+
+            //创建支付对象
+            $pay = Pay::wechat($config);
+
+            if (self::isVersionV3()) {
+                //V3支付
+                $params = [
+                    'out_trade_no' => $orderid,
+                    'description'  => $title,
+                    'amount'       => [
+                        'total' => $total_fee,
+                    ]
+                ];
+                switch ($method) {
+                    case 'mp':
+                        //公众号支付
+                        //公众号支付必须有openid
+                        $params['payer'] = [$openidName => $openid];
+                        $result = $pay->mp($params);
+                        break;
+                    case 'wap':
+                        //手机网页支付,跳转
+                        $params['scene_info'] = [
+                            'payer_client_ip' => $ip,
+                            'h5_info'         => [
+                                'type' => '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;
+                    case 'mini':
+                    case 'miniapp':
+                        //小程序支付,直接返回字符串
+                        //小程序支付必须要有openid
+                        $params['payer'] = [$openidName => $openid];
+                        $result = $pay->mini($params);
+                        break;
+                    default:
+                }
+            } else {
+                //V2支付
+                $params = [
+                    'out_trade_no' => $orderid,
+                    'body'         => $title,
+                    'total_fee'    => $total_fee,
+                ];
+                switch ($method) {
+                    case 'mp':
+                        //公众号支付
+                        //公众号支付必须有openid
+                        $params[$openidName] = $openid;
+                        $result = $pay->mp($params);
+                        break;
+                    case 'wap':
+                        //手机网页支付,跳转
+                        $params['spbill_create_ip'] = $ip;
+                        $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 'mini':
+                    case 'miniapp':
+                        //小程序支付,直接返回字符串
+                        //小程序支付必须要有openid
+                        $params[$openidName] = $openid;
+                        $result = $pay->miniapp($params);
+                        break;
+                    default:
+                }
+            }
+        }
+
+        //使用重写的Response类、RedirectResponse、Collection类
+        if ($result instanceof \Symfony\Component\HttpFoundation\RedirectResponse) {
+            $result = new RedirectResponse($result->getTargetUrl());
+        } elseif ($result instanceof \Symfony\Component\HttpFoundation\Response) {
+            $result = new Response($result->getContent());
+        } elseif ($result instanceof \Yansongda\Supports\Collection) {
+            $result = Collection::make($result->all());
+        } elseif ($result instanceof \GuzzleHttp\Psr7\Response) {
+            $result = new Response($result->getBody());
+        }
+
+        return $result;
+    }
+
+    /**
+     * 验证回调是否成功
+     * @param string $type   支付类型
+     * @param array  $custom 自定义配置信息
+     * @return bool|\Yansongda\Pay\Gateways\Alipay|\Yansongda\Pay\Gateways\Wechat|\Yansongda\Pay\Provider\Wechat|\Yansongda\Pay\Provider\Alipay
+     */
+    public static function checkNotify($type, $custom = [])
+    {
+        $type = strtolower($type);
+        if (!in_array($type, ['wechat', 'alipay'])) {
+            return false;
+        }
+
+        $version = self::getSdkVersion();
+
+        try {
+            $config = self::getConfig($type, $custom);
+            $pay = $type == 'wechat' ? Pay::wechat($config) : Pay::alipay($config);
+
+            $data = Service::isVersionV3() ? $pay->callback() : $pay->verify();
+            if ($type == 'alipay') {
+                if (in_array($data['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED'])) {
+                    return $pay;
+                }
+            } else {
+                return $pay;
+            }
+        } catch (Exception $e) {
+            \think\Log::record("回调请求参数解析错误", "error");
+            return false;
+        }
+
+        return false;
+    }
+
+    /**
+     * 验证返回是否成功,请勿用于判断是否支付成功的逻辑验证
+     * 已弃用
+     *
+     * @param string $type   支付类型
+     * @param array  $custom 自定义配置信息
+     * @return bool
+     * @deprecated  已弃用,请勿用于逻辑验证
+     */
+    public static function checkReturn($type, $custom = [])
+    {
+        //由于PC及移动端无法获取请求的参数信息,取消return验证,均返回true
+        return true;
+    }
+
+    /**
+     * 处理证书路径
+     * @param array  $config 配置
+     * @param string $field  字段
+     * @return void
+     */
+    private static function processAddonsPath(&$config, $field)
+    {
+        if (isset($config[$field]) && substr($config[$field], 0, 8) == '/addons/') {
+            $config[$field] = ROOT_PATH . str_replace('/', DS, substr($config[$field], 1));
+        }
+    }
+
+    /**
+     * 获取配置
+     * @param string $type   支付类型
+     * @param array  $custom 自定义配置,用于覆盖插件默认配置
+     * @return array
+     */
+    public static function getConfig($type = 'wechat', $custom = [])
+    {
+        $addonConfig = get_addon_config('epay');
+        $config = $addonConfig[$type] ?? $addonConfig['wechat'];
+
+        // SDK版本
+        $version = self::getSdkVersion();
+
+        // 处理微信证书路径
+        if ($type === 'wechat') {
+            $certFields = ['cert_client', 'cert_key', 'public_key'];
+            foreach ($certFields as $field) {
+                self::processAddonsPath($config, $field);
+            }
+        }
+
+        // 处理支付宝证书路径
+        if ($type === 'alipay') {
+            $config['signtype'] = $config['signtype'] ?? 'publickey';
+            if ($config['signtype'] == 'cert') {
+                $certFields = ['app_cert_public_key', 'alipay_root_cert', 'ali_public_key'];
+                foreach ($certFields as $field) {
+                    self::processAddonsPath($config, $field);
+                }
+            } else {
+                // 如果是普通公钥需要将app_cert_public_key和alipay_root_cert设置为空,不然会导致错误
+                $config['app_cert_public_key'] = '';
+                $config['alipay_root_cert'] = '';
+            }
+        }
+
+        // V3支付
+        if (self::isVersionV3()) {
+            if ($type == 'wechat') {
+                $config['mp_app_id'] = $config['app_id'] ?? '';
+                $config['app_id'] = $config['appid'] ?? '';
+                $config['mini_app_id'] = $config['miniapp_id'] ?? '';
+                $config['combine_mch_id'] = $config['combine_mch_id'] ?? '';
+                $config['mch_secret_key'] = $config['key_v3'] ?? '';
+                $config['mch_secret_cert'] = $config['cert_key'];
+                $config['mch_public_cert_path'] = $config['cert_client'];
+
+                // 配置微信支付公钥ID及证书路径
+                $config['wechat_public_cert_path'] = [
+                    ($config['public_key_id'] ?? '') => ($config['public_key'] ?? '')
+                ];
+
+                $config['sub_mp_app_id'] = $config['sub_appid'] ?? '';
+                $config['sub_app_id'] = $config['sub_app_id'] ?? '';
+                $config['sub_mini_app_id'] = $config['sub_miniapp_id'] ?? '';
+                $config['sub_mch_id'] = $config['sub_mch_id'] ?? '';
+            } elseif ($type == 'alipay') {
+                $config['app_secret_cert'] = $config['private_key'] ?? '';
+                $config['app_public_cert_path'] = $config['app_cert_public_key'] ?? '';
+                $config['alipay_public_cert_path'] = $config['ali_public_key'] ?? '';
+                $config['alipay_root_cert_path'] = $config['alipay_root_cert'] ?? '';
+                $config['service_provider_id'] = $config['pid'] ?? '';
+            }
+            $modeArr = ['normal' => 0, 'dev' => 1, 'service' => 2];
+            $config['mode'] = $modeArr[$config['mode']] ?? 0;
+        }
+
+        // 日志
+        if ($config['log']) {
+            $config['log'] = [
+                'enable' => true,
+                'file'   => LOG_PATH . 'epaylogs' . DS . $type . '-' . date("Y-m-d") . '.log',
+                'level'  => 'debug'
+            ];
+        } else {
+            $config['log'] = [
+                'enable' => false,
+            ];
+        }
+
+        // GuzzleHttp配置,可选
+        $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'];
+
+        //合并自定义配置
+        $config = array_merge($config, $custom);
+
+        //v3版本时返回的结构不同
+        if (self::isVersionV3()) {
+            $config = [$type => ['default' => $config], 'logger' => $config['log'], 'http' => $config['http'], '_force' => true];
+
+        }
+        return $config;
+    }
+
+    /**
+     * 获取微信Openid
+     *
+     * @param array $custom 自定义配置信息
+     * @return mixed|string
+     */
+    public static function getOpenid($custom = [])
+    {
+        $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) {
+                $addonConfig = get_addon_config('epay');
+                $wechat = new Wechat($custom['app_id'] ?? $addonConfig['wechat']['app_id'], $custom['app_secret'] ?? $addonConfig['wechat']['app_secret']);
+                $openid = $wechat->getOpenid();
+            }
+        }
+        return $openid;
+    }
+
+    /**
+     * 获取SDK版本
+     * @return mixed|string
+     */
+    public static function getSdkVersion()
+    {
+        $addonConfig = get_addon_config('epay');
+        return $addonConfig['version'] ?? self::SDK_VERSION_V2;
+    }
+
+    /**
+     * 判断是否V2支付
+     * @return bool
+     */
+    public static function isVersionV2()
+    {
+        return self::getSdkVersion() === self::SDK_VERSION_V2;
+    }
+
+    /**
+     * 判断是否V3支付
+     * @return bool
+     */
+    public static function isVersionV3()
+    {
+        return self::getSdkVersion() === self::SDK_VERSION_V3;
+    }
+}

+ 110 - 0
addons/epay/library/Wechat.php

@@ -0,0 +1,110 @@
+<?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);
+                    if (!isset($token['openid']) && isset($token['errmsg'])) {
+                        exception($token['errmsg']);
+                    }
+                    $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;
+    }
+}

+ 2 - 0
addons/epay/library/hyperf/context/.gitattributes

@@ -0,0 +1,2 @@
+/tests export-ignore
+/.github export-ignore

+ 21 - 0
addons/epay/library/hyperf/context/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) Hyperf
+
+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.

+ 40 - 0
addons/epay/library/hyperf/context/composer.json

@@ -0,0 +1,40 @@
+{
+    "name": "hyperf/context",
+    "description": "A coroutine context library.",
+    "license": "MIT",
+    "keywords": [
+        "php",
+        "swoole",
+        "hyperf",
+        "context"
+    ],
+    "homepage": "https://hyperf.io",
+    "support": {
+        "docs": "https://hyperf.wiki",
+        "issues": "https://github.com/hyperf/hyperf/issues",
+        "pull-request": "https://github.com/hyperf/hyperf/pulls",
+        "source": "https://github.com/hyperf/hyperf"
+    },
+    "require": {
+        "php": ">=7.2",
+        "hyperf/engine": "^1.1"
+    },
+    "autoload": {
+        "psr-4": {
+            "Hyperf\\Context\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "HyperfTest\\Context\\": "tests/"
+        }
+    },
+    "config": {
+        "sort-packages": true
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "2.2-dev"
+        }
+    }
+}

+ 112 - 0
addons/epay/library/hyperf/context/src/Context.php

@@ -0,0 +1,112 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Context;
+
+use Hyperf\Engine\Coroutine;
+
+class Context
+{
+    protected static $nonCoContext = [];
+
+    public static function set(string $id, $value)
+    {
+        if (Coroutine::id() > 0) {
+            Coroutine::getContextFor()[$id] = $value;
+        } else {
+            static::$nonCoContext[$id] = $value;
+        }
+        return $value;
+    }
+
+    public static function get(string $id, $default = null, $coroutineId = null)
+    {
+        if (Coroutine::id() > 0) {
+            return Coroutine::getContextFor($coroutineId)[$id] ?? $default;
+        }
+
+        return static::$nonCoContext[$id] ?? $default;
+    }
+
+    public static function has(string $id, $coroutineId = null)
+    {
+        if (Coroutine::id() > 0) {
+            return isset(Coroutine::getContextFor($coroutineId)[$id]);
+        }
+
+        return isset(static::$nonCoContext[$id]);
+    }
+
+    /**
+     * Release the context when you are not in coroutine environment.
+     */
+    public static function destroy(string $id)
+    {
+        unset(static::$nonCoContext[$id]);
+    }
+
+    /**
+     * Copy the context from a coroutine to current coroutine.
+     * This method will delete the origin values in current coroutine.
+     */
+    public static function copy(int $fromCoroutineId, array $keys = []): void
+    {
+        $from = Coroutine::getContextFor($fromCoroutineId);
+        if ($from === null) {
+            return;
+        }
+
+        $current = Coroutine::getContextFor();
+
+        if ($keys) {
+            $map = array_intersect_key($from->getArrayCopy(), array_flip($keys));
+        } else {
+            $map = $from->getArrayCopy();
+        }
+
+        $current->exchangeArray($map);
+    }
+
+    /**
+     * Retrieve the value and override it by closure.
+     */
+    public static function override(string $id, \Closure $closure)
+    {
+        $value = null;
+        if (self::has($id)) {
+            $value = self::get($id);
+        }
+        $value = $closure($value);
+        self::set($id, $value);
+        return $value;
+    }
+
+    /**
+     * Retrieve the value and store it if not exists.
+     * @param mixed $value
+     */
+    public static function getOrSet(string $id, $value)
+    {
+        if (! self::has($id)) {
+            return self::set($id, value($value));
+        }
+        return self::get($id);
+    }
+
+    public static function getContainer()
+    {
+        if (Coroutine::id() > 0) {
+            return Coroutine::getContextFor();
+        }
+
+        return static::$nonCoContext;
+    }
+}

+ 2 - 0
addons/epay/library/hyperf/contract/.gitattributes

@@ -0,0 +1,2 @@
+/tests export-ignore
+/.github export-ignore

+ 21 - 0
addons/epay/library/hyperf/contract/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) Hyperf
+
+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.

+ 33 - 0
addons/epay/library/hyperf/contract/composer.json

@@ -0,0 +1,33 @@
+{
+    "name": "hyperf/contract",
+    "description": "The contracts of Hyperf.",
+    "license": "MIT",
+    "keywords": [
+        "php",
+        "swoole",
+        "hyperf"
+    ],
+    "homepage": "https://hyperf.io",
+    "support": {
+        "docs": "https://hyperf.wiki",
+        "issues": "https://github.com/hyperf/hyperf/issues",
+        "pull-request": "https://github.com/hyperf/hyperf/pulls",
+        "source": "https://github.com/hyperf/hyperf"
+    },
+    "require": {
+        "php": ">=7.2"
+    },
+    "autoload": {
+        "psr-4": {
+            "Hyperf\\Contract\\": "src/"
+        }
+    },
+    "config": {
+        "sort-packages": true
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "2.2-dev"
+        }
+    }
+}

+ 16 - 0
addons/epay/library/hyperf/contract/src/ApplicationInterface.php

@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface ApplicationInterface
+{
+}

+ 22 - 0
addons/epay/library/hyperf/contract/src/Castable.php

@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface Castable
+{
+    /**
+     * Get the name of the caster class to use when casting from / to this cast target.
+     *
+     * @return CastsAttributes|CastsInboundAttributes|string
+     */
+    public static function castUsing();
+}

+ 33 - 0
addons/epay/library/hyperf/contract/src/CastsAttributes.php

@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface CastsAttributes
+{
+    /**
+     * Transform the attribute from the underlying model values.
+     *
+     * @param object $model
+     * @param mixed $value
+     * @return mixed
+     */
+    public function get($model, string $key, $value, array $attributes);
+
+    /**
+     * Transform the attribute to its underlying model values.
+     *
+     * @param object $model
+     * @param mixed $value
+     * @return array|string
+     */
+    public function set($model, string $key, $value, array $attributes);
+}

+ 24 - 0
addons/epay/library/hyperf/contract/src/CastsInboundAttributes.php

@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface CastsInboundAttributes
+{
+    /**
+     * Transform the attribute to its underlying model values.
+     *
+     * @param object $model
+     * @param mixed $value
+     * @return array
+     */
+    public function set($model, string $key, $value, array $attributes);
+}

+ 17 - 0
addons/epay/library/hyperf/contract/src/CompressInterface.php

@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface CompressInterface
+{
+    public function compress(): UnCompressInterface;
+}

+ 41 - 0
addons/epay/library/hyperf/contract/src/ConfigInterface.php

@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface ConfigInterface
+{
+    /**
+     * Finds an entry of the container by its identifier and returns it.
+     *
+     * @param string $key identifier of the entry to look for
+     * @param mixed $default default value of the entry when does not found
+     * @return mixed entry
+     */
+    public function get(string $key, $default = null);
+
+    /**
+     * Returns true if the container can return an entry for the given identifier.
+     * Returns false otherwise.
+     *
+     * @param string $keys identifier of the entry to look for
+     * @return bool
+     */
+    public function has(string $keys);
+
+    /**
+     * Set a value to the container by its identifier.
+     *
+     * @param string $key identifier of the entry to set
+     * @param mixed $value the value that save to container
+     */
+    public function set(string $key, $value);
+}

+ 40 - 0
addons/epay/library/hyperf/contract/src/ConnectionInterface.php

@@ -0,0 +1,40 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface ConnectionInterface
+{
+    /**
+     * Get the real connection from pool.
+     */
+    public function getConnection();
+
+    /**
+     * Reconnect the connection.
+     */
+    public function reconnect(): bool;
+
+    /**
+     * Check the connection is valid.
+     */
+    public function check(): bool;
+
+    /**
+     * Close the connection.
+     */
+    public function close(): bool;
+
+    /**
+     * Release the connection to pool.
+     */
+    public function release(): void;
+}

+ 53 - 0
addons/epay/library/hyperf/contract/src/ContainerInterface.php

@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Psr\Container\ContainerInterface as PsrContainerInterface;
+
+interface ContainerInterface extends PsrContainerInterface
+{
+    /**
+     * Build an entry of the container by its name.
+     * This method behave like get() except resolves the entry again every time.
+     * For example if the entry is a class then a new instance will be created each time.
+     * This method makes the container behave like a factory.
+     *
+     * @param string $name entry name or a class name
+     * @param array $parameters Optional parameters to use to build the entry. Use this to force specific parameters
+     *                          to specific values. Parameters not defined in this array will be resolved using
+     *                          the container.
+     * @throws InvalidArgumentException the name parameter must be of type string
+     * @throws NotFoundException no entry found for the given name
+     */
+    public function make(string $name, array $parameters = []);
+
+    /**
+     * Bind an arbitrary resolved entry to an identifier.
+     * Useful for testing 'get'.
+     *
+     * @param mixed $entry
+     */
+    public function set(string $name, $entry);
+
+    /**
+     * Unbind an arbitrary resolved entry.
+     */
+    public function unbind(string $name);
+
+    /**
+     * Bind an arbitrary definition to an identifier.
+     * Useful for testing 'make'.
+     *
+     * @param array|callable|string $definition
+     */
+    public function define(string $name, $definition);
+}

+ 17 - 0
addons/epay/library/hyperf/contract/src/DispatcherInterface.php

@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface DispatcherInterface
+{
+    public function dispatch(...$params);
+}

+ 25 - 0
addons/epay/library/hyperf/contract/src/FrequencyInterface.php

@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface FrequencyInterface
+{
+    /**
+     * Number of hit per time.
+     */
+    public function hit(int $number = 1): bool;
+
+    /**
+     * Hits per second.
+     */
+    public function frequency(): float;
+}

+ 17 - 0
addons/epay/library/hyperf/contract/src/IdGeneratorInterface.php

@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface IdGeneratorInterface
+{
+    public function generate();
+}

+ 30 - 0
addons/epay/library/hyperf/contract/src/LengthAwarePaginatorInterface.php

@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface LengthAwarePaginatorInterface extends PaginatorInterface
+{
+    /**
+     * Create a range of pagination URLs.
+     */
+    public function getUrlRange(int $start, int $end): array;
+
+    /**
+     * Determine the total number of items in the data store.
+     */
+    public function total(): int;
+
+    /**
+     * Get the page number of the last available page.
+     */
+    public function lastPage(): int;
+}

+ 17 - 0
addons/epay/library/hyperf/contract/src/MiddlewareInitializerInterface.php

@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface MiddlewareInitializerInterface
+{
+    public function initCoreMiddleware(string $serverName): void;
+}

+ 32 - 0
addons/epay/library/hyperf/contract/src/NormalizerInterface.php

@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface NormalizerInterface
+{
+    /**
+     * Normalizes an object into a set of arrays/scalars.
+     *
+     * @param mixed $object
+     * @return null|array|\ArrayObject|bool|float|int|string
+     */
+    public function normalize($object);
+
+    /**
+     * Denormalizes data back into an object of the given class.
+     *
+     * @param mixed $data Data to restore
+     * @param string $class The expected class to instantiate
+     * @return mixed|object
+     */
+    public function denormalize($data, string $class);
+}

+ 23 - 0
addons/epay/library/hyperf/contract/src/OnCloseInterface.php

@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Swoole\Http\Response;
+use Swoole\Server;
+
+interface OnCloseInterface
+{
+    /**
+     * @param Response|Server $server
+     */
+    public function onClose($server, int $fd, int $reactorId): void;
+}

+ 20 - 0
addons/epay/library/hyperf/contract/src/OnHandShakeInterface.php

@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Swoole\Http\Request;
+use Swoole\Http\Response;
+
+interface OnHandShakeInterface
+{
+    public function onHandShake(Request $request, Response $response): void;
+}

+ 24 - 0
addons/epay/library/hyperf/contract/src/OnMessageInterface.php

@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Swoole\Http\Response;
+use Swoole\WebSocket\Frame;
+use Swoole\WebSocket\Server;
+
+interface OnMessageInterface
+{
+    /**
+     * @param Response|Server $server
+     */
+    public function onMessage($server, Frame $frame): void;
+}

+ 24 - 0
addons/epay/library/hyperf/contract/src/OnOpenInterface.php

@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Swoole\Http\Request;
+use Swoole\Http\Response;
+use Swoole\WebSocket\Server;
+
+interface OnOpenInterface
+{
+    /**
+     * @param Response|Server $server
+     */
+    public function onOpen($server, Request $request): void;
+}

+ 24 - 0
addons/epay/library/hyperf/contract/src/OnPacketInterface.php

@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Swoole\WebSocket\Server;
+
+interface OnPacketInterface
+{
+    /**
+     * @param Server $server
+     * @param mixed $data
+     * @param array $clientInfo
+     */
+    public function onPacket($server, $data, $clientInfo): void;
+}

+ 23 - 0
addons/epay/library/hyperf/contract/src/OnReceiveInterface.php

@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Swoole\Coroutine\Server\Connection;
+use Swoole\Server as SwooleServer;
+
+interface OnReceiveInterface
+{
+    /**
+     * @param Connection|SwooleServer $server
+     */
+    public function onReceive($server, int $fd, int $reactorId, string $data): void;
+}

+ 21 - 0
addons/epay/library/hyperf/contract/src/OnRequestInterface.php

@@ -0,0 +1,21 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface OnRequestInterface
+{
+    /**
+     * @param mixed $request swoole request or psr server request
+     * @param mixed $response swoole response or swow session
+     */
+    public function onRequest($request, $response): void;
+}

+ 19 - 0
addons/epay/library/hyperf/contract/src/PackerInterface.php

@@ -0,0 +1,19 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface PackerInterface
+{
+    public function pack($data): string;
+
+    public function unpack(string $data);
+}

+ 95 - 0
addons/epay/library/hyperf/contract/src/PaginatorInterface.php

@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface PaginatorInterface
+{
+    /**
+     * Get the URL for a given page.
+     */
+    public function url(int $page): string;
+
+    /**
+     * Add a set of query string values to the paginator.
+     *
+     * @param array|string $key
+     * @return $this
+     */
+    public function appends($key, ?string $value = null);
+
+    /**
+     * Get / set the URL fragment to be appended to URLs.
+     *
+     * @return $this|string
+     */
+    public function fragment(?string $fragment = null);
+
+    /**
+     * The URL for the next page, or null.
+     */
+    public function nextPageUrl(): ?string;
+
+    /**
+     * Get the URL for the previous page, or null.
+     */
+    public function previousPageUrl(): ?string;
+
+    /**
+     * Get all of the items being paginated.
+     */
+    public function items(): array;
+
+    /**
+     * Get the "index" of the first item being paginated.
+     */
+    public function firstItem(): ?int;
+
+    /**
+     * Get the "index" of the last item being paginated.
+     */
+    public function lastItem(): ?int;
+
+    /**
+     * Determine how many items are being shown per page.
+     */
+    public function perPage(): int;
+
+    /**
+     * Determine the current page being paginated.
+     */
+    public function currentPage(): int;
+
+    /**
+     * Determine if there are enough items to split into multiple pages.
+     */
+    public function hasPages(): bool;
+
+    /**
+     * Determine if there is more items in the data store.
+     */
+    public function hasMorePages(): bool;
+
+    /**
+     * Determine if the list of items is empty or not.
+     */
+    public function isEmpty(): bool;
+
+    /**
+     * Determine if the list of items is not empty.
+     */
+    public function isNotEmpty(): bool;
+
+    /**
+     * Render the paginator using a given view.
+     */
+    public function render(?string $view = null, array $data = []): string;
+}

+ 30 - 0
addons/epay/library/hyperf/contract/src/PoolInterface.php

@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface PoolInterface
+{
+    /**
+     * Get a connection from the connection pool.
+     */
+    public function get(): ConnectionInterface;
+
+    /**
+     * Release a connection back to the connection pool.
+     */
+    public function release(ConnectionInterface $connection): void;
+
+    /**
+     * Close and clear the connection pool.
+     */
+    public function flush(): void;
+}

+ 27 - 0
addons/epay/library/hyperf/contract/src/PoolOptionInterface.php

@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface PoolOptionInterface
+{
+    public function getMaxConnections(): int;
+
+    public function getMinConnections(): int;
+
+    public function getConnectTimeout(): float;
+
+    public function getWaitTimeout(): float;
+
+    public function getHeartbeat(): float;
+
+    public function getMaxIdleTime(): float;
+}

+ 36 - 0
addons/epay/library/hyperf/contract/src/ProcessInterface.php

@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Swoole\Coroutine\Http\Server as CoHttpServer;
+use Swoole\Coroutine\Server as CoServer;
+use Swoole\Server;
+
+interface ProcessInterface
+{
+    /**
+     * Create the process object according to process number and bind to server.
+     * @param CoHttpServer|CoServer|Server $server
+     */
+    public function bind($server): void;
+
+    /**
+     * Determine if the process should start ?
+     * @param CoServer|Server $server
+     */
+    public function isEnable($server): bool;
+
+    /**
+     * The logical of process will place in here.
+     */
+    public function handle(): void;
+}

+ 22 - 0
addons/epay/library/hyperf/contract/src/ResponseEmitterInterface.php

@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Psr\Http\Message\ResponseInterface;
+
+interface ResponseEmitterInterface
+{
+    /**
+     * @param mixed $connection swoole response or swow session
+     */
+    public function emit(ResponseInterface $response, $connection, bool $withContent = true);
+}

+ 158 - 0
addons/epay/library/hyperf/contract/src/SessionInterface.php

@@ -0,0 +1,158 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface SessionInterface
+{
+    /**
+     * Starts the session storage.
+     *
+     * @throws \RuntimeException if session fails to start
+     * @return bool True if session started
+     */
+    public function start(): bool;
+
+    /**
+     * Returns the session ID.
+     *
+     * @return string The session ID
+     */
+    public function getId(): string;
+
+    /**
+     * Sets the session ID.
+     */
+    public function setId(string $id);
+
+    /**
+     * Returns the session name.
+     */
+    public function getName(): string;
+
+    /**
+     * Sets the session name.
+     */
+    public function setName(string $name);
+
+    /**
+     * Invalidates the current session.
+     *
+     * Clears all session attributes and flashes and regenerates the
+     * session and deletes the old session from persistence.
+     *
+     * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
+     *                      will leave the system settings unchanged, 0 sets the cookie
+     *                      to expire with browser session. Time is in seconds, and is
+     *                      not a Unix timestamp.
+     *
+     * @return bool True if session invalidated, false if error
+     */
+    public function invalidate(?int $lifetime = null): bool;
+
+    /**
+     * Migrates the current session to a new session id while maintaining all
+     * session attributes.
+     *
+     * @param bool $destroy Whether to delete the old session or leave it to garbage collection
+     * @param int $lifetime Sets the cookie lifetime for the session cookie. A null value
+     *                      will leave the system settings unchanged, 0 sets the cookie
+     *                      to expire with browser session. Time is in seconds, and is
+     *                      not a Unix timestamp.
+     *
+     * @return bool True if session migrated, false if error
+     */
+    public function migrate(bool $destroy = false, ?int $lifetime = null): bool;
+
+    /**
+     * Force the session to be saved and closed.
+     *
+     * This method is generally not required for real sessions as
+     * the session will be automatically saved at the end of
+     * code execution.
+     */
+    public function save(): void;
+
+    /**
+     * Checks if an attribute is defined.
+     *
+     * @param string $name The attribute name
+     *
+     * @return bool true if the attribute is defined, false otherwise
+     */
+    public function has(string $name): bool;
+
+    /**
+     * Returns an attribute.
+     *
+     * @param string $name The attribute name
+     * @param mixed $default The default value if not found
+     */
+    public function get(string $name, $default = null);
+
+    /**
+     * Sets an attribute.
+     * @param mixed $value
+     */
+    public function set(string $name, $value): void;
+
+    /**
+     * Put a key / value pair or array of key / value pairs in the session.
+     *
+     * @param array|string $key
+     * @param null|mixed $value
+     */
+    public function put($key, $value = null): void;
+
+    /**
+     * Returns attributes.
+     */
+    public function all(): array;
+
+    /**
+     * Sets attributes.
+     */
+    public function replace(array $attributes): void;
+
+    /**
+     * Removes an attribute, returning its value.
+     *
+     * @return mixed The removed value or null when it does not exist
+     */
+    public function remove(string $name);
+
+    /**
+     * Remove one or many items from the session.
+     *
+     * @param array|string $keys
+     */
+    public function forget($keys): void;
+
+    /**
+     * Clears all attributes.
+     */
+    public function clear(): void;
+
+    /**
+     * Checks if the session was started.
+     */
+    public function isStarted(): bool;
+
+    /**
+     * Get the previous URL from the session.
+     */
+    public function previousUrl(): ?string;
+
+    /**
+     * Set the "previous" URL in the session.
+     */
+    public function setPreviousUrl(string $url): void;
+}

+ 18 - 0
addons/epay/library/hyperf/contract/src/StdoutLoggerInterface.php

@@ -0,0 +1,18 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Psr\Log\LoggerInterface;
+
+interface StdoutLoggerInterface extends LoggerInterface
+{
+}

+ 20 - 0
addons/epay/library/hyperf/contract/src/Synchronized.php

@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface Synchronized
+{
+    /**
+     * Whether the data has been synchronized.
+     */
+    public function isSynchronized(): bool;
+}

+ 37 - 0
addons/epay/library/hyperf/contract/src/TranslatorInterface.php

@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface TranslatorInterface
+{
+    /**
+     * Get the translation for a given key.
+     */
+    public function trans(string $key, array $replace = [], ?string $locale = null);
+
+    /**
+     * Get a translation according to an integer value.
+     *
+     * @param array|\Countable|int $number
+     */
+    public function transChoice(string $key, $number, array $replace = [], ?string $locale = null): string;
+
+    /**
+     * Get the default locale being used.
+     */
+    public function getLocale(): string;
+
+    /**
+     * Set the default locale.
+     */
+    public function setLocale(string $locale);
+}

+ 35 - 0
addons/epay/library/hyperf/contract/src/TranslatorLoaderInterface.php

@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface TranslatorLoaderInterface
+{
+    /**
+     * Load the messages for the given locale.
+     */
+    public function load(string $locale, string $group, ?string $namespace = null): array;
+
+    /**
+     * Add a new namespace to the loader.
+     */
+    public function addNamespace(string $namespace, string $hint);
+
+    /**
+     * Add a new JSON path to the loader.
+     */
+    public function addJsonPath(string $path);
+
+    /**
+     * Get an array of all the registered namespaces.
+     */
+    public function namespaces(): array;
+}

+ 17 - 0
addons/epay/library/hyperf/contract/src/UnCompressInterface.php

@@ -0,0 +1,17 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+interface UnCompressInterface
+{
+    public function uncompress();
+}

+ 60 - 0
addons/epay/library/hyperf/contract/src/ValidatorInterface.php

@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Contract;
+
+use Hyperf\Utils\Contracts\MessageBag;
+use Hyperf\Utils\Contracts\MessageProvider;
+
+interface ValidatorInterface extends MessageProvider
+{
+    /**
+     * Run the validator's rules against its data.
+     */
+    public function validate(): array;
+
+    /**
+     * Get the attributes and values that were validated.
+     */
+    public function validated(): array;
+
+    /**
+     * Determine if the data fails the validation rules.
+     */
+    public function fails(): bool;
+
+    /**
+     * Get the failed validation rules.
+     */
+    public function failed(): array;
+
+    /**
+     * Add conditions to a given field based on a Closure.
+     *
+     * @param array|string $attribute
+     * @param array|string $rules
+     * @return $this
+     */
+    public function sometimes($attribute, $rules, callable $callback);
+
+    /**
+     * Add an after validation callback.
+     *
+     * @param callable|string $callback
+     * @return $this
+     */
+    public function after($callback);
+
+    /**
+     * Get all of the validation error messages.
+     */
+    public function errors(): MessageBag;
+}

+ 4 - 0
addons/epay/library/hyperf/engine/.gitattributes

@@ -0,0 +1,4 @@
+/.github export-ignore
+/examples export-ignore
+/tests export-ignore
+

+ 4 - 0
addons/epay/library/hyperf/engine/.gitignore

@@ -0,0 +1,4 @@
+/vendor/
+composer.lock
+*.cache
+*.log

+ 89 - 0
addons/epay/library/hyperf/engine/.php-cs-fixer.php

@@ -0,0 +1,89 @@
+<?php
+
+$header = <<<'EOF'
+This file is part of Hyperf.
+
+@link     https://www.hyperf.io
+@document https://hyperf.wiki
+@contact  group@hyperf.io
+@license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+EOF;
+
+return (new PhpCsFixer\Config())
+    ->setRiskyAllowed(true)
+    ->setRules([
+        '@PSR2' => true,
+        '@Symfony' => true,
+        '@DoctrineAnnotation' => true,
+        '@PhpCsFixer' => true,
+        'header_comment' => [
+            'comment_type' => 'PHPDoc',
+            'header' => $header,
+            'separate' => 'none',
+            'location' => 'after_declare_strict',
+        ],
+        'array_syntax' => [
+            'syntax' => 'short'
+        ],
+        'list_syntax' => [
+            'syntax' => 'short'
+        ],
+        'concat_space' => [
+            'spacing' => 'one'
+        ],
+        'blank_line_before_statement' => [
+            'statements' => [
+                'declare',
+            ],
+        ],
+        'general_phpdoc_annotation_remove' => [
+            'annotations' => [
+                'author'
+            ],
+        ],
+        'ordered_imports' => [
+            'imports_order' => [
+                'class', 'function', 'const',
+            ],
+            'sort_algorithm' => 'alpha',
+        ],
+        'single_line_comment_style' => [
+            'comment_types' => [
+            ],
+        ],
+        'yoda_style' => [
+            'always_move_variable' => false,
+            'equal' => false,
+            'identical' => false,
+        ],
+        'phpdoc_align' => [
+            'align' => 'left',
+        ],
+        'multiline_whitespace_before_semicolons' => [
+            'strategy' => 'no_multi_line',
+        ],
+        'constant_case' => [
+            'case' => 'lower',
+        ],
+        'class_attributes_separation' => true,
+        'combine_consecutive_unsets' => true,
+        'declare_strict_types' => true,
+        'linebreak_after_opening_tag' => true,
+        'lowercase_static_reference' => true,
+        'no_useless_else' => true,
+        'no_unused_imports' => true,
+        'not_operator_with_successor_space' => true,
+        'not_operator_with_space' => false,
+        'ordered_class_elements' => true,
+        'php_unit_strict' => false,
+        'phpdoc_separation' => false,
+        'single_quote' => true,
+        'standardize_not_equals' => true,
+        'multiline_comment_opening_closing' => true,
+    ])
+    ->setFinder(
+        PhpCsFixer\Finder::create()
+            ->exclude('vendor')
+            ->in(__DIR__)
+    )
+    ->setUsingCache(false);

+ 6 - 0
addons/epay/library/hyperf/engine/.phpstorm.meta.php

@@ -0,0 +1,6 @@
+<?php
+
+namespace PHPSTORM_META {
+    // Reflect
+    override(\Psr\Container\ContainerInterface::get(0), map('@'));
+}

+ 46 - 0
addons/epay/library/hyperf/engine/Dockerfile

@@ -0,0 +1,46 @@
+# Default Dockerfile
+#
+# @link     https://www.hyperf.io
+# @document https://hyperf.wiki
+# @contact  group@hyperf.io
+# @license  https://github.com/hyperf-cloud/hyperf/blob/master/LICENSE
+
+ARG PHP_VERSION
+ARG ALPINE_VERSION
+
+FROM hyperf/hyperf:${PHP_VERSION}-alpine-${ALPINE_VERSION}-swoole
+LABEL maintainer="Hyperf Developers <group@hyperf.io>" version="1.0" license="MIT" app.name="Hyperf"
+
+ARG timezone
+ARG PHP_VERSION
+
+ENV TIMEZONE=${timezone:-"Asia/Shanghai"}
+ENV COMPOSER_ROOT_VERSION="v1.2.0"
+
+# update
+RUN set -ex \
+    # show php version and extensions
+    && php -v \
+    && php -m \
+    && php --ri swoole \
+    #  ---------- some config ----------
+    && cd "/etc/php${PHP_VERSION%\.*}" \
+    # - config PHP
+    && { \
+        echo "upload_max_filesize=128M"; \
+        echo "post_max_size=128M"; \
+        echo "memory_limit=1G"; \
+        echo "date.timezone=${TIMEZONE}"; \
+    } | tee conf.d/99_overrides.ini \
+    # - config timezone
+    && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \
+    && echo "${TIMEZONE}" > /etc/timezone \
+    # ---------- clear works ----------
+    && rm -rf /var/cache/apk/* /tmp/* /usr/share/man \
+    && echo -e "\033[42;37m Build Completed :).\033[0m\n"
+
+WORKDIR /opt/www
+
+COPY . /opt/www
+
+RUN composer install -o

+ 7 - 0
addons/epay/library/hyperf/engine/README.md

@@ -0,0 +1,7 @@
+# Swoole Engine
+
+![Swoole Engine Test](https://github.com/hyperf/engine/workflows/Swoole%20Engine%20Test/badge.svg)
+
+```
+composer require hyperf/engine
+```

+ 49 - 0
addons/epay/library/hyperf/engine/composer.json

@@ -0,0 +1,49 @@
+{
+    "name": "hyperf/engine",
+    "type": "library",
+    "license": "MIT",
+    "keywords": [
+        "php",
+        "hyperf"
+    ],
+    "description": "",
+    "autoload": {
+        "psr-4": {
+            "Hyperf\\Engine\\": "src/"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "HyperfTest\\": "tests"
+        }
+    },
+    "require": {
+        "php": ">=7.4"
+    },
+    "require-dev": {
+        "friendsofphp/php-cs-fixer": "^3.0",
+        "hyperf/guzzle": "^2.2",
+        "phpstan/phpstan": "^1.0",
+        "phpunit/phpunit": "^9.4",
+        "swoole/ide-helper": "dev-master"
+    },
+    "suggest": {
+        "ext-swoole": ">=4.5"
+    },
+    "minimum-stability": "dev",
+    "prefer-stable": true,
+    "config": {
+        "optimize-autoloader": true,
+        "sort-packages": true
+    },
+    "extra": {
+        "branch-alias": {
+            "dev-master": "1.2-dev"
+        }
+    },
+    "scripts": {
+        "test": "phpunit -c phpunit.xml --colors=always",
+        "analyse": "phpstan analyse --memory-limit 1024M -l 0 ./src",
+        "cs-fix": "php-cs-fixer fix $1"
+    }
+}

+ 15 - 0
addons/epay/library/hyperf/engine/phpunit.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit bootstrap="tests/bootstrap.php"
+         backupGlobals="false"
+         backupStaticAttributes="false"
+         verbose="true"
+         colors="true"
+         convertErrorsToExceptions="true"
+         convertNoticesToExceptions="true"
+         convertWarningsToExceptions="true"
+         processIsolation="false"
+         stopOnFailure="false">
+    <testsuite name="Testsuite">
+        <directory>./tests/</directory>
+    </testsuite>
+</phpunit>

+ 142 - 0
addons/epay/library/hyperf/engine/src/Channel.php

@@ -0,0 +1,142 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine;
+
+use Hyperf\Engine\Contract\ChannelInterface;
+use Hyperf\Engine\Exception\RuntimeException;
+
+if (PHP_VERSION_ID > 80000 && SWOOLE_VERSION_ID >= 50000) {
+    class Channel extends \Swoole\Coroutine\Channel implements ChannelInterface
+    {
+        protected bool $closed = false;
+
+        public function push(mixed $data, float $timeout = -1): bool
+        {
+            return parent::push($data, $timeout);
+        }
+
+        public function pop(float $timeout = -1): mixed
+        {
+            return parent::pop($timeout);
+        }
+
+        public function getCapacity(): int
+        {
+            return $this->capacity;
+        }
+
+        public function getLength(): int
+        {
+            return $this->length();
+        }
+
+        public function isAvailable(): bool
+        {
+            return ! $this->isClosing();
+        }
+
+        public function close(): bool
+        {
+            $this->closed = true;
+            return parent::close();
+        }
+
+        public function hasProducers(): bool
+        {
+            throw new RuntimeException('Not supported.');
+        }
+
+        public function hasConsumers(): bool
+        {
+            throw new RuntimeException('Not supported.');
+        }
+
+        public function isReadable(): bool
+        {
+            throw new RuntimeException('Not supported.');
+        }
+
+        public function isWritable(): bool
+        {
+            throw new RuntimeException('Not supported.');
+        }
+
+        public function isClosing(): bool
+        {
+            return $this->closed || $this->errCode === SWOOLE_CHANNEL_CLOSED;
+        }
+
+        public function isTimeout(): bool
+        {
+            return ! $this->closed && $this->errCode === SWOOLE_CHANNEL_TIMEOUT;
+        }
+    }
+} else {
+    class Channel extends \Swoole\Coroutine\Channel implements ChannelInterface
+    {
+        /**
+         * @var bool
+         */
+        protected $closed = false;
+
+        public function getCapacity(): int
+        {
+            return $this->capacity;
+        }
+
+        public function getLength(): int
+        {
+            return $this->length();
+        }
+
+        public function isAvailable(): bool
+        {
+            return ! $this->isClosing();
+        }
+
+        public function close(): bool
+        {
+            $this->closed = true;
+            return parent::close();
+        }
+
+        public function hasProducers(): bool
+        {
+            throw new RuntimeException('Not supported.');
+        }
+
+        public function hasConsumers(): bool
+        {
+            throw new RuntimeException('Not supported.');
+        }
+
+        public function isReadable(): bool
+        {
+            throw new RuntimeException('Not supported.');
+        }
+
+        public function isWritable(): bool
+        {
+            throw new RuntimeException('Not supported.');
+        }
+
+        public function isClosing(): bool
+        {
+            return $this->closed || $this->errCode === SWOOLE_CHANNEL_CLOSED;
+        }
+
+        public function isTimeout(): bool
+        {
+            return ! $this->closed && $this->errCode === SWOOLE_CHANNEL_TIMEOUT;
+        }
+    }
+}

+ 25 - 0
addons/epay/library/hyperf/engine/src/Constant.php

@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine;
+
+use Swoole\Coroutine\Http\Server as HttpServer;
+use Swoole\Coroutine\Server;
+
+class Constant
+{
+    public const ENGINE = 'Swoole';
+
+    public static function isCoroutineServer($server): bool
+    {
+        return $server instanceof Server || $server instanceof HttpServer;
+    }
+}

+ 134 - 0
addons/epay/library/hyperf/engine/src/Contract/ChannelInterface.php

@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Contract;
+
+if (PHP_VERSION_ID > 80000 && SWOOLE_VERSION_ID >= 50000) {
+    interface ChannelInterface
+    {
+        /**
+         * @param float|int $timeout [optional] = -1
+         */
+        public function push(mixed $data, float $timeout = -1): bool;
+
+        /**
+         * @param float $timeout seconds [optional] = -1
+         * @return mixed when pop failed, return false
+         */
+        public function pop(float $timeout = -1): mixed;
+
+        /**
+         * Swow: When the channel is closed, all the data in it will be destroyed.
+         * Swoole: When the channel is closed, the data in it can still be popped out, but push behavior will no longer succeed.
+         */
+        public function close(): bool;
+
+        public function getCapacity(): int;
+
+        public function getLength(): int;
+
+        public function isAvailable(): bool;
+
+        public function hasProducers(): bool;
+
+        public function hasConsumers(): bool;
+
+        public function isEmpty(): bool;
+
+        public function isFull(): bool;
+
+        public function isReadable(): bool;
+
+        public function isWritable(): bool;
+
+        public function isClosing(): bool;
+
+        public function isTimeout(): bool;
+    }
+} else {
+    interface ChannelInterface
+    {
+        /**
+         * @param mixed $data [required]
+         * @param float|int $timeout [optional] = -1
+         * @return bool
+         */
+        public function push($data, $timeout = -1);
+
+        /**
+         * @param float $timeout seconds [optional] = -1
+         * @return mixed when pop failed, return false
+         */
+        public function pop($timeout = -1);
+
+        /**
+         * Swow: When the channel is closed, all the data in it will be destroyed.
+         * Swoole: When the channel is closed, the data in it can still be popped out, but push behavior will no longer succeed.
+         * @return mixed
+         */
+        public function close(): bool;
+
+        /**
+         * @return int
+         */
+        public function getCapacity();
+
+        /**
+         * @return int
+         */
+        public function getLength();
+
+        /**
+         * @return bool
+         */
+        public function isAvailable();
+
+        /**
+         * @return bool
+         */
+        public function hasProducers();
+
+        /**
+         * @return bool
+         */
+        public function hasConsumers();
+
+        /**
+         * @return bool
+         */
+        public function isEmpty();
+
+        /**
+         * @return bool
+         */
+        public function isFull();
+
+        /**
+         * @return bool
+         */
+        public function isReadable();
+
+        /**
+         * @return bool
+         */
+        public function isWritable();
+
+        /**
+         * @return bool
+         */
+        public function isClosing();
+
+        /**
+         * @return bool
+         */
+        public function isTimeout();
+    }
+}

+ 70 - 0
addons/epay/library/hyperf/engine/src/Contract/CoroutineInterface.php

@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Contract;
+
+use Hyperf\Engine\Exception\CoroutineDestroyedException;
+use Hyperf\Engine\Exception\RunningInNonCoroutineException;
+
+interface CoroutineInterface
+{
+    /**
+     * @param callable $callable [required]
+     */
+    public function __construct(callable $callable);
+
+    /**
+     * @param mixed ...$data
+     * @return $this
+     */
+    public function execute(...$data);
+
+    /**
+     * @return int
+     */
+    public function getId();
+
+    /**
+     * @param callable $callable [required]
+     * @param mixed ...$data
+     * @return $this
+     */
+    public static function create(callable $callable, ...$data);
+
+    /**
+     * @return int returns coroutine id from current coroutine, -1 in non coroutine environment
+     */
+    public static function id();
+
+    /**
+     * Returns the parent coroutine ID.
+     * Returns 0 when running in the top level coroutine.
+     * @throws RunningInNonCoroutineException when running in non-coroutine context
+     * @throws CoroutineDestroyedException when the coroutine has been destroyed
+     */
+    public static function pid(?int $id = null);
+
+    /**
+     * Set config to coroutine.
+     */
+    public static function set(array $config);
+
+    /**
+     * @param null|int $id coroutine id
+     * @return null|\ArrayObject
+     */
+    public static function getContextFor(?int $id = null);
+
+    /**
+     * Execute callback when coroutine destruct.
+     */
+    public static function defer(callable $callable);
+}

+ 24 - 0
addons/epay/library/hyperf/engine/src/Contract/Http/ClientInterface.php

@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Contract\Http;
+
+use Hyperf\Engine\Http\RawResponse;
+
+interface ClientInterface
+{
+    public function set(array $settings): bool;
+
+    /**
+     * @param string[][] $headers
+     */
+    public function request(string $method = 'GET', string $path = '/', array $headers = [], string $contents = '', string $version = '1.1'): RawResponse;
+}

+ 23 - 0
addons/epay/library/hyperf/engine/src/Contract/WebSocket/WebSocketInterface.php

@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Contract\WebSocket;
+
+interface WebSocketInterface
+{
+    public const ON_MESSAGE = 'message';
+
+    public const ON_CLOSE = 'close';
+
+    public function on(string $event, callable $callback): void;
+
+    public function start(): void;
+}

+ 100 - 0
addons/epay/library/hyperf/engine/src/Coroutine.php

@@ -0,0 +1,100 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine;
+
+use Hyperf\Engine\Contract\CoroutineInterface;
+use Hyperf\Engine\Exception\CoroutineDestroyedException;
+use Hyperf\Engine\Exception\RunningInNonCoroutineException;
+use Hyperf\Engine\Exception\RuntimeException;
+use Swoole\Coroutine as SwooleCo;
+
+class Coroutine implements CoroutineInterface
+{
+    /**
+     * @var callable
+     */
+    private $callable;
+
+    /**
+     * @var int
+     */
+    private $id;
+
+    public function __construct(callable $callable)
+    {
+        $this->callable = $callable;
+    }
+
+    public static function create(callable $callable, ...$data)
+    {
+        $coroutine = new static($callable);
+        $coroutine->execute(...$data);
+        return $coroutine;
+    }
+
+    public function execute(...$data)
+    {
+        $this->id = SwooleCo::create($this->callable, ...$data);
+        return $this;
+    }
+
+    public function getId()
+    {
+        if (is_null($this->id)) {
+            throw new RuntimeException('Coroutine was not be executed.');
+        }
+        return $this->id;
+    }
+
+    public static function id()
+    {
+        return SwooleCo::getCid();
+    }
+
+    public static function pid(?int $id = null)
+    {
+        if ($id) {
+            $cid = SwooleCo::getPcid($id);
+            if ($cid === false) {
+                throw new CoroutineDestroyedException(sprintf('Coroutine #%d has been destroyed.', $id));
+            }
+        } else {
+            $cid = SwooleCo::getPcid();
+        }
+        if ($cid === false) {
+            throw new RunningInNonCoroutineException('Non-Coroutine environment don\'t has parent coroutine id.');
+        }
+        return max(0, $cid);
+    }
+
+    public static function set(array $config)
+    {
+        SwooleCo::set($config);
+    }
+
+    /**
+     * @return null|\ArrayObject
+     */
+    public static function getContextFor(?int $id = null)
+    {
+        if ($id === null) {
+            return SwooleCo::getContext();
+        }
+
+        return SwooleCo::getContext($id);
+    }
+
+    public static function defer(callable $callable)
+    {
+        SwooleCo::defer($callable);
+    }
+}

+ 16 - 0
addons/epay/library/hyperf/engine/src/Exception/CoroutineDestroyedException.php

@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Exception;
+
+class CoroutineDestroyedException extends RuntimeException
+{
+}

+ 16 - 0
addons/epay/library/hyperf/engine/src/Exception/HttpClientException.php

@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Exception;
+
+class HttpClientException extends RuntimeException
+{
+}

+ 16 - 0
addons/epay/library/hyperf/engine/src/Exception/RunningInNonCoroutineException.php

@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Exception;
+
+class RunningInNonCoroutineException extends RuntimeException
+{
+}

+ 16 - 0
addons/epay/library/hyperf/engine/src/Exception/RuntimeException.php

@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Exception;
+
+class RuntimeException extends \RuntimeException
+{
+}

+ 20 - 0
addons/epay/library/hyperf/engine/src/Extension.php

@@ -0,0 +1,20 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine;
+
+class Extension
+{
+    public static function isLoaded(): bool
+    {
+        return extension_loaded('Swoole');
+    }
+}

+ 76 - 0
addons/epay/library/hyperf/engine/src/Http/Client.php

@@ -0,0 +1,76 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Http;
+
+use Hyperf\Engine\Contract\Http\ClientInterface;
+use Hyperf\Engine\Exception\HttpClientException;
+use Swoole\Coroutine\Http\Client as HttpClient;
+
+class Client extends HttpClient implements ClientInterface
+{
+    public function set(array $settings): bool
+    {
+        return parent::set($settings);
+    }
+
+    /**
+     * @param string[][] $headers
+     */
+    public function request(string $method = 'GET', string $path = '/', array $headers = [], string $contents = '', string $version = '1.1'): RawResponse
+    {
+        $this->setMethod($method);
+        $this->setData($contents);
+        $this->setHeaders($this->encodeHeaders($headers));
+        $this->execute($path);
+        if ($this->errCode !== 0) {
+            throw new HttpClientException($this->errMsg, $this->errCode);
+        }
+        return new RawResponse(
+            $this->statusCode,
+            $this->decodeHeaders($this->headers ?? []),
+            $this->body,
+            $version
+        );
+    }
+
+    /**
+     * @param string[] $headers
+     * @return string[][]
+     */
+    private function decodeHeaders(array $headers): array
+    {
+        $result = [];
+        foreach ($headers as $name => $header) {
+            // The key of header is lower case.
+            $result[$name][] = $header;
+        }
+        if ($this->set_cookie_headers) {
+            $result['set-cookie'] = $this->set_cookie_headers;
+        }
+        return $result;
+    }
+
+    /**
+     * Swoole engine not support two dimensional array.
+     * @param string[][] $headers
+     * @return string[]
+     */
+    private function encodeHeaders(array $headers): array
+    {
+        $result = [];
+        foreach ($headers as $name => $value) {
+            $result[$name] = is_array($value) ? implode(',', $value) : $value;
+        }
+
+        return $result;
+    }
+}

+ 22 - 0
addons/epay/library/hyperf/engine/src/Http/FdGetter.php

@@ -0,0 +1,22 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Http;
+
+use Swoole\Http\Response;
+
+class FdGetter
+{
+    public function get(Response $response): int
+    {
+        return $response->fd;
+    }
+}

+ 47 - 0
addons/epay/library/hyperf/engine/src/Http/RawResponse.php

@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine\Http;
+
+final class RawResponse
+{
+    /**
+     * @var int
+     */
+    public $statusCode = 0;
+
+    /**
+     * @var string[][]
+     */
+    public $headers = [];
+
+    /**
+     * @var string
+     */
+    public $body = '';
+
+    /**
+     * Protocol version.
+     * @var string
+     */
+    public $version = '';
+
+    /**
+     * @param string[][] $headers
+     */
+    public function __construct(int $statusCode, array $headers, string $body, string $version)
+    {
+        $this->statusCode = $statusCode;
+        $this->headers = $headers;
+        $this->body = $body;
+        $this->version = $version;
+    }
+}

+ 16 - 0
addons/epay/library/hyperf/engine/src/Socket.php

@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * This file is part of Hyperf.
+ *
+ * @link     https://www.hyperf.io
+ * @document https://hyperf.wiki
+ * @contact  group@hyperf.io
+ * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
+ */
+namespace Hyperf\Engine;
+
+class Socket extends \Swoole\Coroutine\Socket
+{
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff