commit e983a4d1782ded8bdd6ac1dfa0f1af5910da8d91 Author: xxm1995 Date: Mon May 8 13:53:44 2023 +0800 feat 拆分支付模块为单独的项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..b28673a4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.metadata +.classpath +.project +.module_prj +.settings/ +.idea/ +.run/ +.svn/ +*.iml +*.log +bin/ +out/ +target/ +build/ +logs/ diff --git a/.springjavaformatconfig b/.springjavaformatconfig new file mode 100644 index 000000000..522e93aa9 --- /dev/null +++ b/.springjavaformatconfig @@ -0,0 +1,4 @@ +# 启用版本为java8 +java-baseline=8 +# 缩进使用空格 +indentation-style=spaces diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..9785b4c80 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# java8基础镜像 +FROM registry.cn-beijing.aliyuncs.com/xxm1995/java8 + +# 作者信息 +MAINTAINER xxm1995@outlook.com + +# 时区设置 +ENV TZ=Asia/Shanghai +RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# jvm启动参数, 启动内存256M,最大内存1G,使用G1垃圾回收器 +ENV JAVA_OPTS="-Xms256m -Xmx1G -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom" + +# 工作目录 +WORKDIR / + +# 端口暴露 +EXPOSE 9999 + +# 添加执行jar包 +ADD bootx-start/target/bootx-start.jar bootx-start.jar + +# 执行启动命令 +CMD java $JAVA_OPTS -jar bootx-start.jar diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..bfa5a8673 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 (c) 2021 bootx Authors. All Rights Reserved. + + 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. diff --git a/README.md b/README.md new file mode 100644 index 000000000..8caca8fc9 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# Dax-Pay(开源支付系统) + +

+ star + Build Status +Build Status +Build Status + Downloads + +

+ +## 🙏🙏🙏 求star呀,走过路过留个star吧,非常非常感谢。🙏🙏🙏 + +## 🍈项目介绍 + +Dax-Pay是Bootx-Platform的子项目之一,主要是对支付收单和账务进行优化,精简与支付无关的模块,专注支付领域,对微信和支付宝的各种支付方式进行封装, +同时扩展了更多支付方式,如储值卡、现金卡等,可以作为一个简单的四方支付进行使用。 + +## 🛠️功能亮点 +- 支持单通道支付、聚合支付、组合支付、部分和全部退款等支付功能 +- 支持支付宝、微信、云闪付、现金、钱包、储值卡等多种支付方式 +- 支持退款 +- 支持对账 + +结算台演示地址:[http://daxpay.demo.bootx.cn/cashier](http://daxpay.demo.bootx.cn/cashier) + +![](https://oscimg.oschina.net/oscnet/up-9f0044b76071d5a7f598ceab591c5fedb02.png) + +## 🍒文档 +- 系统演示地址:[Dax支付系统](http://v3.platform.bootx.cn/) +- 前端项目地址:[前端项目(vue3)](https://gitee.com/bootx/bootx-platform-vue3) +- 项目文档:[项目文档(GITEE)](https://bootx.gitee.io/)、[项目文档(备用)](https://daxpay.doc.bootx.cn/) +- 更新日志:[更新日志](./_doc/ChangeLog.md) + +## 🥂 Quick Start + +- 后端: [后端启动流程](https://https://daxpay.doc.bootx.cn/) +- 前端: [Vue3前端启动流程](https://https://daxpay.doc.bootx.cn/) +- 支付通道配置: [支付通道配置](https://https://daxpay.doc.bootx.cn/) + +## 🥞项目结构(dax-pay) +```lua +dax-pay + ├──java + ├── code -- 项目相关配置 + ├── configuration -- 项目文档 + ├── controller -- 使用外部项目对应开源协议 + ├── core -- 核心包 + ├── aggregate -- Quartz定时任务模块 + ├── cashier -- 微信对接模块 + ├── pay -- 企业微信对接模块 + ├── payment -- 企业微信对接模块 + ├── paymodel -- 企业微信对接模块 + ├── refund -- 企业微信对接模块 + ├── order -- 企业微信对接模块 + ├── bill -- 账单 + ├── dto -- 业务实体类 + ├── event -- 事件 + ├── exception -- 异常 + ├── mq -- 消息队列 + ├── param -- 参数 + ├── task -- 定时任务 + ├── util -- 工具类 + ├── DaxPayApplication -- 启动类 + ├── resources + ├── mapper -- MyBatis映射文件 + ├── templates -- 静态网页 + ├── logback-spring -- 日志打印配置 + +``` +## 🥂 Bootx 项目合集 +- Bootx-Platform:单体版脚手架 [Gitee地址](https://gitee.com/bootx/bootx-platform) +- Bootx-Cloud:微服务版脚手架 [Gitee地址](https://gitee.com/bootx/bootx-cloud) +- dax-pay:开源支付系统 [Gitee地址](https://gitee.com/bootx/daxpay) +- mybatis-table-modify:数据表结构管理 [Gitee地址](https://gitee.com/bootx/mybatis-table-modify) + +## 🥪 关于我们 + +QQ扫码加入QQ交流群 +

+ + +

+ +## 🍻 鸣谢 +感谢 JetBrains 提供的免费开源 License: + +[![JetBrains](https://oscimg.oschina.net/oscnet/up-4aab9fa8bc769295b48c888d93e71320d93.png)](https://www.jetbrains.com/?from=bootx) + +感谢其他提供灵感和思路的开源项目 + +[部分参考的开源项目和开源许可列表](./_license/LICENSE.md) + + +## 🍷License + +Apache License Version 2.0_ diff --git a/_doc/ChangeLog.md b/_doc/ChangeLog.md new file mode 100644 index 000000000..90b48b52f --- /dev/null +++ b/_doc/ChangeLog.md @@ -0,0 +1,3 @@ +# CHANGELOG +## [v1.0.0] 支付之路 +- diff --git a/_doc/Task.md b/_doc/Task.md new file mode 100644 index 000000000..71bb1fac8 --- /dev/null +++ b/_doc/Task.md @@ -0,0 +1 @@ +[ ] diff --git a/_license/LICENSE.md b/_license/LICENSE.md new file mode 100644 index 000000000..19fcc34a6 --- /dev/null +++ b/_license/LICENSE.md @@ -0,0 +1,43 @@ +# 借鉴和部分参考的开源项目 + +Spring Cloud Gateway整合Swagger2 Demo,全网首例: +https://gitee.com/wxdfun/sw + +JEECG BOOT 低代码开发平台: +https://github.com/jeecgboot/jeecg-boot + +HZERO-基于微服务架构开源免费的企业级PaaS平台: +https://gitee.com/open-hand/hzero + +RuoYi-Vue 全新 Pro 版本: +https://gitee.com/zhijiantianya/ruoyi-vue-pro + +Snowy国产密码算法后台权限管理系统: +https://gitee.com/xiaonuobase/snowy + +表单设计器 k-form-design: +https://gitee.com/kcz66/k-form-design + +Vue微信菜单编辑器: +https://github.com/hopex/vue-menu + +flowable antd vue 的工作流设计器: +https://gitee.com/Vincent-Vic/workflow-bpmn-modeler-antdv + +flowable 工作流相关思路和实现 乐之终曲: +https://blog.csdn.net/qq_37143673 + +GoView 一个Vue3搭建的低代码数据可视化开发平台: +https://gitee.com/dromara/go-view + +easy-cron 这是基于Vue.js和iviewui封装一个crontab表达式的组件: +https://gitee.com/toktok/easy-cron + +ACTable是对Mybatis做的增强功能,通过配置model注解的方式来创建表,修改表结构,并且实现了共通的CUDR功能提升开发效率: +https://gitee.com/sunchenbin/mybatis-enhance + +Knife4j是一个集Swagger2 和 OpenAPI3为一体的增强解决方案: +https://gitee.com/xiaoym/knife4j + +easy_trans 一个注解搞定数据翻译: +https://gitee.com/dromara/easy_trans diff --git a/_license/easy-cron/LICENSE b/_license/easy-cron/LICENSE new file mode 100644 index 000000000..08eddc924 --- /dev/null +++ b/_license/easy-cron/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 知行合一 + +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. diff --git a/_license/easy_trans/LICENSE b/_license/easy_trans/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/_license/easy_trans/LICENSE @@ -0,0 +1,201 @@ + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 [yyyy] [name of copyright owner] + + 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. diff --git a/_license/go-view/LICENSE b/_license/go-view/LICENSE new file mode 100644 index 000000000..a44f18f96 --- /dev/null +++ b/_license/go-view/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present GoView + +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. diff --git a/_license/go-view/LICENSE2 b/_license/go-view/LICENSE2 new file mode 100644 index 000000000..6f55195df --- /dev/null +++ b/_license/go-view/LICENSE2 @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 奔跑的面条 + +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. + diff --git a/_license/hand_hzero/LICENSE b/_license/hand_hzero/LICENSE new file mode 100644 index 000000000..0c8a39b8d --- /dev/null +++ b/_license/hand_hzero/LICENSE @@ -0,0 +1,202 @@ + + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 2015-2020 Hand China Co.,Ltd. + + 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. diff --git a/_license/jeecg-boot/LICENSE b/_license/jeecg-boot/LICENSE new file mode 100644 index 000000000..938940f9d --- /dev/null +++ b/_license/jeecg-boot/LICENSE @@ -0,0 +1,201 @@ + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 (c) 2019 Jeecg Boot All rights reserved. + + 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. \ No newline at end of file diff --git a/_license/k-form-design/LICENSE b/_license/k-form-design/LICENSE new file mode 100644 index 000000000..bd1e84226 --- /dev/null +++ b/_license/k-form-design/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Kchengz + +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. diff --git a/_license/knife4j/LICENSE b/_license/knife4j/LICENSE new file mode 100644 index 000000000..22f2733ab --- /dev/null +++ b/_license/knife4j/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 小明 + + 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. \ No newline at end of file diff --git a/_license/mybatis-enhance-actable/LICENSE b/_license/mybatis-enhance-actable/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/_license/mybatis-enhance-actable/LICENSE @@ -0,0 +1,201 @@ + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 [yyyy] [name of copyright owner] + + 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. diff --git a/_license/ruoyi-vue-pro/LICENSE b/_license/ruoyi-vue-pro/LICENSE new file mode 100644 index 000000000..bd9da6235 --- /dev/null +++ b/_license/ruoyi-vue-pro/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2021 ruoyi-vue-pro + +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. diff --git a/_license/snowy/LICENSE b/_license/snowy/LICENSE new file mode 100644 index 000000000..d6aac6c35 --- /dev/null +++ b/_license/snowy/LICENSE @@ -0,0 +1,53 @@ +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 \ No newline at end of file diff --git a/_license/workflow-bpmn-modeler-antdv/LICENSE b/_license/workflow-bpmn-modeler-antdv/LICENSE new file mode 100644 index 000000000..9caf51634 --- /dev/null +++ b/_license/workflow-bpmn-modeler-antdv/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 GoldSubmarine + +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. diff --git a/_license/wxdfun-sw/LICENSE b/_license/wxdfun-sw/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/_license/wxdfun-sw/LICENSE @@ -0,0 +1,201 @@ + 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: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) 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 + + (d) 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 [yyyy] [name of copyright owner] + + 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. diff --git a/dax-pay/pom.xml b/dax-pay/pom.xml new file mode 100644 index 000000000..9d058eb2a --- /dev/null +++ b/dax-pay/pom.xml @@ -0,0 +1,173 @@ + + + 4.0.0 + + cn.bootx.platform + bootx-platform + 1.2.3 + + + cn.bootxtest + dax-pay + + + 8 + 8 + UTF-8 + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + org.projectlombok + lombok + provided + + + + + org.projectlombok + lombok-mapstruct-binding + provided + + + + + org.mapstruct + mapstruct-processor + provided + + + + + org.mapstruct + mapstruct + provided + + + + + cn.bootx.platform + service-baseapi + ${bootx-platform.version} + + + + cn.bootx.platform + service-iam + ${bootx-platform.version} + + + + cn.bootx.platform + service-notice + ${bootx-platform.version} + + + + + cn.bootx.platform + common-starter-file + + + + cn.bootx.platform + common-websocket + + + + cn.bootx.platform + common-lock + + + + cn.bootx.platform + common-starter-quartz + + + + cn.bootx.platform + common-rabbitmq + + + + cn.bootx + mybatis-table-modify + + + + + cn.bootx.platform + common-starter-monitor + + + + + com.github.javen205 + IJPay-AliPay + ${IJPay.version} + + + cn.hutool + hutool-all + + + xml-apis + xml-apis + + + fastjson + com.alibaba + + + + + + + com.github.javen205 + IJPay-WxPay + ${IJPay.version} + + + + + + xml-apis + xml-apis + ${xml-apis.version} + + + + + + dax-pay + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/DaxPayApplication.java b/dax-pay/src/main/java/cn/bootx/daxpay/DaxPayApplication.java new file mode 100644 index 000000000..32f2fc127 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/DaxPayApplication.java @@ -0,0 +1,47 @@ +package cn.bootx.daxpay; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.annotations.Mapper; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.Environment; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** + * DaxPay 支付开发平台 + * @author xxm + * @date 2023/4/20 + */ +@Slf4j +@ConfigurationPropertiesScan +@MapperScan(annotationClass = Mapper.class) +@SpringBootApplication +public class DaxPayApplication { + + public static void main(String[] args) throws UnknownHostException { + ConfigurableApplicationContext application = SpringApplication.run(DaxPayApplication.class, args); + Environment env = application.getEnvironment(); + // 环境变量 + String appName = env.getProperty("spring.application.name"); + String host = InetAddress.getLocalHost().getHostAddress(); + String port = env.getProperty("server.port"); + String contextPath = env.getProperty("server.servlet.context-path", ""); + String path = env.getProperty("spring.mvc.servlet.path", ""); + + // 应用信息栏 + String appInfo = StrUtil.format("应用 '{}' 运行成功! \n\t", appName); + // swagger栏 + String swagger = StrUtil.format("Swagger文档: \t\thttp://{}:{}{}{}/doc.html\n\t", host, port, contextPath, path); + + log.info("\n----------------------------------------------------------\n\t" + "{}{} \n" + + "----------------------------------------------------------", appInfo, swagger); + } + +} + diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/CachingCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/CachingCode.java new file mode 100644 index 000000000..99f9d8f26 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/CachingCode.java @@ -0,0 +1,17 @@ +package cn.bootx.daxpay.code; + +/** + * 支付服务缓存 + * + * @author xxm + * @date 2022/7/11 + */ +public interface CachingCode { + + /** 支付单(主键) */ + String PAYMENT_ID = "pay:payment:id"; + + /** 支付单(业务号码) */ + String PAYMENT_BUSINESS_ID = "pay:payment:business"; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/PaymentCenterErrorCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/PaymentCenterErrorCode.java new file mode 100644 index 000000000..325b5f796 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/PaymentCenterErrorCode.java @@ -0,0 +1,77 @@ +package cn.bootx.daxpay.code; + +/** + * 错误码 + * + * @author xxm + * @date 2020/12/7 + */ +public interface PaymentCenterErrorCode { + + // 支付过程相关 + /** + * 支付金额异常 + */ + int PAYMENT_AMOUNT_ABNORMAL = 28100; + + /** + * 支付记录不存在 + */ + int PAYMENT_RECORD_NOT_EXISTED = 28101; + + /** + * 支付在进行中 + */ + int PAYMENT_IS_PROCESSING = 28102; + + /** + * 支付失败 + */ + int PAY_FAILURE = 28103; + + /** + * 支付已经存在 + */ + int PAYMENT_HAS_EXISTED = 28104; + + /** + * 支付手动取消 + */ + int PAYMENT_CANCEL = 28105; + + /** + * 不支持的支付方式 + */ + int PAYMENT_METHOD_UNSUPPORT = 28106; + + /** + * 钱包已存在 + */ + int WALLET_ALREADY_EXISTS = 28814; + + /** + * 钱包不存在 + */ + int WALLET_NOT_EXISTS = 28815; + + /** + * 钱包已被禁用 + */ + int WALLET_BANNED = 28816; + + /** + * 钱包余额不足 + */ + int WALLET_BALANCE_NOT_ENOUGH = 28817; + + /** + * wallet 信息不存在 + */ + int WALLET_INFO_NOT_EXISTS = 28819; + + /** + * 钱包日志异常(类型不正确,或者充值金额小于0等场景) + */ + int WALLET_LOG_ERROR = 28827; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/PaymentEventCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/PaymentEventCode.java new file mode 100644 index 000000000..df4e58ad4 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/PaymentEventCode.java @@ -0,0 +1,20 @@ +package cn.bootx.daxpay.code; + +public interface PaymentEventCode { + + /** 支付中心交换机 */ + String EXCHANGE_PAYMENT = "service.payment"; + + /** 支付完成 */ + String PAY_COMPLETE = "pay.complete"; + + /** 支付取消 */ + String PAY_CANCEL = "pay.cancel"; + + /** 支付退款 */ + String PAY_REFUND = "pay.refund"; + + /** 支付单超时 */ + String PAYMENT_EXPIRED_TIME = "payment.expired:time"; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayChannelCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayChannelCode.java new file mode 100644 index 000000000..8d5c93856 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayChannelCode.java @@ -0,0 +1,46 @@ +package cn.bootx.daxpay.code.pay; + +import java.util.Arrays; +import java.util.List; + +/** + * 支付通道 + * + * @author xxm + * @date 2021/2/24 + */ +public interface PayChannelCode { + + /** + * 支付通道类型 1.支付宝 2.微信 3.云闪付 4.现金 5.钱包 6.储值卡 8.信用卡 9.ApplePay 10.渠道方支付 99.聚合支付 + */ + int ALI = 1; + + int WECHAT = 2; + + int UNION_PAY = 3; + + int CASH = 4; + + int WALLET = 5; + + int VOUCHER = 6; + + int CREDIT_CARD = 8; + + int APPLE_PAY = 9; + + int CHANNEL_PAY = 10; + + int AGGREGATION = 99; + + /** 支付宝 UA */ + String UA_ALI_PAY = "Alipay"; + + /** 微信 UA */ + String UA_WECHAT_PAY = "MicroMessenger"; + + /** 异步支付通道 */ + List ASYNC_TYPE = Arrays.asList(ALI, WECHAT, UNION_PAY, APPLE_PAY); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayChannelEnum.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayChannelEnum.java new file mode 100644 index 000000000..59e6de8c0 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayChannelEnum.java @@ -0,0 +1,57 @@ +package cn.bootx.daxpay.code.pay; + +import cn.bootx.daxpay.exception.payment.PayFailureException; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Objects; + +/** + * 支付通道枚举 + * + * @author xxm + * @date 2021/7/26 + */ +@Getter +@AllArgsConstructor +public enum PayChannelEnum { + + ALI(PayChannelCode.ALI, "ALI"), WECHAT(PayChannelCode.WECHAT, "WECHAT"), + UNION_PAY(PayChannelCode.UNION_PAY, "UNION_PAY"), CASH(PayChannelCode.CASH, "CASH"), + WALLET(PayChannelCode.WALLET, "WALLET"), VOUCHER(PayChannelCode.VOUCHER, "VOUCHER"), + CREDIT_CARD(PayChannelCode.CREDIT_CARD, "CREDIT"), APPLE_PAY(PayChannelCode.APPLE_PAY, "APPLE"), + CHANNEL_PAY(PayChannelCode.CHANNEL_PAY, "CHANNEL"), AGGREGATION(PayChannelCode.AGGREGATION, "AGGREGATION"); + + /** 支付通道数字编码 */ + private final int no; + + /** 支付通道字符编码 */ + private final String code; + + /** + * 根据数字编号获取 + */ + public static PayChannelEnum findByNo(int no) { + return Arrays.stream(PayChannelEnum.values()) + .filter(e -> e.getNo() == no) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付通道")); + } + + /** + * 根据字符编码获取 + */ + public static PayChannelEnum findByCode(String code) { + return Arrays.stream(PayChannelEnum.values()) + .filter(e -> Objects.equals(code, e.getCode())) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付通道")); + } + + public static boolean existsByCode(String code) { + return Arrays.stream(PayChannelEnum.values()) + .anyMatch(payChannelEnum -> Objects.equals(payChannelEnum.getCode(), code)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayModelExtraCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayModelExtraCode.java new file mode 100644 index 000000000..0269c280a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayModelExtraCode.java @@ -0,0 +1,23 @@ +package cn.bootx.daxpay.code.pay; + +/** + * 支付方式扩展字段 + * + * @author xxm + * @date 2022/2/27 + */ +public interface PayModelExtraCode { + + /** 付款码 */ + String AUTH_CODE = "authCode"; + + /** openId */ + String OPEN_ID = "openId"; + + /** 单张储值卡 */ + String VOUCHER_NO = "voucherNo"; + + /** 同步通知路径 支付完成跳转的页面地址 */ + String RETURN_URL = "returnUrl"; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayStatusCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayStatusCode.java new file mode 100644 index 000000000..2c98c9d70 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayStatusCode.java @@ -0,0 +1,45 @@ +package cn.bootx.daxpay.code.pay; + +/** + * 支付状态 + * + * @author xxm + * @date 2021/3/1 + */ +public interface PayStatusCode { + + /** + * 支付状态 0.支付中 1.成功 2.失败 3.支付取消(超时/手动取消/订单已经关闭,撤销支付单) 4.退款中 5.已退款 + */ + int TRADE_PROGRESS = 0; + + int TRADE_SUCCESS = 1; + + int TRADE_FAIL = 2; + + int TRADE_CANCEL = 3; + + int TRADE_REFUNDING = 4; + + int TRADE_REFUNDED = 5; + + /** + * 回调信息支付状态 0.失败 1.成功 + */ + int NOTIFY_TRADE_FAIL = 0; + + int NOTIFY_TRADE_SUCCESS = 1; + + /** 回调处理状态 */ + int NOTIFY_PROCESS_FAIL = 0; + + int NOTIFY_PROCESS_SUCCESS = 1; + + int NOTIFY_PROCESS_IGNORE = 2; + + /** 退款处理状态 0.失败 1.成功 */ + int REFUND_PROCESS_FAIL = 0; + + int REFUND_PROCESS_SUCCESS = 1; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PaySyncStatus.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PaySyncStatus.java new file mode 100644 index 000000000..d37b9cfde --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PaySyncStatus.java @@ -0,0 +1,32 @@ +package cn.bootx.daxpay.code.pay; + +/** + * 支付网关同步状态 + * + * @author xxm + * @date 2021/4/21 + */ +public interface PaySyncStatus { + + /** -1 不需要同步 */ + int NOT_SYNC = -1; + + /** 1 远程支付成功 */ + int TRADE_SUCCESS = 1; + + /** 2 交易创建,等待买家付款 */ + int WAIT_BUYER_PAY = 2; + + /** 3 已关闭 */ + int TRADE_CLOSED = 3; + + /** 4 已退款 */ + int TRADE_REFUND = 4; + + /** 5 查询不到订单 */ + int NOT_FOUND = 5; + + /** 4 查询失败 */ + int FAIL = 6; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayWayCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayWayCode.java new file mode 100644 index 000000000..ea8f15bb6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayWayCode.java @@ -0,0 +1,36 @@ +package cn.bootx.daxpay.code.pay; + +/** + * 支付方式 1-10 通用支付方式 10 以上是各支付通道特色支付方式, 规则 {通道代码}+{特色支付方式代码}, 如微信小程序支付, 23 微信代码2, 小程序3 + * + * @author xxm + * @date 2021/7/26 + */ +public interface PayWayCode { + + /** + * 常规支付. 钱包/积分等 + */ + int NORMAL = 0; + + /** wap支付 */ + int WAP = 1; + + /** 应用支付 */ + int APP = 2; + + /** web支付 */ + int WEB = 3; + + /** 二维码扫码支付 */ + int QRCODE = 4; + + /** 付款码支付 */ + int BARCODE = 5; + + /** + * 微信公众号支付或者小程序支付 + */ + int JSAPI = 23; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayWayEnum.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayWayEnum.java new file mode 100644 index 000000000..fe1526af1 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/pay/PayWayEnum.java @@ -0,0 +1,55 @@ +package cn.bootx.daxpay.code.pay; + +import cn.bootx.daxpay.exception.payment.PayFailureException; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; +import java.util.Objects; + +/** + * 支付方式 + * + * @author xxm + * @date 2021/7/26 + */ +@Getter +@AllArgsConstructor +public enum PayWayEnum { + + NORMAL(PayWayCode.NORMAL, "NORMAL", "常规支付"), WAP(PayWayCode.WAP, "WAP", "wap支付"), + APP(PayWayCode.APP, "APP", "应用支付"), WEB(PayWayCode.WEB, "WEB", "web支付"), + QRCODE(PayWayCode.QRCODE, "QRCODE", "扫码支付"), BARCODE(PayWayCode.BARCODE, "BARCODE", "付款码"), + + JSAPI(PayWayCode.JSAPI, "JSAPI", "公众号/小程序支付"); + + /** 支付方式数字编码 */ + private final int no; + + /** 支付方式字符编码 */ + private final String code; + + /** 名称 */ + private final String name; + + /** + * 根据数字编号获取 + */ + public static PayWayEnum findByNo(int no) { + return Arrays.stream(PayWayEnum.values()) + .filter(e -> e.getNo() == no) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + + /** + * 根据字符编码获取 + */ + public static PayWayEnum findByCode(String code) { + return Arrays.stream(PayWayEnum.values()) + .filter(e -> Objects.equals(code, e.getCode())) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/AliPayCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/AliPayCode.java new file mode 100644 index 000000000..8f6ffc1fc --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/AliPayCode.java @@ -0,0 +1,86 @@ +package cn.bootx.daxpay.code.paymodel; + +/** + * 支付宝支付参数 + * + * @author xxm + * @date 2021/2/27 + */ +public interface AliPayCode { + + // 认证类型 + /** 公钥 */ + int AUTH_TYPE_KEY = 1; + + /** 证书 */ + int AUTH_TYPE_CART = 2; + + // 渠道枚举 + /** 目前PC支付必填 */ + String FAST_INSTANT_TRADE_PAY = "FAST_INSTANT_TRADE_PAY"; + + /** WAP支付必填 手机网站支付产品 */ + String QUICK_WAP_PAY = "QUICK_WAP_WAY"; + + /** APP支付必填 APP支付产品 */ + String QUICK_MSECURITY_PAY = "QUICK_MSECURITY_PAY"; + + /** 付款码支付 */ + String BAR_CODE = "bar_code"; + + // 响应字段 + /** 支付状态 */ + String TRADE_STATUS = "trade_status"; + + /** 公用回传参数 */ + String PASS_BACK_PARAMS = "passback_params"; + + /** 对交易或商品的描述(在没有公用回传参数的时候, 这个作为公用回传参数) */ + String BODY = "body"; + + /** 外部订单号-paymentId */ + String OUT_TRADE_NO = "out_trade_no"; + + /** 支付宝流水号 */ + String TRADE_NO = "trade_no"; + + /** appId */ + String APP_ID = "app_id"; + + // 交易状态说明 + /** 交易创建,等待买家付款 */ + String PAYMENT_WAIT_BUYER_PAY = "WAIT_BUYER_PAY"; + + /** 未付款交易超时关闭,或支付完成后全额退款 */ + String PAYMENT_TRADE_CLOSED = "TRADE_CLOSED"; + + /** 交易支付成功 */ + String PAYMENT_TRADE_SUCCESS = "TRADE_SUCCESS"; + + /** 交易结束,不可退款 */ + String PAYMENT_TRADE_FINISHED = "TRADE_FINISHED"; + + // 通知触发条件 + /** 交易完成 */ + String NOTIFY_TRADE_FINISHED = "TRADE_FINISHED"; + + /** 支付成功 */ + String NOTIFY_TRADE_SUCCESS = "TRADE_SUCCESS"; + + /** 交易创建,不触发通知 */ + String NOTIFY_WAIT_BUYER_PAY = "WAIT_BUYER_PAY"; + + /** 交易关闭 */ + String NOTIFY_TRADE_CLOSED = "TRADE_CLOSED"; + + // 错误提示 + /** 交易不存在 */ + String ACQ_TRADE_NOT_EXIST = "ACQ.TRADE_NOT_EXIST"; + + // 网关返回码 + String SUCCESS = "10000"; + + // 网关返回码 支付进行中 order success pay inprocess + String INPROCESS = "10003"; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/AliPayWay.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/AliPayWay.java new file mode 100644 index 000000000..de4a47aae --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/AliPayWay.java @@ -0,0 +1,51 @@ +package cn.bootx.daxpay.code.paymodel; + +import cn.bootx.daxpay.code.pay.PayWayEnum; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * 支付宝支付方式 + * + * @author xxm + * @date 2021/7/2 + */ +@UtilityClass +public class AliPayWay { + + // 支付方式 + private static final List PAY_WAYS = Arrays.asList(PayWayEnum.WAP, PayWayEnum.APP, PayWayEnum.WEB, + PayWayEnum.QRCODE, PayWayEnum.BARCODE); + + /** + * 根据数字编号获取 + */ + public PayWayEnum findByNo(int no) { + return PAY_WAYS.stream() + .filter(e -> e.getNo() == no) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + + /** + * 根据数字编号获取 + */ + public PayWayEnum findByCode(String code) { + return PAY_WAYS.stream() + .filter(e -> Objects.equals(code, e.getCode())) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + + /** + * 获取支持的支付方式 + */ + public List getPayWays() { + return PAY_WAYS; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/VoucherCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/VoucherCode.java new file mode 100644 index 000000000..72284335d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/VoucherCode.java @@ -0,0 +1,46 @@ +package cn.bootx.daxpay.code.paymodel; + +/** + * 储值卡常量 + * + * @author xxm + * @date 2022/3/14 + */ +public interface VoucherCode { + + /** + * 状态-启用 + */ + int STATUS_NORMAL = 1; + + /** + * 状态-停用 + */ + int STATUS_FORBIDDEN = 2; + + /** + * 储值卡日志-开通 + */ + int LOG_ACTIVE = 1; + + /** + * 储值卡日志-支付 + */ + int LOG_PAY = 2; + + /** + * 储值卡日志-退款 + */ + int LOG_CLOSE = 3; + + /** + * 储值卡日志-退款 + */ + int LOG_REFUND = 4; + + /** + * 储值卡日志-Admin余额变动 + */ + int LOG_ADMIN_CHANGER = 4; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WalletCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WalletCode.java new file mode 100644 index 000000000..add45ca3a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WalletCode.java @@ -0,0 +1,76 @@ +package cn.bootx.daxpay.code.paymodel; + +/** + * 钱包涉及到的常量 + * + * @author xxm + * @date 2020/12/8 + */ +public interface WalletCode { + + /** + * 系统操作 + */ + int OPERATION_SOURCE_SYSTEM = 1; + + /** + * 管理员操作 + */ + int OPERATION_SOURCE_ADMIN = 2; + + /** + * 用户操作 + */ + int OPERATION_SOURCE_USER = 3; + + /** + * 钱包状态-正常 + */ + int STATUS_NORMAL = 1; + + /** + * 钱包状态-禁用 + */ + int STATUS_FORBIDDEN = 2; + + /** + * 钱包日志-开通 + */ + int LOG_ACTIVE = 1; + + /** + * 钱包日志-主动充值 + */ + int LOG_RECHARGE = 2; + + /** + * 钱包日志-自动充值 + */ + int LOG_AUTO_RECHARGE = 3; + + /** + * 钱包日志-Admin余额变动 + */ + int LOG_ADMIN_CHANGER = 4; + + /** + * 钱包日志-支付 + */ + int LOG_PAY = 5; + + /** + * 钱包日志-系统扣除余额的日志 + */ + int LOG_SYSTEM_REDUCE_BALANCE = 6; + + /** + * 钱包日志-退款 + */ + int LOG_REFUND = 7; + + /** + * 钱包日志-取消支付返还 + */ + int LOG_PAY_CLOSE = 8; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WeChatPayCode.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WeChatPayCode.java new file mode 100644 index 000000000..c040fbd44 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WeChatPayCode.java @@ -0,0 +1,88 @@ +package cn.bootx.daxpay.code.paymodel; + +/** + * 微信参数 + * + * @author xxm + * @date 2021/6/21 + */ +public interface WeChatPayCode { + + // 版本 + String API_V2 = "api_v2"; + + String API_V3 = "api_v3"; + + // 请求参数 + /** jsapi发起获取AuthCode时的重定向参数 */ + String JSAPI_REDIRECT_URL = "JsapiRedirectUrl"; + + // 返回参数 + /** 二维码链接 */ + String CODE_URL = "code_url"; + + /** 支付跳转链接 */ + String MWEB_URL = "mweb_url"; + + /** 预支付交易会话ID */ + String PREPAY_ID = "prepay_id"; + + /** 返回状态码 */ + String RETURN_CODE = "return_code"; + + /** 返回信息 */ + String RETURN_MSG = "return_msg"; + + /** 返回错误代码(例如付款码返回的支付中状态就在这里面) */ + String ERR_CODE = "err_code"; + + /** 返回错误信息 */ + String ERR_CODE_DES = "err_code_des"; + + /** 业务结果(部分结果不在这个参数里, 例如付款码的响应码) */ + String RESULT_CODE = "result_code"; + + /** 交易类型 */ + String TRADE_TYPE = "trade_type"; + + /** appid */ + String APPID = "appid"; + + /** 交易状态 */ + String TRADE_STATE = "trade_state"; + + /** 商户订单号 */ + String OUT_TRADE_NO = "out_trade_no"; + + /** 微信交易单号 */ + String TRANSACTION_ID = "transaction_id"; + + // 交易状态 + /** 支付成功 */ + String TRADE_SUCCESS = "SUCCESS"; + + /** 支付失败 */ + String TRADE_FAIL = "FAIL"; + + /** 退款 */ + String TRADE_REFUND = "REFUND"; + + /** 未支付 */ + String TRADE_NOTPAY = "NOTPAY"; + + /** 已关闭 */ + String TRADE_CLOSED = "CLOSED"; + + /** 已接收,等待扣款 */ + String TRADE_ACCEPT = "ACCEPT"; + + /** 已撤销(刷卡支付) */ + String TRADE_REVOKED = "REVOKED"; + + /** 用户支付中(刷卡支付) */ + String TRADE_USERPAYING = "USERPAYING"; + + /** 支付失败(刷卡支付) */ + String TRADE_PAYERROR = "PAYERROR"; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WeChatPayWay.java b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WeChatPayWay.java new file mode 100644 index 000000000..339f7297d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/code/paymodel/WeChatPayWay.java @@ -0,0 +1,50 @@ +package cn.bootx.daxpay.code.paymodel; + +import cn.bootx.daxpay.code.pay.PayWayEnum; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import lombok.experimental.UtilityClass; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * 微信支付方式 + * + * @author xxm + * @date 2021/7/2 + */ +@UtilityClass +public class WeChatPayWay { + + private static final List PAY_WAYS = Arrays.asList(PayWayEnum.WAP, PayWayEnum.APP, PayWayEnum.JSAPI, + PayWayEnum.QRCODE, PayWayEnum.BARCODE); + + /** + * 根据数字编号获取 + */ + public PayWayEnum findByNo(int no) { + return PAY_WAYS.stream() + .filter(e -> e.getNo() == no) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + + /** + * 根据数字编号获取 + */ + public PayWayEnum findByCode(String code) { + return PAY_WAYS.stream() + .filter(e -> Objects.equals(code, e.getCode())) + .findFirst() + .orElseThrow(() -> new PayFailureException("不存在的支付方式")); + } + + /** + * 获取支持的支付方式 + */ + public List getPayWays() { + return PAY_WAYS; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/configuration/PaymentRabbitMqConfiguration.java b/dax-pay/src/main/java/cn/bootx/daxpay/configuration/PaymentRabbitMqConfiguration.java new file mode 100644 index 000000000..62b34d991 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/configuration/PaymentRabbitMqConfiguration.java @@ -0,0 +1,76 @@ +package cn.bootx.daxpay.configuration; + +import cn.bootx.daxpay.code.PaymentEventCode; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 消息队列配置 + * + * @author xxm + * @date 2021/6/25 + */ +@Configuration +public class PaymentRabbitMqConfiguration { + + /** 支付完成队列 */ + @Bean + public Queue payComplete() { + return new Queue(PaymentEventCode.PAY_COMPLETE); + } + + /** 支付关闭/撤销队列 */ + @Bean + public Queue payCancel() { + return new Queue(PaymentEventCode.PAY_CANCEL); + } + + /** 支付退款队列 */ + @Bean + public Queue payRefund() { + return new Queue(PaymentEventCode.PAY_REFUND); + } + + /** 支付超时通知队列 */ + @Bean + public Queue paymentExpiredTime() { + return new Queue(PaymentEventCode.PAYMENT_EXPIRED_TIME); + } + + /** 交换机 */ + @Bean + public DirectExchange paymentExchange() { + return new DirectExchange(PaymentEventCode.EXCHANGE_PAYMENT); + } + + /** 绑定支付完成 */ + @Bean + public Binding bindPayComplete() { + return BindingBuilder.bind(payComplete()).to(paymentExchange()).with(PaymentEventCode.PAY_COMPLETE); + } + + /** 绑定支付关闭/撤销 */ + @Bean + public Binding bindPayCancel() { + return BindingBuilder.bind(payCancel()).to(paymentExchange()).with(PaymentEventCode.PAY_CANCEL); + } + + /** 绑定支付退款 */ + @Bean + public Binding bindPayRefund() { + return BindingBuilder.bind(payRefund()).to(paymentExchange()).with(PaymentEventCode.PAY_REFUND); + } + + /** 绑定支付超时通知 */ + @Bean + public Binding bindPaymentExpiredTime() { + return BindingBuilder.bind(paymentExpiredTime()) + .to(paymentExchange()) + .with(PaymentEventCode.PAYMENT_EXPIRED_TIME); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/AggregateController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/AggregateController.java new file mode 100644 index 000000000..26cb52b6f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/AggregateController.java @@ -0,0 +1,37 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.annotation.IgnoreAuth; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.daxpay.core.aggregate.service.AggregateService; +import cn.bootx.daxpay.param.cashier.CashierSinglePayParam; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 聚合支付 + * + * @author xxm + * @date 2022/3/6 + */ +@IgnoreAuth +@Tag(name = "聚合支付") +@RestController +@RequestMapping("/aggregate") +@RequiredArgsConstructor +public class AggregateController { + + private final AggregateService aggregateService; + + @Operation(summary = "创建聚合支付") + @PostMapping("/createAggregatePay") + public ResResult createAggregatePay(@RequestBody CashierSinglePayParam param) { + return Res.ok(aggregateService.createAggregatePay(param)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/AlipayConfigController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/AlipayConfigController.java new file mode 100644 index 000000000..667ec719b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/AlipayConfigController.java @@ -0,0 +1,77 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.common.core.rest.dto.KeyValue; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.daxpay.core.paymodel.alipay.service.AlipayConfigService; +import cn.bootx.daxpay.dto.paymodel.alipay.AlipayConfigDto; +import cn.bootx.daxpay.param.paymodel.alipay.AlipayConfigParam; +import cn.bootx.daxpay.param.paymodel.alipay.AlipayConfigQuery; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author xxm + * @date 2021/2/26 + */ +@Tag(name = "支付宝配置") +@RestController +@RequestMapping("/alipay") +@AllArgsConstructor +public class AlipayConfigController { + + private final AlipayConfigService alipayConfigService; + + @Operation(summary = "添加") + @PostMapping("/add") + public ResResult add(@RequestBody AlipayConfigParam param) { + alipayConfigService.add(param); + return Res.ok(); + } + + @Operation(summary = "更新") + @PostMapping("/update") + public ResResult update(@RequestBody AlipayConfigParam param) { + alipayConfigService.update(param); + return Res.ok(); + } + + @Operation(summary = "启用指定的支付宝配置") + @PostMapping("/setUpActivity") + public ResResult setUpActivity(Long id) { + alipayConfigService.setUpActivity(id); + return Res.ok(); + } + + @Operation(summary = "清除指定的支付宝配置") + @PostMapping("/clearActivity") + public ResResult clearActivity(Long id) { + alipayConfigService.clearActivity(id); + return Res.ok(); + } + + @Operation(summary = "分页") + @GetMapping("/page") + public ResResult> page(PageParam pageParam, AlipayConfigQuery param) { + return Res.ok(alipayConfigService.page(pageParam, param)); + } + + @Operation(summary = "根据Id查询") + @GetMapping("/findById") + public ResResult findById(Long id) { + return Res.ok(alipayConfigService.findById(id)); + } + + @Operation(summary = "支付宝支持支付方式") + @GetMapping("/findPayWayList") + public ResResult> findPayWayList() { + return Res.ok(alipayConfigService.findPayWayList()); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/CashierController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/CashierController.java new file mode 100644 index 000000000..9e30bfd55 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/CashierController.java @@ -0,0 +1,66 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.annotation.IgnoreAuth; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.daxpay.core.cashier.service.CashierService; +import cn.bootx.daxpay.dto.pay.PayResult; +import cn.bootx.daxpay.exception.payment.PayUnsupportedMethodException; +import cn.bootx.daxpay.param.cashier.CashierCombinationPayParam; +import cn.bootx.daxpay.param.cashier.CashierSinglePayParam; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +import java.util.Map; + +import static org.springframework.http.HttpHeaders.USER_AGENT; + +/** + * @author xxm + * @date 2022/2/23 + */ +@IgnoreAuth +@Tag(name = "结算台") +@RestController +@RequestMapping("/cashier") +@RequiredArgsConstructor +public class CashierController { + + private final CashierService cashierService; + + @Operation(summary = "发起支付(单渠道,包括聚合付款码方式)") + @PostMapping("/singlePay") + public ResResult singlePay(@RequestBody CashierSinglePayParam cashierSinglePayParam) { + return Res.ok(cashierService.singlePay(cashierSinglePayParam)); + } + + @Operation(summary = "发起支付(组合支付)") + @PostMapping("/combinationPay") + public ResResult combinationPay(@RequestBody CashierCombinationPayParam param) { + return Res.ok(cashierService.combinationPay(param)); + } + + @Operation(summary = "扫码聚合支付(单渠道)") + @GetMapping("/aggregatePay") + public ModelAndView aggregatePay(String key, @RequestHeader(USER_AGENT) String ua) { + try { + String url = cashierService.aggregatePay(key, ua); + return new ModelAndView("redirect:" + url); + } + catch (PayUnsupportedMethodException e) { + return new ModelAndView("errorCashier"); + } + } + + @Operation(summary = "微信jsapi支付(回调)") + @GetMapping("/wxJsapiPay") + public ModelAndView wxJsapiPay(String code, String state) { + Map map = cashierService.wxJsapiPay(code, state); + // 跳转页面, 调起微信jsapi支付 + return new ModelAndView("wechatJsapiPay").addAllObjects(map); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayCallbackController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayCallbackController.java new file mode 100644 index 000000000..cbbb375b3 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayCallbackController.java @@ -0,0 +1,54 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.annotation.IgnoreAuth; +import cn.bootx.daxpay.core.paymodel.alipay.service.AliPayCallbackService; +import cn.bootx.daxpay.core.paymodel.wechat.service.WeChatPayCallbackService; +import com.ijpay.alipay.AliPayApi; +import com.ijpay.core.kit.HttpKit; +import com.ijpay.core.kit.WxPayKit; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * @author xxm + * @date 2021/2/27 + */ +@IgnoreAuth +@Slf4j +@Tag(name = "支付回调") +@RestController +@RequestMapping("/pay/callback") +@AllArgsConstructor +public class PayCallbackController { + + private final AliPayCallbackService aliPayCallbackService; + + private final WeChatPayCallbackService weChatPayCallbackService; + + @SneakyThrows + @Operation(summary = "支付宝回调") + @PostMapping("/alipay") + public String aliPay(HttpServletRequest request) { + Map stringStringMap = AliPayApi.toMap(request); + return aliPayCallbackService.payCallback(stringStringMap); + } + + @SneakyThrows + @Operation(summary = "微信支付回调") + @PostMapping("/wechat") + public String wechat(HttpServletRequest request) { + String xmlMsg = HttpKit.readData(request); + Map params = WxPayKit.xmlToMap(xmlMsg); + return weChatPayCallbackService.payCallback(params); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayController.java new file mode 100644 index 000000000..1f1d0dcb1 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayController.java @@ -0,0 +1,80 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.daxpay.core.pay.service.PayCancelService; +import cn.bootx.daxpay.core.pay.service.PayRefundService; +import cn.bootx.daxpay.core.pay.service.PayService; +import cn.bootx.daxpay.core.pay.service.PaySyncService; +import cn.bootx.daxpay.dto.pay.PayResult; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.bootx.daxpay.param.refund.RefundParam; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author xxm + * @date 2020/12/9 + */ +@Tag(name = "统一支付") +@RestController +@RequestMapping("/uni_pay") +@AllArgsConstructor +public class PayController { + + private final PayService payService; + + private final PayCancelService payCancelService; + + private final PayRefundService payRefundService; + + private final PaySyncService paySyncService; + + @Operation(summary = "支付") + @PostMapping("/pay") + public ResResult pay(@RequestBody PayParam payParam) { + return Res.ok(payService.pay(payParam)); + } + + @Operation(summary = "取消支付(支付id)") + @PostMapping("/cancelByPaymentId") + public ResResult cancelByPaymentId(@Parameter(description = "支付id") Long paymentId) { + payCancelService.cancelByPaymentId(paymentId); + return Res.ok(); + } + + @Operation(summary = "取消支付(业务id)") + @PostMapping("/cancelByBusinessId") + public ResResult cancelByBusinessId(@Parameter(description = "业务id") String businessId) { + payCancelService.cancelByBusinessId(businessId); + return Res.ok(); + } + + @Operation(summary = "刷新指定业务id的支付单状态") + @PostMapping("/syncByBusinessId") + public ResResult syncByBusinessId(@Parameter(description = "业务id") String businessId) { + paySyncService.syncByBusinessId(businessId); + return Res.ok(); + } + + @Operation(summary = "退款") + @PostMapping("/refund") + public ResResult refund(@RequestBody RefundParam refundParam) { + payRefundService.refund(refundParam); + return Res.ok(); + } + + @Operation(summary = "全额退款(业务id)") + @PostMapping("/refundByBusinessId") + public ResResult refundByBusinessId(@Parameter(description = "业务id") String businessId) { + payRefundService.refundByBusinessId(businessId); + return Res.ok(); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayNotifyRecordController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayNotifyRecordController.java new file mode 100644 index 000000000..04cd1e3cd --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/PayNotifyRecordController.java @@ -0,0 +1,42 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.daxpay.core.notify.service.PayNotifyRecordService; +import cn.bootx.daxpay.dto.notify.PayNotifyRecordDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 回调记录 + * + * @author xxm + * @date 2021/7/22 + */ +@Tag(name = "支付回调记录") +@RestController +@RequestMapping("/pay/notify/record") +@RequiredArgsConstructor +public class PayNotifyRecordController { + + private final PayNotifyRecordService notifyRecordService; + + @Operation(summary = "分页") + @GetMapping("/page") + public ResResult> page(PageParam pageParam, PayNotifyRecordDto param) { + return Res.ok(notifyRecordService.page(pageParam, param)); + } + + @Operation(summary = "根据id查询") + @GetMapping("/findById") + public ResResult findById(Long id) { + return Res.ok(notifyRecordService.findById(id)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/PaymentController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/PaymentController.java new file mode 100644 index 000000000..6ce370e5f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/PaymentController.java @@ -0,0 +1,77 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.annotation.IgnoreAuth; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.common.core.rest.param.OrderParam; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.query.entity.QueryParams; +import cn.bootx.daxpay.core.payment.service.PaymentQueryService; +import cn.bootx.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.daxpay.dto.payment.PaymentDto; +import cn.bootx.daxpay.param.payment.PaymentQuery; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author xxm + * @date 2021/6/28 + */ +@Tag(name = "支付记录") +@RestController +@RequestMapping("/payment") +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentQueryService paymentQueryService; + + @Operation(summary = "根据id获取") + @GetMapping("/findById") + public ResResult findById(Long id) { + return Res.ok(paymentQueryService.findById(id)); + } + + @Operation(summary = "根据userId获取列表") + @GetMapping("/findByUser") + public ResResult> findByUser(Long userid) { + return Res.ok(paymentQueryService.findByUser(userid)); + } + + @Operation(summary = "分页查询") + @GetMapping("/page") + public ResResult> page(PageParam pageParam, PaymentQuery param, OrderParam orderParam) { + return Res.ok(paymentQueryService.page(pageParam, param, orderParam)); + } + + @Operation(summary = "分页查询(超级查询)") + @PostMapping("/superPage") + public ResResult> superPage(PageParam pageParam, @RequestBody QueryParams queryParams) { + return Res.ok(paymentQueryService.superPage(pageParam, queryParams)); + } + + @IgnoreAuth + @Operation(summary = "根据业务ID获取支付状态`") + @GetMapping("/findStatusByBusinessId") + public ResResult findStatusByBusinessId(String businessId) { + return Res.ok(paymentQueryService.findStatusByBusinessId(businessId)); + } + + @IgnoreAuth + @Operation(summary = "根据businessId获取订单支付方式") + @GetMapping("/findPayTypeInfoByBusinessId") + public ResResult> findPayTypeInfoByBusinessId(String businessId) { + return Res.ok(paymentQueryService.findPayTypeInfoByBusinessId(businessId)); + } + + @Operation(summary = "根据id获取订单支付方式") + @GetMapping("/findPayTypeInfoById") + public ResResult> findPayTypeInfoById(Long id) { + return Res.ok(paymentQueryService.findPayTypeInfoById(id)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/RefundRecordController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/RefundRecordController.java new file mode 100644 index 000000000..ac3191ea5 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/RefundRecordController.java @@ -0,0 +1,42 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.daxpay.core.refund.service.RefundRecordService; +import cn.bootx.daxpay.dto.refund.RefundRecordDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 退款记录 + * + * @author xxm + * @date 2022/3/3 + */ +@Tag(name = "退款记录") +@RestController +@RequestMapping("/pay/refund") +@RequiredArgsConstructor +public class RefundRecordController { + + private final RefundRecordService refundRecordService; + + @Operation(summary = "分页") + @GetMapping("/page") + public ResResult> page(PageParam pageParam, RefundRecordDto param) { + return Res.ok(refundRecordService.page(pageParam, param)); + } + + @Operation(summary = "根据id查询") + @GetMapping("/findById") + public ResResult findById(Long id) { + return Res.ok(refundRecordService.findById(id)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/VoucherController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/VoucherController.java new file mode 100644 index 000000000..ae131fbc6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/VoucherController.java @@ -0,0 +1,86 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.daxpay.core.paymodel.voucher.service.VoucherQueryService; +import cn.bootx.daxpay.core.paymodel.voucher.service.VoucherService; +import cn.bootx.daxpay.dto.paymodel.voucher.VoucherDto; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherGenerationParam; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherParam; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Tag(name = "储值卡") +@RestController +@RequestMapping("/voucher") +@RequiredArgsConstructor +public class VoucherController { + + private final VoucherService voucherService; + + private final VoucherQueryService voucherQueryService; + + @Operation(summary = "分页") + @GetMapping("/page") + public ResResult> page(PageParam pageParam, VoucherParam param) { + return Res.ok(voucherQueryService.page(pageParam, param)); + } + + @Operation(summary = "单条查询") + @GetMapping("/findById") + public ResResult findById(Long id) { + return Res.ok(voucherQueryService.findById(id)); + } + + @Operation(summary = "根据卡号查询") + @GetMapping("/findByCardNo") + public ResResult findByCardNo(String cardNo) { + return Res.ok(voucherQueryService.findByCardNo(cardNo)); + } + + @Operation(summary = "批量生成储值卡") + @PostMapping("/generationBatch") + public ResResult generationBatch(@RequestBody VoucherGenerationParam param) { + voucherService.generationBatch(param); + return Res.ok(); + } + + @Operation(summary = "冻结") + @PostMapping("/lock") + public ResResult lock(Long id) { + voucherService.lock(id); + return Res.ok(); + } + + @Operation(summary = "启用") + @PostMapping("/unlock") + public ResResult unlock(Long id) { + voucherService.unlock(id); + return Res.ok(); + } + + @Operation(summary = "批量冻结") + @PostMapping("/lockBatch") + public ResResult lockBatch(@RequestBody List ids) { + voucherService.lockBatch(ids); + return Res.ok(); + } + + @Operation(summary = "批量启用") + @PostMapping("/unlockBatch") + public ResResult unlockBatch(@RequestBody List ids) { + voucherService.unlockBatch(ids); + return Res.ok(); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/WalletController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/WalletController.java new file mode 100644 index 000000000..1afc535f6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/WalletController.java @@ -0,0 +1,106 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.annotation.OperateLog; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.daxpay.core.paymodel.wallet.service.WalletQueryService; +import cn.bootx.daxpay.core.paymodel.wallet.service.WalletService; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletDto; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletInfoDto; +import cn.bootx.daxpay.param.paymodel.wallet.WalletPayParam; +import cn.bootx.daxpay.param.paymodel.wallet.WalletRechargeParam; +import cn.bootx.iam.dto.user.UserInfoDto; +import cn.bootx.iam.param.user.UserInfoParam; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 钱包 + * + * @author xxm + * @date 2021/2/24 + */ +@Tag(name = "钱包相关的接口") +@RestController +@RequestMapping("wallet") +@AllArgsConstructor +public class WalletController { + + private final WalletService walletService; + + private final WalletQueryService walletQueryService; + + @Operation(summary = "开通用户钱包操作") + @PostMapping("createWallet") + public ResResult createWallet(Long userId) { + walletService.createWallet(userId); + return Res.ok(); + } + + @Operation(summary = "批量开通用户钱包操作") + @PostMapping("createWalletBatch") + public ResResult createWalletBatch(@RequestBody List userIds) { + walletService.createWalletBatch(userIds); + return Res.ok(); + } + + @Operation(summary = "解锁钱包") + @OperateLog(title = "解锁钱包", businessType = OperateLog.BusinessType.UPDATE, saveParam = true) + @PostMapping("/unlock") + public ResResult unlock(Long walletId) { + walletService.unlock(walletId); + return Res.ok(); + } + + @Operation(summary = "锁定钱包") + @OperateLog(title = "锁定钱包", businessType = OperateLog.BusinessType.UPDATE, saveParam = true) + @PostMapping("/lock") + public ResResult lock(Long walletId) { + walletService.lock(walletId); + return Res.ok(); + } + + @Operation(summary = "充值操作(增减余额)") + @PostMapping("/changerBalance") + public ResResult changerBalance(@RequestBody WalletRechargeParam param) { + walletService.changerBalance(param); + return Res.ok(); + } + + @Operation(summary = "分页") + @GetMapping("/page") + public ResResult> page(PageParam pageParam, WalletPayParam param) { + return Res.ok(walletQueryService.page(pageParam, param)); + } + + @Operation(summary = "分页") + @GetMapping("/pageByNotWallet") + public ResResult> pageByNotWallet(PageParam pageParam, UserInfoParam param) { + return Res.ok(walletQueryService.pageByNotWallet(pageParam, param)); + } + + @Operation(summary = "根据用户查询钱包") + @GetMapping("/findByUser") + public ResResult findByUser() { + return Res.ok(walletQueryService.findByUser()); + } + + @Operation(summary = "根据钱包ID查询钱包") + @GetMapping("/findById") + public ResResult findById(Long walletId) { + return Res.ok(walletQueryService.findById(walletId)); + } + + @Operation(summary = "获取钱包综合信息") + @GetMapping("/getWalletInfo") + public ResResult getWalletInfo(Long walletId) { + return Res.ok(walletQueryService.getWalletInfo(walletId)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/WalletLogController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/WalletLogController.java new file mode 100644 index 000000000..7b490cc02 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/WalletLogController.java @@ -0,0 +1,54 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.daxpay.core.paymodel.wallet.service.WalletLogService; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletLogDto; +import cn.bootx.daxpay.param.paymodel.wallet.WalletLogQueryParam; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 钱包日志相关接口 + * + * @author xxm + * @date 2020/12/8 + */ +@Tag(name = "钱包日志相关的接口") +@RestController +@RequestMapping("/wallet/log") +@AllArgsConstructor +public class WalletLogController { + + private final WalletLogService walletLogService; + + @Operation(summary = "个人钱包日志") + @PostMapping("/pageByPersonal") + public ResResult> pageByPersonal(@ParameterObject PageParam pageParam, + @ParameterObject WalletLogQueryParam param) { + return Res.ok(walletLogService.pageByPersonal(pageParam, param)); + } + + @Operation(summary = "查询钱包日志(分页)") + @GetMapping("/page") + public ResResult> page(@ParameterObject PageParam pageParam, + @ParameterObject WalletLogQueryParam param) { + return Res.ok(walletLogService.page(pageParam, param)); + } + + @Operation(summary = "根据钱包id查询钱包日志(分页)") + @GetMapping("/pageByWalletId") + public ResResult> pageByWalletId(@ParameterObject PageParam pageParam, + @ParameterObject WalletLogQueryParam param) { + return Res.ok(walletLogService.pageByWalletId(pageParam, param)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/controller/WeChatPayConfigController.java b/dax-pay/src/main/java/cn/bootx/daxpay/controller/WeChatPayConfigController.java new file mode 100644 index 000000000..e06533447 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/controller/WeChatPayConfigController.java @@ -0,0 +1,76 @@ +package cn.bootx.daxpay.controller; + +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.Res; +import cn.bootx.common.core.rest.ResResult; +import cn.bootx.common.core.rest.dto.KeyValue; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.daxpay.core.paymodel.wechat.service.WeChatPayConfigService; +import cn.bootx.daxpay.dto.paymodel.wechat.WeChatPayConfigDto; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayConfigParam; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * @author xxm + * @date 2021/3/19 + */ +@Tag(name = "微信支付配置") +@RestController +@RequestMapping("/wechat/pay") +@AllArgsConstructor +public class WeChatPayConfigController { + + private final WeChatPayConfigService weChatPayConfigService; + + @Operation(summary = "添加微信支付配置") + @PostMapping("/add") + public ResResult add(@RequestBody WeChatPayConfigParam param) { + weChatPayConfigService.add(param); + return Res.ok(); + } + + @Operation(summary = "更新") + @PostMapping("/update") + public ResResult update(@RequestBody WeChatPayConfigParam param) { + weChatPayConfigService.update(param); + return Res.ok(); + } + + @Operation(summary = "设置启用的微信支付配置") + @PostMapping("/setUpActivity") + public ResResult setUpActivity(Long id) { + weChatPayConfigService.setUpActivity(id); + return Res.ok(); + } + + @Operation(summary = "清除指定的微信支付配置") + @PostMapping("/clearActivity") + public ResResult clearActivity(Long id) { + weChatPayConfigService.clearActivity(id); + return Res.ok(); + } + + @Operation(summary = "分页") + @GetMapping("/page") + public ResResult> page(PageParam pageParam, WeChatPayConfigParam param) { + return Res.ok(weChatPayConfigService.page(pageParam, param)); + } + + @Operation(summary = "根据Id查询") + @GetMapping("/findById") + public ResResult findById(Long id) { + return Res.ok(weChatPayConfigService.findById(id)); + } + + @Operation(summary = "微信支持支付方式") + @GetMapping("/findPayWayList") + public ResResult> findPayWayList() { + return Res.ok(weChatPayConfigService.findPayWayList()); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/aggregate/entity/AggregatePayInfo.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/aggregate/entity/AggregatePayInfo.java new file mode 100644 index 000000000..ca9d4d816 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/aggregate/entity/AggregatePayInfo.java @@ -0,0 +1,27 @@ +package cn.bootx.daxpay.core.aggregate.entity; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 聚合支付发起信息 + * + * @author xxm + * @date 2022/3/5 + */ +@Data +@Accessors(chain = true) +public class AggregatePayInfo { + + /** 标题 */ + private String title; + + /** 订单ID */ + private String businessId; + + /** 支付金额 */ + private BigDecimal amount; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/aggregate/service/AggregateService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/aggregate/service/AggregateService.java new file mode 100644 index 000000000..58f4a446f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/aggregate/service/AggregateService.java @@ -0,0 +1,77 @@ +package cn.bootx.daxpay.core.aggregate.service; + +import cn.bootx.common.redis.RedisClient; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.core.aggregate.entity.AggregatePayInfo; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.cashier.CashierSinglePayParam; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +/** + * 聚合支付 + * + * @author xxm + * @date 2022/3/6 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AggregateService { + + private final RedisClient redisClient; + + private final String PREFIX_KEY = "cashier:pay:aggregate:"; + + /** + * 创建聚合支付QR支付, 单渠道 + */ + public String createAggregatePay(CashierSinglePayParam param) { + // 保存并生成code + AggregatePayInfo aggregatePayInfo = new AggregatePayInfo().setAmount(param.getAmount()) + .setTitle(param.getTitle()) + .setBusinessId(param.getBusinessId()); + String key = RandomUtil.randomString(10); + redisClient.setWithTimeout(PREFIX_KEY + key, JSONUtil.toJsonStr(aggregatePayInfo), 5 * 60 * 1000); + return key; + } + + /** + * 获取聚合支付信息 + */ + public AggregatePayInfo getAggregateInfo(String key) { + String jsonStr = Optional.ofNullable(redisClient.get(PREFIX_KEY + key)) + .orElseThrow(() -> new PayFailureException("支付超时")); + return JSONUtil.toBean(jsonStr, AggregatePayInfo.class); + } + + /** + * 聚合付款码支付处理 + */ + public int getPayChannel(String authCode) { + if (StrUtil.isBlank(authCode)) { + throw new PayFailureException("付款码不可为空"); + } + String[] wx = { "10", "11", "12", "13", "14", "15" }; + String[] ali = { "25", "26", "27", "28", "29", "30" }; + + // 微信 + if (StrUtil.startWithAny(authCode.substring(0, 2), wx)) { + return PayChannelCode.WECHAT; + } + // 支付宝 + else if (StrUtil.startWithAny(authCode.substring(0, 2), ali)) { + return PayChannelCode.ALI; + } + else { + throw new PayFailureException("不支持的支付方式"); + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/cashier/service/CashierService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/cashier/service/CashierService.java new file mode 100644 index 000000000..d1b3240ef --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/cashier/service/CashierService.java @@ -0,0 +1,197 @@ +package cn.bootx.daxpay.core.cashier.service; + +import cn.bootx.baseapi.core.parameter.dao.SystemParamManager; +import cn.bootx.baseapi.core.parameter.entity.SystemParameter; +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayModelExtraCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.code.pay.PayWayCode; +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.daxpay.core.aggregate.entity.AggregatePayInfo; +import cn.bootx.daxpay.core.aggregate.service.AggregateService; +import cn.bootx.daxpay.core.pay.service.PayService; +import cn.bootx.daxpay.core.paymodel.wechat.dao.WeChatPayConfigManager; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import cn.bootx.daxpay.dto.pay.PayResult; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.exception.payment.PayUnsupportedMethodException; +import cn.bootx.daxpay.param.cashier.CashierCombinationPayParam; +import cn.bootx.daxpay.param.cashier.CashierSinglePayParam; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.bootx.daxpay.util.PayModelUtil; +import cn.bootx.starter.auth.util.SecurityUtil; +import cn.hutool.core.collection.CollUtil; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.kit.WxPayKit; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.*; + +/** + * 结算台 + * + * @author xxm + * @date 2022/2/23 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CashierService { + + private final PayService payService; + + private final AggregateService aggregateService; + + private final WeChatPayConfigManager weChatPayConfigManager; + + private final SystemParamManager systemParamManager; + + /** + * 发起支付(单渠道支付) + */ + public PayResult singlePay(CashierSinglePayParam param) { + + // 如果是聚合支付,存在付款码时特殊处理(聚合扫码支付不用额外处理) + if (Objects.equals(PayChannelCode.AGGREGATION, param.getPayChannel())) { + int payChannel = aggregateService.getPayChannel(param.getAuthCode()); + param.setPayChannel(payChannel); + } + // 构建支付方式参数 + PayModeParam payModeParam = new PayModeParam().setPayChannel(param.getPayChannel()) + .setPayWay(param.getPayWay()) + .setAmount(param.getAmount()); + + // 处理附加参数 + HashMap map = new HashMap<>(1); + map.put(PayModelExtraCode.AUTH_CODE, param.getAuthCode()); + map.put(PayModelExtraCode.OPEN_ID, param.getOpenId()); + map.put(PayModelExtraCode.VOUCHER_NO, param.getVoucherNo()); + String extraParamsJson = PayModelUtil.buildExtraParamsJson(param.getPayChannel(), map); + payModeParam.setExtraParamsJson(extraParamsJson); + + PayParam payParam = new PayParam().setTitle(param.getTitle()) + .setBusinessId(param.getBusinessId()) + .setUserId(SecurityUtil.getUserIdOrDefaultId()) + .setPayModeList(Collections.singletonList(payModeParam)); + PayResult payResult = payService.pay(payParam); + + if (PayStatusCode.TRADE_REFUNDED == payResult.getPayStatus()) { + throw new PayFailureException("已经退款"); + } + return payResult; + } + + /** + * 扫码发起自动支付 + */ + public String aggregatePay(String key, String ua) { + CashierSinglePayParam cashierSinglePayParam = new CashierSinglePayParam().setPayWay(PayWayCode.QRCODE); + // 判断是哪种支付方式 + if (ua.contains(PayChannelCode.UA_ALI_PAY)) { + cashierSinglePayParam.setPayChannel(PayChannelCode.ALI); + } + else if (ua.contains(PayChannelCode.UA_WECHAT_PAY)) { + // 跳转微信授权页面, 调用jsapi进行支付 + return this.wxJsapiAuth(key); + } + else { + throw new PayUnsupportedMethodException(); + } + + AggregatePayInfo aggregatePayInfo = aggregateService.getAggregateInfo(key); + cashierSinglePayParam.setTitle(aggregatePayInfo.getTitle()) + .setAmount(aggregatePayInfo.getAmount()) + .setBusinessId(aggregatePayInfo.getBusinessId()); + PayResult payResult = this.singlePay(cashierSinglePayParam); + return payResult.getAsyncPayInfo().getPayBody(); + } + + /** + * 微信jsapi支付 - 跳转到授权页面 + */ + private String wxJsapiAuth(String key) { + WeChatPayConfig config = weChatPayConfigManager.findActivity() + .orElseThrow(() -> new PayFailureException("未找到启用的微信支付配置")); + WxMpService wxMpService = getWxMpService(config.getAppId(), config.getAppSecret()); + // 回调地址为 结算台微信jsapi支付的回调地址 + SystemParameter systemParameter = systemParamManager.findByParamKey(WeChatPayCode.JSAPI_REDIRECT_URL) + .orElseThrow(() -> new PayFailureException("微信支付回调地址参数不存在")); + String url = systemParameter.getValue() + "cashier/wxJsapiPay"; + return wxMpService.getOAuth2Service().buildAuthorizationUrl(url, WxConsts.OAuth2Scope.SNSAPI_BASE, key); + } + + /** + * 微信jsapi支付 - 回调发起预支付, 同时调起微信页面jsapi支付 + * @param code 微信授权码, 用来获取id + * @param state 聚合支付参数记录的key + * @return 页面中调起jsapi支付的参数 + */ + @SneakyThrows + public Map wxJsapiPay(String code, String state) { + WeChatPayConfig config = weChatPayConfigManager.findActivity() + .orElseThrow(() -> new PayFailureException("未找到启用的微信支付配置")); + WxMpService wxMpService = this.getWxMpService(config.getAppId(), config.getAppSecret()); + WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().getAccessToken(code); + String openId = accessToken.getOpenId(); + AggregatePayInfo aggregatePayInfo = aggregateService.getAggregateInfo(state); + // 构造微信API支付参数 + CashierSinglePayParam cashierSinglePayParam = new CashierSinglePayParam().setPayChannel(PayChannelCode.WECHAT) + .setPayWay(PayWayCode.JSAPI) + .setTitle(aggregatePayInfo.getTitle()) + .setAmount(aggregatePayInfo.getAmount()) + .setOpenId(openId) + .setBusinessId(aggregatePayInfo.getBusinessId()); + PayResult payResult = this.singlePay(cashierSinglePayParam); + + return WxPayKit.prepayIdCreateSign(payResult.getAsyncPayInfo().getPayBody(), config.getAppId(), + config.getApiKeyV2(), SignType.HMACSHA256); + } + + /** + * 获取微信公众号API的Service + */ + private WxMpService getWxMpService(String appId, String secret) { + WxMpService wxMpService = new WxMpServiceImpl(); + WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl(); + config.setAppId(appId); // 设置微信公众号的appid + config.setSecret(secret); // 设置微信公众号的app corpSecret + wxMpService.setWxMpConfigStorage(config); + return wxMpService; + } + + /** + * 组合支付 + */ + public PayResult combinationPay(CashierCombinationPayParam param) { + // 处理支付参数 + List payModeList = param.getPayModeList(); + // 删除小于等于零的 + payModeList.removeIf(payModeParam -> BigDecimalUtil.compareTo(payModeParam.getAmount(), BigDecimal.ZERO) < 1); + if (CollUtil.isEmpty(payModeList)) { + throw new PayFailureException("支付参数有误"); + } + // 发起支付 + PayParam payParam = new PayParam().setTitle(param.getTitle()) + .setBusinessId(param.getBusinessId()) + .setUserId(SecurityUtil.getUserIdOrDefaultId()) + .setPayModeList(param.getPayModeList()); + PayResult payResult = payService.pay(payParam); + + if (PayStatusCode.TRADE_REFUNDED == payResult.getPayStatus()) { + throw new PayFailureException("已经退款"); + } + return payResult; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/convert/PayNotifyConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/convert/PayNotifyConvert.java new file mode 100644 index 000000000..6f3da1b4f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/convert/PayNotifyConvert.java @@ -0,0 +1,21 @@ +package cn.bootx.daxpay.core.notify.convert; + +import cn.bootx.daxpay.core.notify.entity.PayNotifyRecord; +import cn.bootx.daxpay.dto.notify.PayNotifyRecordDto; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 转换 + * + * @author xxm + * @date 2021/6/22 + */ +@Mapper +public interface PayNotifyConvert { + + PayNotifyConvert CONVERT = Mappers.getMapper(PayNotifyConvert.class); + + PayNotifyRecordDto convert(PayNotifyRecord in); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/dao/PayNotifyRecordManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/dao/PayNotifyRecordManager.java new file mode 100644 index 000000000..599d22b42 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/dao/PayNotifyRecordManager.java @@ -0,0 +1,36 @@ +package cn.bootx.daxpay.core.notify.dao; + +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.base.MpIdEntity; +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.notify.entity.PayNotifyRecord; +import cn.bootx.daxpay.dto.notify.PayNotifyRecordDto; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Objects; + +/** + * 支付消息通知回调 + * + * @author xxm + * @date 2021/6/22 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class PayNotifyRecordManager extends BaseManager { + + public Page page(PageParam pageParam, PayNotifyRecordDto param) { + Page mpPage = MpUtil.getMpPage(pageParam, PayNotifyRecord.class); + return lambdaQuery().orderByDesc(MpIdEntity::getId) + .like(Objects.nonNull(param.getPaymentId()), PayNotifyRecord::getPaymentId, param.getPaymentId()) + .eq(Objects.nonNull(param.getPayChannel()), PayNotifyRecord::getPayChannel, param.getPayChannel()) + .eq(Objects.nonNull(param.getStatus()), PayNotifyRecord::getStatus, param.getStatus()) + .page(mpPage); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/dao/PayNotifyRecordMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/dao/PayNotifyRecordMapper.java new file mode 100644 index 000000000..6a43ecb65 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/dao/PayNotifyRecordMapper.java @@ -0,0 +1,10 @@ +package cn.bootx.daxpay.core.notify.dao; + +import cn.bootx.daxpay.core.notify.entity.PayNotifyRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayNotifyRecordMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/entity/PayNotifyRecord.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/entity/PayNotifyRecord.java new file mode 100644 index 000000000..903c0c9ba --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/entity/PayNotifyRecord.java @@ -0,0 +1,57 @@ +package cn.bootx.daxpay.core.notify.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.notify.convert.PayNotifyConvert; +import cn.bootx.daxpay.dto.notify.PayNotifyRecordDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 回调记录 + * + * @author xxm + * @date 2021/6/22 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_pay_notify_record") +public class PayNotifyRecord extends MpBaseEntity implements EntityBaseFunction { + + /** 支付记录id */ + private Long paymentId; + + /** + * 支付通道 + * @see PayChannelCode + */ + private int payChannel; + + /** 通知消息 */ + private String notifyInfo; + + /** + * 处理状态 + * @see PayStatusCode#NOTIFY_PROCESS_SUCCESS + */ + private int status; + + /** 提示信息 */ + private String msg; + + /** 回调时间 */ + private LocalDateTime notifyTime; + + @Override + public PayNotifyRecordDto toDto() { + return PayNotifyConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/service/PayNotifyRecordService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/service/PayNotifyRecordService.java new file mode 100644 index 000000000..2404087db --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/notify/service/PayNotifyRecordService.java @@ -0,0 +1,43 @@ +package cn.bootx.daxpay.core.notify.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.notify.dao.PayNotifyRecordManager; +import cn.bootx.daxpay.core.notify.entity.PayNotifyRecord; +import cn.bootx.daxpay.dto.notify.PayNotifyRecordDto; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 回调记录 + * + * @author xxm + * @date 2021/7/5 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayNotifyRecordService { + + private final PayNotifyRecordManager payNotifyRecordManager; + + /** + * 分页查询 + */ + public PageResult page(PageParam pageParam, PayNotifyRecordDto param) { + Page page = payNotifyRecordManager.page(pageParam, param); + return MpUtil.convert2DtoPageResult(page); + } + + /** + * 根据id查询 + */ + public PayNotifyRecordDto findById(Long id) { + return payNotifyRecordManager.findById(id).map(PayNotifyRecord::toDto).orElseThrow(DataNotExistException::new); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/builder/PayEventBuilder.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/builder/PayEventBuilder.java new file mode 100644 index 000000000..c7b09945d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/builder/PayEventBuilder.java @@ -0,0 +1,45 @@ +package cn.bootx.daxpay.core.pay.builder; + +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.event.PayCancelEvent; +import cn.bootx.daxpay.event.PayCompleteEvent; +import cn.bootx.daxpay.event.PayRefundEvent; +import lombok.experimental.UtilityClass; + +/** + * 支付事件生成器 + * + * @author xxm + * @date 2022/7/11 + */ +@UtilityClass +public class PayEventBuilder { + + /** + * 支付完成 + */ + public PayCompleteEvent buildPayComplete(Payment payment) { + PayCompleteEvent event = new PayCompleteEvent().setPaymentId(payment.getId()) + .setBusinessId(payment.getBusinessId()); + return event; + } + + /** + * 支付撤销/关闭 + */ + public PayCancelEvent buildPayCancel(Payment payment) { + PayCancelEvent event = new PayCancelEvent().setPaymentId(payment.getId()) + .setBusinessId(payment.getBusinessId()); + return event; + } + + /** + * 支付退款 + */ + public PayRefundEvent buildPayRefund(Payment payment) { + PayRefundEvent event = new PayRefundEvent().setPaymentId(payment.getId()) + .setBusinessId(payment.getBusinessId()); + return event; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/builder/PaymentBuilder.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/builder/PaymentBuilder.java new file mode 100644 index 000000000..43f559f3a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/builder/PaymentBuilder.java @@ -0,0 +1,135 @@ +package cn.bootx.daxpay.core.pay.builder; + +import cn.bootx.common.spring.util.WebServletUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.pay.local.AsyncPayInfoLocal; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.dto.pay.PayResult; +import cn.bootx.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.extra.servlet.ServletUtil; +import lombok.experimental.UtilityClass; + +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 支付对象构建器 + * + * @author xxm + * @date 2021/2/25 + */ +@UtilityClass +public class PaymentBuilder { + + /** + * 构建payment记录 + */ + public Payment buildPayment(PayParam payParam) { + Payment payment = new Payment(); + + HttpServletRequest request = WebServletUtil.getRequest(); + String ip = ServletUtil.getClientIP(request); + // 基础信息 + payment.setBusinessId(payParam.getBusinessId()) + .setUserId(payParam.getUserId()) + .setTitle(payParam.getTitle()) + .setDescription(payParam.getDescription()); + + // 支付方式和状态 + List payTypeInfos = buildPayTypeInfo(payParam.getPayModeList()); + List refundableInfos = buildRefundableInfo(payParam.getPayModeList()); + // 计算总价 + BigDecimal sumAmount = payTypeInfos.stream() + .map(PayChannelInfo::getAmount) + .filter(Objects::nonNull) + .reduce(BigDecimal::add) + .orElse(BigDecimal.ZERO); + // 支付通道信息 + payment.setPayChannelInfo(payTypeInfos) + .setRefundableInfo(refundableInfos) + .setPayStatus(PayStatusCode.TRADE_PROGRESS) + .setAmount(sumAmount) + .setClientIp(ip) + .setRefundableBalance(sumAmount); + return payment; + } + + /** + * 构建PayTypeInfo + */ + private List buildPayTypeInfo(List payModeParamList) { + return CollectionUtil.isEmpty(payModeParamList) ? Collections.emptyList() + : payModeParamList.stream().map(PayModeParam::toPayTypeInfo).collect(Collectors.toList()); + } + + /** + * 构建RefundableInfo + */ + private List buildRefundableInfo(List payModeParamList) { + return CollectionUtil.isEmpty(payModeParamList) ? Collections.emptyList() + : payModeParamList.stream().map(PayModeParam::toRefundableInfo).collect(Collectors.toList()); + } + + /** + * 根据Payment构建PayParam支付参数 + */ + public PayParam buildPayParamByPayment(Payment payment) { + PayParam payParam = new PayParam(); + // 恢复 payModeList + List payModeParams = payment.getPayChannelInfo() + .stream() + .map(payTypeInfo -> new PayModeParam().setAmount(payTypeInfo.getAmount()) + .setPayChannel(payTypeInfo.getPayChannel()) + .setExtraParamsJson(payTypeInfo.getExtraParamsJson())) + .collect(Collectors.toList()); + payParam.setPayModeList(payModeParams) + .setBusinessId(payment.getBusinessId()) + .setUserId(payment.getUserId()) + .setTitle(payment.getTitle()) + .setTitle(payment.getTitle()) + .setDescription(payment.getDescription()); + return payParam; + } + + /** + * 根据Payment构建PaymentResult + * @param payment payment + * @return paymentVO + */ + public PayResult buildResultByPayment(Payment payment) { + PayResult paymentResult; + try { + paymentResult = new PayResult(); + // 异步支付信息 + paymentResult.setAsyncPayChannel(payment.getAsyncPayChannel()) + .setAsyncPayMode(payment.isAsyncPayMode()) + .setPayStatus(payment.getPayStatus()); + + List channelInfos = payment.getPayChannelInfo(); + + // 设置异步支付参数 + List moneyPayTypeInfos = channelInfos.stream() + .filter(payTypeInfo -> PayChannelCode.ASYNC_TYPE.contains(payTypeInfo.getPayChannel())) + .collect(Collectors.toList()); + if (!CollUtil.isEmpty(moneyPayTypeInfos)) { + paymentResult.setAsyncPayInfo(AsyncPayInfoLocal.get()); + } + // 清空线程变量 + } + finally { + AsyncPayInfoLocal.clear(); + } + return paymentResult; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/convert/PayConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/convert/PayConvert.java new file mode 100644 index 000000000..97854dd94 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/convert/PayConvert.java @@ -0,0 +1,21 @@ +package cn.bootx.daxpay.core.pay.convert; + +import cn.bootx.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.daxpay.param.pay.PayModeParam; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 支付对象转换 + * + * @author xxm + * @date 2022/3/2 + */ +@Mapper +public interface PayConvert { + + PayConvert CONVERT = Mappers.getMapper(PayConvert.class); + + PayChannelInfo convert(PayModeParam in); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/exception/BaseException.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/exception/BaseException.java new file mode 100644 index 000000000..068c695af --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/exception/BaseException.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.core.pay.exception; + +/** + * 支付异常信息 + * + * @author xxm + * @date 2020/12/9 + */ +public interface BaseException { + + /** + * 获取错误信息 + */ + ExceptionInfo getExceptionInfo(); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/exception/ExceptionInfo.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/exception/ExceptionInfo.java new file mode 100644 index 000000000..33a5f40f4 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/exception/ExceptionInfo.java @@ -0,0 +1,27 @@ +package cn.bootx.daxpay.core.pay.exception; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @date 2021/5/24 + */ +@Data +@Accessors(chain = true) +@Schema(title = "异常信息") +public class ExceptionInfo { + + /** 错误码 */ + private int errorCode; + + /** 错误信息 */ + private String errorMsg; + + public ExceptionInfo(int errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/factory/PayStrategyFactory.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/factory/PayStrategyFactory.java new file mode 100644 index 000000000..22ffcb71e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/factory/PayStrategyFactory.java @@ -0,0 +1,120 @@ +package cn.bootx.daxpay.core.pay.factory; + +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.strategy.*; +import cn.bootx.daxpay.exception.payment.PayUnsupportedMethodException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.extra.spring.SpringUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 支付策略工厂 + * + * @author xxm + * @date 2020/12/11 + */ +public class PayStrategyFactory { + + /** + * 根据传入的支付通道创建策略 + * @param payModeParam 支付类型 + * @return 支付策略 + */ + public static AbsPayStrategy create(PayModeParam payModeParam) { + + AbsPayStrategy strategy = null; + switch (payModeParam.getPayChannel()) { + case PayChannelCode.ALI: + strategy = SpringUtil.getBean(AliPayStrategy.class); + break; + case PayChannelCode.WECHAT: + strategy = SpringUtil.getBean(WeChatPayStrategy.class); + break; + case PayChannelCode.UNION_PAY: + strategy = SpringUtil.getBean(UnionPayStrategy.class); + break; + case PayChannelCode.CASH: + strategy = SpringUtil.getBean(CashPayStrategy.class); + break; + case PayChannelCode.WALLET: + strategy = SpringUtil.getBean(WalletPayStrategy.class); + break; + case PayChannelCode.VOUCHER: + strategy = SpringUtil.getBean(VoucherStrategy.class); + break; + case PayChannelCode.CREDIT_CARD: + break; + case PayChannelCode.APPLE_PAY: + break; + case PayChannelCode.CHANNEL_PAY: + break; + default: + throw new PayUnsupportedMethodException(); + } + // noinspection ConstantConditions + strategy.setPayMode(payModeParam); + return strategy; + } + + /** + * 根据传入的支付类型批量创建策略, 异步支付在后面 + */ + public static List createDesc(List payModeParamList) { + return create(payModeParamList, true); + } + + /** + * 根据传入的支付类型批量创建策略, 默认异步支付在前面 + */ + public static List create(List payModeParamList) { + return create(payModeParamList, false); + } + + /** + * 根据传入的支付类型批量创建策略 + * @param payModeParamList 支付类型 + * @return 支付策略 + */ + private static List create(List payModeParamList, boolean description) { + if (CollectionUtil.isEmpty(payModeParamList)) { + return Collections.emptyList(); + } + List list = new ArrayList<>(payModeParamList.size()); + + // 同步支付 + List syncPayModeParamList = payModeParamList.stream() + .filter(Objects::nonNull) + .filter(payModeParam -> !PayChannelCode.ASYNC_TYPE.contains(payModeParam.getPayChannel())) + .collect(Collectors.toList()); + + // 异步支付 + List asyncPayModeParamList = payModeParamList.stream() + .filter(Objects::nonNull) + .filter(payModeParam -> PayChannelCode.ASYNC_TYPE.contains(payModeParam.getPayChannel())) + .collect(Collectors.toList()); + + List sortList = new ArrayList<>(payModeParamList.size()); + + // 异步在后面 + if (description) { + sortList.addAll(syncPayModeParamList); + sortList.addAll(asyncPayModeParamList); + } + else { + sortList.addAll(asyncPayModeParamList); + sortList.addAll(syncPayModeParamList); + } + + // 此处有一个根据Type的反转排序, + sortList.stream().filter(Objects::nonNull).forEach(payMode -> list.add(create(payMode))); + return list; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/AbsPayCallbackStrategy.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/AbsPayCallbackStrategy.java new file mode 100644 index 000000000..f3e4b8344 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/AbsPayCallbackStrategy.java @@ -0,0 +1,111 @@ +package cn.bootx.daxpay.core.pay.func; + +import cn.bootx.common.redis.RedisClient; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.notify.dao.PayNotifyRecordManager; +import cn.bootx.daxpay.core.notify.entity.PayNotifyRecord; +import cn.bootx.daxpay.core.pay.result.PayCallbackResult; +import cn.bootx.daxpay.core.pay.service.PayCallbackService; +import cn.hutool.json.JSONUtil; +import com.alibaba.ttl.TransmittableThreadLocal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 支付回调处理抽象接口 + * + * @author xxm + * @date 2021/6/21 + */ +@Slf4j +@RequiredArgsConstructor +public abstract class AbsPayCallbackStrategy { + + protected static final ThreadLocal> PARAMS = new TransmittableThreadLocal<>(); + + private final RedisClient redisClient; + + private final PayNotifyRecordManager payNotifyRecordManager; + + private final PayCallbackService payCallbackService; + + /** + * 支付回调 + */ + public String payCallback(Map params) { + PARAMS.set(params); + try { + log.info("支付回调处理: {}", params); + // 验证消息 + if (!this.verifyNotify()) { + return null; + } + // 去重处理 + if (!this.duplicateChecker()) { + return this.getReturnMsg(); + } + // 调用统一回调处理 + PayCallbackResult result = payCallbackService.callback(this.getPaymentId(), this.getTradeStatus(), params); + // 记录回调记录 + this.saveNotifyRecord(result); + } + finally { + PARAMS.remove(); + } + return this.getReturnMsg(); + } + + /** + * 支付类型 + * @see PayChannelCode + */ + public abstract int getPayChannel(); + + /** + * 去重处理 + */ + public boolean duplicateChecker() { + // 判断10秒内是否已经回调处理 + String key = "payment:callback:duplicate:" + this.getPaymentId(); + return redisClient.setIfAbsent(key, "", 10 * 1000); + } + + /** + * 验证信息格式 + */ + public abstract boolean verifyNotify(); + + /** + * 获取paymentId + */ + public abstract Long getPaymentId(); + + /** + * 获取支付状态 + * @see PayStatusCode + */ + public abstract int getTradeStatus(); + + /** + * 返回响应结果 + */ + public abstract String getReturnMsg(); + + /** + * 保存回调记录 + */ + public void saveNotifyRecord(PayCallbackResult result) { + PayNotifyRecord payNotifyRecord = new PayNotifyRecord().setNotifyInfo(JSONUtil.toJsonStr(PARAMS.get())) + .setNotifyTime(LocalDateTime.now()) + .setPaymentId(this.getPaymentId()) + .setPayChannel(this.getPayChannel()) + .setStatus(result.getCode()) + .setMsg(result.getMsg()); + payNotifyRecordManager.save(payNotifyRecord); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/AbsPayStrategy.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/AbsPayStrategy.java new file mode 100644 index 000000000..a0defaa91 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/AbsPayStrategy.java @@ -0,0 +1,109 @@ +package cn.bootx.daxpay.core.pay.func; + +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PaySyncStatus; +import cn.bootx.daxpay.core.pay.exception.ExceptionInfo; +import cn.bootx.daxpay.core.pay.result.PaySyncResult; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import lombok.Getter; +import lombok.Setter; + +import java.util.Map; + +/** + * 抽象支付策略基类 同步支付 异步支付 错误处理 关闭支付 撤销支付 支付网关同步 退款 + * + * @author xxm + * @date 2020/12/11 + */ +@Getter +@Setter +public abstract class AbsPayStrategy { + + /** 支付对象 */ + private Payment payment = null; + + /** 支付参数 */ + private PayParam payParam = null; + + /** 支付方式参数 支付参数中的与这个不一致, 以这个为准 */ + private PayModeParam payMode = null; + + /** + * 策略标示 + * @see PayChannelCode + */ + public abstract int getType(); + + /** + * 初始化支付的参数 + */ + public void initPayParam(Payment payment, PayParam payParam) { + this.payment = payment; + this.payParam = payParam; + } + + /** + * 支付前对处理 包含必要的校验以及对Payment对象的创建和保存操作 + */ + public void doBeforePayHandler() { + } + + /** + * 支付操作 + */ + public abstract void doPayHandler(); + + /** + * 支付成功的处理方式 + */ + public void doSuccessHandler() { + } + + /** + * 支付失败的处理方式 + */ + public void doErrorHandler(ExceptionInfo exceptionInfo) { + } + + /** + * 异步支付成功的处理方式 + */ + public void doAsyncSuccessHandler(Map map) { + } + + /** + * 异步支付失败的处理方式, 默认使用支付失败的处理方式 同步支付方式调用时同 this#doErrorHandler + */ + public void doAsyncErrorHandler(ExceptionInfo exceptionInfo) { + this.doErrorHandler(exceptionInfo); + } + + /** + * 撤销支付操作,支付交易返回失败或支付系统超时,调用该接口撤销交易 默认为关闭本地支付记录 + */ + public void doCancelHandler() { + this.doCloseHandler(); + } + + /** + * 关闭本地支付记录 + */ + public abstract void doCloseHandler(); + + /** + * 退款 + */ + public abstract void doRefundHandler(); + + /** + * 异步支付单与支付网关进行状态比对 + * @see PaySyncStatus + */ + public PaySyncResult doSyncPayStatusHandler() { + return new PaySyncResult(); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/PayStrategyConsumer.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/PayStrategyConsumer.java new file mode 100644 index 000000000..1af86c891 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/func/PayStrategyConsumer.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.core.pay.func; + +import cn.bootx.daxpay.core.payment.entity.Payment; + +import java.util.List; + +/** + * 支付策略接口 + * + * @author xxm + * @date 2020/12/9 + */ +@FunctionalInterface +public interface PayStrategyConsumer, S extends Payment> { + + void accept(T t, S s); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/local/AsyncPayInfoLocal.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/local/AsyncPayInfoLocal.java new file mode 100644 index 000000000..7bb16e3ee --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/local/AsyncPayInfoLocal.java @@ -0,0 +1,37 @@ +package cn.bootx.daxpay.core.pay.local; + +import cn.bootx.daxpay.dto.pay.AsyncPayInfo; +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 异步支付线程变量 + * + * @author xxm + * @date 2021/4/21 + */ +public final class AsyncPayInfoLocal { + + private static final ThreadLocal THREAD_LOCAL = new TransmittableThreadLocal<>(); + + /** + * 设置 + */ + public static void set(AsyncPayInfo asyncPayInfo) { + THREAD_LOCAL.set(asyncPayInfo); + } + + /** + * 获取 + */ + public static AsyncPayInfo get() { + return THREAD_LOCAL.get(); + } + + /** + * 清除 + */ + public static void clear() { + THREAD_LOCAL.remove(); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/local/AsyncRefundLocal.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/local/AsyncRefundLocal.java new file mode 100644 index 000000000..0a86b4fa8 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/local/AsyncRefundLocal.java @@ -0,0 +1,70 @@ +package cn.bootx.daxpay.core.pay.local; + +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 异步退款线程变量 + * + * @author xxm + * @date 2022/3/9 + */ +public final class AsyncRefundLocal { + + private static final ThreadLocal THREAD_LOCAL = new TransmittableThreadLocal<>(); + + private static final ThreadLocal ERROR_MSG = new TransmittableThreadLocal<>(); + + private static final ThreadLocal ERROR_CODE = new TransmittableThreadLocal<>(); + + /** + * 设置 退款号 + */ + public static void set(String refundId) { + THREAD_LOCAL.set(refundId); + } + + /** + * 获取 退款号 + */ + public static String get() { + return THREAD_LOCAL.get(); + } + + /** + * 设置 错误内容 + */ + public static void setErrorMsg(String errorMsg) { + ERROR_MSG.set(errorMsg); + } + + /** + * 获取 错误内容 + */ + public static String getErrorMsg() { + return ERROR_MSG.get(); + } + + /** + * 设置 错误码 + */ + public static void setErrorCode(String errorCode) { + ERROR_CODE.set(errorCode); + } + + /** + * 获取 错误码 + */ + public static String getErrorCode() { + return ERROR_CODE.get(); + } + + /** + * 清除 + */ + public static void clear() { + THREAD_LOCAL.remove(); + ERROR_MSG.remove(); + ERROR_CODE.remove(); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/result/PayCallbackResult.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/result/PayCallbackResult.java new file mode 100644 index 000000000..2648e7d2a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/result/PayCallbackResult.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.core.pay.result; + +import cn.bootx.daxpay.code.pay.PayStatusCode; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付回调处理结果 + * + * @author xxm + * @date 2021/6/22 + */ +@Data +@Accessors(chain = true) +public class PayCallbackResult { + + /** + * 处理状态 + * @see PayStatusCode#NOTIFY_PROCESS_SUCCESS + */ + private int code; + + /** 提示信息 */ + private String msg; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/result/PaySyncResult.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/result/PaySyncResult.java new file mode 100644 index 000000000..39ba0c0f6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/result/PaySyncResult.java @@ -0,0 +1,28 @@ +package cn.bootx.daxpay.core.pay.result; + +import cn.bootx.daxpay.code.pay.PaySyncStatus; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.Map; + +/** + * 支付网关通知状态对象 + * + * @author xxm + * @date 2021/4/21 + */ +@Data +@Accessors(chain = true) +public class PaySyncResult { + + /** + * 支付网关同步状态 + * @see PaySyncStatus + */ + private int paySyncStatus = -1; + + /** 网关返回参数 */ + private Map map; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayCallbackService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayCallbackService.java new file mode 100644 index 000000000..e0b7544f8 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayCallbackService.java @@ -0,0 +1,224 @@ +package cn.bootx.daxpay.core.pay.service; + +import cn.bootx.common.core.exception.ErrorCodeRuntimeException; +import cn.bootx.common.core.util.LocalDateTimeUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.pay.builder.PayEventBuilder; +import cn.bootx.daxpay.core.pay.builder.PaymentBuilder; +import cn.bootx.daxpay.core.pay.exception.BaseException; +import cn.bootx.daxpay.core.pay.exception.ExceptionInfo; +import cn.bootx.daxpay.core.pay.factory.PayStrategyFactory; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.func.PayStrategyConsumer; +import cn.bootx.daxpay.core.pay.result.PayCallbackResult; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.mq.PaymentEventSender; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.hutool.core.collection.CollectionUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 支付回调处理 + * + * @author xxm + * @date 2021/2/27 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayCallbackService { + + private final PaymentService paymentService; + + private final PaymentEventSender eventSender; + + /** + * 统一回调处理 + * @see PayStatusCode + * @param tradeStatus 支付状态 + */ + public PayCallbackResult callback(Long paymentId, int tradeStatus, Map map) { + + // 获取payment和paymentParam数据 + Payment payment = paymentService.findById(paymentId).orElse(null); + + // 支付单不存在,记录回调记录 + if (Objects.isNull(payment)) { + return new PayCallbackResult().setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单不存在,记录回调记录"); + } + + // 回调时间超出了支付单超时时间, 记录一下, 不做处理 + if (Objects.nonNull(payment.getExpiredTime()) + && LocalDateTimeUtil.ge(LocalDateTime.now(), payment.getExpiredTime())) { + return new PayCallbackResult().setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("回调时间超出了支付单支付有效时间"); + } + + // 成功状态 + if (PayStatusCode.NOTIFY_TRADE_SUCCESS == tradeStatus) { + return this.success(payment, map); + } + else { + // 失败状态 + return this.fail(payment, map); + } + } + + /** + * 成功处理 + */ + private PayCallbackResult success(Payment payment, Map map) { + PayCallbackResult result = new PayCallbackResult().setCode(PayStatusCode.NOTIFY_PROCESS_SUCCESS); + + // payment已经被支付,不需要重复处理 + if (Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_SUCCESS)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_IGNORE).setMsg("支付单已经是支付成功状态,不进行处理"); + } + + // payment已被取消,记录回调记录 + if (!Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_PROGRESS)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单不是待支付状态,记录回调记录"); + } + + // 2.通过工厂生成对应的策略组 + PayParam payParam = PaymentBuilder.buildPayParamByPayment(payment); + + List paymentStrategyList = PayStrategyFactory.create(payParam.getPayModeList()); + if (CollectionUtil.isEmpty(paymentStrategyList)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单数据非法,未找到对应的支付方式"); + } + + // 3.初始化支付的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + // 4.处理方法, 支付时只有一种payModel(异步支付), 失败时payment的所有payModel都会生效 + boolean handlerFlag = this.doHandler(payment, paymentStrategyList, (strategyList, paymentObj) -> { + // 执行异步支付方式的成功回调(不会有同步payModel) + strategyList.forEach(absPaymentStrategy -> absPaymentStrategy.doAsyncSuccessHandler(map)); + + // 修改payment支付状态为成功 + paymentObj.setPayStatus(PayStatusCode.TRADE_SUCCESS); + paymentObj.setPayTime(LocalDateTime.now()); + paymentService.updateById(paymentObj); + }); + + if (handlerFlag) { + // 5. 发送成功事件 + eventSender.sendPayComplete(PayEventBuilder.buildPayComplete(payment)); + } + else { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("回调处理过程报错"); + } + return result; + } + + /** + * 失败处理, 关闭并退款 按说这块不会发生 + */ + private PayCallbackResult fail(Payment payment, Map map) { + PayCallbackResult result = new PayCallbackResult().setCode(PayStatusCode.NOTIFY_PROCESS_SUCCESS); + + // payment已被取消,记录回调记录,后期处理 + if (!Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_PROGRESS)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_IGNORE).setMsg("支付单已经取消,记录回调记录"); + } + + // payment支付成功, 状态非法 + if (!Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_SUCCESS)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单状态非法,支付网关状态为失败,但支付单状态为已完成"); + } + + // 2.通过工厂生成对应的策略组 + PayParam payParam = PaymentBuilder.buildPayParamByPayment(payment); + List paymentStrategyList = PayStrategyFactory.create(payParam.getPayModeList()); + if (CollectionUtil.isEmpty(paymentStrategyList)) { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("支付单数据非法,未找到对应的支付方式"); + } + // 3.初始化支付关闭的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + // 4.处理方法, 支付时只有一种payModel(异步支付), 失败时payment的所有payModel都会生效 + boolean handlerFlag = this.doHandler(payment, paymentStrategyList, (strategyList, paymentObj) -> { + // 执行异步支付方式的成功回调(不会有同步payModel) + strategyList.forEach(AbsPayStrategy::doCancelHandler); + + // 修改payment支付状态为成功 + paymentObj.setPayStatus(PayStatusCode.TRADE_CANCEL); + paymentService.updateById(paymentObj); + }); + + if (handlerFlag) { + // 5. 发送退款事件 + eventSender.sendPayRefund(PayEventBuilder.buildPayRefund(payment)); + } + else { + return result.setCode(PayStatusCode.NOTIFY_PROCESS_FAIL).setMsg("回调处理过程报错"); + } + + return result; + } + + /** + * 处理方法 + * @param payment 支付记录 + * @param strategyList 支付策略 + * @param successCallback 成功操作 + */ + private boolean doHandler(Payment payment, List strategyList, + PayStrategyConsumer, Payment> successCallback) { + + try { + // 1.获取异步支付方式,通过工厂生成对应的策略组 + List syncPaymentStrategyList = strategyList.stream() + .filter(paymentStrategy -> PayChannelCode.ASYNC_TYPE.contains(paymentStrategy.getType())) + .collect(Collectors.toList()); + // 执行成功方法 + successCallback.accept(syncPaymentStrategyList, payment); + } + catch (Exception e) { + // error事件的处理 + this.asyncErrorHandler(payment, strategyList, e); + return false; + } + return true; + } + + /** + * 对Error的处理 + */ + private void asyncErrorHandler(Payment payment, List strategyList, Exception e) { + + // 默认的错误信息 + ExceptionInfo exceptionInfo = new ExceptionInfo(PayStatusCode.TRADE_FAIL, e.getMessage()); + if (e instanceof BaseException) { + exceptionInfo = ((BaseException) e).getExceptionInfo(); + } + else if (e instanceof ErrorCodeRuntimeException) { + ErrorCodeRuntimeException ex = (ErrorCodeRuntimeException) e; + exceptionInfo = new ExceptionInfo(ex.getCode(), ex.getMessage()); + } + + // 更新Payment的状态 + payment.setErrorCode(String.valueOf(exceptionInfo.getErrorCode())); + payment.setErrorMsg(String.valueOf(exceptionInfo.getErrorMsg())); + payment.setPayStatus(PayStatusCode.TRADE_FAIL); + paymentService.updateById(payment); + + // 调用失败处理 + for (AbsPayStrategy paymentStrategy : strategyList) { + paymentStrategy.doAsyncErrorHandler(exceptionInfo); + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayCancelService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayCancelService.java new file mode 100644 index 000000000..2cd8ef781 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayCancelService.java @@ -0,0 +1,121 @@ +package cn.bootx.daxpay.core.pay.service; + +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.pay.builder.PayEventBuilder; +import cn.bootx.daxpay.core.pay.builder.PaymentBuilder; +import cn.bootx.daxpay.core.pay.factory.PayStrategyFactory; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.func.PayStrategyConsumer; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.exception.payment.PayNotExistedException; +import cn.bootx.daxpay.exception.payment.PayUnsupportedMethodException; +import cn.bootx.daxpay.mq.PaymentEventSender; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.hutool.core.collection.CollectionUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; + +import static cn.bootx.daxpay.code.pay.PayStatusCode.*; + +/** + * 取消订单处理 + * + * @author xxm + * @date 2021/3/2 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayCancelService { + + private final PaymentService paymentService; + + private final PaymentEventSender paymentEventSender; + + /** + * 根据业务id取消支付记录 + */ + @Transactional(rollbackFor = Exception.class) + public void cancelByBusinessId(String businessId) { + Payment payment = paymentService.findByBusinessId(businessId) + .orElseThrow(() -> new PayFailureException("未找到支付单")); + this.cancelPayment(payment); + } + + /** + * 根据paymentId取消支付记录 + */ + @Transactional(rollbackFor = Exception.class) + public void cancelByPaymentId(Long paymentId) { + Payment payment = paymentService.findById(paymentId).orElseThrow(() -> new PayFailureException("未找到支付单")); + this.cancelPayment(payment); + } + + /** + * 取消支付记录 + */ + private void cancelPayment(Payment payment) { + // 状态检查, 成功/退款/退款中 不处理 + List trades = Arrays.asList(TRADE_SUCCESS, TRADE_REFUNDING, TRADE_REFUNDED); + if (trades.contains(payment.getPayStatus())) { + throw new PayFailureException("支付已完成, 无法撤销"); + } + + // 获取 paymentParam + PayParam payParam = PaymentBuilder.buildPayParamByPayment(payment); + + // 1.获取支付方式,通过工厂生成对应的策略组 + List paymentStrategyList = PayStrategyFactory.create(payParam.getPayModeList()); + if (CollectionUtil.isEmpty(paymentStrategyList)) { + throw new PayUnsupportedMethodException(); + } + + // 2.初始化支付的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + + // 3.执行取消订单 + this.doHandler(payment, paymentStrategyList, (strategyList, paymentObj) -> { + // 发起取消进行的执行方法 + strategyList.forEach(AbsPayStrategy::doCancelHandler); + // 取消订单 + paymentObj.setPayStatus(PayStatusCode.TRADE_CANCEL); + paymentService.updateById(paymentObj); + }); + + // 4. 获取支付记录信息 + payment = paymentService.findById(payment.getId()).orElseThrow(PayNotExistedException::new); + + // 5. 发布撤销事件 + paymentEventSender.sendPayCancel(PayEventBuilder.buildPayCancel(payment)); + } + + /** + * 处理方法 + * @param payment 支付记录 + * @param strategyList 支付策略 + * @param successCallback 成功操作 + */ + private void doHandler(Payment payment, List strategyList, + PayStrategyConsumer, Payment> successCallback) { + + try { + // 执行 + successCallback.accept(strategyList, payment); + } + catch (Exception e) { + // error事件的处理 + log.warn("取消订单失败"); + throw e; + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayExpiredTimeService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayExpiredTimeService.java new file mode 100644 index 000000000..012c51951 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayExpiredTimeService.java @@ -0,0 +1,183 @@ +package cn.bootx.daxpay.core.pay.service; + +import cn.bootx.daxpay.code.pay.PaySyncStatus; +import cn.bootx.daxpay.core.pay.builder.PayEventBuilder; +import cn.bootx.daxpay.core.pay.builder.PaymentBuilder; +import cn.bootx.daxpay.core.pay.factory.PayStrategyFactory; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.result.PaySyncResult; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.exception.payment.PayUnsupportedMethodException; +import cn.bootx.daxpay.mq.PaymentEventSender; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.bootx.daxpay.util.PayModelUtil; +import cn.hutool.core.collection.CollUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static cn.bootx.daxpay.code.pay.PayStatusCode.*; + +/** + * 支付超时处理 + * + * @author xxm + * @date 2022/7/13 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayExpiredTimeService { + + private final PaymentService paymentService; + + private final PaymentEventSender eventSender; + + /** + * 支付单超时支付单处理 + */ + @Async("bigExecutor") + @Transactional(rollbackFor = Exception.class) + public void expiredTime(Long paymentId) { + + Payment payment = paymentService.findById(paymentId).orElseThrow(() -> new PayFailureException("支付单未找到")); + // 只处理支付中 + if (!Objects.equals(payment.getPayStatus(), TRADE_PROGRESS)) { + return; + } + // 获取支付网关状态 + PayParam payParam = PaymentBuilder.buildPayParamByPayment(payment); + // 1.获取支付方式,通过工厂生成对应的策略组 + List paymentStrategyList = PayStrategyFactory.create(payParam.getPayModeList()); + if (CollUtil.isEmpty(paymentStrategyList)) { + throw new PayUnsupportedMethodException(); + } + + // 2.初始化支付的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + + // 3 拿到异步支付方法, 与支付网关进行同步 + PayModeParam asyncPayMode = PayModelUtil.getAsyncPayModeParam(payParam); + AbsPayStrategy syncPayStrategy = PayStrategyFactory.create(asyncPayMode); + syncPayStrategy.initPayParam(payment, payParam); + PaySyncResult paySyncResult = syncPayStrategy.doSyncPayStatusHandler(); + + // 4 对返回的支付网关各种状态进行处理 + int paySyncStatus = paySyncResult.getPaySyncStatus(); + switch (paySyncStatus) { + // 成功状态 + case PaySyncStatus.TRADE_SUCCESS: { + this.paySuccess(payment, syncPayStrategy, paySyncResult); + break; + } + // 待付款/ 支付中 + case PaySyncStatus.WAIT_BUYER_PAY: { + this.payCancel(payment, paymentStrategyList); + break; + } + // 超时关闭 和 网关没找到记录 + case PaySyncStatus.TRADE_CLOSED: + case PaySyncStatus.NOT_FOUND: { + this.payClose(payment, paymentStrategyList); + break; + } + // 交易退款 + case PaySyncStatus.TRADE_REFUND: { + log.info("交易退款不需要关闭: {}", payment.getId()); + break; + } + // 调用出错 进行重试 + case PaySyncStatus.FAIL: { + log.warn("支付状态同步接口调用出错"); + } + case PaySyncStatus.NOT_SYNC: + default: { + log.error("支付超时代码有问题"); + } + } + } + + /** + * 如果支付网关是支付中状态, 关闭网关支付, 然后再关闭本地支付单, 排除退款 + */ + private void payCancel(Payment payment, List payStrategies) { + try { + // 异常情况, 不继续进行处理 + if (!this.check(payment)) { + return; + } + // 撤销和关闭支付单 + payStrategies.forEach(AbsPayStrategy::doCancelHandler); + payment.setPayStatus(TRADE_CANCEL); + paymentService.updateById(payment); + // 发送事件 + eventSender.sendPayCancel(PayEventBuilder.buildPayCancel(payment)); + } + catch (Exception e) { + log.warn("支付状态同步后关闭支付单报错了", e); + } + } + + /** + * 如果支付网关是关闭或未找到, 关闭本地支付单 + */ + private void payClose(Payment payment, List absPayStrategies) { + try { + // 已关闭的不再进行关闭 + if (Objects.equals(payment.getPayStatus(), TRADE_CANCEL)) { + return; + } + // 退款状态则不进行更新 + if (Objects.equals(payment.getPayStatus(), TRADE_REFUNDED) + || Objects.equals(payment.getPayStatus(), TRADE_REFUNDING)) { + return; + } + // 异常情况, 不继续进行处理 + if (!this.check(payment)) { + return; + } + // 关闭支付单 + absPayStrategies.forEach(AbsPayStrategy::doCloseHandler); + payment.setPayStatus(TRADE_CANCEL); + paymentService.updateById(payment); + // 发送事件 + eventSender.sendPayCancel(PayEventBuilder.buildPayCancel(payment)); + } + catch (Exception e) { + log.warn("支付状态同步后关闭支付单报错了", e); + } + } + + /** + * 如果是支付网关是支付完成状态, 记录为异常支付单 + */ + private void paySuccess(Payment payment, AbsPayStrategy syncPayStrategy, PaySyncResult paySyncResult) { + // 修改payment支付状态为成功 + log.error("支付网关支付单已被支付, 需要线下处理: {}", payment.getId()); + } + + /** + * 校验状态, 处理在发起撤销与正式处理之间订单被完成的情况. 理论上不会发生 + */ + private boolean check(Payment payment) { + // 支付失败/撤销/退款不需要处理 + List trades = Arrays.asList(TRADE_FAIL, TRADE_CANCEL, TRADE_REFUNDING, TRADE_REFUNDED); + if (trades.contains(payment.getPayStatus())) { + log.info("订单在超时撤销期间发生了操作, 需要人工介入处理"); + return false; + } + return true; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayRefundService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayRefundService.java new file mode 100644 index 000000000..997674e6d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayRefundService.java @@ -0,0 +1,251 @@ +package cn.bootx.daxpay.core.pay.service; + +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.common.spring.util.WebServletUtil; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.pay.builder.PaymentBuilder; +import cn.bootx.daxpay.core.pay.factory.PayStrategyFactory; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.func.PayStrategyConsumer; +import cn.bootx.daxpay.core.pay.local.AsyncRefundLocal; +import cn.bootx.daxpay.core.payment.dao.PaymentManager; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.core.refund.dao.RefundRecordManager; +import cn.bootx.daxpay.core.refund.entity.RefundRecord; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import cn.bootx.daxpay.exception.payment.PayAmountAbnormalException; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.exception.payment.PayUnsupportedMethodException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.bootx.daxpay.param.refund.RefundModeParam; +import cn.bootx.daxpay.param.refund.RefundParam; +import cn.bootx.starter.auth.util.SecurityUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.extra.spring.SpringUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.bootx.daxpay.code.pay.PayStatusCode.*; + +/** + * 支付退款 + * + * @author xxm + * @date 2022/2/26 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayRefundService { + + private final PaymentService paymentService; + + private final PaymentManager paymentManager; + + private final RefundRecordManager refundRecordManager; + + /** + * 退款 + */ + @Transactional(rollbackFor = Exception.class) + public void refund(RefundParam refundParam) { + Payment payment = paymentService.findByBusinessId(refundParam.getBusinessId()) + .orElseThrow(() -> new PayFailureException("未找到支付单")); + + this.refundPayment(payment, refundParam.getRefundModeParams()); + } + + /** + * 根据业务id取消支付记录 + */ + @Transactional(rollbackFor = Exception.class) + public void refundByBusinessId(String businessId) { + Payment payment = paymentService.findByBusinessId(businessId) + .orElseThrow(() -> new PayFailureException("未找到支付单")); + List refundModeParams = payment.getRefundableInfo() + .stream() + .map(o -> new RefundModeParam().setPayChannel(o.getPayChannel()).setAmount(o.getAmount())) + .collect(Collectors.toList()); + this.refundPayment(payment, refundModeParams); + + } + + /** + * 退款 + */ + private void refundPayment(Payment payment, List refundModeParams) { + // 状态判断, 支付中/失败/撤销不处理 + List trades = Arrays.asList(TRADE_PROGRESS, TRADE_CANCEL, TRADE_FAIL); + if (trades.contains(payment.getPayStatus())) { + throw new PayFailureException("状态非法, 无法退款"); + } + + // 过滤退款金额为0的支付通道参数 + refundModeParams + .removeIf(refundModeParam -> BigDecimalUtil.compareTo(refundModeParam.getAmount(), BigDecimal.ZERO) == 0); + // 获取 paymentParam + PayParam payParam = PaymentBuilder.buildPayParamByPayment(payment); + // 退款参数检查 + this.payModeCheck(refundModeParams, payment.getRefundableInfo()); + + // 1.获取退款参数方式,通过工厂生成对应的策略组 + List payModeParams = refundModeParams.stream() + .map(RefundModeParam::toPayModeParam) + .collect(Collectors.toList()); + List paymentStrategyList = PayStrategyFactory.create(payModeParams); + if (CollectionUtil.isEmpty(paymentStrategyList)) { + throw new PayUnsupportedMethodException(); + } + + // 2.初始化支付的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + + // 3.执行退款 + this.doHandler(payment, paymentStrategyList, (strategyList, paymentObj) -> { + // 发起支付成功进行的执行方法 + try { + strategyList.forEach(AbsPayStrategy::doRefundHandler); + } + catch (Exception e) { + // 记录退款失败的记录 + BigDecimal amount = refundModeParams.stream() + .map(RefundModeParam::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + SpringUtil.getBean(this.getClass()).saveRefund(payment, amount, refundModeParams); + throw e; + } + // 处理支付单 + this.paymentHandler(paymentObj, refundModeParams); + }); + } + + /** + * 支付单处理 + */ + private void paymentHandler(Payment payment, List refundModeParams) { + BigDecimal amount = refundModeParams.stream() + .map(RefundModeParam::getAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + // 剩余可退款余额 + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + + // 退款完成 + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) < 1) { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + + payment.setRefundableBalance(refundableBalance); + paymentManager.updateById(payment); + // 记录退款成功的记录 + SpringUtil.getBean(this.getClass()).saveRefund(payment, amount, refundModeParams); + } + + /** + * 处理方法 + * @param payment 支付记录 + * @param strategyList 支付策略 + * @param successCallback 成功操作 + */ + private void doHandler(Payment payment, List strategyList, + PayStrategyConsumer, Payment> successCallback) { + + try { + // 执行 + successCallback.accept(strategyList, payment); + } + catch (Exception e) { + log.warn("退款失败"); + throw e; + } + finally { + // 清除 + AsyncRefundLocal.clear(); + } + } + + /** + * 支付方式检查 + * @param refundModeParams 退款参数 + * @param refundableInfos 可退款信息 + */ + private void payModeCheck(List refundModeParams, List refundableInfos) { + if (CollUtil.isEmpty(refundModeParams)) { + throw new PayFailureException("传入的退款参数不合法"); + } + Map payModeMap = refundableInfos.stream() + .collect(Collectors.toMap(RefundableInfo::getPayChannel, Function.identity())); + for (RefundModeParam refundPayMode : refundModeParams) { + this.payModeCheck(refundPayMode, payModeMap.get(refundPayMode.getPayChannel())); + } + } + + /** + * 支付方式检查 + * @param refundModeParam 退款参数 + * @param refundableInfo 可退款对象 + */ + public void payModeCheck(RefundModeParam refundModeParam, RefundableInfo refundableInfo) { + if (Objects.isNull(refundableInfo)) { + throw new PayFailureException("退款参数非法"); + } + // 退款金额为负数的 + if (BigDecimalUtil.compareTo(refundModeParam.getAmount(), BigDecimal.ZERO) < 1) { + throw new PayAmountAbnormalException(); + } + // 退款金额大于可退款金额 + if (BigDecimalUtil.compareTo(refundModeParam.getAmount(), refundableInfo.getAmount()) == 1) { + throw new PayAmountAbnormalException("退款金额大于可退款金额"); + } + + } + + /** + * 保存退款记录 成不成功都记录 + */ + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void saveRefund(Payment payment, BigDecimal amount, List refundModeParams) { + List refundableInfos = refundModeParams.stream() + .map(RefundModeParam::toRefundableInfo) + .collect(Collectors.toList()); + HttpServletRequest request = WebServletUtil.getRequest(); + String ip = ServletUtil.getClientIP(request); + RefundRecord refundRecord = new RefundRecord().setRefundRequestNo(AsyncRefundLocal.get()) + .setRefundableInfo(refundableInfos) + .setAmount(amount) + .setRefundableBalance(payment.getRefundableBalance()) + .setClientIp(ip) + .setPaymentId(payment.getId()) + .setBusinessId(payment.getBusinessId()) + .setUserId(SecurityUtil.getUserIdOrDefaultId()) + .setRefundTime(LocalDateTime.now()) + .setTitle(payment.getTitle()) + .setErrorMsg(AsyncRefundLocal.getErrorMsg()) + .setErrorCode(AsyncRefundLocal.getErrorCode()) + .setRefundStatus(Objects.isNull(AsyncRefundLocal.getErrorCode()) ? PayStatusCode.REFUND_PROCESS_SUCCESS + : PayStatusCode.REFUND_PROCESS_FAIL); + refundRecordManager.save(refundRecord); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayService.java new file mode 100644 index 000000000..8ba6c3caf --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PayService.java @@ -0,0 +1,251 @@ +package cn.bootx.daxpay.core.pay.service; + +import cn.bootx.common.core.util.LocalDateTimeUtil; +import cn.bootx.common.core.util.ValidationUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.core.pay.builder.PayEventBuilder; +import cn.bootx.daxpay.core.pay.builder.PaymentBuilder; +import cn.bootx.daxpay.core.pay.factory.PayStrategyFactory; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.func.PayStrategyConsumer; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.dto.pay.PayResult; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.exception.payment.PayNotExistedException; +import cn.bootx.daxpay.exception.payment.PayUnsupportedMethodException; +import cn.bootx.daxpay.mq.PaymentEventSender; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.bootx.daxpay.util.PayModelUtil; +import cn.hutool.core.collection.CollectionUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.function.Consumer; + +import static cn.bootx.daxpay.code.pay.PayStatusCode.*; + +/** + * 支付流程 + * + * @author xxm + * @date 2020/12/9 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayService { + + private final PaymentService paymentService; + + private final PaymentEventSender eventSender; + + /** + * 支付方法(同步/异步/组合支付) 同步支付:都只会在第一次执行中就完成支付,例如钱包、积分都是调用完就进行了扣减,完成了支付记录 + * 异步支付:例如支付宝、微信,发起支付后还需要跳转第三方平台进行支付,支付后通常需要进行回调,之后才完成支付记录 + * 组合支付:主要是混合了同步支付和异步支付,同时异步支付只能有一个,在支付时先对同步支付进行扣减,然后异步支付回调结束后完成整个支付单 + * 组合支付在非第一次支付的时候,只对新传入的异步支付PayMode进行处理,PayMode的价格使用第一次发起的价格,旧的同步支付如果传入后也不做处理, + * Payment中PayModeList将会为 旧有的同步支付+新传入的异步支付方式(在具体支付实现中处理) + */ + @Transactional(rollbackFor = Exception.class) + public PayResult pay(PayParam payParam) { + ValidationUtil.validateParam(payParam); + // 异步支付方法检查 + PayModelUtil.validationAsyncPayMode(payParam); + + // 获取并校验支付状态 + Payment payment = this.getAndCheckPaymentByBusinessId(payParam.getBusinessId()); + + // 异步支付且非第一次支付 + if (Objects.nonNull(payment) && payment.isAsyncPayMode()) { + return this.paySyncNotFirst(payParam, payment); + } + else { + // 首次支付或同步支付 + return this.payFirst(payParam, payment); + } + } + + /** + * 发起的第一次支付请求(同步/异步) + */ + private PayResult payFirst(PayParam payParam, Payment payment) { + // 0. 支付成功直接返回 + if (Objects.nonNull(payment)) { + return PaymentBuilder.buildResultByPayment(payment); + } + + // 1. 价格检测 + PayModelUtil.validationAmount(payParam.getPayModeList()); + + // 2. 创建支付记录 + payment = this.createPayment(payParam); + + // 3. 调用支付方法进行发起支付 + this.payFirstMethod(payParam, payment); + + // 4. 获取支付记录信息 + payment = paymentService.findById(payment.getId()).orElseThrow(PayNotExistedException::new); + + // 5. 返回支付结果 + PayResult payResult = PaymentBuilder.buildResultByPayment(payment); + + // 如果是支付成功, 发送事件 + if (Objects.equals(payResult.getPayStatus(), TRADE_SUCCESS)) { + eventSender.sendPayComplete(PayEventBuilder.buildPayComplete(payment)); + } + return payResult; + } + + /** + * 执行支付方法 (第一次支付) + */ + private void payFirstMethod(PayParam payParam, Payment payment) { + + // 1.获取支付方式,通过工厂生成对应的策略组 + List paymentStrategyList = PayStrategyFactory.create(payParam.getPayModeList()); + if (CollectionUtil.isEmpty(paymentStrategyList)) { + throw new PayUnsupportedMethodException(); + } + + // 2.初始化支付的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + + // 3.支付前准备 + this.doHandler(payment, paymentStrategyList, AbsPayStrategy::doBeforePayHandler, null); + + // 4.支付 + this.doHandler(payment, paymentStrategyList, AbsPayStrategy::doPayHandler, (strategyList, paymentObj) -> { + // 发起支付成功进行的执行方法 + strategyList.forEach(AbsPayStrategy::doSuccessHandler); + // 所有支付方式都是同步时进行Payment处理 + if (PayModelUtil.isNotSync(payParam.getPayModeList())) { + // 修改payment支付状态为成功 + paymentObj.setPayStatus(TRADE_SUCCESS); + paymentObj.setPayTime(LocalDateTime.now()); + } + paymentService.updateById(paymentObj); + }); + } + + /** + * 异步支付执行(非第一次请求), 只执行异步支付策略, 报错不影响继续发起支付 + */ + private PayResult paySyncNotFirst(PayParam payParam, Payment payment) { + + // 0. 处理支付完成情况(完成/退款) + List trades = Arrays.asList(TRADE_SUCCESS, TRADE_CANCEL, TRADE_REFUNDING, TRADE_REFUNDED); + if (trades.contains(payment.getPayStatus())) { + return PaymentBuilder.buildResultByPayment(payment); + } + + // 1.获取 异步支付 通道,通过工厂生成对应的策略组 + PayParam oldPayParam = PaymentBuilder.buildPayParamByPayment(payment); + PayModeParam payModeParam = this.getAsyncPayModeParam(payParam, oldPayParam); + List paymentStrategyList = PayStrategyFactory.create(Collections.singletonList(payModeParam)); + + // 2.初始化支付的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + // 3.支付前准备 + this.doHandler(payment, paymentStrategyList, AbsPayStrategy::doBeforePayHandler, null); + + // 4. 发起支付 + this.doHandler(payment, paymentStrategyList, AbsPayStrategy::doPayHandler, (strategyList, paymentObj) -> { + // 发起支付成功进行的执行方法 + strategyList.forEach(AbsPayStrategy::doSuccessHandler); + paymentService.updateById(paymentObj); + }); + + // 5. 获取支付记录信息 + payment = paymentService.findById(payment.getId()).orElseThrow(PayNotExistedException::new); + + // 6. 组装返回参数 + return PaymentBuilder.buildResultByPayment(payment); + } + + /** + * 执行策略中不同的handler + * @param payment 主支付对象 + * @param strategyList 策略列表 + * @param payMethod 执行支付/支付前的函数 + * @param successMethod 执行成功的函数 + */ + private void doHandler(Payment payment, List strategyList, Consumer payMethod, + PayStrategyConsumer, Payment> successMethod) { + // 执行策略操作,如支付前/支付时 + // 等同strategyList.forEach(payMethod.accept(PaymentStrategy)) + strategyList.forEach(payMethod); + + // 执行操作成功的处理 + Optional.ofNullable(successMethod).ifPresent(fun -> fun.accept(strategyList, payment)); + } + + /** + * 获取异步支付参数 + */ + private PayModeParam getAsyncPayModeParam(PayParam payParam, PayParam oldPaymentParam) { + + List oldPayModes = oldPaymentParam.getPayModeList(); + // 旧的异步支付方式 + PayModeParam oldModeParam = oldPayModes.stream() + .filter(payMode -> PayChannelCode.ASYNC_TYPE.contains(payMode.getPayChannel())) + .findFirst() + .orElseThrow(() -> new PayFailureException("支付方式数据异常")); + + // 新的异步支付方式 + PayModeParam payModeParam = payParam.getPayModeList() + .stream() + .filter(payMode -> PayChannelCode.ASYNC_TYPE.contains(payMode.getPayChannel())) + .findFirst() + .orElseThrow(() -> new PayFailureException("支付方式数据异常")); + payModeParam.setAmount(oldModeParam.getAmount()); + + return payModeParam; + } + + /** + * 创建支付记录 + */ + private Payment createPayment(PayParam payParam) { + // 构建payment记录 并保存 + Payment payment = PaymentBuilder.buildPayment(payParam); + return paymentService.save(payment); + } + + /** + * 校验支付状态,支付成功则返回,支付失败则抛出对应的异常 + */ + private Payment getAndCheckPaymentByBusinessId(String businessId) { + // 根据订单查询支付记录 + Payment payment = paymentService.findByBusinessId(businessId).orElse(null); + if (Objects.nonNull(payment)) { + // 支付失败 + List trades = Arrays.asList(TRADE_FAIL, TRADE_CANCEL); + if (trades.contains(payment.getPayStatus())) { + throw new PayFailureException("支付失败或已经被撤销"); + } + // 退款状态 + trades = Arrays.asList(TRADE_REFUNDING, TRADE_REFUNDED); + if (trades.contains(payment.getPayStatus())) { + throw new PayFailureException("支付失败或已经被撤销"); + } + // 支付超时 + if (Objects.nonNull(payment.getExpiredTime()) + && LocalDateTimeUtil.ge(LocalDateTime.now(), payment.getExpiredTime())) { + throw new PayFailureException("支付已超时"); + } + return payment; + } + return null; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PaySyncService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PaySyncService.java new file mode 100644 index 000000000..5249a8246 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/service/PaySyncService.java @@ -0,0 +1,183 @@ +package cn.bootx.daxpay.core.pay.service; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.pay.PaySyncStatus; +import cn.bootx.daxpay.core.pay.builder.PayEventBuilder; +import cn.bootx.daxpay.core.pay.builder.PaymentBuilder; +import cn.bootx.daxpay.core.pay.factory.PayStrategyFactory; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.result.PaySyncResult; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.exception.payment.PayUnsupportedMethodException; +import cn.bootx.daxpay.mq.PaymentEventSender; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.bootx.daxpay.util.PayModelUtil; +import cn.hutool.core.collection.CollUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +import static cn.bootx.daxpay.code.pay.PayStatusCode.*; + +/** + * 未完成的异步支付单与支付网关进行对比 + * + * @author xxm + * @date 2021/4/21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PaySyncService { + + private final PaymentService paymentService; + + private final PaymentEventSender eventSender; + + /** + * 同步订单的支付状态 + */ + public void syncByPaymentId(Long id) { + Payment payment = paymentService.findById(id).orElse(null); + if (Objects.isNull(payment)) { + return; + } + this.syncPayment(payment); + } + + /** + * 同步订单的支付状态 + */ + public void syncByBusinessId(String businessId) { + Payment payment = paymentService.findByBusinessId(businessId).orElse(null); + if (Objects.isNull(payment)) { + return; + } + this.syncPayment(payment); + } + + /** + * 同步支付状态 传入 payment 对象 + */ + public void syncPayment(Payment payment) { + PayParam payParam = PaymentBuilder.buildPayParamByPayment(payment); + // 1.获取支付方式,通过工厂生成对应的策略组 + List paymentStrategyList = PayStrategyFactory.create(payParam.getPayModeList()); + if (CollUtil.isEmpty(paymentStrategyList)) { + throw new PayUnsupportedMethodException(); + } + + // 2.初始化支付的参数 + for (AbsPayStrategy paymentStrategy : paymentStrategyList) { + paymentStrategy.initPayParam(payment, payParam); + } + + // 3 拿到异步支付方法, 与支付网关进行同步 + PayModeParam asyncPayMode = PayModelUtil.getAsyncPayModeParam(payParam); + AbsPayStrategy syncPayStrategy = PayStrategyFactory.create(asyncPayMode); + syncPayStrategy.initPayParam(payment, payParam); + PaySyncResult paySyncResult = syncPayStrategy.doSyncPayStatusHandler(); + int paySyncStatus = paySyncResult.getPaySyncStatus(); + + switch (paySyncStatus) { + // 支付成功 支付宝退款时也是支付成功状态, 除非支付完成 + case PaySyncStatus.TRADE_SUCCESS: { + this.paymentSuccess(payment, syncPayStrategy, paySyncResult); + break; + } + // 待付款/ 支付中 + case PaySyncStatus.WAIT_BUYER_PAY: { + log.info("依然是付款状态"); + break; + } + // 订单已经关闭超时关闭 和 网关没找到记录, 支付宝退款完成也是这个状态 + case PaySyncStatus.TRADE_CLOSED: + case PaySyncStatus.NOT_FOUND: { + // 判断下是否超时, 同时payment 变更为取消支付 + this.paymentCancel(payment, paymentStrategyList); + break; + } + // 交易退款 支付宝没这个状态 + case PaySyncStatus.TRADE_REFUND: { + this.paymentRefund(payment, syncPayStrategy, paySyncResult); + break; + } + // 调用出错 + case PaySyncStatus.FAIL: { + // 不进行处理 + log.warn("支付状态同步接口调用出错"); + break; + } + case PaySyncStatus.NOT_SYNC: + default: { + throw new BizException("代码有问题"); + } + } + } + + /** + * payment 变更为已支付 + */ + private void paymentSuccess(Payment payment, AbsPayStrategy syncPayStrategy, PaySyncResult paySyncResult) { + + // 已支付不在重复处理 + if (Objects.equals(payment.getPayStatus(), TRADE_SUCCESS)) { + return; + } + // 退款的不处理 + if (Objects.equals(payment.getPayStatus(), TRADE_REFUNDED) + || Objects.equals(payment.getPayStatus(), TRADE_REFUNDING)) { + return; + } + // 修改payment支付状态为成功 + syncPayStrategy.doAsyncSuccessHandler(paySyncResult.getMap()); + payment.setPayStatus(TRADE_SUCCESS); + payment.setPayTime(LocalDateTime.now()); + paymentService.updateById(payment); + + // 发送成功事件 + eventSender.sendPayComplete(PayEventBuilder.buildPayComplete(payment)); + } + + /** + * payment 变更为取消支付 + */ + private void paymentCancel(Payment payment, List absPayStrategies) { + try { + // 已关闭的不再进行关闭 + if (Objects.equals(payment.getPayStatus(), TRADE_CANCEL)) { + return; + } + // 修改payment支付状态为取消, 退款状态则不进行更新 + if (Objects.equals(payment.getPayStatus(), TRADE_REFUNDED) + || Objects.equals(payment.getPayStatus(), TRADE_REFUNDING)) { + return; + } + payment.setPayStatus(TRADE_CANCEL); + // 执行策略的关闭方法 + absPayStrategies.forEach(AbsPayStrategy::doCloseHandler); + paymentService.updateById(payment); + // 发送事件 + eventSender.sendPayCancel(PayEventBuilder.buildPayCancel(payment)); + } + catch (Exception e) { + log.warn("支付状态同步后关闭支付单报错了", e); + throw new PayFailureException("支付状态同步后关闭支付单报错了"); + } + } + + /** + * payment 退款处理 TODO 需要考虑退款详情的合并处理 + */ + private void paymentRefund(Payment payment, AbsPayStrategy syncPayStrategy, PaySyncResult paySyncResult) { + + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/AliPayStrategy.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/AliPayStrategy.java new file mode 100644 index 000000000..2d18a2362 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/AliPayStrategy.java @@ -0,0 +1,233 @@ +package cn.bootx.daxpay.core.pay.strategy; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import cn.bootx.daxpay.code.paymodel.AliPayCode; +import cn.bootx.daxpay.core.pay.exception.ExceptionInfo; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.result.PaySyncResult; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.core.paymodel.alipay.dao.AlipayConfigManager; +import cn.bootx.daxpay.core.paymodel.alipay.entity.AlipayConfig; +import cn.bootx.daxpay.core.paymodel.alipay.service.AliPayCancelService; +import cn.bootx.daxpay.core.paymodel.alipay.service.AliPayService; +import cn.bootx.daxpay.core.paymodel.alipay.service.AliPaymentService; +import cn.bootx.daxpay.core.paymodel.alipay.service.AlipaySyncService; +import cn.bootx.daxpay.exception.payment.PayAmountAbnormalException; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.paymodel.alipay.AliPayParam; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONUtil; +import com.ijpay.alipay.AliPayApiConfig; +import com.ijpay.alipay.AliPayApiConfigKit; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.Objects; + +import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; + +/** + * 支付宝支付 + * + * @author xxm + * @date 2021/2/27 + */ +@Scope(SCOPE_PROTOTYPE) +@Component +@RequiredArgsConstructor +public class AliPayStrategy extends AbsPayStrategy { + + private final AliPaymentService aliPaymentService; + + private final AlipaySyncService alipaySyncService; + + private final AliPayService aliPayService; + + private final AliPayCancelService aliPayCancelService; + + private final AlipayConfigManager alipayConfigManager; + + private final PaymentService paymentService; + + private AlipayConfig alipayConfig; + + private AliPayParam aliPayParam; + + @Override + public int getType() { + return PayChannelCode.ALI; + } + + /** + * 支付前操作 + */ + @Override + public void doBeforePayHandler() { + try { + // 支付宝参数验证 + String extraParamsJson = this.getPayMode().getExtraParamsJson(); + if (StrUtil.isNotBlank(extraParamsJson)) { + this.aliPayParam = JSONUtil.toBean(extraParamsJson, AliPayParam.class); + } + else { + this.aliPayParam = new AliPayParam(); + } + } + catch (JSONException e) { + throw new PayFailureException("支付参数错误"); + } + // 检查金额 + PayModeParam payMode = this.getPayMode(); + if (BigDecimalUtil.compareTo(payMode.getAmount(), BigDecimal.ZERO) < 1) { + throw new PayAmountAbnormalException(); + } + // 检查并获取支付宝支付配置 + this.initAlipayConfig(); + aliPayService.validation(this.getPayMode(), alipayConfig); + // 如果没有显式传入同步回调地址, 使用默认配置 + if (StrUtil.isBlank(aliPayParam.getReturnUrl())) { + aliPayParam.setReturnUrl(alipayConfig.getReturnUrl()); + } + this.initAlipayConfig(); + } + + /** + * 发起支付操作 + */ + @Override + public void doPayHandler() { + aliPayService.pay(this.getPayMode().getAmount(), this.getPayment(), this.aliPayParam, this.getPayMode(), + this.alipayConfig); + } + + /** + * 支付调起成功 + */ + @Override + public void doSuccessHandler() { + aliPaymentService.updatePaySuccess(this.getPayment(), this.getPayMode()); + } + + /** + * 发起支付失败 + */ + @Override + public void doErrorHandler(ExceptionInfo exceptionInfo) { + this.doCloseHandler(); + } + + /** + * 异步支付成功 + */ + @Override + public void doAsyncSuccessHandler(Map map) { + String tradeNo = map.get(AliPayCode.TRADE_NO); + aliPaymentService.updateAsyncSuccess(this.getPayment().getId(), this.getPayMode(), tradeNo); + } + + /** + * 异步支付失败 + */ + @Override + public void doAsyncErrorHandler(ExceptionInfo exceptionInfo) { + // 调用撤销支付 + this.doCancelHandler(); + } + + /** + * 撤销支付 + */ + @Override + public void doCancelHandler() { + this.initAlipayConfig(); + // 撤销支付 + aliPayCancelService.cancelRemote(this.getPayment()); + // 调用关闭本地支付记录 + this.doCloseHandler(); + } + + /** + * 关闭本地支付记录 + */ + @Override + public void doCloseHandler() { + aliPaymentService.updateClose(this.getPayment().getId()); + } + + /** + * 退款 + */ + @Override + public void doRefundHandler() { + this.initAlipayConfig(); + aliPayCancelService.refund(this.getPayment(), this.getPayMode().getAmount()); + aliPaymentService.updatePayRefund(this.getPayment().getId(), this.getPayMode().getAmount()); + paymentService.updateRefundSuccess(this.getPayment(), this.getPayMode().getAmount(), PayChannelEnum.ALI); + } + + /** + * 异步支付单与支付网关进行状态比对 + */ + @Override + public PaySyncResult doSyncPayStatusHandler() { + this.initAlipayConfig(); + return alipaySyncService.syncPayStatus(this.getPayment()); + } + + /** + * 初始化支付宝配置信息 + */ + private void initAlipayConfig() { + // 检查并获取支付宝支付配置 + this.alipayConfig = alipayConfigManager.findActivity().orElseThrow(() -> new PayFailureException("支付配置不存在")); + this.initApiConfig(this.alipayConfig); + } + + /** + * 初始化IJPay 服务 + */ + @SneakyThrows + private void initApiConfig(AlipayConfig alipayConfig) { + + AliPayApiConfig aliPayApiConfig; + // 公钥 + if (Objects.equals(alipayConfig.getAuthType(), AliPayCode.AUTH_TYPE_KEY)) { + aliPayApiConfig = AliPayApiConfig.builder() + .setAppId(alipayConfig.getAppId()) + .setPrivateKey(alipayConfig.getPrivateKey()) + .setAliPayPublicKey(alipayConfig.getAlipayPublicKey()) + .setCharset(CharsetUtil.UTF_8) + .setServiceUrl(alipayConfig.getServerUrl()) + .setSignType(alipayConfig.getSignType()) + .build(); + } + // 证书 + else if (Objects.equals(alipayConfig.getAuthType(), AliPayCode.AUTH_TYPE_CART)) { + aliPayApiConfig = AliPayApiConfig.builder() + .setAppId(alipayConfig.getAppId()) + .setPrivateKey(alipayConfig.getPrivateKey()) + .setAppCertContent(alipayConfig.getAppCert()) + .setAliPayCertContent(alipayConfig.getAlipayCert()) + .setAliPayRootCertContent(alipayConfig.getAlipayRootCert()) + .setCharset(CharsetUtil.UTF_8) + .setServiceUrl(alipayConfig.getServerUrl()) + .setSignType(alipayConfig.getSignType()) + .buildByCertContent(); + } + else { + throw new BizException("支付宝认证方式不可为空"); + } + AliPayApiConfigKit.setThreadLocalAliPayApiConfig(aliPayApiConfig); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/CashPayStrategy.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/CashPayStrategy.java new file mode 100644 index 000000000..db61cbfab --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/CashPayStrategy.java @@ -0,0 +1,81 @@ +package cn.bootx.daxpay.core.pay.strategy; + +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.core.paymodel.cash.service.CashService; +import cn.bootx.daxpay.exception.payment.PayAmountAbnormalException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; + +/** + * 现金支付 + * + * @author xxm + * @date 2021/6/23 + */ +@Slf4j +@Scope(SCOPE_PROTOTYPE) +@Service +@RequiredArgsConstructor +public class CashPayStrategy extends AbsPayStrategy { + + private final CashService cashService; + + private final PaymentService paymentService; + + /** + * 现金支付 + */ + @Override + public int getType() { + return PayChannelCode.CASH; + } + + /** + * 支付前检查 + */ + @Override + public void doBeforePayHandler() { + // 检查金额 + PayModeParam payMode = this.getPayMode(); + if (BigDecimalUtil.compareTo(payMode.getAmount(), BigDecimal.ZERO) < 1) { + throw new PayAmountAbnormalException(); + } + } + + /** + * 支付操作 + */ + @Override + public void doPayHandler() { + cashService.pay(this.getPayMode(), this.getPayment(), this.getPayParam()); + } + + /** + * 关闭本地支付记录 + */ + @Override + public void doCloseHandler() { + cashService.close(this.getPayment().getId()); + } + + /** + * 退款 + */ + @Override + public void doRefundHandler() { + cashService.refund(this.getPayment().getId(), this.getPayMode().getAmount()); + paymentService.updateRefundSuccess(this.getPayment(), this.getPayMode().getAmount(), PayChannelEnum.CASH); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/UnionPayStrategy.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/UnionPayStrategy.java new file mode 100644 index 000000000..72c122d37 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/UnionPayStrategy.java @@ -0,0 +1,50 @@ +package cn.bootx.daxpay.core.pay.strategy; + +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.core.pay.exception.ExceptionInfo; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; + +import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; + +/** + * 云闪付 + * + * @author xxm + * @date 2022/3/8 + */ +@Slf4j +@Scope(SCOPE_PROTOTYPE) +@Service +@RequiredArgsConstructor +public class UnionPayStrategy extends AbsPayStrategy { + + @Override + public int getType() { + return PayChannelCode.UNION_PAY; + } + + @Override + public void doPayHandler() { + + } + + @Override + public void doErrorHandler(ExceptionInfo exceptionInfo) { + + } + + @Override + public void doCloseHandler() { + + } + + @Override + public void doRefundHandler() { + + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/VoucherStrategy.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/VoucherStrategy.java new file mode 100644 index 000000000..86933ef4a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/VoucherStrategy.java @@ -0,0 +1,89 @@ +package cn.bootx.daxpay.core.pay.strategy; + +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.core.paymodel.voucher.entity.Voucher; +import cn.bootx.daxpay.core.paymodel.voucher.service.VoucherPayService; +import cn.bootx.daxpay.core.paymodel.voucher.service.VoucherPaymentService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; + +/** + * 储值卡支付 + * + * @author xxm + * @date 2022/3/13 + */ +@Slf4j +@Scope(SCOPE_PROTOTYPE) +@Service +@RequiredArgsConstructor +public class VoucherStrategy extends AbsPayStrategy { + + private final VoucherPayService voucherPayService; + + private final VoucherPaymentService voucherPaymentService; + + private final PaymentService paymentService; + + private List vouchers; + + @Override + public int getType() { + return PayChannelCode.VOUCHER; + } + + /** + * 支付前处理 + */ + @Override + public void doBeforePayHandler() { + // 获取并校验余额 + this.vouchers = voucherPayService.getAndCheckVoucher(this.getPayMode()); + } + + /** + * 支付操作 + */ + @Override + public void doPayHandler() { + voucherPayService.pay(getPayMode().getAmount(), this.getPayment(), this.vouchers); + voucherPaymentService.savePayment(getPayment(), getPayParam(), getPayMode(), vouchers); + } + + /** + * 成功 + */ + @Override + public void doSuccessHandler() { + voucherPaymentService.updateSuccess(this.getPayment().getId()); + } + + /** + * 关闭支付 + */ + @Override + public void doCloseHandler() { + voucherPayService.close(this.getPayment().getId()); + voucherPaymentService.updateClose(this.getPayment().getId()); + } + + /** + * 退款 + */ + @Override + public void doRefundHandler() { + voucherPayService.refund(this.getPayment().getId(), this.getPayMode().getAmount()); + voucherPaymentService.updateRefund(this.getPayment().getId(), this.getPayMode().getAmount()); + paymentService.updateRefundSuccess(this.getPayment(), this.getPayMode().getAmount(), PayChannelEnum.VOUCHER); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/WalletPayStrategy.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/WalletPayStrategy.java new file mode 100644 index 000000000..64f6aaab5 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/WalletPayStrategy.java @@ -0,0 +1,96 @@ +package cn.bootx.daxpay.core.pay.strategy; + +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.core.paymodel.wallet.entity.Wallet; +import cn.bootx.daxpay.core.paymodel.wallet.service.WalletPayService; +import cn.bootx.daxpay.core.paymodel.wallet.service.WalletPaymentService; +import cn.bootx.daxpay.core.paymodel.wallet.service.WalletService; +import cn.bootx.daxpay.exception.waller.WalletLackOfBalanceException; +import cn.bootx.daxpay.param.pay.PayParam; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; + +/** + * 钱包支付策略 + * + * @author xxm + * @date 2020/12/11 + */ +@Scope(SCOPE_PROTOTYPE) +@Component +@RequiredArgsConstructor +public class WalletPayStrategy extends AbsPayStrategy { + + private final WalletPaymentService walletPaymentService; + + private final WalletPayService walletPayService; + + private final WalletService walletService; + + private final PaymentService paymentService; + + private Wallet wallet; + + @Override + public int getType() { + return PayChannelCode.WALLET; + } + + /** + * 支付前处理 + */ + @Override + public void doBeforePayHandler() { + PayParam payParam = this.getPayParam(); + // 获取并校验钱包 + this.wallet = walletService.getNormalWalletByUserId(payParam.getUserId()); + // 判断余额 + if (BigDecimalUtil.compareTo(this.wallet.getBalance(), getPayMode().getAmount()) < 0) { + throw new WalletLackOfBalanceException(); + } + } + + /** + * 支付操作 + */ + @Override + public void doPayHandler() { + walletPayService.pay(getPayMode().getAmount(), this.getPayment(), this.wallet); + walletPaymentService.savePayment(this.getPayment(), this.getPayParam(), this.getPayMode(), this.wallet); + } + + /** + * 成功 + */ + @Override + public void doSuccessHandler() { + walletPaymentService.updateSuccess(this.getPayment().getId()); + } + + /** + * 取消支付并返还金额 + */ + @Override + public void doCloseHandler() { + walletPayService.close(this.getPayment().getId()); + walletPaymentService.updateClose(this.getPayment().getId()); + } + + /** + * 退款 + */ + @Override + public void doRefundHandler() { + walletPayService.refund(this.getPayment().getId(), this.getPayMode().getAmount()); + walletPaymentService.updateRefund(this.getPayment().getId(), this.getPayMode().getAmount()); + paymentService.updateRefundSuccess(this.getPayment(), this.getPayMode().getAmount(), PayChannelEnum.WALLET); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/WeChatPayStrategy.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/WeChatPayStrategy.java new file mode 100644 index 000000000..cfb3edcf3 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/pay/strategy/WeChatPayStrategy.java @@ -0,0 +1,199 @@ +package cn.bootx.daxpay.core.pay.strategy; + +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.daxpay.core.pay.exception.ExceptionInfo; +import cn.bootx.daxpay.core.pay.func.AbsPayStrategy; +import cn.bootx.daxpay.core.pay.result.PaySyncResult; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.core.paymodel.wechat.dao.WeChatPayConfigManager; +import cn.bootx.daxpay.core.paymodel.wechat.dao.WeChatPaymentManager; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayment; +import cn.bootx.daxpay.core.paymodel.wechat.service.WeChatPayCancelService; +import cn.bootx.daxpay.core.paymodel.wechat.service.WeChatPayService; +import cn.bootx.daxpay.core.paymodel.wechat.service.WeChatPaySyncService; +import cn.bootx.daxpay.core.paymodel.wechat.service.WeChatPaymentService; +import cn.bootx.daxpay.exception.payment.PayAmountAbnormalException; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayParam; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.Map; +import java.util.Optional; + +import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE; + +/** + * 微信支付 + * + * @author xxm + * @date 2021/4/5 + */ +@Scope(SCOPE_PROTOTYPE) +@Component +@RequiredArgsConstructor +public class WeChatPayStrategy extends AbsPayStrategy { + + private final WeChatPayConfigManager weChatPayConfigManager; + + private final WeChatPayService weChatPayService; + + private final WeChatPaymentService weChatPaymentService; + + private final WeChatPaymentManager weChatPaymentManager; + + private final WeChatPayCancelService weChatPayCancelService; + + private final WeChatPaySyncService weChatPaySyncService; + + private final PaymentService paymentService; + + private WeChatPayConfig weChatPayConfig; + + private WeChatPayParam weChatPayParam; + + /** + * 类型 + */ + @Override + public int getType() { + return PayChannelCode.WECHAT; + } + + /** + * 支付前操作 + */ + @Override + public void doBeforePayHandler() { + try { + // 微信参数验证 + String extraParamsJson = this.getPayMode().getExtraParamsJson(); + if (StrUtil.isNotBlank(extraParamsJson)) { + this.weChatPayParam = JSONUtil.toBean(extraParamsJson, WeChatPayParam.class); + } + else { + this.weChatPayParam = new WeChatPayParam(); + } + } + catch (JSONException e) { + throw new PayFailureException("支付参数错误"); + } + + // 检查金额 + PayModeParam payMode = this.getPayMode(); + if (BigDecimalUtil.compareTo(payMode.getAmount(), BigDecimal.ZERO) < 1) { + throw new PayAmountAbnormalException(); + } + + // 检查并获取微信支付配置 + this.initWeChatPayConfig(); + weChatPayService.validation(this.getPayMode(), weChatPayConfig); + } + + /** + * 发起支付 + */ + @Override + public void doPayHandler() { + weChatPayService.pay(this.getPayMode().getAmount(), this.getPayment(), this.weChatPayParam, this.getPayMode(), + this.weChatPayConfig); + } + + /** + * 支付调起成功 + */ + @Override + public void doSuccessHandler() { + weChatPaymentService.updatePaySuccess(this.getPayment(), this.getPayMode()); + } + + /** + * 错误处理 + */ + @Override + public void doErrorHandler(ExceptionInfo exceptionInfo) { + this.doCloseHandler(); + } + + /** + * 异步支付成功 + */ + @Override + public void doAsyncSuccessHandler(Map map) { + String tradeNo = map.get(WeChatPayCode.TRANSACTION_ID); + weChatPaymentService.updateAsyncSuccess(this.getPayment().getId(), this.getPayMode(), tradeNo); + } + + /** + * 异步支付失败 + */ + @Override + public void doAsyncErrorHandler(ExceptionInfo exceptionInfo) { + // 调用撤销支付 + this.doCancelHandler(); + } + + /** + * 撤销支付 + */ + @Override + public void doCancelHandler() { + // 检查并获取微信支付配置 + this.initWeChatPayConfig(); + weChatPayCancelService.cancelRemote(this.getPayment(), weChatPayConfig); + // 调用关闭本地支付记录 + this.doCloseHandler(); + } + + /** + * 关闭本地支付记录 + */ + @Override + public void doCloseHandler() { + weChatPaymentService.updateClose(this.getPayment().getId()); + } + + /** + * 退款 + */ + @Override + public void doRefundHandler() { + this.initWeChatPayConfig(); + WeChatPayment weChatPayment = weChatPaymentManager.findByPaymentId(this.getPayment().getId()) + .orElseThrow(() -> new PayFailureException("微信支付记录不存在")); + weChatPayCancelService.refund(this.getPayment(), weChatPayment, this.getPayMode().getAmount(), + this.weChatPayConfig); + weChatPaymentService.updatePayRefund(weChatPayment, this.getPayMode().getAmount()); + paymentService.updateRefundSuccess(this.getPayment(), this.getPayMode().getAmount(), PayChannelEnum.WECHAT); + } + + /** + * 异步支付单与支付网关进行状态比对 + */ + @Override + public PaySyncResult doSyncPayStatusHandler() { + // 检查并获取微信支付配置 + this.initWeChatPayConfig(); + return weChatPaySyncService.syncPayStatus(this.getPayment().getId(), this.weChatPayConfig); + } + + /** + * 初始化微信支付 + */ + private void initWeChatPayConfig() { + // 检查并获取微信支付配置 + this.weChatPayConfig = Optional.ofNullable(this.weChatPayConfig) + .orElse(weChatPayConfigManager.findActivity().orElseThrow(() -> new PayFailureException("支付配置不存在"))); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/convert/PaymentConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/convert/PaymentConvert.java new file mode 100644 index 000000000..7eb9b951d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/convert/PaymentConvert.java @@ -0,0 +1,21 @@ +package cn.bootx.daxpay.core.payment.convert; + +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.dto.payment.PaymentDto; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 支付记录转换 + * + * @author xxm + * @date 2021/8/20 + */ +@Mapper +public interface PaymentConvert { + + PaymentConvert CONVERT = Mappers.getMapper(PaymentConvert.class); + + PaymentDto convert(Payment payment); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentExpiredTimeRepository.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentExpiredTimeRepository.java new file mode 100644 index 000000000..9a74d098b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentExpiredTimeRepository.java @@ -0,0 +1,52 @@ +package cn.bootx.daxpay.core.payment.dao; + +import cn.bootx.common.core.util.LocalDateTimeUtil; +import cn.bootx.common.redis.RedisClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * 支付单过去处理 + * + * @author xxm + * @date 2022/7/12 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class PaymentExpiredTimeRepository { + + private static final String KEY = "pay:payment:expiredtime"; + + private final RedisClient redisClient; + + /** + * 根据 token 存储对应的 ExpiredTokenKey + */ + public void store(Long paymentId, LocalDateTime expiredTime) { + long time = LocalDateTimeUtil.timestamp(expiredTime); + redisClient.zadd(KEY, String.valueOf(paymentId), time); + } + + /** + * 获取所有已过期的ExpiredTokenKey + */ + public Set retrieveExpiredKeys(LocalDateTime expiredTime) { + long time = LocalDateTimeUtil.timestamp(expiredTime); + return redisClient.zrangeByScore(KEY, 0L, time); + } + + /** + * 删除指定的ExpiredTokenKey + */ + public void removeKeys(String... keys) { + if (keys != null && keys.length > 0) { + redisClient.zremByMembers(KEY, keys); + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentManager.java new file mode 100644 index 000000000..2e3ac6392 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentManager.java @@ -0,0 +1,84 @@ +package cn.bootx.daxpay.core.payment.dao; + +import cn.bootx.common.core.rest.param.OrderParam; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.common.query.entity.QueryParams; +import cn.bootx.common.query.generator.QueryGenerator; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.param.payment.PaymentQuery; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.stereotype.Repository; + +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static cn.bootx.daxpay.code.CachingCode.PAYMENT_BUSINESS_ID; +import static cn.bootx.daxpay.code.CachingCode.PAYMENT_ID; + +@Repository +@RequiredArgsConstructor +public class PaymentManager extends BaseManager { + + @Override + @Caching(evict = { @CacheEvict(value = PAYMENT_ID, key = "#payment.id"), + @CacheEvict(value = PAYMENT_BUSINESS_ID, key = "#payment.businessId") }) + public Payment updateById(Payment payment) { + return super.updateById(payment); + } + + @Override + @Cacheable(value = { PAYMENT_ID }, key = "#id") + public Optional findById(Serializable id) { + return super.findById(id); + } + + /** + * 根据BusinessId查询 + */ + @Cacheable(value = { PAYMENT_BUSINESS_ID }, key = "#businessId") + public Optional findByBusinessId(String businessId) { + return findByField(Payment::getBusinessId, businessId); + } + + /** + * 根据用户查询 + */ + public List findByUserId(Long userId) { + return this.findAllByField(Payment::getUserId, userId); + } + + /** + * 分页查询 + */ + public Page page(PageParam pageParam, PaymentQuery param, OrderParam orderParam) { + Page mpPage = MpUtil.getMpPage(pageParam, Payment.class); + return query().select(Payment.class, MpUtil::excludeBigField) + .orderBy(Objects.nonNull(orderParam.getSortField()), orderParam.isAsc(), + StrUtil.toUnderlineCase(orderParam.getSortField())) + .like(Objects.nonNull(param.getPaymentId()), MpUtil.getColumnName(Payment::getId), param.getPaymentId()) + .like(Objects.nonNull(param.getBusinessId()), MpUtil.getColumnName(Payment::getBusinessId), + param.getBusinessId()) + .like(Objects.nonNull(param.getTitle()), MpUtil.getColumnName(Payment::getTitle), param.getTitle()) + .page(mpPage); + } + + /** + * 分页查询 + */ + public Page superPage(PageParam pageParam, QueryParams queryParams) { + QueryWrapper wrapper = QueryGenerator.generator(queryParams); + Page mpPage = MpUtil.getMpPage(pageParam, Payment.class); + return this.page(mpPage, wrapper); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentMapper.java new file mode 100644 index 000000000..e19406b01 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/dao/PaymentMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.core.payment.dao; + +import cn.bootx.daxpay.core.payment.entity.Payment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 支付记录 + * + * @author xxm + * @date 2021/7/27 + */ +@Mapper +public interface PaymentMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/entity/Payment.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/entity/Payment.java new file mode 100644 index 000000000..90f35dd2c --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/entity/Payment.java @@ -0,0 +1,102 @@ +package cn.bootx.daxpay.core.payment.entity; + +import cn.bootx.common.core.annotation.BigField; +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.common.mybatisplus.handler.JacksonRawTypeHandler; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.payment.convert.PaymentConvert; +import cn.bootx.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.daxpay.dto.payment.PaymentDto; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import lombok.experimental.FieldNameConstants; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 支付记录 + * + * @author xxm + * @date 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@FieldNameConstants +@Accessors(chain = true) +@TableName(value = "pay_payment", autoResultMap = true) +public class Payment extends MpBaseEntity implements EntityBaseFunction { + + /** 关联的业务id */ + private String businessId; + + /** 用户ID */ + private Long userId; + + /** 标题 */ + private String title; + + /** 描述 */ + private String description; + + /** 是否是异步支付 */ + private boolean asyncPayMode; + + /** 异步支付通道 */ + private Integer asyncPayChannel; + + /** 金额 */ + private BigDecimal amount; + + /** 可退款余额 */ + private BigDecimal refundableBalance; + + /** 错误码 */ + private String errorCode; + + /** 错误信息 */ + private String errorMsg; + + /** + * 支付通道信息列表 + * @see PayChannelInfo + */ + @TableField(typeHandler = JacksonRawTypeHandler.class) + @BigField + private List payChannelInfo; + + /** + * 退款信息列表 + * @see RefundableInfo + */ + @TableField(typeHandler = JacksonRawTypeHandler.class) + @BigField + private List refundableInfo; + + /** + * 支付状态 + * @see PayStatusCode#TRADE_PROGRESS + */ + private Integer payStatus; + + /** 支付时间 */ + private LocalDateTime payTime; + + /** 支付终端ip */ + private String clientIp; + + /** 过期时间 */ + private LocalDateTime expiredTime; + + @Override + public PaymentDto toDto() { + return PaymentConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/service/PaymentQueryService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/service/PaymentQueryService.java new file mode 100644 index 000000000..14e06245d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/service/PaymentQueryService.java @@ -0,0 +1,84 @@ +package cn.bootx.daxpay.core.payment.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.param.OrderParam; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.common.query.entity.QueryParams; +import cn.bootx.daxpay.core.payment.dao.PaymentManager; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.daxpay.dto.payment.PaymentDto; +import cn.bootx.daxpay.param.payment.PaymentQuery; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 支付单查询 + * + * @author xxm + * @date 2021/6/28 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PaymentQueryService { + + private final PaymentManager paymentManager; + + /** + * 根据支付Id查询支付单 + */ + public PaymentDto findById(Long id) { + return paymentManager.findById(id).map(Payment::toDto).orElseThrow(DataNotExistException::new); + } + + /** + * 根据业务ID获取支付状态 + */ + public Integer findStatusByBusinessId(String businessId) { + return paymentManager.findByBusinessId(businessId).map(Payment::getPayStatus).orElse(-1); + } + + /** + * 根据businessId获取订单支付方式 + */ + public List findPayTypeInfoByBusinessId(String businessId) { + return paymentManager.findByBusinessId(businessId).map(Payment::getPayChannelInfo).orElse(new ArrayList<>(1)); + } + + /** + * 根据id获取订单支付方式 + */ + public List findPayTypeInfoById(Long id) { + return paymentManager.findById(id).map(Payment::getPayChannelInfo).orElse(new ArrayList<>(1)); + } + + /** + * 根据用户id查询 + */ + public List findByUser(Long userId) { + return paymentManager.findByUserId(userId).stream().map(Payment::toDto).collect(Collectors.toList()); + } + + /** + * 分页 + */ + public PageResult page(PageParam pageParam, PaymentQuery param, OrderParam orderParam) { + return MpUtil.convert2DtoPageResult(paymentManager.page(pageParam, param, orderParam)); + } + + /** + * 超级查询 + */ + public PageResult superPage(PageParam pageParam, QueryParams queryParams) { + return MpUtil.convert2DtoPageResult(paymentManager.superPage(pageParam, queryParams)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/service/PaymentService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/service/PaymentService.java new file mode 100644 index 000000000..9840552d6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/payment/service/PaymentService.java @@ -0,0 +1,109 @@ +package cn.bootx.daxpay.core.payment.service; + +import cn.bootx.common.core.util.LocalDateTimeUtil; +import cn.bootx.common.spring.exception.RetryableException; +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import cn.bootx.daxpay.core.payment.dao.PaymentExpiredTimeRepository; +import cn.bootx.daxpay.core.payment.dao.PaymentManager; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static cn.bootx.daxpay.code.pay.PayStatusCode.TRADE_PROGRESS; + +/** + * 支付记录 + * + * @author xxm + * @date 2021/3/8 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PaymentService { + + private final PaymentManager paymentManager; + + private final PaymentExpiredTimeRepository expiredTimeRepository; + + /** + * 保存 + */ + public Payment save(Payment payment) { + return paymentManager.save(payment); + } + + /** + * 更新支付记录 + */ + public Payment updateById(Payment payment) { + // 超时注册 + this.registerExpiredTime(payment); + return paymentManager.updateById(payment); + } + + /** + * 根据id查询 + */ + public Optional findById(Serializable id) { + return paymentManager.findById(id); + } + + /** + * 根据BusinessId查询 + */ + public Optional findByBusinessId(String businessId) { + return paymentManager.findByBusinessId(businessId); + } + + /** + * 退款成功处理, 更新可退款信息 不进行持久化 + */ + public void updateRefundSuccess(Payment payment, BigDecimal amount, PayChannelEnum payChannelEnum) { + // 删除旧有的退款记录, 替换退款完的新的 + List refundableInfos = payment.getRefundableInfo(); + RefundableInfo refundableInfo = refundableInfos.stream() + .filter(o -> o.getPayChannel() == payChannelEnum.getNo()) + .findFirst() + .orElseThrow(() -> new PayFailureException("数据不存在")); + refundableInfos.remove(refundableInfo); + refundableInfo.setAmount(refundableInfo.getAmount().subtract(amount)); + refundableInfos.add(refundableInfo); + payment.setRefundableInfo(refundableInfos); + } + + /** + * 支付单超时关闭事件注册, 失败重试3次, 间隔一秒 + */ + @Async("bigExecutor") + @Retryable(value = RetryableException.class) + public void registerExpiredTime(Payment payment) { + LocalDateTime expiredTime = payment.getExpiredTime(); + // 支付中且有超时时间才会注册超时关闭时间 + if (Objects.equals(payment.getPayStatus(), TRADE_PROGRESS) && Objects.nonNull(expiredTime)) { + try { + // 将过期时间添加到redis中, 往后延时一分钟 + expiredTime = LocalDateTimeUtil.offset(expiredTime, 1, ChronoUnit.MINUTES); + expiredTimeRepository.store(payment.getId(), expiredTime); + } + catch (Exception e) { + log.error("注册支付单超时关闭失败"); + throw new RetryableException(); + } + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/convert/AlipayConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/convert/AlipayConvert.java new file mode 100644 index 000000000..fdc163566 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/convert/AlipayConvert.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.core.paymodel.alipay.convert; + +import cn.bootx.daxpay.core.paymodel.alipay.entity.AlipayConfig; +import cn.bootx.daxpay.dto.paymodel.alipay.AlipayConfigDto; +import cn.bootx.daxpay.param.paymodel.alipay.AlipayConfigParam; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 支付宝转换 + * + * @author xxm + * @date 2021/7/5 + */ +@Mapper +public interface AlipayConvert { + + AlipayConvert CONVERT = Mappers.getMapper(AlipayConvert.class); + + AlipayConfig convert(AlipayConfigDto in); + + AlipayConfig convert(AlipayConfigParam in); + + AlipayConfigDto convert(AlipayConfig in); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AliPaymentManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AliPaymentManager.java new file mode 100644 index 000000000..c7538464f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AliPaymentManager.java @@ -0,0 +1,25 @@ +package cn.bootx.daxpay.core.paymodel.alipay.dao; + +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.daxpay.core.paymodel.alipay.entity.AliPayment; +import cn.bootx.daxpay.core.paymodel.base.entity.BasePayment; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 支付宝 + * + * @author xxm + * @date 2021/2/26 + */ +@Repository +@RequiredArgsConstructor +public class AliPaymentManager extends BaseManager { + + public Optional findByPaymentId(Long paymentId) { + return this.findByField(BasePayment::getPaymentId, paymentId); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AliPaymentMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AliPaymentMapper.java new file mode 100644 index 000000000..40618118c --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AliPaymentMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.core.paymodel.alipay.dao; + +import cn.bootx.daxpay.core.paymodel.alipay.entity.AliPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 支付宝支付 + * + * @author xxm + * @date 2021/2/26 + */ +@Mapper +public interface AliPaymentMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AlipayConfigManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AlipayConfigManager.java new file mode 100644 index 000000000..9d180accd --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AlipayConfigManager.java @@ -0,0 +1,78 @@ +package cn.bootx.daxpay.core.paymodel.alipay.dao; + +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.paymodel.alipay.entity.AlipayConfig; +import cn.bootx.daxpay.param.paymodel.alipay.AlipayConfigQuery; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Objects; +import java.util.Optional; + +/** + * 支付宝配置 + * + * @author xxm + * @date 2021/2/26 + */ +@Repository +@RequiredArgsConstructor +public class AlipayConfigManager extends BaseManager { + + private Optional alipayConfig; + + @Override + public AlipayConfig saveOrUpdate(AlipayConfig entity) { + this.clearCache(); + return super.saveOrUpdate(entity); + } + + @Override + public AlipayConfig updateById(AlipayConfig alipayConfig) { + this.clearCache(); + return super.updateById(alipayConfig); + } + + /** + * 获取启用的支付宝配置 + */ + public Optional findActivity() { + if (Objects.isNull(alipayConfig)) { + alipayConfig = findByField(AlipayConfig::getActivity, Boolean.TRUE); + } + return alipayConfig; + } + + /** + * 分页 + */ + public Page page(PageParam pageParam, AlipayConfigQuery param) { + Page mpPage = MpUtil.getMpPage(pageParam, AlipayConfig.class); + return lambdaQuery().select(AlipayConfig.class, MpUtil::excludeBigField) + .like(StrUtil.isNotBlank(param.getName()), AlipayConfig::getName, param.getName()) + .like(StrUtil.isNotBlank(param.getAppId()), AlipayConfig::getAppId, param.getAppId()) + .page(mpPage); + } + + /** + * 清除所有启用的支付配置 + */ + public void removeAllActivity() { + this.clearCache(); + lambdaUpdate().eq(AlipayConfig::getActivity, Boolean.TRUE) + .set(AlipayConfig::getActivity, Boolean.FALSE) + .update(); + } + + /** + * 清除缓存 + */ + public void clearCache() { + alipayConfig = null; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AlipayConfigMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AlipayConfigMapper.java new file mode 100644 index 000000000..4b610d93a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/dao/AlipayConfigMapper.java @@ -0,0 +1,10 @@ +package cn.bootx.daxpay.core.paymodel.alipay.dao; + +import cn.bootx.daxpay.core.paymodel.alipay.entity.AlipayConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface AlipayConfigMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/entity/AliPayment.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/entity/AliPayment.java new file mode 100644 index 000000000..5d09e11ec --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/entity/AliPayment.java @@ -0,0 +1,34 @@ +package cn.bootx.daxpay.core.paymodel.alipay.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.daxpay.core.paymodel.base.entity.BasePayment; +import cn.bootx.daxpay.dto.paymodel.alipay.AliPaymentDto; +import cn.hutool.core.bean.BeanUtil; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 支付宝支付记录 + * + * @author xxm + * @date 2021/2/26 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_ali_payment") +public class AliPayment extends BasePayment implements EntityBaseFunction { + + /** 支付宝交易号 */ + private String tradeNo; + + @Override + public AliPaymentDto toDto() { + AliPaymentDto dto = new AliPaymentDto(); + BeanUtil.copyProperties(this, dto); + return dto; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/entity/AlipayConfig.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/entity/AlipayConfig.java new file mode 100644 index 000000000..1b0f4c69a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/entity/AlipayConfig.java @@ -0,0 +1,114 @@ +package cn.bootx.daxpay.core.paymodel.alipay.entity; + +import cn.bootx.common.core.annotation.BigField; +import cn.bootx.common.core.annotation.EncryptionField; +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.core.paymodel.alipay.convert.AlipayConvert; +import cn.bootx.daxpay.dto.paymodel.alipay.AlipayConfigDto; +import cn.bootx.daxpay.param.paymodel.alipay.AlipayConfigParam; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 支付宝支付配置 + * + * @author xxm + * @date 2020/12/15 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_alipay_config") +public class AlipayConfig extends MpBaseEntity implements EntityBaseFunction { + + /** 名称 */ + private String name; + + /** 支付宝商户appId */ + private String appId; + + /** 服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 */ + private String notifyUrl; + + /** + * 页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址 + */ + private String returnUrl; + + /** 请求网关地址 */ + private String serverUrl; + + /** 认证类型 证书/公钥 */ + private Integer authType; + + /** 签名类型 */ + public String signType; + + /** 支付宝公钥 */ + @BigField + public String alipayPublicKey; + + /** 私钥 */ + @BigField + @EncryptionField + private String privateKey; + + /** 应用公钥证书 */ + @BigField + @EncryptionField + private String appCert; + + /** 支付宝公钥证书 */ + @BigField + @EncryptionField + private String alipayCert; + + /** 支付宝CA根证书 */ + @BigField + @EncryptionField + private String alipayRootCert; + + /** 是否沙箱环境 */ + private boolean sandbox; + + /** 超时配置 */ + private Integer expireTime; + + /** 可用支付方式 */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + private String payWays; + + /** 是否启用 */ + private Boolean activity; + + /** 状态 暂时没什么用 */ + private Integer state; + + /** 备注 */ + private String remark; + + @Override + public AlipayConfigDto toDto() { + AlipayConfigDto convert = AlipayConvert.CONVERT.convert(this); + if (StrUtil.isNotBlank(this.getPayWays())) { + convert.setPayWayList(StrUtil.split(this.getPayWays(), ',')); + } + return convert; + } + + public static AlipayConfig init(AlipayConfigParam in) { + AlipayConfig convert = AlipayConvert.CONVERT.convert(in); + if (CollUtil.isNotEmpty(in.getPayWayList())) { + convert.setPayWays(String.join(",", in.getPayWayList())); + } + return convert; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayCallbackService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayCallbackService.java new file mode 100644 index 000000000..c5ece948c --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayCallbackService.java @@ -0,0 +1,103 @@ +package cn.bootx.daxpay.core.paymodel.alipay.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.util.CertUtil; +import cn.bootx.common.redis.RedisClient; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.code.paymodel.AliPayCode; +import cn.bootx.daxpay.core.notify.dao.PayNotifyRecordManager; +import cn.bootx.daxpay.core.pay.func.AbsPayCallbackStrategy; +import cn.bootx.daxpay.core.pay.service.PayCallbackService; +import cn.bootx.daxpay.core.paymodel.alipay.dao.AlipayConfigManager; +import cn.bootx.daxpay.core.paymodel.alipay.entity.AlipayConfig; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayConstants; +import com.alipay.api.internal.util.AlipaySignature; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Objects; + +/** + * 支付宝回调处理 + * + * @author xxm + * @date 2021/2/28 + */ +@Slf4j +@Service +public class AliPayCallbackService extends AbsPayCallbackStrategy { + + private final AlipayConfigManager alipayConfigManager; + + public AliPayCallbackService(RedisClient redisClient, PayNotifyRecordManager payNotifyRecordManager, + PayCallbackService payCallbackService, AlipayConfigManager alipayConfigManager) { + super(redisClient, payNotifyRecordManager, payCallbackService); + this.alipayConfigManager = alipayConfigManager; + } + + @Override + public int getPayChannel() { + return PayChannelCode.ALI; + } + + @Override + public int getTradeStatus() { + Map params = PARAMS.get(); + String tradeStatus = params.get(AliPayCode.TRADE_STATUS); + if (Objects.equals(tradeStatus, AliPayCode.NOTIFY_TRADE_SUCCESS)) { + return PayStatusCode.NOTIFY_TRADE_SUCCESS; + } + return PayStatusCode.NOTIFY_TRADE_FAIL; + } + + @SneakyThrows + @Override + public boolean verifyNotify() { + Map params = PARAMS.get(); + String callReq = JSONUtil.toJsonStr(params); + String appId = params.get(AliPayCode.APP_ID); + if (StrUtil.isBlank(appId)) { + log.error("支付宝回调报文 appId 为空 {}", callReq); + return false; + } + AlipayConfig alipayConfig = alipayConfigManager.findActivity().orElseThrow(DataNotExistException::new); + if (alipayConfig == null) { + log.error("支付宝支付配置不存在: {}", callReq); + return false; + } + + try { + if (Objects.equals(alipayConfig.getAuthType(), AliPayCode.AUTH_TYPE_KEY)) { + return AlipaySignature.rsaCheckV1(params, alipayConfig.getAlipayPublicKey(), CharsetUtil.UTF_8, + AlipayConstants.SIGN_TYPE_RSA2); + } + else { + return AlipaySignature.verifyV1(params, CertUtil.getCertByContent(alipayConfig.getAlipayCert()), + CharsetUtil.UTF_8, AlipayConstants.SIGN_TYPE_RSA2); + } + } + catch (AlipayApiException e) { + log.error("支付宝验签失败", e); + return false; + } + } + + @Override + public Long getPaymentId() { + Map params = PARAMS.get(); + return Long.valueOf(params.get(AliPayCode.OUT_TRADE_NO)); + } + + @Override + public String getReturnMsg() { + return "success"; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayCancelService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayCancelService.java new file mode 100644 index 000000000..717621ba2 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayCancelService.java @@ -0,0 +1,84 @@ +package cn.bootx.daxpay.core.paymodel.alipay.service; + +import cn.bootx.common.spring.exception.RetryableException; +import cn.bootx.daxpay.code.paymodel.AliPayCode; +import cn.bootx.daxpay.core.pay.local.AsyncRefundLocal; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.hutool.core.util.IdUtil; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeCancelModel; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.response.AlipayTradeCancelResponse; +import com.alipay.api.response.AlipayTradeRefundResponse; +import com.ijpay.alipay.AliPayApi; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Objects; + +/** + * 支付宝支付取消和退款 + * + * @author xxm + * @date 2021/4/20 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AliPayCancelService { + + /** + * 关闭支付 + */ + @Retryable(value = RetryableException.class) + public void cancelRemote(Payment payment) { + // 只有部分需要调用支付宝网关进行关闭 + AlipayTradeCancelModel model = new AlipayTradeCancelModel(); + model.setOutTradeNo(String.valueOf(payment.getId())); + + try { + AlipayTradeCancelResponse response = AliPayApi.tradeCancelToResponse(model); + if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) { + log.error("网关返回撤销失败: {}", response.getSubMsg()); + throw new PayFailureException(response.getSubMsg()); + } + } + catch (AlipayApiException e) { + log.error("关闭订单失败:", e); + throw new PayFailureException("关闭订单失败"); + } + } + + /** + * 退款 + */ + public void refund(Payment payment, BigDecimal amount) { + AlipayTradeRefundModel refundModel = new AlipayTradeRefundModel(); + refundModel.setOutTradeNo(String.valueOf(payment.getId())); + refundModel.setRefundAmount(amount.toPlainString()); + + // 设置退款号 + AsyncRefundLocal.set(IdUtil.getSnowflakeNextIdStr()); + refundModel.setOutRequestNo(AsyncRefundLocal.get()); + try { + AlipayTradeRefundResponse response = AliPayApi.tradeRefundToResponse(refundModel); + if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) { + AsyncRefundLocal.setErrorMsg(response.getSubMsg()); + AsyncRefundLocal.setErrorCode(response.getCode()); + log.error("网关返回退款失败: {}", response.getSubMsg()); + throw new PayFailureException(response.getSubMsg()); + } + } + catch (AlipayApiException e) { + log.error("订单退款失败:", e); + AsyncRefundLocal.setErrorMsg(e.getErrMsg()); + AsyncRefundLocal.setErrorCode(e.getErrCode()); + throw new PayFailureException("订单退款失败"); + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayService.java new file mode 100644 index 000000000..182c7b985 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPayService.java @@ -0,0 +1,262 @@ +package cn.bootx.daxpay.core.paymodel.alipay.service; + +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.code.pay.PayWayCode; +import cn.bootx.daxpay.code.pay.PayWayEnum; +import cn.bootx.daxpay.code.paymodel.AliPayCode; +import cn.bootx.daxpay.code.paymodel.AliPayWay; +import cn.bootx.daxpay.core.pay.local.AsyncPayInfoLocal; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.alipay.entity.AlipayConfig; +import cn.bootx.daxpay.dto.pay.AsyncPayInfo; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.paymodel.alipay.AliPayParam; +import cn.bootx.daxpay.util.PayModelUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.Method; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayResponse; +import com.alipay.api.domain.*; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.alipay.api.response.*; +import com.ijpay.alipay.AliPayApi; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static cn.bootx.daxpay.code.paymodel.AliPayCode.BAR_CODE; +import static cn.bootx.daxpay.code.paymodel.AliPayCode.QUICK_MSECURITY_PAY; + + +/** + * 支付宝支付service + * + * @author xxm + * @date 2021/2/26 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AliPayService { + + /** + * 支付前检查支付方式是否可用 + */ + public void validation(PayModeParam payModeParam, AlipayConfig alipayConfig) { + List payWays = Optional.ofNullable(alipayConfig.getPayWays()) + .filter(StrUtil::isNotBlank) + .map(s -> StrUtil.split(s, ',')) + .orElse(new ArrayList<>(1)); + // 发起的支付类型是否在支持的范围内 + PayWayEnum payWayEnum = Optional.ofNullable(AliPayWay.findByNo(payModeParam.getPayWay())) + .orElseThrow(() -> new PayFailureException("非法的支付宝支付类型")); + if (!payWays.contains(payWayEnum.getCode())) { + throw new PayFailureException("该支付宝支付方式不可用"); + } + } + + /** + * 调起支付 + */ + public void pay(BigDecimal amount, Payment payment, AliPayParam aliPayParam, PayModeParam payModeParam, + AlipayConfig alipayConfig) { + String payBody = null; + // 线程存储 + AsyncPayInfo asyncPayInfo = Optional.ofNullable(AsyncPayInfoLocal.get()).orElse(new AsyncPayInfo()); + // wap支付 + if (payModeParam.getPayWay() == PayWayCode.WAP) { + payBody = this.wapPay(amount, payment, alipayConfig, aliPayParam); + } + // 程序支付 + else if (payModeParam.getPayWay() == PayWayCode.APP) { + payBody = this.appPay(amount, payment, alipayConfig); + } + // pc支付 + else if (payModeParam.getPayWay() == PayWayCode.WEB) { + payBody = this.webPay(amount, payment, alipayConfig, aliPayParam); + } + // 二维码支付 + else if (payModeParam.getPayWay() == PayWayCode.QRCODE) { + payBody = this.qrCodePay(amount, payment, alipayConfig); + } + // 付款码支付 + else if (payModeParam.getPayWay() == PayWayCode.BARCODE) { + String tradeNo = this.barCode(amount, payment, aliPayParam, alipayConfig); + asyncPayInfo.setExpiredTime(false).setTradeNo(tradeNo); + } + // 通常是发起支付的参数 + asyncPayInfo.setPayBody(payBody); + AsyncPayInfoLocal.set(asyncPayInfo); + } + + /** + * wap支付 + */ + public String wapPay(BigDecimal amount, Payment payment, AlipayConfig alipayConfig, AliPayParam aliPayParam) { + + AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); + model.setSubject(payment.getTitle()); + model.setOutTradeNo(String.valueOf(payment.getId())); + model.setTotalAmount(amount.toPlainString()); + // 过期时间 + model.setTimeoutExpress(PayModelUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayModelUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + model.setProductCode(AliPayCode.QUICK_WAP_PAY); + model.setQuitUrl(aliPayParam.getReturnUrl()); + + AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(alipayConfig.getNotifyUrl()); + request.setReturnUrl(aliPayParam.getReturnUrl()); + + try { + // 通过GET方式的请求, 返回URL的响应, 默认是POST方式的请求, 返回的是表单响应 + AlipayTradeWapPayResponse response = AliPayApi.pageExecute(request, Method.GET.name()); + return response.getBody(); + } + catch (AlipayApiException e) { + log.error("支付宝手机支付失败", e); + throw new PayFailureException("支付宝手机支付失败"); + } + } + + /** + * app支付 + */ + public String appPay(BigDecimal amount, Payment payment, AlipayConfig alipayConfig) { + AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); + + model.setSubject(payment.getTitle()); + model.setProductCode(QUICK_MSECURITY_PAY); + model.setOutTradeNo(String.valueOf(payment.getId())); + // 过期时间 + model.setTimeoutExpress(PayModelUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayModelUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + model.setTotalAmount(amount.toPlainString()); + + try { + AlipayTradeAppPayResponse response = AliPayApi.appPayToResponse(model, alipayConfig.getNotifyUrl()); + return response.getBody(); + } + catch (AlipayApiException e) { + log.error("支付宝APP支付失败", e); + throw new PayFailureException("支付宝APP支付失败"); + } + } + + /** + * PC支付 + */ + public String webPay(BigDecimal amount, Payment payment, AlipayConfig alipayConfig, AliPayParam aliPayParam) { + + AlipayTradePagePayModel model = new AlipayTradePagePayModel(); + + model.setSubject(payment.getTitle()); + model.setOutTradeNo(String.valueOf(payment.getId())); + // 过期时间 + model.setTimeoutExpress(PayModelUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayModelUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + model.setTotalAmount(amount.toPlainString()); + // 目前仅支持FAST_INSTANT_TRADE_PAY + model.setProductCode(AliPayCode.FAST_INSTANT_TRADE_PAY); + + AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); + request.setBizModel(model); + request.setNotifyUrl(alipayConfig.getNotifyUrl()); + request.setReturnUrl(aliPayParam.getReturnUrl()); + try { + // 通过GET方式的请求, 返回URL的响应, 默认是POST方式的请求, 返回的是表单响应 + AlipayTradePagePayResponse response = AliPayApi.pageExecute(request, Method.GET.name()); + return response.getBody(); + } + catch (AlipayApiException e) { + log.error("支付宝PC支付失败", e); + throw new PayFailureException("支付宝PC支付失败"); + } + } + + /** + * 二维码支付(扫码支付) + */ + public String qrCodePay(BigDecimal amount, Payment payment, AlipayConfig alipayConfig) { + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); + model.setSubject(payment.getTitle()); + model.setOutTradeNo(String.valueOf(payment.getId())); + model.setTotalAmount(amount.toPlainString()); + + // 过期时间 + model.setTimeoutExpress(PayModelUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayModelUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + + try { + AlipayTradePrecreateResponse response = AliPayApi.tradePrecreatePayToResponse(model, + alipayConfig.getNotifyUrl()); + this.verifyErrorMsg(response); + return response.getQrCode(); + } + catch (AlipayApiException e) { + log.error("支付宝手机支付失败", e); + throw new PayFailureException("支付宝手机支付失败"); + } + } + + /** + * 付款码支付 + */ + public String barCode(BigDecimal amount, Payment payment, AliPayParam aliPayParam, AlipayConfig alipayConfig) { + AlipayTradePayModel model = new AlipayTradePayModel(); + + model.setSubject(payment.getTitle()); + model.setOutTradeNo(String.valueOf(payment.getId())); + model.setScene(BAR_CODE); + model.setAuthCode(aliPayParam.getAuthCode()); + + // 过期时间 + model.setTimeoutExpress(PayModelUtil.getAliExpiredTime(alipayConfig.getExpireTime())); + payment.setExpiredTime(PayModelUtil.getPaymentExpiredTime(alipayConfig.getExpireTime())); + model.setTotalAmount(amount.toPlainString()); + + try { + AlipayTradePayResponse response = AliPayApi.tradePayToResponse(model, alipayConfig.getNotifyUrl()); + + // 支付成功处理 金额2000以下免密支付 + if (Objects.equals(response.getCode(), AliPayCode.SUCCESS)) { + payment.setPayStatus(PayStatusCode.TRADE_SUCCESS).setPayTime(LocalDateTime.now()); + return response.getTradeNo(); + } + // 非支付中响应码, 进行错误处理 + if (!Objects.equals(response.getCode(), AliPayCode.INPROCESS)) { + this.verifyErrorMsg(response); + } + } + catch (AlipayApiException e) { + log.error("主动扫码支付失败", e); + throw new PayFailureException("主动扫码支付失败"); + } + return null; + } + + /** + * 验证错误信息 + */ + private void verifyErrorMsg(AlipayResponse alipayResponse) { + if (!Objects.equals(alipayResponse.getCode(), AliPayCode.SUCCESS)) { + String errorMsg = alipayResponse.getSubMsg(); + if (StrUtil.isBlank(errorMsg)) { + errorMsg = alipayResponse.getMsg(); + } + log.error("支付失败 {}", errorMsg); + throw new PayFailureException(errorMsg); + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPaymentService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPaymentService.java new file mode 100644 index 000000000..41fa400d9 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AliPaymentService.java @@ -0,0 +1,122 @@ +package cn.bootx.daxpay.core.paymodel.alipay.service; + +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.pay.local.AsyncPayInfoLocal; +import cn.bootx.daxpay.core.payment.dao.PaymentManager; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.alipay.dao.AliPaymentManager; +import cn.bootx.daxpay.core.paymodel.alipay.entity.AliPayment; +import cn.bootx.daxpay.dto.pay.AsyncPayInfo; +import cn.bootx.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * 支付宝支付记录 + * + * @author xxm + * @date 2021/2/26 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AliPaymentService { + + private final AliPaymentManager aliPaymentManager; + + private final PaymentManager paymentManager; + + /** + * 支付调起成功 更新 payment 中 异步支付类型信息 + */ + public void updatePaySuccess(Payment payment, PayModeParam payModeParam) { + AsyncPayInfo asyncPayInfo = AsyncPayInfoLocal.get(); + payment.setAsyncPayMode(true).setAsyncPayChannel(PayChannelCode.ALI); + List payTypeInfos = payment.getPayChannelInfo(); + List refundableInfos = payment.getRefundableInfo(); + // 清除已有的异步支付类型信息 + payTypeInfos.removeIf(payTypeInfo -> PayChannelCode.ASYNC_TYPE.contains(payTypeInfo.getPayChannel())); + refundableInfos.removeIf(payTypeInfo -> PayChannelCode.ASYNC_TYPE.contains(payTypeInfo.getPayChannel())); + // 更新支付宝支付类型信息 + payTypeInfos.add(new PayChannelInfo().setPayChannel(PayChannelCode.ALI) + .setPayWay(payModeParam.getPayWay()) + .setAmount(payModeParam.getAmount()) + .setExtraParamsJson(payModeParam.getExtraParamsJson())); + payment.setPayChannelInfo(payTypeInfos); + // 更新支付宝可退款类型信息 + refundableInfos.add(new RefundableInfo().setPayChannel(PayChannelCode.ALI).setAmount(payModeParam.getAmount())); + payment.setRefundableInfo(refundableInfos); + // 如果支付完成(付款码情况) 调用 updateSyncSuccess 创建支付宝支付记录 + if (Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_SUCCESS)) { + this.createAliPayment(payment, payModeParam, asyncPayInfo.getTradeNo()); + } + } + + /** + * 更新异步支付记录成功状态, 并创建支付宝支付记录 + */ + public void updateAsyncSuccess(Long id, PayModeParam payModeParam, String tradeNo) { + // 更新支付记录 + Payment payment = paymentManager.findById(id).orElseThrow(() -> new PayFailureException("支付记录不存在")); + + } + + /** + * 创建支付宝支付记录 + */ + private void createAliPayment(Payment payment, PayModeParam payModeParam, String tradeNo) { + // 创建支付宝支付记录 + AliPayment aliPayment = new AliPayment(); + aliPayment.setTradeNo(tradeNo) + .setPaymentId(payment.getId()) + .setAmount(payModeParam.getAmount()) + .setRefundableBalance(payModeParam.getAmount()) + .setBusinessId(payment.getBusinessId()) + .setUserId(payment.getUserId()) + .setPayStatus(PayStatusCode.TRADE_SUCCESS) + .setPayTime(LocalDateTime.now()); + aliPaymentManager.save(aliPayment); + } + + /** + * 取消状态 + */ + public void updateClose(Long paymentId) { + Optional aliPaymentOptional = aliPaymentManager.findByPaymentId(paymentId); + aliPaymentOptional.ifPresent(aliPayment -> { + aliPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + aliPaymentManager.updateById(aliPayment); + }); + } + + /** + * 更新退款 + */ + public void updatePayRefund(Long paymentId, BigDecimal amount) { + Optional aliPaymentOptional = aliPaymentManager.findByPaymentId(paymentId); + aliPaymentOptional.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + payment.setRefundableBalance(refundableBalance); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + aliPaymentManager.updateById(payment); + }); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AlipayConfigService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AlipayConfigService.java new file mode 100644 index 000000000..d04af7a38 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AlipayConfigService.java @@ -0,0 +1,119 @@ +package cn.bootx.daxpay.core.paymodel.alipay.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.dto.KeyValue; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.code.paymodel.AliPayWay; +import cn.bootx.daxpay.core.paymodel.alipay.dao.AlipayConfigManager; +import cn.bootx.daxpay.core.paymodel.alipay.entity.AlipayConfig; +import cn.bootx.daxpay.dto.paymodel.alipay.AlipayConfigDto; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.paymodel.alipay.AlipayConfigParam; +import cn.bootx.daxpay.param.paymodel.alipay.AlipayConfigQuery; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.collection.CollUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 支付宝支付 + * + * @author xxm + * @date 2020/12/15 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AlipayConfigService { + + private final AlipayConfigManager alipayConfigManager; + + /** + * 添加支付宝配置 + */ + @Transactional(rollbackFor = Exception.class) + public void add(AlipayConfigParam param) { + AlipayConfig alipayConfig = AlipayConfig.init(param); + alipayConfig.setActivity(false).setState(1); + alipayConfigManager.save(alipayConfig); + } + + /** + * 设置启用的支付宝配置 + */ + @Transactional(rollbackFor = Exception.class) + public void setUpActivity(Long id) { + AlipayConfig alipayConfig = alipayConfigManager.findById(id).orElseThrow(DataNotExistException::new); + if (Objects.equals(alipayConfig.getActivity(), Boolean.TRUE)) { + return; + } + alipayConfigManager.removeAllActivity(); + alipayConfig.setActivity(true); + alipayConfigManager.updateById(alipayConfig); + } + + /** + * 清除启用状态 + */ + @Transactional(rollbackFor = Exception.class) + public void clearActivity(Long id) { + AlipayConfig alipayConfig = alipayConfigManager.findById(id) + .orElseThrow(() -> new PayFailureException("支付宝配置不存在")); + if (Objects.equals(alipayConfig.getActivity(), Boolean.FALSE)) { + return; + } + alipayConfig.setActivity(false); + alipayConfigManager.updateById(alipayConfig); + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void update(AlipayConfigParam param) { + AlipayConfig alipayConfig = alipayConfigManager.findById(param.getId()).orElseThrow(DataNotExistException::new); + BeanUtil.copyProperties(param, alipayConfig, CopyOptions.create().ignoreNullValue()); + // 支付方式 + if (CollUtil.isNotEmpty(param.getPayWayList())) { + alipayConfig.setPayWays(String.join(",", param.getPayWayList())); + } + else { + alipayConfig.setPayWays(null); + } + alipayConfigManager.updateById(alipayConfig); + } + + /** + * 获取 + */ + public AlipayConfigDto findById(Long id) { + return alipayConfigManager.findById(id).map(AlipayConfig::toDto).orElseThrow(DataNotExistException::new); + } + + /** + * 分页 + */ + public PageResult page(PageParam pageParam, AlipayConfigQuery param) { + return MpUtil.convert2DtoPageResult(alipayConfigManager.page(pageParam, param)); + } + + /** + * 支付宝支持支付方式 + */ + public List findPayWayList() { + return AliPayWay.getPayWays() + .stream() + .map(e -> new KeyValue(e.getCode(), e.getName())) + .collect(Collectors.toList()); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AlipaySyncService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AlipaySyncService.java new file mode 100644 index 000000000..655592933 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/alipay/service/AlipaySyncService.java @@ -0,0 +1,72 @@ +package cn.bootx.daxpay.core.paymodel.alipay.service; + +import cn.bootx.daxpay.code.pay.PaySyncStatus; +import cn.bootx.daxpay.code.paymodel.AliPayCode; +import cn.bootx.daxpay.core.pay.result.PaySyncResult; +import cn.bootx.daxpay.core.payment.entity.Payment; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeQueryModel; +import com.alipay.api.response.AlipayTradeQueryResponse; +import com.ijpay.alipay.AliPayApi; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Objects; + +/** + * 支付宝同步 + * + * @author xxm + * @date 2021/5/17 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AlipaySyncService { + + /** + * 与支付宝网关同步状态 1 远程支付成功 2 交易创建,等待买家付款 3 超时关闭 4 查询不到 5 查询失败 + */ + public PaySyncResult syncPayStatus(Payment payment) { + PaySyncResult paySyncResult = new PaySyncResult().setPaySyncStatus(PaySyncStatus.FAIL); + + // 查询 + try { + AlipayTradeQueryModel queryModel = new AlipayTradeQueryModel(); + queryModel.setOutTradeNo(String.valueOf(payment.getId())); + // 查询退款参数 + AlipayTradeQueryResponse response = AliPayApi.tradeQueryToResponse(queryModel); + String tradeStatus = response.getTradeStatus(); + + // 支付完成 + if (Objects.equals(tradeStatus, AliPayCode.PAYMENT_TRADE_SUCCESS) + || Objects.equals(tradeStatus, AliPayCode.PAYMENT_TRADE_FINISHED)) { + + HashMap map = new HashMap<>(1); + map.put(AliPayCode.TRADE_NO, response.getTradeNo()); + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_SUCCESS).setMap(map); + } + // 待支付 + if (Objects.equals(tradeStatus, AliPayCode.PAYMENT_WAIT_BUYER_PAY)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.WAIT_BUYER_PAY); + } + // 已关闭 + if (Objects.equals(tradeStatus, AliPayCode.PAYMENT_TRADE_CLOSED)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_CLOSED); + } + // 未找到 + if (Objects.equals(response.getSubCode(), AliPayCode.ACQ_TRADE_NOT_EXIST)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.NOT_FOUND); + } + // 退款 支付宝查不到 + + } + catch (AlipayApiException e) { + log.error("查询订单失败:", e); + } + return paySyncResult; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/base/entity/BasePayment.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/base/entity/BasePayment.java new file mode 100644 index 000000000..5f685499d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/base/entity/BasePayment.java @@ -0,0 +1,47 @@ +package cn.bootx.daxpay.core.paymodel.base.entity; + +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 基础支付记录类 + * + * @author xxm + * @date 2021/2/25 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class BasePayment extends MpBaseEntity { + + /** 交易记录ID */ + private Long paymentId; + + /** 用户ID */ + private Long userId; + + /** 交易金额 */ + private BigDecimal amount; + + /** 可退款金额 */ + private BigDecimal refundableBalance; + + /** 关联的业务id */ + private String businessId; + + /** + * 支付状态 + * @see PayStatusCode + */ + private Integer payStatus; + + /** 支付时间 */ + private LocalDateTime payTime; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/dao/CashPaymentManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/dao/CashPaymentManager.java new file mode 100644 index 000000000..bdebdabc8 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/dao/CashPaymentManager.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.core.paymodel.cash.dao; + +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.daxpay.core.paymodel.cash.entity.CashPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 现金支付 + * + * @author xxm + * @date 2021/6/23 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class CashPaymentManager extends BaseManager { + + public Optional findByPaymentId(Long paymentId) { + return findByField(CashPayment::getPaymentId, paymentId); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/dao/CashPaymentMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/dao/CashPaymentMapper.java new file mode 100644 index 000000000..17933d76b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/dao/CashPaymentMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.core.paymodel.cash.dao; + +import cn.bootx.daxpay.core.paymodel.cash.entity.CashPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 现金支付 + * + * @author xxm + * @date 2021/6/23 + */ +@Mapper +public interface CashPaymentMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/entity/CashPayment.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/entity/CashPayment.java new file mode 100644 index 000000000..c27c82984 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/entity/CashPayment.java @@ -0,0 +1,21 @@ +package cn.bootx.daxpay.core.paymodel.cash.entity; + +import cn.bootx.daxpay.core.paymodel.base.entity.BasePayment; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 现金支付记录 + * + * @author xxm + * @date 2021/6/23 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("pay_cash_payment") +@Accessors(chain = true) +public class CashPayment extends BasePayment { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/service/CashService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/service/CashService.java new file mode 100644 index 000000000..d6eb399c2 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/cash/service/CashService.java @@ -0,0 +1,72 @@ +package cn.bootx.daxpay.core.paymodel.cash.service; + +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.cash.dao.CashPaymentManager; +import cn.bootx.daxpay.core.paymodel.cash.entity.CashPayment; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.util.Optional; + +/** + * 现金支付 + * + * @author xxm + * @date 2021/6/23 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CashService { + + private final CashPaymentManager cashPaymentManager; + + /** + * 支付 + */ + public void pay(PayModeParam payMode, Payment payment, PayParam payParam) { + CashPayment walletPayment = new CashPayment(); + walletPayment.setPaymentId(payment.getId()) + .setUserId(payment.getUserId()) + .setBusinessId(payParam.getBusinessId()) + .setAmount(payMode.getAmount()) + .setRefundableBalance(payMode.getAmount()) + .setPayStatus(payment.getPayStatus()); + cashPaymentManager.save(walletPayment); + } + + /** + * 关闭 + */ + public void close(Long paymentId) { + Optional cashPaymentOpt = cashPaymentManager.findByPaymentId(paymentId); + cashPaymentOpt.ifPresent(cashPayment -> { + cashPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + cashPaymentManager.updateById(cashPayment); + }); + } + + /** + * 退款 + */ + public void refund(Long paymentId, BigDecimal amount) { + Optional cashPayment = cashPaymentManager.findByPaymentId(paymentId); + cashPayment.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + cashPaymentManager.updateById(payment); + }); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/convert/UnionPayConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/convert/UnionPayConvert.java new file mode 100644 index 000000000..5dac96721 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/convert/UnionPayConvert.java @@ -0,0 +1,19 @@ +package cn.bootx.daxpay.core.paymodel.union.convert; + +import cn.bootx.daxpay.core.paymodel.union.entity.UnionPayConfig; +import cn.bootx.daxpay.dto.paymodel.union.UnionPayConfigDto; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author xxm + * @date 2022/3/11 + */ +@Mapper +public interface UnionPayConvert { + + UnionPayConvert CONVERT = Mappers.getMapper(UnionPayConvert.class); + + UnionPayConfigDto convert(UnionPayConfig in); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPayConfigManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPayConfigManager.java new file mode 100644 index 000000000..55f1797e6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPayConfigManager.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.core.paymodel.union.dao; + +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.daxpay.core.paymodel.union.entity.UnionPayConfig; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +/** + * @author xxm + * @date 2022/3/11 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class UnionPayConfigManager extends BaseManager { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPayConfigMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPayConfigMapper.java new file mode 100644 index 000000000..a7d59a95d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPayConfigMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.daxpay.core.paymodel.union.dao; + +import cn.bootx.daxpay.core.paymodel.union.entity.UnionPayConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @date 2022/3/11 + */ +@Mapper +public interface UnionPayConfigMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPaymentManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPaymentManager.java new file mode 100644 index 000000000..10cbf650c --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPaymentManager.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.core.paymodel.union.dao; + +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.daxpay.core.paymodel.union.entity.UnionPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +/** + * @author xxm + * @date 2022/3/11 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class UnionPaymentManager extends BaseManager { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPaymentMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPaymentMapper.java new file mode 100644 index 000000000..26c0bcade --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/dao/UnionPaymentMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.daxpay.core.paymodel.union.dao; + +import cn.bootx.daxpay.core.paymodel.union.entity.UnionPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @date 2022/3/11 + */ +@Mapper +public interface UnionPaymentMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/entity/UnionPayConfig.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/entity/UnionPayConfig.java new file mode 100644 index 000000000..69582e7fb --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/entity/UnionPayConfig.java @@ -0,0 +1,29 @@ +package cn.bootx.daxpay.core.paymodel.union.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.core.paymodel.union.convert.UnionPayConvert; +import cn.bootx.daxpay.dto.paymodel.union.UnionPayConfigDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 云闪付 + * + * @author xxm + * @date 2022/3/11 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_union_pay_config") +public class UnionPayConfig extends MpBaseEntity implements EntityBaseFunction { + + @Override + public UnionPayConfigDto toDto() { + return UnionPayConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/entity/UnionPayment.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/entity/UnionPayment.java new file mode 100644 index 000000000..22920f410 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/entity/UnionPayment.java @@ -0,0 +1,19 @@ +package cn.bootx.daxpay.core.paymodel.union.entity; + +import cn.bootx.daxpay.core.paymodel.base.entity.BasePayment; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @date 2022/3/11 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_union_payment") +public class UnionPayment extends BasePayment { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPayConfigService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPayConfigService.java new file mode 100644 index 000000000..1ed24f954 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPayConfigService.java @@ -0,0 +1,19 @@ +package cn.bootx.daxpay.core.paymodel.union.service; + +import cn.bootx.daxpay.core.paymodel.union.dao.UnionPayConfigManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author xxm + * @date 2022/3/11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UnionPayConfigService { + + private final UnionPayConfigManager unionPayConfigManager; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPayService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPayService.java new file mode 100644 index 000000000..78aaaa51c --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPayService.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.core.paymodel.union.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author xxm + * @date 2022/3/11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UnionPayService { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPaymentService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPaymentService.java new file mode 100644 index 000000000..77cfc3d1b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/union/service/UnionPaymentService.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.core.paymodel.union.service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * @author xxm + * @date 2022/3/11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class UnionPaymentService { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/convert/VoucherConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/convert/VoucherConvert.java new file mode 100644 index 000000000..b1e146a15 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/convert/VoucherConvert.java @@ -0,0 +1,23 @@ +package cn.bootx.daxpay.core.paymodel.voucher.convert; + +import cn.bootx.daxpay.core.paymodel.voucher.entity.Voucher; +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherPayment; +import cn.bootx.daxpay.dto.paymodel.voucher.VoucherDto; +import cn.bootx.daxpay.dto.paymodel.voucher.VoucherPaymentDto; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Mapper +public interface VoucherConvert { + + VoucherConvert CONVERT = Mappers.getMapper(VoucherConvert.class); + + VoucherDto convert(Voucher in); + + VoucherPaymentDto convert(VoucherPayment in); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherLogManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherLogManager.java new file mode 100644 index 000000000..64537fff2 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherLogManager.java @@ -0,0 +1,27 @@ +package cn.bootx.daxpay.core.paymodel.voucher.dao; + +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherLog; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * @author xxm + * @date 2022/3/19 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class VoucherLogManager extends BaseManager { + + /** + * 根据支付id和类型进行查询 + */ + public List findByPaymentIdAndType(Long paymentId, int type) { + return lambdaQuery().eq(VoucherLog::getPaymentId, paymentId).eq(VoucherLog::getType, type).list(); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherLogMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherLogMapper.java new file mode 100644 index 000000000..f6fdf1a3b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherLogMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.daxpay.core.paymodel.voucher.dao; + +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherLog; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @date 2022/3/19 + */ +@Mapper +public interface VoucherLogMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherManager.java new file mode 100644 index 000000000..f575d5f43 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherManager.java @@ -0,0 +1,72 @@ +package cn.bootx.daxpay.core.paymodel.voucher.dao; + +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.base.MpIdEntity; +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.paymodel.voucher.entity.Voucher; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherParam; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class VoucherManager extends BaseManager { + + /** + * 分页 + */ + public Page page(PageParam pageParam, VoucherParam param) { + Page mpPage = MpUtil.getMpPage(pageParam, Voucher.class); + return this.lambdaQuery() + .ge(Objects.nonNull(param.getStartTime()), Voucher::getStartTime, param.getStartTime()) + .le(Objects.nonNull(param.getEndTime()), Voucher::getEndTime, param.getEndTime()) + .eq(Objects.nonNull(param.getEnduring()), Voucher::getEnduring, param.getEnduring()) + .like(StrUtil.isNotBlank(param.getCardNo()), Voucher::getCardNo, param.getCardNo()) + .like(Objects.nonNull(param.getBatchNo()), Voucher::getBatchNo, param.getBatchNo()) + .orderByDesc(MpIdEntity::getId) + .page(mpPage); + } + + /** + * 根据卡号查询 + */ + public Optional findByCardNo(String cardNo) { + return this.findByField(Voucher::getCardNo, cardNo); + } + + /** + * 根据卡号查询 + */ + public List findByCardNoList(List cardNos) { + return this.findAllByFields(Voucher::getCardNo, cardNos); + } + + /** + * 更改状态 + */ + public void changeStatus(Long id, int status) { + this.lambdaUpdate().eq(MpIdEntity::getId, id).set(Voucher::getStatus, status).update(); + + } + + /** + * 批量更改状态 + */ + public void changeStatusBatch(List ids, int status) { + this.lambdaUpdate().in(MpIdEntity::getId, ids).set(Voucher::getStatus, status).update(); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherMapper.java new file mode 100644 index 000000000..af137cb5b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.daxpay.core.paymodel.voucher.dao; + +import cn.bootx.daxpay.core.paymodel.voucher.entity.Voucher; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Mapper +public interface VoucherMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherPaymentManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherPaymentManager.java new file mode 100644 index 000000000..9acb47ed3 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherPaymentManager.java @@ -0,0 +1,27 @@ +package cn.bootx.daxpay.core.paymodel.voucher.dao; + +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class VoucherPaymentManager extends BaseManager { + + /** + * 根据支付id + */ + public Optional findByPaymentId(Long paymentId) { + return this.findByField(VoucherPayment::getPaymentId, paymentId); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherPaymentMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherPaymentMapper.java new file mode 100644 index 000000000..a3a045426 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/dao/VoucherPaymentMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.daxpay.core.paymodel.voucher.dao; + +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Mapper +public interface VoucherPaymentMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/Voucher.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/Voucher.java new file mode 100644 index 000000000..6321effdd --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/Voucher.java @@ -0,0 +1,60 @@ +package cn.bootx.daxpay.core.paymodel.voucher.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.code.paymodel.VoucherCode; +import cn.bootx.daxpay.core.paymodel.voucher.convert.VoucherConvert; +import cn.bootx.daxpay.dto.paymodel.voucher.VoucherDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 储值卡 + * + * @author xxm + * @date 2022/3/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_voucher") +public class Voucher extends MpBaseEntity implements EntityBaseFunction { + + /** 卡号 */ + private String cardNo; + + /** 生成批次号 */ + private Long batchNo; + + /** 面值 */ + private BigDecimal faceValue; + + /** 余额 */ + private BigDecimal balance; + + /** 是否长期有效 */ + private Boolean enduring; + + /** 开始时间 */ + private LocalDateTime startTime; + + /** 结束时间 */ + private LocalDateTime endTime; + + /** + * 状态 + * @see VoucherCode + */ + private Integer status; + + @Override + public VoucherDto toDto() { + return VoucherConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/VoucherLog.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/VoucherLog.java new file mode 100644 index 000000000..ddcc0684a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/VoucherLog.java @@ -0,0 +1,48 @@ +package cn.bootx.daxpay.core.paymodel.voucher.entity; + +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.code.paymodel.VoucherCode; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 储值卡日志 + * + * @author xxm + * @date 2022/3/17 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_voucher_log") +public class VoucherLog extends MpBaseEntity { + + /** 储值卡id */ + private Long voucherId; + + /** 储值卡号 */ + private String voucherNo; + + /** 金额 */ + private BigDecimal amount; + + /** + * 类型 + * @see VoucherCode + */ + private Integer type; + + /** 交易记录ID */ + private Long paymentId; + + /** 业务ID */ + private String businessId; + + /** 备注 */ + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/VoucherPayment.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/VoucherPayment.java new file mode 100644 index 000000000..2b11d4041 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/entity/VoucherPayment.java @@ -0,0 +1,32 @@ +package cn.bootx.daxpay.core.paymodel.voucher.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.daxpay.core.paymodel.base.entity.BasePayment; +import cn.bootx.daxpay.core.paymodel.voucher.convert.VoucherConvert; +import cn.bootx.daxpay.dto.paymodel.voucher.VoucherPaymentDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 储值卡支付记录 + * + * @author xxm + * @date 2022/3/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_voucher_payment") +public class VoucherPayment extends BasePayment implements EntityBaseFunction { + + /** 储值卡id列表 */ + private String voucherIds; + + @Override + public VoucherPaymentDto toDto() { + return VoucherConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherLogService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherLogService.java new file mode 100644 index 000000000..c92e443b9 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherLogService.java @@ -0,0 +1,21 @@ +package cn.bootx.daxpay.core.paymodel.voucher.service; + +import cn.bootx.daxpay.core.paymodel.voucher.dao.VoucherLogManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 储值卡日志 + * + * @author xxm + * @date 2022/3/19 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherLogService { + + private final VoucherLogManager voucherLogManager; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherPayService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherPayService.java new file mode 100644 index 000000000..5123b2e89 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherPayService.java @@ -0,0 +1,185 @@ +package cn.bootx.daxpay.core.paymodel.voucher.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.common.core.util.LocalDateTimeUtil; +import cn.bootx.daxpay.code.paymodel.VoucherCode; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.voucher.dao.VoucherLogManager; +import cn.bootx.daxpay.core.paymodel.voucher.dao.VoucherManager; +import cn.bootx.daxpay.core.paymodel.voucher.dao.VoucherPaymentManager; +import cn.bootx.daxpay.core.paymodel.voucher.entity.Voucher; +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherLog; +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherPayment; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherPayParam; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONException; +import cn.hutool.json.JSONUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 储值卡支付 + * + * @author xxm + * @date 2022/3/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherPayService { + + private final VoucherManager voucherManager; + + private final VoucherPaymentManager voucherPaymentManager; + + private final VoucherLogManager voucherLogManager; + + /** + * 获取并检查储值卡 + */ + public List getAndCheckVoucher(PayModeParam payModeParam) { + VoucherPayParam voucherPayParam; + try { + // 储值卡参数验证 + String extraParamsJson = payModeParam.getExtraParamsJson(); + if (StrUtil.isNotBlank(extraParamsJson)) { + voucherPayParam = JSONUtil.toBean(extraParamsJson, VoucherPayParam.class); + } + else { + throw new PayFailureException("储值卡支付参数错误"); + } + } + catch (JSONException e) { + throw new PayFailureException("储值卡支付参数错误"); + } + + List cardNoList = voucherPayParam.getCardNoList(); + List vouchers = voucherManager.findByCardNoList(cardNoList); + // 判断是否有重复or无效的储值卡 + if (vouchers.size() != cardNoList.size()) { + throw new PayFailureException("储值卡支付参数错误"); + } + boolean timeCheck = this.check(vouchers); + if (!timeCheck) { + throw new PayFailureException("储值卡不再有效期内"); + } + // 金额是否满足 + BigDecimal amount = vouchers.stream().map(Voucher::getBalance).reduce(BigDecimal::add).orElse(BigDecimal.ZERO); + if (BigDecimalUtil.compareTo(amount, payModeParam.getAmount()) < 0) { + throw new PayFailureException("储值卡余额不足"); + } + + return vouchers; + } + + /** + * 支付 TODO 有期限的在前面, 同样有期限到期时间短的在前面, 同样到期日金额小的在前面 + */ + public void pay(BigDecimal amount, Payment payment, List vouchers) { + vouchers.sort((o1, o2) -> BigDecimalUtil.compareTo(o1.getBalance(), o2.getBalance())); + List voucherLogs = new ArrayList<>(); + + for (Voucher voucher : vouchers) { + // 待支付余额为零, 不在处理后面的储值卡 + if (BigDecimalUtil.compareTo(amount, BigDecimal.ZERO) < 1) { + break; + } + + BigDecimal balance = voucher.getBalance(); + // 日志 + VoucherLog voucherLog = new VoucherLog().setPaymentId(payment.getId()) + .setBusinessId(payment.getBusinessId()) + .setType(VoucherCode.LOG_PAY) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo()); + + // 待支付额大于储值卡余额. 全扣光 + if (BigDecimalUtil.compareTo(amount, balance) == 1) { + amount = amount.subtract(balance); + voucher.setBalance(BigDecimal.ZERO); + voucherLog.setAmount(balance); + } + else { + voucher.setBalance(balance.subtract(amount)); + voucherLog.setAmount(amount); + } + voucherLogs.add(voucherLog); + } + voucherManager.updateAllById(vouchers); + voucherLogManager.saveAll(voucherLogs); + } + + /** + * 取消支付 + */ + public void close(Long paymentId) { + // 查找支付记录日志 + List voucherLogs = voucherLogManager.findByPaymentIdAndType(paymentId, VoucherCode.LOG_PAY); + // 查出关联的储值卡 + Map voucherLogMap = voucherLogs.stream() + .collect(Collectors.toMap(VoucherLog::getVoucherId, Function.identity())); + List vouchers = voucherManager.findAllByIds(voucherLogMap.keySet()); + // 执行退款并记录日志 + List logs = new ArrayList<>(); + for (Voucher voucher : vouchers) { + VoucherLog voucherLog = voucherLogMap.get(voucher.getId()); + voucher.setBalance(voucher.getBalance().add(voucherLog.getAmount())); + VoucherLog log = new VoucherLog().setAmount(voucherLog.getAmount()) + .setPaymentId(paymentId) + .setBusinessId(voucherLog.getBusinessId()) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo()) + .setType(VoucherCode.LOG_CLOSE); + logs.add(log); + } + voucherManager.updateAllById(vouchers); + voucherLogManager.saveAll(logs); + } + + /** + * 退款 TODO 延长卡的有效期, + */ + public void refund(Long paymentId, BigDecimal amount) { + VoucherPayment voucherPayment = voucherPaymentManager.findByPaymentId(paymentId) + .orElseThrow(() -> new PayFailureException("储值卡支付记录不存在")); + + Long voucherId = Long.valueOf(voucherPayment.getVoucherIds().split(",")[0]); + Voucher voucher = voucherManager.findById(voucherId).orElseThrow(DataNotExistException::new); + + voucher.setBalance(voucher.getBalance().add(amount)); + + VoucherLog log = new VoucherLog().setAmount(amount) + .setPaymentId(paymentId) + .setBusinessId(voucherPayment.getBusinessId()) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo()) + .setType(VoucherCode.LOG_REFUND); + voucherManager.updateById(voucher); + voucherLogManager.save(log); + } + + /** + * 卡信息检查 + */ + private boolean check(List vouchers) { + // 判断有效期 + return vouchers.stream() + .filter(voucher -> !Objects.equals(voucher.getEnduring(), true)) + .allMatch(voucher -> LocalDateTimeUtil.between(LocalDateTime.now(), voucher.getStartTime(), + voucher.getEndTime())); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherPaymentService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherPaymentService.java new file mode 100644 index 000000000..721c88bf3 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherPaymentService.java @@ -0,0 +1,95 @@ +package cn.bootx.daxpay.core.paymodel.voucher.service; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.common.mybatisplus.base.MpIdEntity; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.voucher.dao.VoucherPaymentManager; +import cn.bootx.daxpay.core.paymodel.voucher.entity.Voucher; +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherPayment; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 储值卡支付记录 + * + * @author xxm + * @date 2022/3/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherPaymentService { + + private final VoucherPaymentManager voucherPaymentManager; + + /** + * 添加支付记录 + */ + public void savePayment(Payment payment, PayParam payParam, PayModeParam payMode, List vouchers) { + String voucherIds = vouchers.stream() + .map(MpIdEntity::getId) + .map(String::valueOf) + .collect(Collectors.joining(",")); + + VoucherPayment walletPayment = new VoucherPayment().setVoucherIds(voucherIds); + walletPayment.setPaymentId(payment.getId()) + .setUserId(payment.getUserId()) + .setBusinessId(payParam.getBusinessId()) + .setAmount(payMode.getAmount()) + .setRefundableBalance(payMode.getAmount()) + .setPayStatus(payment.getPayStatus()); + voucherPaymentManager.save(walletPayment); + } + + /** + * 更新成功状态 + */ + public void updateSuccess(Long paymentId) { + Optional payment = voucherPaymentManager.findByPaymentId(paymentId); + if (payment.isPresent()) { + VoucherPayment voucherPayment = payment.get(); + voucherPayment.setPayStatus(PayStatusCode.TRADE_SUCCESS).setPayTime(LocalDateTime.now()); + voucherPaymentManager.updateById(voucherPayment); + } + } + + /** + * 关闭操作 + */ + public void updateClose(Long paymentId) { + VoucherPayment payment = voucherPaymentManager.findByPaymentId(paymentId) + .orElseThrow(() -> new BizException("未查询到查询交易记录")); + payment.setPayStatus(PayStatusCode.TRADE_CANCEL); + voucherPaymentManager.updateById(payment); + } + + /** + * 更新退款 + */ + public void updateRefund(Long paymentId, BigDecimal amount) { + Optional voucherPayment = voucherPaymentManager.findByPaymentId(paymentId); + voucherPayment.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + payment.setRefundableBalance(refundableBalance); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + voucherPaymentManager.updateById(payment); + }); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherQueryService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherQueryService.java new file mode 100644 index 000000000..205fcb87b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherQueryService.java @@ -0,0 +1,51 @@ +package cn.bootx.daxpay.core.paymodel.voucher.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.paymodel.voucher.dao.VoucherManager; +import cn.bootx.daxpay.core.paymodel.voucher.entity.Voucher; +import cn.bootx.daxpay.dto.paymodel.voucher.VoucherDto; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 储值卡查询 + * + * @author xxm + * @date 2022/3/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherQueryService { + + private final VoucherManager voucherManager; + + /** + * 分页 + */ + public PageResult page(PageParam pageParam, VoucherParam param) { + return MpUtil.convert2DtoPageResult(voucherManager.page(pageParam, param)); + } + + /** + * 根据id查询 + */ + public VoucherDto findById(Long id) { + return voucherManager.findById(id).map(Voucher::toDto).orElseThrow(() -> new DataNotExistException("储值卡不存在")); + } + + /** + * 根据卡号查询 + */ + public VoucherDto findByCardNo(String cardNo) { + return voucherManager.findByCardNo(cardNo) + .map(Voucher::toDto) + .orElseThrow(() -> new DataNotExistException("储值卡不存在")); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherService.java new file mode 100644 index 000000000..a2ca2b4c0 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/voucher/service/VoucherService.java @@ -0,0 +1,111 @@ +package cn.bootx.daxpay.core.paymodel.voucher.service; + +import cn.bootx.daxpay.code.paymodel.VoucherCode; +import cn.bootx.daxpay.core.paymodel.voucher.dao.VoucherLogManager; +import cn.bootx.daxpay.core.paymodel.voucher.dao.VoucherManager; +import cn.bootx.daxpay.core.paymodel.voucher.entity.Voucher; +import cn.bootx.daxpay.core.paymodel.voucher.entity.VoucherLog; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherGenerationParam; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherImportParam; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.RandomUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 储值卡 + * + * @author xxm + * @date 2022/3/14 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class VoucherService { + + private final VoucherManager voucherManager; + + private final VoucherLogManager voucherLogManager; + + /** + * 批量生成 + */ + @Transactional(rollbackFor = Exception.class) + public void generationBatch(VoucherGenerationParam param) { + Integer count = param.getCount(); + List vouchers = new ArrayList<>(count); + long batchNo = IdUtil.getSnowflakeNextId(); + for (int i = 0; i < count; i++) { + Voucher voucher = new Voucher() + .setCardNo('V' + IdUtil.getSnowflakeNextIdStr() + RandomUtil.randomNumbers(2)) + .setBatchNo(batchNo) + .setBalance(param.getFaceValue()) + .setFaceValue(param.getFaceValue()) + .setEnduring(param.getEnduring()) + .setStatus(param.getStatus()); + if (Objects.equals(param.getEnduring(), Boolean.FALSE)) { + voucher.setStartTime(param.getStartTime()).setEndTime(param.getEndTime()); + } + vouchers.add(voucher); + } + voucherManager.saveAll(vouchers); + // 日志 + List voucherLogs = vouchers.stream() + .map(voucher -> new VoucherLog().setType(VoucherCode.LOG_ACTIVE) + .setAmount(voucher.getBalance()) + .setVoucherId(voucher.getId()) + .setVoucherNo(voucher.getCardNo())) + .collect(Collectors.toList()); + voucherLogManager.saveAll(voucherLogs); + } + + /** + * 批量导入 + */ + public void importBatch(VoucherImportParam param) { + + } + + /** + * 启用 + */ + public void unlock(Long id) { + voucherManager.changeStatus(id, VoucherCode.STATUS_NORMAL); + } + + /** + * 冻结 + */ + public void lock(Long id) { + voucherManager.changeStatus(id, VoucherCode.STATUS_FORBIDDEN); + } + + /** + * 批量启用 + */ + public void unlockBatch(List ids) { + voucherManager.changeStatusBatch(ids, VoucherCode.STATUS_NORMAL); + } + + /** + * 批量冻结 + */ + public void lockBatch(List ids) { + voucherManager.changeStatusBatch(ids, VoucherCode.STATUS_FORBIDDEN); + } + + /** + * 更改有效期 + */ + public void changeEnduring() { + + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/convert/WalletConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/convert/WalletConvert.java new file mode 100644 index 000000000..503a5eabe --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/convert/WalletConvert.java @@ -0,0 +1,29 @@ +package cn.bootx.daxpay.core.paymodel.wallet.convert; + +import cn.bootx.daxpay.core.paymodel.wallet.entity.Wallet; +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletLog; +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletPayment; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletDto; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletLogDto; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletPaymentDto; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 转换 + * + * @author xxm + * @date 2021/8/20 + */ +@Mapper +public interface WalletConvert { + + WalletConvert CONVERT = Mappers.getMapper(WalletConvert.class); + + WalletDto convert(Wallet in); + + WalletPaymentDto convert(WalletPayment in); + + WalletLogDto convert(WalletLog walletLog); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletLogManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletLogManager.java new file mode 100644 index 000000000..6aa2d544f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletLogManager.java @@ -0,0 +1,65 @@ +package cn.bootx.daxpay.core.paymodel.wallet.dao; + +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.base.MpIdEntity; +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletLog; +import cn.bootx.daxpay.param.paymodel.wallet.WalletLogQueryParam; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Objects; +import java.util.Optional; + +/** + * 钱包日志 + * + * @author xxm + * @date 2020/12/8 + */ +@Repository +@RequiredArgsConstructor +public class WalletLogManager extends BaseManager { + + /** + * 按付款查找优先 + */ + public Optional findFirstByPayment(Long paymentId) { + return MpUtil.findOne(lambdaQuery().eq(WalletLog::getPaymentId, paymentId).orderByDesc(MpIdEntity::getId)); + } + + /** + * 分页查询指定用户的钱包日志 + */ + public Page pageByUserId(PageParam pageParam, WalletLogQueryParam param, Long userId) { + Page mpPage = MpUtil.getMpPage(pageParam, WalletLog.class); + + return this.lambdaQuery().orderByDesc(MpIdEntity::getId).eq(WalletLog::getUserId, userId).page(mpPage); + } + + /** + * 分页查询 + */ + public Page page(PageParam pageParam, WalletLogQueryParam query) { + Page mpPage = MpUtil.getMpPage(pageParam, WalletLog.class); + return this.lambdaQuery() + .orderByDesc(MpIdEntity::getId) + .like(Objects.nonNull(query.getUserId()), WalletLog::getUserId, query.getUserId()) + .like(Objects.nonNull(query.getWalletId()), WalletLog::getWalletId, query.getWalletId()) + .page(mpPage); + } + + /** + * 分页查询 根据钱包id + */ + public Page pageByWalletId(PageParam pageParam, WalletLogQueryParam param) { + Page mpPage = MpUtil.getMpPage(pageParam, WalletLog.class); + return this.lambdaQuery() + .orderByDesc(MpIdEntity::getId) + .eq(WalletLog::getWalletId, param.getWalletId()) + .page(mpPage); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletLogMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletLogMapper.java new file mode 100644 index 000000000..51a0c2abf --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletLogMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.core.paymodel.wallet.dao; + +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletLog; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 钱包日志 + * + * @author xxm + * @date 2020/12/8 + */ +@Mapper +public interface WalletLogMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletManager.java new file mode 100644 index 000000000..6de0f183f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletManager.java @@ -0,0 +1,126 @@ +package cn.bootx.daxpay.core.paymodel.wallet.dao; + +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.base.MpIdEntity; +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.paymodel.wallet.entity.Wallet; +import cn.bootx.daxpay.param.paymodel.wallet.WalletPayParam; +import cn.bootx.iam.core.user.entity.UserInfo; +import cn.bootx.iam.param.user.UserInfoParam; +import cn.bootx.starter.auth.util.SecurityUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 钱包管理 + * + * @author xxm + * @date 2020/12/8 + */ +@Repository +@RequiredArgsConstructor +public class WalletManager extends BaseManager { + + private final WalletMapper walletMapper; + + /** + * 增加余额 + * @param walletId 钱包 + * @param amount 金额 + * @return 更新数量 + */ + public int increaseBalance(Long walletId, BigDecimal amount) { + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.increaseBalance(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 扣减余额 + * @param walletId 钱包ID + * @param amount 扣减金额 + * @return 操作条数 + */ + public int reduceBalance(Long walletId, BigDecimal amount) { + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.reduceBalance(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 扣减余额-允许扣成负数 + * @param walletId 钱包ID + * @param amount 扣减金额 + * @return 剩余条数 + */ + public int reduceBalanceUnlimited(Long walletId, BigDecimal amount) { + Long userId = SecurityUtil.getUserIdOrDefaultId(); + return walletMapper.reduceBalanceUnlimited(walletId, amount, userId, LocalDateTime.now()); + } + + /** + * 更新钱包状态 + */ + public void setUpStatus(Long walletId, int status) { + lambdaUpdate().eq(Wallet::getId, walletId).set(Wallet::getStatus, status).update(); + } + + /** + * 用户钱包是否存在 + */ + public boolean existsByUser(Long userId) { + return existedByField(Wallet::getUserId, userId); + } + + /** + * 查询用户的钱包 + */ + public Optional findByUser(Long userId) { + return findByField(Wallet::getUserId, userId); + } + + /** + * 分页查询 + */ + public Page page(PageParam pageParam, WalletPayParam param) { + + Page mpPage = MpUtil.getMpPage(pageParam, Wallet.class); + return this.lambdaQuery().orderByDesc(MpIdEntity::getId).page(mpPage); + } + + /** + * 待开通钱包的用户列表 + */ + public Page pageByNotWallet(PageParam pageParam, UserInfoParam userInfoParam) { + Page mpPage = MpUtil.getMpPage(pageParam, UserInfo.class); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.isNull("w.id") + .orderByDesc("w.id") + .like(StrUtil.isNotBlank(userInfoParam.getUsername()), "w.username", userInfoParam.getUsername()) + .like(StrUtil.isNotBlank(userInfoParam.getName()), "w.name", userInfoParam.getName()); + return walletMapper.pageByNotWallet(mpPage, wrapper); + } + + /** + * 查询已经存在钱包的用户id + */ + public List findExistUserIds(List userIds) { + return this.lambdaQuery() + .select(Wallet::getUserId) + .in(Wallet::getUserId, userIds) + .list() + .stream() + .map(Wallet::getUserId) + .collect(Collectors.toList()); + + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletMapper.java new file mode 100644 index 000000000..38dcf46d1 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletMapper.java @@ -0,0 +1,62 @@ +package cn.bootx.daxpay.core.paymodel.wallet.dao; + +import cn.bootx.daxpay.core.paymodel.wallet.entity.Wallet; +import cn.bootx.iam.core.user.entity.UserInfo; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.toolkit.Constants; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 钱包 + * + * @author xxm + * @date 2021/2/24 + */ +@Mapper +public interface WalletMapper extends BaseMapper { + + /** + * 增加余额 + * @param walletId 钱包ID + * @param amount 增加的额度 + * @param operator 操作人 + * @param date 时间 + * @return 更新数量 + */ + int increaseBalance(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + /** + * 减少余额 + * @param walletId 钱包ID + * @param amount 减少的额度 + * @param operator 操作人 + * @param date 操作时间 + * @return 操作条数 + */ + int reduceBalance(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + /** + * 减少余额,允许扣成负数 + * @param walletId 钱包ID + * @param amount 减少的额度 + * @param operator 操作人 + * @param date 操作时间 + * @return 操作条数 + */ + int reduceBalanceUnlimited(@Param("walletId") Long walletId, @Param("amount") BigDecimal amount, + @Param("operator") Long operator, @Param("date") LocalDateTime date); + + /** + * 待开通钱包的用户列表 + */ + Page pageByNotWallet(Page mpPage, @Param(Constants.WRAPPER) Wrapper wrapper); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletPaymentManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletPaymentManager.java new file mode 100644 index 000000000..600ac00f8 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletPaymentManager.java @@ -0,0 +1,19 @@ +package cn.bootx.daxpay.core.paymodel.wallet.dao; + +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.daxpay.core.paymodel.base.entity.BasePayment; +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletPayment; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +@RequiredArgsConstructor +public class WalletPaymentManager extends BaseManager { + + public Optional findByPaymentId(Long paymentId) { + return findByField(BasePayment::getPaymentId, paymentId); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletPaymentMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletPaymentMapper.java new file mode 100644 index 000000000..af72e955f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/dao/WalletPaymentMapper.java @@ -0,0 +1,10 @@ +package cn.bootx.daxpay.core.paymodel.wallet.dao; + +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WalletPaymentMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/Wallet.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/Wallet.java new file mode 100644 index 000000000..7faad9b41 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/Wallet.java @@ -0,0 +1,40 @@ +package cn.bootx.daxpay.core.paymodel.wallet.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.core.paymodel.wallet.convert.WalletConvert; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 钱包 + * + * @author xxm + * @date 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_wallet") +public class Wallet extends MpBaseEntity implements EntityBaseFunction { + + /** 关联用户id */ + private Long userId; + + /** 余额 */ + private BigDecimal balance; + + /** 状态 */ + private Integer status; + + @Override + public WalletDto toDto() { + return WalletConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/WalletLog.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/WalletLog.java new file mode 100644 index 000000000..acbfcd170 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/WalletLog.java @@ -0,0 +1,55 @@ +package cn.bootx.daxpay.core.paymodel.wallet.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.core.paymodel.wallet.convert.WalletConvert; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletLogDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 钱包日志表 + * + * @author xxm + * @date 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_wallet_log") +public class WalletLog extends MpBaseEntity implements EntityBaseFunction { + + /** 钱包id */ + private Long walletId; + + /** 用户id */ + private Long userId; + + /** 类型 */ + private Integer type; + + /** 交易记录ID */ + private Long paymentId; + + /** 备注 */ + private String remark; + + /** 业务ID */ + private String businessId; + + /** 操作类型 */ + private int operationSource; + + /** 金额 */ + private BigDecimal amount; + + @Override + public WalletLogDto toDto() { + return WalletConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/WalletPayment.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/WalletPayment.java new file mode 100644 index 000000000..ec06563ca --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/entity/WalletPayment.java @@ -0,0 +1,32 @@ +package cn.bootx.daxpay.core.paymodel.wallet.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.daxpay.core.paymodel.base.entity.BasePayment; +import cn.bootx.daxpay.core.paymodel.wallet.convert.WalletConvert; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletPaymentDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 钱包交易记录表 + * + * @author xxm + * @date 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_wallet_payment") +public class WalletPayment extends BasePayment implements EntityBaseFunction { + + /** 钱包ID */ + private Long walletId; + + @Override + public WalletPaymentDto toDto() { + return WalletConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletLogService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletLogService.java new file mode 100644 index 000000000..53c173c69 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletLogService.java @@ -0,0 +1,49 @@ +package cn.bootx.daxpay.core.paymodel.wallet.service; + +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.paymodel.wallet.dao.WalletLogManager; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletLogDto; +import cn.bootx.daxpay.param.paymodel.wallet.WalletLogQueryParam; +import cn.bootx.starter.auth.util.SecurityUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 钱包日志 + * + * @author xxm + * @date 2020/12/8 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletLogService { + + private final WalletLogManager walletLogManager; + + /** + * 个人钱包日志分页 + */ + public PageResult pageByPersonal(PageParam pageParam, WalletLogQueryParam param) { + Long userId = SecurityUtil.getUserId(); + return MpUtil.convert2DtoPageResult(walletLogManager.pageByUserId(pageParam, param, userId)); + } + + /** + * 钱包日志分页 + */ + public PageResult page(PageParam pageParam, WalletLogQueryParam param) { + return MpUtil.convert2DtoPageResult(walletLogManager.page(pageParam, param)); + } + + /** + * 根据钱包id查询钱包日志(分页) + */ + public PageResult pageByWalletId(PageParam pageParam, WalletLogQueryParam param) { + return MpUtil.convert2DtoPageResult(walletLogManager.pageByWalletId(pageParam, param)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletPayService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletPayService.java new file mode 100644 index 000000000..a4443a50e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletPayService.java @@ -0,0 +1,122 @@ +package cn.bootx.daxpay.core.paymodel.wallet.service; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.code.paymodel.WalletCode; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.wallet.dao.WalletLogManager; +import cn.bootx.daxpay.core.paymodel.wallet.dao.WalletManager; +import cn.bootx.daxpay.core.paymodel.wallet.dao.WalletPaymentManager; +import cn.bootx.daxpay.core.paymodel.wallet.entity.Wallet; +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletLog; +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletPayment; +import cn.bootx.daxpay.exception.waller.WalletLackOfBalanceException; +import cn.bootx.daxpay.exception.waller.WalletNotExistsException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Optional; + +/** + * 钱包支付操作 + * + * @author xxm + * @date 2021/2/27 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletPayService { + + private final WalletManager walletManager; + + private final WalletPaymentManager walletPaymentManager; + + private final WalletLogManager walletLogManager; + + /** + * 支付操作 + * @param amount 付款金额 + * @param payment 支付记录 + * @param wallet 钱包 + */ + public void pay(BigDecimal amount, Payment payment, Wallet wallet) { + // 扣减余额 + int i = walletManager.reduceBalance(wallet.getId(), amount); + + // 判断操作结果 + if (i < 1) { + throw new WalletLackOfBalanceException(); + } + // 日志 + WalletLog walletLog = new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setPaymentId(payment.getId()) + .setAmount(amount) + .setType(WalletCode.LOG_PAY) + .setRemark(String.format("钱包支付金额 %.2f ", amount)) + .setOperationSource(WalletCode.OPERATION_SOURCE_USER) + .setBusinessId(payment.getBusinessId()); + walletLogManager.save(walletLog); + } + + /** + * 取消支付并返还金额 + */ + public void close(Long paymentId) { + // 钱包支付记录 + walletPaymentManager.findByPaymentId(paymentId).ifPresent(walletPayment -> { + Optional walletOpt = walletManager.findById(walletPayment.getWalletId()); + if (!walletOpt.isPresent()) { + log.error("钱包出现恶性问题,需要人工排查"); + return; + } + Wallet wallet = walletOpt.get(); + walletPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + walletPaymentManager.save(walletPayment); + + // 金额返还 + walletManager.increaseBalance(wallet.getId(), walletPayment.getAmount()); + + // 记录日志 + WalletLog walletLog = new WalletLog().setAmount(walletPayment.getAmount()) + .setPaymentId(walletPayment.getPaymentId()) + .setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setType(WalletCode.LOG_PAY_CLOSE) + .setRemark(String.format("取消支付返回金额 %.2f ", walletPayment.getAmount())) + .setOperationSource(WalletCode.OPERATION_SOURCE_SYSTEM) + .setBusinessId(walletPayment.getBusinessId()); + // save log + walletLogManager.save(walletLog); + }); + } + + /** + * 退款 + */ + @Transactional(rollbackFor = Exception.class) + public void refund(Long paymentId, BigDecimal amount) { + // 钱包支付记录 + WalletPayment walletPayment = walletPaymentManager.findByPaymentId(paymentId) + .orElseThrow(() -> new BizException("钱包支付记录不存在")); + // 获取钱包 + Wallet wallet = walletManager.findById(walletPayment.getWalletId()).orElseThrow(WalletNotExistsException::new); + walletManager.increaseBalance(wallet.getId(), amount); + + WalletLog walletLog = new WalletLog().setAmount(amount) + .setPaymentId(walletPayment.getPaymentId()) + .setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setType(WalletCode.LOG_REFUND) + .setRemark(String.format("钱包退款金额 %.2f ", amount)) + .setOperationSource(WalletCode.OPERATION_SOURCE_ADMIN) + .setBusinessId(walletPayment.getBusinessId()); + // save log + walletLogManager.save(walletLog); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletPaymentService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletPaymentService.java new file mode 100644 index 000000000..e60d6c15a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletPaymentService.java @@ -0,0 +1,87 @@ +package cn.bootx.daxpay.core.paymodel.wallet.service; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.wallet.dao.WalletPaymentManager; +import cn.bootx.daxpay.core.paymodel.wallet.entity.Wallet; +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletPayment; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * 钱包交易记录的相关操作 + * + * @author xxm + * @date 2020/12/8 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletPaymentService { + + private final WalletPaymentManager walletPaymentManager; + + /** + * 保存钱包支付记录 + */ + public void savePayment(Payment payment, PayParam payParam, PayModeParam payMode, Wallet wallet) { + WalletPayment walletPayment = new WalletPayment().setWalletId(wallet.getId()); + walletPayment.setPaymentId(payment.getId()) + .setUserId(payment.getUserId()) + .setBusinessId(payParam.getBusinessId()) + .setAmount(payMode.getAmount()) + .setRefundableBalance(payMode.getAmount()) + .setPayStatus(payment.getPayStatus()); + walletPaymentManager.save(walletPayment); + } + + /** + * 更新成功状态 + */ + public void updateSuccess(Long paymentId) { + Optional payment = walletPaymentManager.findByPaymentId(paymentId); + if (payment.isPresent()) { + WalletPayment walletPayment = payment.get(); + walletPayment.setPayStatus(PayStatusCode.TRADE_SUCCESS).setPayTime(LocalDateTime.now()); + walletPaymentManager.updateById(walletPayment); + } + } + + /** + * 关闭操作 + */ + public void updateClose(Long paymentId) { + WalletPayment walletPayment = walletPaymentManager.findByPaymentId(paymentId) + .orElseThrow(() -> new BizException("未查询到查询交易记录")); + walletPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + walletPaymentManager.updateById(walletPayment); + } + + /** + * 更新退款 + */ + public void updateRefund(Long paymentId, BigDecimal amount) { + Optional walletPayment = walletPaymentManager.findByPaymentId(paymentId); + walletPayment.ifPresent(payment -> { + BigDecimal refundableBalance = payment.getRefundableBalance().subtract(amount); + payment.setRefundableBalance(refundableBalance); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + payment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + walletPaymentManager.updateById(payment); + }); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletQueryService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletQueryService.java new file mode 100644 index 000000000..007596c82 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletQueryService.java @@ -0,0 +1,77 @@ +package cn.bootx.daxpay.core.paymodel.wallet.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.paymodel.wallet.dao.WalletManager; +import cn.bootx.daxpay.core.paymodel.wallet.entity.Wallet; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletDto; +import cn.bootx.daxpay.dto.paymodel.wallet.WalletInfoDto; +import cn.bootx.daxpay.param.paymodel.wallet.WalletPayParam; +import cn.bootx.iam.core.user.service.UserQueryService; +import cn.bootx.iam.dto.user.UserInfoDto; +import cn.bootx.iam.param.user.UserInfoParam; +import cn.bootx.starter.auth.util.SecurityUtil; +import cn.hutool.core.bean.BeanUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 钱包 + * + * @author xxm + * @date 2022/3/11 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletQueryService { + + private final WalletManager walletManager; + + private final UserQueryService userQueryService; + + /** + * 根据ID查询Wallet + */ + public WalletDto findById(Long walletId) { + return walletManager.findById(walletId).map(Wallet::toDto).orElseThrow(DataNotExistException::new); + } + + /** + * 根据用户ID查询钱包 + */ + public WalletDto findByUser() { + Long userId = SecurityUtil.getUserId(); + return walletManager.findByUser(userId).map(Wallet::toDto).orElseThrow(DataNotExistException::new); + } + + /** + * 获取钱包综合信息 + */ + public WalletInfoDto getWalletInfo(Long walletId) { + Wallet wallet = walletManager.findById(walletId).orElseThrow(DataNotExistException::new); + UserInfoDto userInfoDto = userQueryService.findById(wallet.getUserId()); + WalletInfoDto walletInfoDto = new WalletInfoDto(); + BeanUtil.copyProperties(wallet, walletInfoDto); + walletInfoDto.setUserName(userInfoDto.getName()); + return walletInfoDto; + } + + /** + * 查询用户 分页 + */ + public PageResult page(PageParam pageParam, WalletPayParam param) { + return MpUtil.convert2DtoPageResult(walletManager.page(pageParam, param)); + } + + /** + * 待开通钱包的用户列表 + */ + public PageResult pageByNotWallet(PageParam pageParam, UserInfoParam userInfoParam) { + return MpUtil.convert2DtoPageResult(walletManager.pageByNotWallet(pageParam, userInfoParam)); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletService.java new file mode 100644 index 000000000..ebbecbe83 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wallet/service/WalletService.java @@ -0,0 +1,192 @@ +package cn.bootx.daxpay.core.paymodel.wallet.service; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.paymodel.WalletCode; +import cn.bootx.daxpay.core.paymodel.wallet.dao.WalletLogManager; +import cn.bootx.daxpay.core.paymodel.wallet.dao.WalletManager; +import cn.bootx.daxpay.core.paymodel.wallet.entity.Wallet; +import cn.bootx.daxpay.core.paymodel.wallet.entity.WalletLog; +import cn.bootx.daxpay.exception.waller.WalletBannedException; +import cn.bootx.daxpay.exception.waller.WalletLogError; +import cn.bootx.daxpay.exception.waller.WalletNotExistsException; +import cn.bootx.daxpay.param.paymodel.wallet.WalletRechargeParam; +import com.google.common.collect.Lists; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 钱包的相关操作 + * + * @author xxm + * @date 2020/12/8 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WalletService { + + private final WalletManager walletManager; + + private final WalletLogManager walletLogManager; + + /** + * 开通操作 创建 + */ + @Transactional(rollbackFor = Exception.class) + public void createWallet(Long userId) { + // 判断钱包是否已开通 + if (walletManager.existsByUser(userId)) { + throw new BizException("钱包已经开通"); + } + Wallet wallet = new Wallet().setUserId(userId).setBalance(BigDecimal.ZERO).setStatus(WalletCode.STATUS_NORMAL); + walletManager.save(wallet); + // 激活 log + WalletLog activeLog = new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setType(WalletCode.LOG_ACTIVE) + .setRemark("激活钱包") + .setOperationSource(WalletCode.OPERATION_SOURCE_USER); + walletLogManager.save(activeLog); + } + + /** + * 批量开通 + */ + public void createWalletBatch(List userIds) { + // 查询出 + List existUserIds = walletManager.findExistUserIds(userIds); + userIds.removeAll(existUserIds); + List wallets = userIds.stream() + .map(userId -> new Wallet().setUserId(userId) + .setStatus(WalletCode.STATUS_NORMAL) + .setBalance(BigDecimal.ZERO)) + .collect(Collectors.toList()); + walletManager.saveAll(wallets); + List walletLogs = wallets.stream() + .map(wallet -> new WalletLog().setWalletId(wallet.getId()) + .setUserId(wallet.getUserId()) + .setAmount(BigDecimal.ZERO) + .setType(WalletCode.LOG_ACTIVE) + .setRemark("激活钱包") + .setOperationSource(WalletCode.OPERATION_SOURCE_USER)) + .collect(Collectors.toList()); + walletLogManager.saveAll(walletLogs); + } + + /** + * 锁定钱包 + */ + public void lock(Long walletId) { + walletManager.setUpStatus(walletId, WalletCode.STATUS_FORBIDDEN); + } + + /** + * 解锁钱包 + */ + public void unlock(Long walletId) { + walletManager.setUpStatus(walletId, WalletCode.STATUS_NORMAL); + } + + /** + * 充值操作 也可以扣款 + */ + @Transactional(rollbackFor = Exception.class) + public void changerBalance(WalletRechargeParam param) { + if (BigDecimalUtil.compareTo(param.getAmount(), BigDecimal.ZERO) == 1) { + walletManager.increaseBalance(param.getWalletId(), param.getAmount()); + } + else if (BigDecimalUtil.compareTo(param.getAmount(), BigDecimal.ZERO) == -1) { + walletManager.reduceBalanceUnlimited(param.getWalletId(), param.getAmount()); + } + else { + return; + } + Wallet wallet = walletManager.findById(param.getWalletId()).orElseThrow(DataNotExistException::new); + WalletLog walletLog = new WalletLog().setAmount(param.getAmount()) + .setWalletId(wallet.getId()) + .setType(WalletCode.LOG_ADMIN_CHANGER) + .setUserId(wallet.getUserId()) + .setRemark(String.format("系统变动余额 %.2f ", param.getAmount())) + .setOperationSource(WalletCode.OPERATION_SOURCE_ADMIN); + walletLogManager.save(walletLog); + } + + /** + * 根据支付单对钱包充值的余额进行扣减 + */ + @Transactional(rollbackFor = Exception.class) + public void deductedBalanceByPaymentId(Long paymentId, Long orderId, String remark, Boolean isThrowError) { + + // 根据支付记录ID查询交易的金额和交易的钱包ID + WalletLog walletLog = walletLogManager.findFirstByPayment(paymentId).orElseThrow(DataNotExistException::new); + if (walletLog == null) { + return; + } + + // 充值类型 + List chargeLogType = Lists.newArrayList(WalletCode.LOG_RECHARGE, WalletCode.LOG_AUTO_RECHARGE, + WalletCode.LOG_ADMIN_CHANGER); + + // 保证是充值类型 且充值金额大于0 + if (!chargeLogType.contains(walletLog.getType()) + || BigDecimalUtil.compareTo(walletLog.getAmount(), BigDecimal.ZERO) < 0) { + log.warn("退款 发现非充值交易,日志ID:{},交易类型:{}", walletLog.getId(), walletLog.getType()); + if (isThrowError) { + throw new WalletLogError(); + } + return; + } + + // 获取钱包ID 并扣减对应金额(允许扣成负数) + walletManager.reduceBalanceUnlimited(walletLog.getWalletId(), walletLog.getAmount()); + + // 记录日志 + WalletLog log = new WalletLog().setWalletId(walletLog.getWalletId()) + .setUserId(walletLog.getUserId()) + .setPaymentId(paymentId) + .setAmount(walletLog.getAmount()) + .setType(WalletCode.LOG_SYSTEM_REDUCE_BALANCE) + .setRemark(String.format("系统减少余额 %.2f (" + remark + ")", walletLog.getAmount())) + .setOperationSource(WalletCode.OPERATION_SOURCE_SYSTEM) + .setPaymentId(paymentId) + .setBusinessId(String.valueOf(orderId)); + walletLogManager.save(log); + + } + + /** + * 查询钱包,如果钱包不存在或者钱包被禁用将抛出异常 + */ + public Wallet getNormalWalletById(Long walletId) { + // 查询Wallet + Wallet wallet = walletManager.findById(walletId).orElseThrow(WalletNotExistsException::new); + // 是否被禁用 + if (Objects.equals(WalletCode.STATUS_FORBIDDEN, wallet.getStatus())) { + throw new WalletBannedException(); + } + return wallet; + } + + /** + * 查询钱包,如果钱包不存在或者钱包被禁用将抛出异常 + */ + public Wallet getNormalWalletByUserId(Long userId) { + // 查询Wallet + Wallet wallet = walletManager.findByUser(userId).orElseThrow(WalletNotExistsException::new); + // 是否被禁用 + if (Objects.equals(WalletCode.STATUS_FORBIDDEN, wallet.getStatus())) { + throw new WalletBannedException(); + } + return wallet; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/convert/WeChatConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/convert/WeChatConvert.java new file mode 100644 index 000000000..12bcdf33e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/convert/WeChatConvert.java @@ -0,0 +1,30 @@ +package cn.bootx.daxpay.core.paymodel.wechat.convert; + +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayment; +import cn.bootx.daxpay.dto.paymodel.wechat.WeChatPayConfigDto; +import cn.bootx.daxpay.dto.paymodel.wechat.WeChatPaymentDto; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayConfigParam; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 微信转换类 + * + * @author xxm + * @date 2021/6/21 + */ +@Mapper +public interface WeChatConvert { + + WeChatConvert CONVERT = Mappers.getMapper(WeChatConvert.class); + + WeChatPayConfig convert(WeChatPayConfigParam in); + + WeChatPayConfigDto convert(WeChatPayConfig in); + + WeChatPaymentDto convert(WeChatPayment in); + + WeChatPayment convert(WeChatPaymentDto in); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPayConfigManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPayConfigManager.java new file mode 100644 index 000000000..a481f810f --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPayConfigManager.java @@ -0,0 +1,79 @@ +package cn.bootx.daxpay.core.paymodel.wechat.dao; + +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.base.MpIdEntity; +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayConfigParam; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Objects; +import java.util.Optional; + +/** + * 微信支付配置 + * + * @author xxm + * @date 2021/3/19 + */ +@Repository +@RequiredArgsConstructor +public class WeChatPayConfigManager extends BaseManager { + + private Optional weChatPayConfig; + + @Override + public WeChatPayConfig saveOrUpdate(WeChatPayConfig entity) { + this.clearCache(); + return super.saveOrUpdate(entity); + } + + @Override + public WeChatPayConfig updateById(WeChatPayConfig weChatPayConfig) { + this.clearCache(); + return super.updateById(weChatPayConfig); + } + + /** + * 获取启用的微信配置 + */ + public Optional findActivity() { + if (Objects.isNull(weChatPayConfig)) { + weChatPayConfig = findByField(WeChatPayConfig::getActivity, Boolean.TRUE); + } + return weChatPayConfig; + } + + /** + * 分页 + */ + public Page page(PageParam pageParam, WeChatPayConfigParam param) { + Page mpPage = MpUtil.getMpPage(pageParam, WeChatPayConfig.class); + return lambdaQuery().select(WeChatPayConfig.class, MpUtil::excludeBigField) + .like(StrUtil.isNotBlank(param.getName()), WeChatPayConfig::getName, param.getName()) + .like(StrUtil.isNotBlank(param.getAppId()), WeChatPayConfig::getAppId, param.getAppId()) + .like(StrUtil.isNotBlank(param.getAppId()), WeChatPayConfig::getMchId, param.getMchId()) + .orderByDesc(MpIdEntity::getId) + .page(mpPage); + } + + /** + * 清除所有的被启用的 + */ + public void removeAllActivity() { + this.clearCache(); + lambdaUpdate().eq(WeChatPayConfig::getActivity, Boolean.TRUE).set(WeChatPayConfig::getActivity, Boolean.FALSE); + } + + /** + * 清除缓存 + */ + public void clearCache() { + weChatPayConfig = null; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPayConfigMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPayConfigMapper.java new file mode 100644 index 000000000..cdd8873b6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPayConfigMapper.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.core.paymodel.wechat.dao; + +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * 微信支付配置 + * + * @author xxm + * @date 2021/3/19 + */ +@Mapper +public interface WeChatPayConfigMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPaymentManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPaymentManager.java new file mode 100644 index 000000000..a822caa91 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPaymentManager.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.core.paymodel.wechat.dao; + +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayment; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 微信支付记录 + * + * @author xxm + * @date 2021/6/21 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class WeChatPaymentManager extends BaseManager { + + public Optional findByPaymentId(Long paymentId) { + return findByField(WeChatPayment::getPaymentId, paymentId); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPaymentMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPaymentMapper.java new file mode 100644 index 000000000..1dd08c257 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/dao/WeChatPaymentMapper.java @@ -0,0 +1,10 @@ +package cn.bootx.daxpay.core.paymodel.wechat.dao; + +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayment; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface WeChatPaymentMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/entity/WeChatPayConfig.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/entity/WeChatPayConfig.java new file mode 100644 index 000000000..b38b86898 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/entity/WeChatPayConfig.java @@ -0,0 +1,123 @@ +package cn.bootx.daxpay.core.paymodel.wechat.entity; + +import cn.bootx.common.core.annotation.BigField; +import cn.bootx.common.core.annotation.EncryptionField; +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.daxpay.core.paymodel.wechat.convert.WeChatConvert; +import cn.bootx.daxpay.dto.paymodel.wechat.WeChatPayConfigDto; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayConfigParam; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.annotation.FieldStrategy; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 微信支付 + * + * @author xxm + * @date 2021/3/1 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_wechat_pay_config") +public class WeChatPayConfig extends MpBaseEntity implements EntityBaseFunction { + + /** 名称 */ + private String name; + + /** 微信商户号 */ + private String mchId; + + /** 微信应用appId */ + private String appId; + + /** + * api版本 + * @see WeChatPayCode#API_V2 + */ + private String apiVersion; + + /** 商户平台「API安全」中的 APIv2 密钥 */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + @EncryptionField + private String apiKeyV2; + + /** 商户平台「API安全」中的 APIv3 密钥 */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + @EncryptionField + private String apiKeyV3; + + /** APPID对应的接口密码,用于获取微信公众号jsapi支付时使用 */ + @EncryptionField + private String appSecret; + + /** API 证书中的 p12 文件存储的 id */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + @EncryptionField + private Long p12; + + /** API 证书中的 cert.pem 证书 */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + @BigField + @EncryptionField + private String certPem; + + /** API 证书中的 key.pem 私钥 */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + @BigField + @EncryptionField + private String keyPem; + + /** 应用域名,回调中会使用此参数 */ + private String domain; + + /** 服务器异步通知页面路径 通知url必须为直接可访问的url,不能携带参数。公网域名必须为https */ + private String notifyUrl; + + /** 页面跳转同步通知页面路径 */ + private String returnUrl; + + /** 是否沙箱环境 */ + private boolean sandbox; + + /** 超时时间(分钟) */ + private Integer expireTime; + + /** 可用支付方式 */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + private String payWays; + + /** 是否启用 */ + private Boolean activity; + + /** 状态 */ + private Integer state; + + /** 备注 */ + private String remark; + + @Override + public WeChatPayConfigDto toDto() { + WeChatPayConfigDto convert = WeChatConvert.CONVERT.convert(this); + if (StrUtil.isNotBlank(this.getPayWays())) { + convert.setPayWayList(StrUtil.split(this.getPayWays(), ',')); + } + return convert; + } + + public static WeChatPayConfig init(WeChatPayConfigParam in) { + WeChatPayConfig convert = WeChatConvert.CONVERT.convert(in); + if (CollUtil.isNotEmpty(in.getPayWayList())) { + convert.setPayWays(String.join(",", in.getPayWayList())); + } + return convert; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/entity/WeChatPayment.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/entity/WeChatPayment.java new file mode 100644 index 000000000..6f9b6202e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/entity/WeChatPayment.java @@ -0,0 +1,32 @@ +package cn.bootx.daxpay.core.paymodel.wechat.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.daxpay.core.paymodel.base.entity.BasePayment; +import cn.bootx.daxpay.core.paymodel.wechat.convert.WeChatConvert; +import cn.bootx.daxpay.dto.paymodel.wechat.WeChatPaymentDto; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @date 2021/6/21 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName("pay_wechat_payment") +public class WeChatPayment extends BasePayment implements EntityBaseFunction { + + /** + * 微信交易号 + */ + private String tradeNo; + + @Override + public WeChatPaymentDto toDto() { + return WeChatConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayCallbackService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayCallbackService.java new file mode 100644 index 000000000..07bec50cc --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayCallbackService.java @@ -0,0 +1,103 @@ +package cn.bootx.daxpay.core.paymodel.wechat.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.redis.RedisClient; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.daxpay.core.notify.dao.PayNotifyRecordManager; +import cn.bootx.daxpay.core.pay.func.AbsPayCallbackStrategy; +import cn.bootx.daxpay.core.pay.service.PayCallbackService; +import cn.bootx.daxpay.core.paymodel.wechat.dao.WeChatPayConfigManager; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.kit.WxPayKit; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +import static cn.bootx.daxpay.code.paymodel.WeChatPayCode.APPID; + +/** + * 微信支付回调 + * + * @author xxm + * @date 2021/6/21 + */ +@Slf4j +@Service +public class WeChatPayCallbackService extends AbsPayCallbackStrategy { + + private final WeChatPayConfigManager weChatPayConfigManager; + + public WeChatPayCallbackService(RedisClient redisClient, PayNotifyRecordManager payNotifyRecordManager, + PayCallbackService payCallbackService, WeChatPayConfigManager weChatPayConfigManager) { + super(redisClient, payNotifyRecordManager, payCallbackService); + this.weChatPayConfigManager = weChatPayConfigManager; + } + + @Override + public int getPayChannel() { + return PayChannelCode.WECHAT; + } + + /** + * 获取支付单id + */ + @Override + public Long getPaymentId() { + Map params = PARAMS.get(); + String paymentId = params.get(WeChatPayCode.OUT_TRADE_NO); + return Long.valueOf(paymentId); + } + + /** + * 获取支付状态 + */ + @Override + public int getTradeStatus() { + Map params = PARAMS.get(); + if (WxPayKit.codeIsOk(params.get(WeChatPayCode.RESULT_CODE))) { + return PayStatusCode.NOTIFY_TRADE_SUCCESS; + } + else { + return PayStatusCode.NOTIFY_TRADE_FAIL; + } + } + + /** + * 验证回调消息 + */ + @Override + public boolean verifyNotify() { + Map params = PARAMS.get(); + String callReq = JSONUtil.toJsonStr(params); + log.info("微信发起回调 报文: {}", callReq); + String appId = params.get(APPID); + + if (StrUtil.isBlank(appId)) { + log.warn("微信回调报文 appId 为空 {}", callReq); + return false; + } + // + WeChatPayConfig weChatPayConfig = weChatPayConfigManager.findActivity().orElseThrow(DataNotExistException::new); + if (weChatPayConfig == null) { + log.warn("微信支付配置不存在: {}", callReq); + return false; + } + return WxPayKit.verifyNotify(params, weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256, null); + } + + @Override + public String getReturnMsg() { + Map xml = new HashMap<>(4); + xml.put(WeChatPayCode.RETURN_CODE, "SUCCESS"); + xml.put(WeChatPayCode.RETURN_MSG, "OK"); + return WxPayKit.toXml(xml); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayCancelService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayCancelService.java new file mode 100644 index 000000000..3c4e3c78a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayCancelService.java @@ -0,0 +1,105 @@ +package cn.bootx.daxpay.core.paymodel.wechat.service; + +import cn.bootx.common.spring.exception.RetryableException; +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.daxpay.core.pay.local.AsyncRefundLocal; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayment; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.starter.file.service.FileUploadService; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.kit.WxPayKit; +import com.ijpay.wxpay.WxPayApi; +import com.ijpay.wxpay.model.CloseOrderModel; +import com.ijpay.wxpay.model.RefundModel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import java.io.ByteArrayInputStream; +import java.math.BigDecimal; +import java.util.Map; +import java.util.Optional; + +/** + * 微信支付关闭和退款 + * + * @author xxm + * @date 2021/6/21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPayCancelService { + + private final FileUploadService uploadService; + + /** + * 关闭支付 + */ + @Retryable(value = RetryableException.class) + public void cancelRemote(Payment payment, WeChatPayConfig weChatPayConfig) { + // 只有部分需要调用微信网关进行关闭 + Map params = CloseOrderModel.builder() + .appid(weChatPayConfig.getAppId()) + .mch_id(weChatPayConfig.getMchId()) + .out_trade_no(String.valueOf(payment.getId())) + .nonce_str(WxPayKit.generateStr()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + String xmlResult = WxPayApi.closeOrder(params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + } + + /** + * 退款 + */ + public void refund(Payment payment, WeChatPayment weChatPayment, BigDecimal amount, + WeChatPayConfig weChatPayConfig) { + String totalFee = weChatPayment.getAmount().multiply(BigDecimal.valueOf(100)).toBigInteger().toString(); + String refundFee = amount.multiply(BigDecimal.valueOf(100)).toBigInteger().toString(); + // 设置退款号 + AsyncRefundLocal.set(IdUtil.getSnowflakeNextIdStr()); + Map params = RefundModel.builder() + .appid(weChatPayConfig.getAppId()) + .mch_id(weChatPayConfig.getMchId()) + .out_trade_no(String.valueOf(payment.getId())) + .out_refund_no(AsyncRefundLocal.get()) + .total_fee(totalFee) + .refund_fee(refundFee) + .nonce_str(WxPayKit.generateStr()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + // 获取证书文件流 + byte[] fileBytes = uploadService.getFileBytes(weChatPayConfig.getP12()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(fileBytes); + // 证书密码为 微信商户号 + String xmlResult = WxPayApi.orderRefund(false, params, inputStream, weChatPayConfig.getMchId()); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + } + + /** + * 验证错误信息 + */ + private void verifyErrorMsg(Map result) { + String returnCode = result.get(WeChatPayCode.RETURN_CODE); + String resultCode = result.get(WeChatPayCode.RESULT_CODE); + if (!WxPayKit.codeIsOk(returnCode) || !WxPayKit.codeIsOk(resultCode)) { + String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES); + if (StrUtil.isBlank(errorMsg)) { + errorMsg = result.get(WeChatPayCode.RETURN_MSG); + } + log.error("订单退款/关闭失败 {}", errorMsg); + AsyncRefundLocal.setErrorMsg(errorMsg); + AsyncRefundLocal.setErrorCode(Optional.ofNullable(resultCode).orElse(returnCode)); + throw new PayFailureException(errorMsg); + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayConfigService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayConfigService.java new file mode 100644 index 000000000..84424f2ea --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayConfigService.java @@ -0,0 +1,121 @@ +package cn.bootx.daxpay.core.paymodel.wechat.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.dto.KeyValue; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.code.paymodel.WeChatPayWay; +import cn.bootx.daxpay.core.paymodel.wechat.dao.WeChatPayConfigManager; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import cn.bootx.daxpay.dto.paymodel.wechat.WeChatPayConfigDto; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayConfigParam; +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.bean.copier.CopyOptions; +import cn.hutool.core.collection.CollUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 微信支付配置 + * + * @author xxm + * @date 2021/3/5 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPayConfigService { + + private final WeChatPayConfigManager weChatPayConfigManager; + + /** + * 添加微信支付配置 + */ + @Transactional(rollbackFor = Exception.class) + public void add(WeChatPayConfigParam param) { + WeChatPayConfig weChatPayConfig = WeChatPayConfig.init(param); + weChatPayConfig.setActivity(false); + weChatPayConfigManager.save(weChatPayConfig); + } + + /** + * 修改 + */ + @Transactional(rollbackFor = Exception.class) + public void update(WeChatPayConfigParam param) { + WeChatPayConfig weChatPayConfig = weChatPayConfigManager.findById(param.getId()) + .orElseThrow(() -> new PayFailureException("微信支付配置不存在")); + param.setActivity(null); + BeanUtil.copyProperties(param, weChatPayConfig, CopyOptions.create().ignoreNullValue()); + // 支付方式 + if (CollUtil.isNotEmpty(param.getPayWayList())) { + weChatPayConfig.setPayWays(String.join(",", param.getPayWayList())); + } + else { + weChatPayConfig.setPayWays(null); + } + weChatPayConfigManager.updateById(weChatPayConfig); + } + + /** + * 分页 + */ + public PageResult page(PageParam pageParam, WeChatPayConfigParam param) { + return MpUtil.convert2DtoPageResult(weChatPayConfigManager.page(pageParam, param)); + } + + /** + * 设置启用的支付宝配置 + */ + @Transactional(rollbackFor = Exception.class) + public void setUpActivity(Long id) { + WeChatPayConfig weChatPayConfig = weChatPayConfigManager.findById(id) + .orElseThrow(() -> new PayFailureException("微信支付配置不存在")); + if (Objects.equals(weChatPayConfig.getActivity(), Boolean.TRUE)) { + return; + } + weChatPayConfigManager.removeAllActivity(); + weChatPayConfig.setActivity(true); + weChatPayConfigManager.updateById(weChatPayConfig); + } + + /** + * 清除启用状态 + */ + @Transactional(rollbackFor = Exception.class) + public void clearActivity(Long id) { + WeChatPayConfig weChatPayConfig = weChatPayConfigManager.findById(id) + .orElseThrow(() -> new PayFailureException("微信支付配置不存在")); + if (Objects.equals(weChatPayConfig.getActivity(), Boolean.TRUE)) { + return; + } + weChatPayConfig.setActivity(false); + weChatPayConfigManager.updateById(weChatPayConfig); + } + + /** + * 获取 + */ + public WeChatPayConfigDto findById(Long id) { + return weChatPayConfigManager.findById(id).map(WeChatPayConfig::toDto).orElseThrow(DataNotExistException::new); + } + + /** + * 微信支持支付方式 + */ + public List findPayWayList() { + return WeChatPayWay.getPayWays() + .stream() + .map(e -> new KeyValue(e.getCode(), e.getName())) + .collect(Collectors.toList()); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayService.java new file mode 100644 index 000000000..d92c11c23 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPayService.java @@ -0,0 +1,275 @@ +package cn.bootx.daxpay.core.paymodel.wechat.service; + +import cn.bootx.common.core.util.LocalDateTimeUtil; +import cn.bootx.common.spring.exception.RetryableException; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.code.pay.PayWayCode; +import cn.bootx.daxpay.code.pay.PayWayEnum; +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.daxpay.code.paymodel.WeChatPayWay; +import cn.bootx.daxpay.core.pay.local.AsyncPayInfoLocal; +import cn.bootx.daxpay.core.pay.result.PaySyncResult; +import cn.bootx.daxpay.core.pay.service.PaySyncService; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import cn.bootx.daxpay.dto.pay.AsyncPayInfo; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayParam; +import cn.bootx.daxpay.util.PayModelUtil; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.enums.TradeType; +import com.ijpay.core.kit.WxPayKit; +import com.ijpay.wxpay.WxPayApi; +import com.ijpay.wxpay.model.MicroPayModel; +import com.ijpay.wxpay.model.UnifiedOrderModel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.retry.annotation.Backoff; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +import static cn.bootx.daxpay.code.pay.PaySyncStatus.WAIT_BUYER_PAY; + +/** + * 微信支付 + * + * @author xxm + * @date 2021/3/2 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPayService { + + private final PaySyncService paySyncService; + + private final WeChatPaySyncService weChatPaySyncService; + + /** + * 校验 + */ + public void validation(PayModeParam payModeParam, WeChatPayConfig weChatPayConfig) { + List payWays = Optional.ofNullable(weChatPayConfig.getPayWays()) + .filter(StrUtil::isNotBlank) + .map(s -> StrUtil.split(s, ',')) + .orElse(new ArrayList<>(1)); + + PayWayEnum payWayEnum = Optional.ofNullable(WeChatPayWay.findByNo(payModeParam.getPayWay())) + .orElseThrow(() -> new PayFailureException("非法的微信支付类型")); + if (!payWays.contains(payWayEnum.getCode())) { + throw new PayFailureException("该微信支付方式不可用"); + } + } + + /** + * 支付 + */ + public void pay(BigDecimal amount, Payment payment, WeChatPayParam weChatPayParam, PayModeParam payModeParam, + WeChatPayConfig weChatPayConfig) { + // 微信传入的是分, 将元转换为分 + String totalFee = String.valueOf(amount.multiply(new BigDecimal(100)).longValue()); + AsyncPayInfo asyncPayInfo = Optional.ofNullable(AsyncPayInfoLocal.get()).orElse(new AsyncPayInfo()); + String payBody = null; + + // wap支付 + if (payModeParam.getPayWay() == PayWayCode.WAP) { + payBody = this.wapPay(totalFee, payment, weChatPayConfig); + } + // APP支付 + else if (payModeParam.getPayWay() == PayWayCode.APP) { + payBody = this.appPay(totalFee, payment, weChatPayConfig); + } + // 微信公众号支付或者小程序支付 + else if (payModeParam.getPayWay() == PayWayCode.JSAPI) { + payBody = this.jsPay(totalFee, payment, weChatPayParam.getOpenId(), weChatPayConfig); + } + // 二维码支付 + else if (payModeParam.getPayWay() == PayWayCode.QRCODE) { + payBody = this.qrCodePay(totalFee, payment, weChatPayConfig); + } + // 付款码支付 + else if (payModeParam.getPayWay() == PayWayCode.BARCODE) { + String tradeNo = this.barCode(totalFee, payment, weChatPayParam.getAuthCode(), weChatPayConfig); + asyncPayInfo.setTradeNo(tradeNo).setExpiredTime(false); + } + asyncPayInfo.setPayBody(payBody); + AsyncPayInfoLocal.set(asyncPayInfo); + } + + /** + * wap支付 + */ + private String wapPay(String amount, Payment payment, WeChatPayConfig weChatPayConfig) { + Map params = this.buildParams(amount, payment, weChatPayConfig, TradeType.MWEB.getTradeType()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.pushOrder(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + return result.get(WeChatPayCode.MWEB_URL); + } + + /** + * 程序支付 + */ + private String appPay(String amount, Payment payment, WeChatPayConfig weChatPayConfig) { + Map params = this.buildParams(amount, payment, weChatPayConfig, TradeType.APP.getTradeType()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.pushOrder(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + return result.get(WeChatPayCode.PREPAY_ID); + } + + /** + * 微信公众号支付或者小程序支付 + */ + private String jsPay(String amount, Payment payment, String openId, WeChatPayConfig weChatPayConfig) { + Map params = this.buildParams(amount, payment, weChatPayConfig, TradeType.JSAPI.getTradeType()) + .openid(openId) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.pushOrder(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + return result.get(WeChatPayCode.PREPAY_ID); + } + + /** + * 二维码支付 + */ + private String qrCodePay(String amount, Payment payment, WeChatPayConfig weChatPayConfig) { + + Map params = this.buildParams(amount, payment, weChatPayConfig, TradeType.NATIVE.getTradeType()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.pushOrder(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + this.verifyErrorMsg(result); + return result.get(WeChatPayCode.CODE_URL); + } + + /** + * 付款码支付 + */ + private String barCode(String amount, Payment payment, String authCode, WeChatPayConfig weChatPayConfig) { + Map params = MicroPayModel.builder() + .appid(weChatPayConfig.getAppId()) + .mch_id(weChatPayConfig.getMchId()) + .nonce_str(WxPayKit.generateStr()) + .body(payment.getTitle()) + .auth_code(authCode) + .out_trade_no(String.valueOf(payment.getId())) + .total_fee(amount) + .spbill_create_ip(NetUtil.getLocalhostStr()) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + + String xmlResult = WxPayApi.microPay(false, params); + Map result = WxPayKit.xmlToMap(xmlResult); + + String returnCode = result.get(WeChatPayCode.RETURN_CODE); + // 支付失败 + if (!WxPayKit.codeIsOk(returnCode)) { + String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES); + throw new PayFailureException(errorMsg); + } + + String resultCode = result.get(WeChatPayCode.RESULT_CODE); + String errCode = result.get(WeChatPayCode.ERR_CODE); + // 支付成功处理 + if (Objects.equals(resultCode, WeChatPayCode.TRADE_SUCCESS)) { + payment.setPayStatus(PayStatusCode.TRADE_SUCCESS).setPayTime(LocalDateTime.now()); + return result.get(WeChatPayCode.TRANSACTION_ID); + } + // 支付中, 发起轮训同步 + if (Objects.equals(resultCode, WeChatPayCode.TRADE_FAIL) + && Objects.equals(errCode, WeChatPayCode.TRADE_USERPAYING)) { + SpringUtil.getBean(this.getClass()).rotationSync(payment, weChatPayConfig); + return result.get(WeChatPayCode.TRANSACTION_ID); + } + + // 支付撤销 + if (Objects.equals(resultCode, WeChatPayCode.TRADE_REVOKED)) { + throw new PayFailureException("用户已撤销支付"); + } + + // 支付失败 + if (Objects.equals(resultCode, WeChatPayCode.TRADE_PAYERROR) + || Objects.equals(resultCode, WeChatPayCode.TRADE_FAIL)) { + String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES); + throw new PayFailureException(errorMsg); + } + return null; + } + + /** + * 构建参数 + */ + private UnifiedOrderModel.UnifiedOrderModelBuilder buildParams(String amount, Payment payment, + WeChatPayConfig weChatPayConfig, String tradeType) { + // 过期时间 + payment.setExpiredTime(PayModelUtil.getPaymentExpiredTime(weChatPayConfig.getExpireTime())); + return UnifiedOrderModel.builder() + .appid(weChatPayConfig.getAppId()) + .mch_id(weChatPayConfig.getMchId()) + .nonce_str(WxPayKit.generateStr()) + .time_start(LocalDateTimeUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN)) + // 反正v2版本的超时时间无效 + .time_expire(PayModelUtil.getWxExpiredTime(weChatPayConfig.getExpireTime())) + .body(payment.getTitle()) + .out_trade_no(String.valueOf(payment.getId())) + .total_fee(amount) + .spbill_create_ip(NetUtil.getLocalhostStr()) + .notify_url(weChatPayConfig.getNotifyUrl()) + .trade_type(tradeType); + } + + /** + * 验证错误信息 + */ + private void verifyErrorMsg(Map result) { + String returnCode = result.get(WeChatPayCode.RETURN_CODE); + String resultCode = result.get(WeChatPayCode.RESULT_CODE); + if (!WxPayKit.codeIsOk(returnCode) || !WxPayKit.codeIsOk(resultCode)) { + String errorMsg = result.get(WeChatPayCode.ERR_CODE_DES); + if (StrUtil.isBlank(errorMsg)) { + errorMsg = result.get(WeChatPayCode.RETURN_MSG); + } + log.error("支付失败 {}", errorMsg); + throw new PayFailureException(errorMsg); + } + } + + /** + * 重试同步支付状态, 最多10次, 30秒不操作微信会自动关闭 + */ + @Async("bigExecutor") + @Retryable(value = RetryableException.class, maxAttempts = 10, backoff = @Backoff(value = 5000L)) + public void rotationSync(Payment payment, WeChatPayConfig weChatPayConfig) { + PaySyncResult paySyncResult = weChatPaySyncService.syncPayStatus(payment.getId(), weChatPayConfig); + // 不为支付中状态后, 调用系统同步更新状态, 支付状态则继续重试 + if (Objects.equals(WAIT_BUYER_PAY, paySyncResult.getPaySyncStatus())) { + throw new RetryableException(); + } + else { + paySyncService.syncPayment(payment); + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPaySyncService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPaySyncService.java new file mode 100644 index 000000000..e8ba3f044 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPaySyncService.java @@ -0,0 +1,85 @@ +package cn.bootx.daxpay.core.paymodel.wechat.service; + +import cn.bootx.daxpay.code.pay.PaySyncStatus; +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.daxpay.core.pay.result.PaySyncResult; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayConfig; +import com.ijpay.core.enums.SignType; +import com.ijpay.core.kit.WxPayKit; +import com.ijpay.wxpay.WxPayApi; +import com.ijpay.wxpay.model.UnifiedOrderModel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.Objects; + +/** + * 微信支付同步服务 + * + * @author xxm + * @date 2021/6/21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPaySyncService { + + /** + * 同步查询 + */ + public PaySyncResult syncPayStatus(Long paymentId, WeChatPayConfig weChatPayConfig) { + PaySyncResult paySyncResult = new PaySyncResult().setPaySyncStatus(PaySyncStatus.FAIL); + Map params = UnifiedOrderModel.builder() + .appid(weChatPayConfig.getAppId()) + .mch_id(weChatPayConfig.getMchId()) + .nonce_str(WxPayKit.generateStr()) + .out_trade_no(String.valueOf(paymentId)) + .build() + .createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256); + try { + String xmlResult = WxPayApi.orderQuery(params); + Map result = WxPayKit.xmlToMap(xmlResult); + // 查询失败 + if (!WxPayKit.codeIsOk(result.get(WeChatPayCode.RETURN_CODE))) { + log.warn("查询微信订单失败:{}", result); + return paySyncResult; + } + + // 未查到订单 + if (!WxPayKit.codeIsOk(result.get(WeChatPayCode.RESULT_CODE))) { + log.warn("疑似未查询到订单:{}", result); + return paySyncResult.setPaySyncStatus(PaySyncStatus.NOT_FOUND); + } + String tradeStatus = result.get(WeChatPayCode.TRADE_STATE); + // 支付完成 + if (Objects.equals(tradeStatus, WeChatPayCode.TRADE_SUCCESS) + || Objects.equals(tradeStatus, WeChatPayCode.TRADE_ACCEPT)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_SUCCESS).setMap(result); + } + // 待支付 + if (Objects.equals(tradeStatus, WeChatPayCode.TRADE_NOTPAY) + || Objects.equals(tradeStatus, WeChatPayCode.TRADE_USERPAYING)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.WAIT_BUYER_PAY); + } + + // 已退款/退款中 + if (Objects.equals(tradeStatus, WeChatPayCode.TRADE_REFUND)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_REFUND); + } + // 已关闭 + if (Objects.equals(tradeStatus, WeChatPayCode.TRADE_CLOSED) + || Objects.equals(tradeStatus, WeChatPayCode.TRADE_REVOKED) + || Objects.equals(tradeStatus, WeChatPayCode.TRADE_PAYERROR)) { + return paySyncResult.setPaySyncStatus(PaySyncStatus.TRADE_CLOSED); + } + + } + catch (RuntimeException e) { + log.error("查询订单失败:", e); + } + return paySyncResult; + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPaymentService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPaymentService.java new file mode 100644 index 000000000..1eebc5916 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/paymodel/wechat/service/WeChatPaymentService.java @@ -0,0 +1,121 @@ +package cn.bootx.daxpay.core.paymodel.wechat.service; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.pay.local.AsyncPayInfoLocal; +import cn.bootx.daxpay.core.payment.entity.Payment; +import cn.bootx.daxpay.core.payment.service.PaymentService; +import cn.bootx.daxpay.core.paymodel.wechat.dao.WeChatPaymentManager; +import cn.bootx.daxpay.core.paymodel.wechat.entity.WeChatPayment; +import cn.bootx.daxpay.dto.pay.AsyncPayInfo; +import cn.bootx.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import cn.bootx.daxpay.param.pay.PayModeParam; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * 微信支付记录单 + * + * @author xxm + * @date 2021/6/21 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class WeChatPaymentService { + + private final PaymentService paymentService; + + private final WeChatPaymentManager weChatPaymentManager; + + /** + * 支付调起成功 更新 payment 中 异步支付类型信息 + */ + public void updatePaySuccess(Payment payment, PayModeParam payModeParam) { + AsyncPayInfo asyncPayInfo = AsyncPayInfoLocal.get(); + payment.setAsyncPayMode(true).setAsyncPayChannel(PayChannelCode.WECHAT); + + List payTypeInfos = payment.getPayChannelInfo(); + List refundableInfos = payment.getRefundableInfo(); + // 清除已有的异步支付类型信息 + payTypeInfos.removeIf(payTypeInfo -> PayChannelCode.ASYNC_TYPE.contains(payTypeInfo.getPayChannel())); + refundableInfos.removeIf(payTypeInfo -> PayChannelCode.ASYNC_TYPE.contains(payTypeInfo.getPayChannel())); + // 添加微信支付类型信息 + payTypeInfos.add(new PayChannelInfo().setPayChannel(PayChannelCode.WECHAT) + .setPayWay(payModeParam.getPayWay()) + .setAmount(payModeParam.getAmount()) + .setExtraParamsJson(payModeParam.getExtraParamsJson())); + payment.setPayChannelInfo(payTypeInfos); + // 更新微信可退款类型信息 + refundableInfos + .add(new RefundableInfo().setPayChannel(PayChannelCode.WECHAT).setAmount(payModeParam.getAmount())); + payment.setRefundableInfo(refundableInfos); + // 如果支付完成(付款码情况) 调用 updateSyncSuccess 创建微信支付记录 + if (Objects.equals(payment.getPayStatus(), PayStatusCode.TRADE_SUCCESS)) { + this.createWeChatPayment(payment, payModeParam, asyncPayInfo.getTradeNo()); + } + } + + /** + * 异步支付成功, 更新支付记录成功状态, 并创建微信支付记录 + */ + public void updateAsyncSuccess(Long id, PayModeParam payModeParam, String tradeNo) { + Payment payment = paymentService.findById(id).orElseThrow(() -> new BizException("支付记录不存在")); + this.createWeChatPayment(payment, payModeParam, tradeNo); + } + + /** + * 更新支付记录成功状态, 并创建微信支付记录 + */ + private void createWeChatPayment(Payment payment, PayModeParam payModeParam, String tradeNo) { + + // 创建微信支付记录 + WeChatPayment wechatPayment = new WeChatPayment(); + wechatPayment.setTradeNo(tradeNo) + .setPaymentId(payment.getId()) + .setAmount(payModeParam.getAmount()) + .setRefundableBalance(payModeParam.getAmount()) + .setBusinessId(payment.getBusinessId()) + .setUserId(payment.getUserId()) + .setPayStatus(PayStatusCode.TRADE_SUCCESS) + .setPayTime(LocalDateTime.now()); + weChatPaymentManager.save(wechatPayment); + } + + /** + * 取消状态 + */ + public void updateClose(Long paymentId) { + Optional weChatPaymentOptional = weChatPaymentManager.findByPaymentId(paymentId); + weChatPaymentOptional.ifPresent(weChatPayment -> { + weChatPayment.setPayStatus(PayStatusCode.TRADE_CANCEL); + weChatPaymentManager.updateById(weChatPayment); + }); + } + + /** + * 更新退款 + */ + public void updatePayRefund(WeChatPayment weChatPayment, BigDecimal amount) { + BigDecimal refundableBalance = weChatPayment.getRefundableBalance().subtract(amount); + weChatPayment.setRefundableBalance(refundableBalance); + if (BigDecimalUtil.compareTo(refundableBalance, BigDecimal.ZERO) == 0) { + weChatPayment.setPayStatus(PayStatusCode.TRADE_REFUNDED); + } + else { + weChatPayment.setPayStatus(PayStatusCode.TRADE_REFUNDING); + } + weChatPaymentManager.updateById(weChatPayment); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/convert/RefundConvert.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/convert/RefundConvert.java new file mode 100644 index 000000000..0aac3cf7d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/convert/RefundConvert.java @@ -0,0 +1,19 @@ +package cn.bootx.daxpay.core.refund.convert; + +import cn.bootx.daxpay.core.refund.entity.RefundRecord; +import cn.bootx.daxpay.dto.refund.RefundRecordDto; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * @author xxm + * @date 2022/3/2 + */ +@Mapper +public interface RefundConvert { + + RefundConvert CONVERT = Mappers.getMapper(RefundConvert.class); + + RefundRecordDto convert(RefundRecord in); + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/dao/RefundRecordManager.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/dao/RefundRecordManager.java new file mode 100644 index 000000000..c9ff7507e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/dao/RefundRecordManager.java @@ -0,0 +1,34 @@ +package cn.bootx.daxpay.core.refund.dao; + +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.base.MpIdEntity; +import cn.bootx.common.mybatisplus.impl.BaseManager; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.refund.entity.RefundRecord; +import cn.bootx.daxpay.dto.refund.RefundRecordDto; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +import java.util.Objects; + +/** + * @author xxm + * @date 2022/3/2 + */ +@Slf4j +@Repository +@RequiredArgsConstructor +public class RefundRecordManager extends BaseManager { + + public Page page(PageParam pageParam, RefundRecordDto param) { + Page mpPage = MpUtil.getMpPage(pageParam, RefundRecord.class); + return lambdaQuery().orderByDesc(MpIdEntity::getId) + .like(Objects.nonNull(param.getPaymentId()), RefundRecord::getPaymentId, param.getPaymentId()) + .like(Objects.nonNull(param.getBusinessId()), RefundRecord::getBusinessId, param.getBusinessId()) + .like(Objects.nonNull(param.getTitle()), RefundRecord::getTitle, param.getTitle()) + .page(mpPage); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/dao/RefundRecordMapper.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/dao/RefundRecordMapper.java new file mode 100644 index 000000000..f0f98a71e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/dao/RefundRecordMapper.java @@ -0,0 +1,14 @@ +package cn.bootx.daxpay.core.refund.dao; + +import cn.bootx.daxpay.core.refund.entity.RefundRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author xxm + * @date 2022/3/2 + */ +@Mapper +public interface RefundRecordMapper extends BaseMapper { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/entity/RefundRecord.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/entity/RefundRecord.java new file mode 100644 index 000000000..309f93c1e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/entity/RefundRecord.java @@ -0,0 +1,82 @@ +package cn.bootx.daxpay.core.refund.entity; + +import cn.bootx.common.core.function.EntityBaseFunction; +import cn.bootx.common.mybatisplus.base.MpBaseEntity; +import cn.bootx.common.mybatisplus.handler.JacksonRawTypeHandler; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.core.refund.convert.RefundConvert; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import cn.bootx.daxpay.dto.refund.RefundRecordDto; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 退款记录 + * + * @author xxm + * @date 2022/3/2 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@TableName(value = "pay_refund_record", autoResultMap = true) +public class RefundRecord extends MpBaseEntity implements EntityBaseFunction { + + /** 支付单号 */ + private Long paymentId; + + /** 关联的业务id */ + private String businessId; + + /** 异步方式关联退款请求号(部分退款情况) */ + private String refundRequestNo; + + /** 用户ID */ + private Long userId; + + /** 标题 */ + private String title; + + /** 退款金额 */ + private BigDecimal amount; + + /** 剩余可退 */ + private BigDecimal refundableBalance; + + /** 退款终端ip */ + private String clientIp; + + /** 退款时间 */ + private LocalDateTime refundTime; + + /** + * 退款信息列表 + */ + @TableField(typeHandler = JacksonRawTypeHandler.class) + private List refundableInfo; + + /** + * 退款状态 + * @see PayStatusCode#REFUND_PROCESS_FAIL + */ + private int refundStatus; + + /** 错误码 */ + private String errorCode; + + /** 错误信息 */ + private String errorMsg; + + @Override + public RefundRecordDto toDto() { + return RefundConvert.CONVERT.convert(this); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/service/RefundRecordService.java b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/service/RefundRecordService.java new file mode 100644 index 000000000..6575a47b5 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/core/refund/service/RefundRecordService.java @@ -0,0 +1,43 @@ +package cn.bootx.daxpay.core.refund.service; + +import cn.bootx.common.core.exception.DataNotExistException; +import cn.bootx.common.core.rest.PageResult; +import cn.bootx.common.core.rest.param.PageParam; +import cn.bootx.common.mybatisplus.util.MpUtil; +import cn.bootx.daxpay.core.refund.dao.RefundRecordManager; +import cn.bootx.daxpay.core.refund.entity.RefundRecord; +import cn.bootx.daxpay.dto.refund.RefundRecordDto; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 退款 + * + * @author xxm + * @date 2022/3/2 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RefundRecordService { + + private final RefundRecordManager refundRecordManager; + + /** + * 分页查询 + */ + public PageResult page(PageParam pageParam, RefundRecordDto param) { + Page page = refundRecordManager.page(pageParam, param); + return MpUtil.convert2DtoPageResult(page); + } + + /** + * 根据id查询 + */ + public RefundRecordDto findById(Long id) { + return refundRecordManager.findById(id).map(RefundRecord::toDto).orElseThrow(DataNotExistException::new); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/notify/PayNotifyRecordDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/notify/PayNotifyRecordDto.java new file mode 100644 index 000000000..6a1f7832d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/notify/PayNotifyRecordDto.java @@ -0,0 +1,42 @@ +package cn.bootx.daxpay.dto.notify; + +import cn.bootx.common.core.rest.dto.BaseDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author xxm + * @date 2021/6/22 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "支付回调记录") +public class PayNotifyRecordDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = -1241346974397068912L; + + @Schema(description = "支付号") + private Long paymentId; + + @Schema(description = "通知消息") + private String notifyInfo; + + @Schema(description = "支付通道") + private Integer payChannel; + + @Schema(description = "处理状态") + private Integer status; + + @Schema(description = "提示信息") + private String msg; + + @Schema(description = "回调时间") + private LocalDateTime notifyTime; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/pay/AsyncPayInfo.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/pay/AsyncPayInfo.java new file mode 100644 index 000000000..966bf674b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/pay/AsyncPayInfo.java @@ -0,0 +1,29 @@ +package cn.bootx.daxpay.dto.pay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @date 2021/2/28 + */ +@Data +@Accessors(chain = true) +@Schema(title = "异步支付线程信息") +public class AsyncPayInfo implements Serializable { + + private static final long serialVersionUID = 8239742916705144905L; + + /** 支付参数体(通常用于发起支付的参数) */ + private String payBody; + + /** 第三方支付平台订单号(付款码支付直接成功时会出现) */ + private String tradeNo; + + /** 是否记录超时时间,默认记录 */ + private boolean expiredTime = true; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/pay/PayResult.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/pay/PayResult.java new file mode 100644 index 000000000..ec93336b6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/pay/PayResult.java @@ -0,0 +1,36 @@ +package cn.bootx.daxpay.dto.pay; + +import cn.bootx.daxpay.code.pay.PayStatusCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @date 2020/12/9 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付返回信息") +public class PayResult implements Serializable { + + private static final long serialVersionUID = 7729669194741851195L; + + @Schema(description = "是否是异步支付") + private boolean asyncPayMode; + + @Schema(description = "异步支付通道") + private Integer asyncPayChannel; + + /** + * @see PayStatusCode#TRADE_PROGRESS + */ + @Schema(description = "支付状态") + private int payStatus; + + @Schema(description = "异步支付参数") + private AsyncPayInfo asyncPayInfo; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/payconfig/PayChannelDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payconfig/PayChannelDto.java new file mode 100644 index 000000000..bfc04d234 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payconfig/PayChannelDto.java @@ -0,0 +1,43 @@ +package cn.bootx.daxpay.dto.payconfig; + +import cn.bootx.common.core.rest.dto.BaseDto; +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 支付通道 + * + * @author xxm + * @date 2021/6/30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class PayChannelDto extends BaseDto implements Serializable { + + /** + * 通道代码(唯一) + * @see PayChannelEnum + */ + private String code; + + /** 名称 */ + private String name; + + /** 页面展示:卡片-图标 */ + private String icon; + + /** 页面展示:卡片-背景色 */ + private String bgColor; + + /** 状态 */ + private Integer state; + + /** 备注 */ + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/payconfig/PayChannelWayDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payconfig/PayChannelWayDto.java new file mode 100644 index 000000000..51f1fe906 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payconfig/PayChannelWayDto.java @@ -0,0 +1,38 @@ +package cn.bootx.daxpay.dto.payconfig; + +import cn.bootx.common.core.rest.dto.BaseDto; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 支付通道支持的支付方式 + * + * @author xxm + * @date 2021/6/30 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +public class PayChannelWayDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = 4579797594767439540L; + + /** 支付方式代码 */ + private String code; + + /** 支付方式名称 */ + private String name; + + /** 通道id */ + private Long channelId; + + /** 通道code */ + private String channelCode; + + /** 备注 */ + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/BasePaymentDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/BasePaymentDto.java new file mode 100644 index 000000000..b3f8915e9 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/BasePaymentDto.java @@ -0,0 +1,47 @@ +package cn.bootx.daxpay.dto.payment; + +import cn.bootx.common.core.rest.dto.BaseDto; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @date 2021/2/25 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "具体支付日志基类") +public class BasePaymentDto extends BaseDto { + + @Schema(description = "支付id") + private Long paymentId; + + @Schema(description = "用户id") + private Long userId; + + @Schema(description = "关联的业务id") + private String businessId; + + @Schema(description = "金额") + private BigDecimal amount; + + @Schema(description = "可退款金额") + private BigDecimal refundableBalance; + + /** + * @see PayStatusCode + */ + @Schema(description = "支付状态") + private int payStatus; + + @Schema(description = "支付时间") + private LocalDateTime payTime; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/PayChannelInfo.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/PayChannelInfo.java new file mode 100644 index 000000000..3a46adb2e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/PayChannelInfo.java @@ -0,0 +1,33 @@ +package cn.bootx.daxpay.dto.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * @author xxm + * @date 2020/12/8 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付通道信息") +public class PayChannelInfo implements Serializable { + + private static final long serialVersionUID = -7757908686367215682L; + + @Schema(description = "支付通道") + private int payChannel; + + @Schema(description = "支付方式") + private int payWay; + + @Schema(description = "金额") + private BigDecimal amount; + + @Schema(description = "扩展参数的json字符串", hidden = true) + private String extraParamsJson; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/PaymentDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/PaymentDto.java new file mode 100644 index 000000000..79344a65e --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/PaymentDto.java @@ -0,0 +1,88 @@ +package cn.bootx.daxpay.dto.payment; + +import cn.bootx.common.core.rest.dto.BaseDto; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author xxm + * @date 2020/12/9 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "支付记录") +public class PaymentDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = 3269223993950227228L; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "关联的业务id") + private String businessId; + + @Schema(description = "标题") + private String title; + + @Schema(description = "描述") + private String description; + + @Schema(description = "是否是异步支付") + private boolean asyncPayMode; + + /** + * @see PayChannelCode + */ + @Schema(description = "异步支付通道") + private Integer asyncPayChannel; + + /** + * @see PayStatusCode + */ + @Schema(description = "支付状态") + private int payStatus; + + @Schema(description = "金额") + private BigDecimal amount; + + @Schema(description = "可退款余额") + private BigDecimal refundableBalance; + + @Schema(description = "错误码") + private String errorCode; + + @Schema(description = "错误信息") + private String errorMsg; + + /** + * @see PayChannelInfo + */ + @Schema(description = "支付通道信息") + private List payChannelInfo; + + /** + * @see RefundableInfo + */ + @Schema(description = "可退款信息列表") + private List refundableInfo; + + @Schema(description = "支付时间") + private LocalDateTime payTime; + + @Schema(description = "支付终端ip") + private String clientIp; + + @Schema(description = "过期时间") + private LocalDateTime expiredTime; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/RefundableInfo.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/RefundableInfo.java new file mode 100644 index 000000000..b8d00fba4 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/payment/RefundableInfo.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.dto.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 可退款信息 + * + * @author xxm + * @date 2022/3/2 + */ +@Data +@Accessors(chain = true) +@Schema(title = "可退款信息") +public class RefundableInfo { + + @Schema(description = "支付通道") + private int payChannel; + + @Schema(description = "金额") + private BigDecimal amount; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/alipay/AliPaymentDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/alipay/AliPaymentDto.java new file mode 100644 index 000000000..880c09329 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/alipay/AliPaymentDto.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.dto.paymodel.alipay; + +import cn.bootx.daxpay.dto.payment.BasePaymentDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @date 2021/2/27 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "支付宝支付记录") +public class AliPaymentDto extends BasePaymentDto implements Serializable { + + private static final long serialVersionUID = 6883103229754466130L; + + @Schema(description = "支付宝交易号") + private String tradeNo; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/alipay/AlipayConfigDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/alipay/AlipayConfigDto.java new file mode 100644 index 000000000..9982902ec --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/alipay/AlipayConfigDto.java @@ -0,0 +1,85 @@ +package cn.bootx.daxpay.dto.paymodel.alipay; + +import cn.bootx.common.core.rest.dto.BaseDto; +import cn.bootx.starter.data.perm.sensitive.SensitiveInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @author xxm + * @date 2021/2/26 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "支付宝配置") +public class AlipayConfigDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = 6641158663606363171L; + + @Schema(description = "名称") + private String name; + + @Schema(description = "支付宝商户appId") + @SensitiveInfo + private String appId; + + @Schema(description = "服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问") + private String notifyUrl; + + @Schema(description = "页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址") + private String returnUrl; + + @Schema(description = "请求网关地址") + private String serverUrl; + + @Schema(description = "认证类型 证书/公钥") + private Integer authType; + + @Schema(description = "签名类型") + private String signType; + + @Schema(description = "支付宝公钥") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String alipayPublicKey; + + @Schema(description = "私钥") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String privateKey; + + @Schema(description = "应用公钥证书") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String appCert; + + @Schema(description = "支付宝公钥证书文件") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String alipayCert; + + @Schema(description = "支付宝CA根证书文件") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String alipayRootCert; + + @Schema(description = "超时配置") + private Integer expireTime; + + @Schema(description = "可用支付方式") + private List payWayList; + + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + @Schema(description = "是否启用") + private Boolean activity; + + @Schema(description = "状态") + private Integer state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/union/UnionPayConfigDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/union/UnionPayConfigDto.java new file mode 100644 index 000000000..70664615d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/union/UnionPayConfigDto.java @@ -0,0 +1,19 @@ +package cn.bootx.daxpay.dto.paymodel.union; + +import cn.bootx.common.core.rest.dto.BaseDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @date 2022/3/11 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "云闪付配置") +public class UnionPayConfigDto extends BaseDto { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/voucher/VoucherDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/voucher/VoucherDto.java new file mode 100644 index 000000000..1251ac7f9 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/voucher/VoucherDto.java @@ -0,0 +1,50 @@ +package cn.bootx.daxpay.dto.paymodel.voucher; + +import cn.bootx.common.core.rest.dto.BaseDto; +import cn.bootx.daxpay.code.paymodel.VoucherCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @date 2022/3/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "储值卡") +public class VoucherDto extends BaseDto { + + @Schema(description = "卡号") + private String cardNo; + + @Schema(description = "生成批次号") + private Long batchNo; + + @Schema(description = "面值") + private BigDecimal faceValue; + + @Schema(description = "余额") + private BigDecimal balance; + + @Schema(description = "是否长期有效") + private Boolean enduring; + + @Schema(description = "开始时间") + private LocalDateTime startTime; + + @Schema(description = "结束时间") + private LocalDateTime endTime; + + /** + * @see VoucherCode + */ + @Schema(description = "状态") + private Integer status; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/voucher/VoucherPaymentDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/voucher/VoucherPaymentDto.java new file mode 100644 index 000000000..40c194466 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/voucher/VoucherPaymentDto.java @@ -0,0 +1,19 @@ +package cn.bootx.daxpay.dto.paymodel.voucher; + +import cn.bootx.daxpay.dto.payment.BasePaymentDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @date 2022/3/14 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "储值卡支付记录") +public class VoucherPaymentDto extends BasePaymentDto { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletDto.java new file mode 100644 index 000000000..163508778 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletDto.java @@ -0,0 +1,36 @@ +package cn.bootx.daxpay.dto.paymodel.wallet; + +import cn.bootx.common.core.rest.dto.BaseDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * @author xxm + * @date 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包") +public class WalletDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = -1563719305334334625L; + + @Schema(description = "ID,钱包的唯一标识") + private Long id; + + @Schema(description = "钱包关联的账号ID") + private Long userId; + + @Schema(description = "钱包余额") + private BigDecimal balance; + + @Schema(description = "状态 1启用 2禁用") + private Integer status; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletInfoDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletInfoDto.java new file mode 100644 index 000000000..3e94ad297 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletInfoDto.java @@ -0,0 +1,23 @@ +package cn.bootx.daxpay.dto.paymodel.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 钱包综合信息 + * + * @author xxm + * @date 2022/3/13 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包综合信息") +public class WalletInfoDto extends WalletDto { + + @Schema(description = "钱包关联的账号名称") + private String userName; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletLogDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletLogDto.java new file mode 100644 index 000000000..2b868b0a0 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletLogDto.java @@ -0,0 +1,59 @@ +package cn.bootx.daxpay.dto.paymodel.wallet; + +import cn.bootx.common.core.rest.dto.BaseDto; +import cn.bootx.daxpay.code.paymodel.WalletCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @date 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包日志") +public class WalletLogDto extends BaseDto implements Serializable { + + private static final long serialVersionUID = -2553004953931903738L; + + @Schema(description = "钱包ID") + private Long walletId; + + @Schema(description = "用户ID") + private Long userId; + + /** + * @see WalletCode + */ + @Schema(description = "类型") + private Integer type; + + @Schema(description = "交易记录ID") + private Long paymentId; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "业务ID") + private String businessId; + + /** + * @see WalletCode + */ + @Schema(description = " 1 系统操作 2管理员操作 3用户操作") + private Integer operationSource; + + @Schema(description = "金额") + private BigDecimal amount; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletPaymentDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletPaymentDto.java new file mode 100644 index 000000000..4b6288728 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wallet/WalletPaymentDto.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.dto.paymodel.wallet; + +import cn.bootx.daxpay.dto.payment.BasePaymentDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @date 2020/12/8 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "钱包支付记录") +public class WalletPaymentDto extends BasePaymentDto implements Serializable { + + private static final long serialVersionUID = 8238920331255597223L; + + @Schema(description = "钱包ID") + private Long walletId; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wechat/WeChatPayConfigDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wechat/WeChatPayConfigDto.java new file mode 100644 index 000000000..896869ce6 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wechat/WeChatPayConfigDto.java @@ -0,0 +1,91 @@ +package cn.bootx.daxpay.dto.paymodel.wechat; + +import cn.bootx.common.core.rest.dto.BaseDto; +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import cn.bootx.starter.data.perm.sensitive.SensitiveInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @author xxm + * @date 2021/3/19 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "微信支付配置") +public class WeChatPayConfigDto extends BaseDto implements Serializable { + + @Schema(description = "名称") + private String name; + + @Schema(description = "微信商户号") + @SensitiveInfo + private String mchId; + + @Schema(description = "微信应用appId") + @SensitiveInfo + private String appId; + + /** + * @see WeChatPayCode#API_V2 + */ + @Schema(description = "api版本") + private String apiVersion; + + @Schema(description = "商户平台「API安全」中的 APIv2 密钥") + @SensitiveInfo + private String apiKeyV2; + + @Schema(description = "商户平台「API安全」中的 APIv3 密钥") + @SensitiveInfo + private String apiKeyV3; + + @Schema(description = "APPID对应的接口密码,用于获取接口调用凭证access_token时使用") + @SensitiveInfo + private String appSecret; + + @Schema(description = "API 证书中的 p12 文件id") + private Long p12; + + @Schema(description = "API 证书中的 cert.pem 证书") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String certPem; + + @Schema(description = "API 证书中的 key.pem 私钥") + @SensitiveInfo(value = SensitiveInfo.SensitiveType.OTHER, front = 15) + private String keyPem; + + @Schema(description = "应用域名,回调中会使用此参数") + private String domain; + + @Schema(description = "服务器异步通知页面路径 通知url必须为直接可访问的url,不能携带参数。公网域名必须为https ") + private String notifyUrl; + + @Schema(description = "页面跳转同步通知页面路径") + private String returnUrl; + + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + @Schema(description = "超时时间(分钟)") + private Integer expireTime; + + @Schema(description = "可用支付方式") + private List payWayList; + + @Schema(description = "是否启用") + private Boolean activity; + + @Schema(description = "状态") + private Integer state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wechat/WeChatPaymentDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wechat/WeChatPaymentDto.java new file mode 100644 index 000000000..94f5be64c --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/paymodel/wechat/WeChatPaymentDto.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.dto.paymodel.wechat; + +import cn.bootx.daxpay.dto.payment.BasePaymentDto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @date 2021/6/21 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "微信支付记录") +public class WeChatPaymentDto extends BasePaymentDto implements Serializable { + + private static final long serialVersionUID = -2400358210732595795L; + + @Schema(description = "微信交易号") + private String tradeNo; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/dto/refund/RefundRecordDto.java b/dax-pay/src/main/java/cn/bootx/daxpay/dto/refund/RefundRecordDto.java new file mode 100644 index 000000000..7e2a70dcb --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/dto/refund/RefundRecordDto.java @@ -0,0 +1,69 @@ +package cn.bootx.daxpay.dto.refund; + +import cn.bootx.common.core.rest.dto.BaseDto; +import cn.bootx.daxpay.code.pay.PayStatusCode; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 退款记录 + * + * @author xxm + * @date 2022/3/2 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@Accessors(chain = true) +@Schema(title = "退款记录") +public class RefundRecordDto extends BaseDto { + + @Schema(description = "关联的业务id") + private String businessId; + + @Schema(description = "付款付单号") + private Long paymentId; + + @Schema(description = "异步方式关联退款请求号(部分退款情况)") + private String refundRequestNo; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "标题") + private String title; + + @Schema(description = "退款金额") + private BigDecimal amount; + + @Schema(description = "剩余可退") + private BigDecimal refundableBalance; + + @Schema(description = "退款终端ip") + private String clientIp; + + @Schema(description = "退款时间") + private LocalDateTime refundTime; + + @Schema(description = "退款信息列表") + private List refundableInfo; + + /** + * @see PayStatusCode#REFUND_PROCESS_FAIL + */ + @Schema(description = "退款状态") + private int refundStatus; + + @Schema(description = "错误码") + private String errorCode; + + @Schema(description = "错误信息") + private String errorMsg; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/event/PayCancelEvent.java b/dax-pay/src/main/java/cn/bootx/daxpay/event/PayCancelEvent.java new file mode 100644 index 000000000..8771bfc16 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/event/PayCancelEvent.java @@ -0,0 +1,22 @@ +package cn.bootx.daxpay.event; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付撤销事件 + * + * @author xxm + * @date 2022/7/11 + */ +@Data +@Accessors(chain = true) +public class PayCancelEvent { + + /** 支付单ID */ + private Long paymentId; + + /** 业务单号 */ + private String businessId; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/event/PayCompleteEvent.java b/dax-pay/src/main/java/cn/bootx/daxpay/event/PayCompleteEvent.java new file mode 100644 index 000000000..0eeaed348 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/event/PayCompleteEvent.java @@ -0,0 +1,22 @@ +package cn.bootx.daxpay.event; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付完成事件 + * + * @author xxm + * @date 2022/7/11 + */ +@Data +@Accessors(chain = true) +public class PayCompleteEvent { + + /** 支付单ID */ + private Long paymentId; + + /** 业务单号 */ + private String businessId; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/event/PayRefundEvent.java b/dax-pay/src/main/java/cn/bootx/daxpay/event/PayRefundEvent.java new file mode 100644 index 000000000..7094bda13 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/event/PayRefundEvent.java @@ -0,0 +1,22 @@ +package cn.bootx.daxpay.event; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付退款事件 + * + * @author xxm + * @date 2022/7/11 + */ +@Data +@Accessors(chain = true) +public class PayRefundEvent { + + /** 支付单ID */ + private Long paymentId; + + /** 业务单号 */ + private String businessId; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayAmountAbnormalException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayAmountAbnormalException.java new file mode 100644 index 000000000..fb0e398b4 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayAmountAbnormalException.java @@ -0,0 +1,22 @@ +package cn.bootx.daxpay.exception.payment; + +import cn.bootx.common.core.exception.FatalException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 异常金额 + * + * @author xxm + * @date 2020/12/8 + */ +public class PayAmountAbnormalException extends FatalException { + + public PayAmountAbnormalException(String msg) { + super(PaymentCenterErrorCode.PAYMENT_AMOUNT_ABNORMAL, msg); + } + + public PayAmountAbnormalException() { + super(PaymentCenterErrorCode.PAYMENT_AMOUNT_ABNORMAL, "异常金额"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayFailureException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayFailureException.java new file mode 100644 index 000000000..19a33a5da --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayFailureException.java @@ -0,0 +1,22 @@ +package cn.bootx.daxpay.exception.payment; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 付款错误 + * + * @author xxm + * @date 2020/12/8 + */ +public class PayFailureException extends BizException { + + public PayFailureException(String message) { + super(PaymentCenterErrorCode.PAY_FAILURE, message); + } + + public PayFailureException() { + super(PaymentCenterErrorCode.PAY_FAILURE, "支付失败"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayHasExistedException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayHasExistedException.java new file mode 100644 index 000000000..b1ef75d82 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayHasExistedException.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.payment; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 付款已存在 + * + * @author xxm + * @date 2020/12/8 + */ +public class PayHasExistedException extends BizException { + + public PayHasExistedException() { + super(PaymentCenterErrorCode.PAYMENT_HAS_EXISTED, "付款已存在"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayIsProcessingException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayIsProcessingException.java new file mode 100644 index 000000000..3bb562140 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayIsProcessingException.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.payment; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 付款正在处理中 + * + * @author xxm + * @date 2020/12/8 + */ +public class PayIsProcessingException extends BizException { + + public PayIsProcessingException() { + super(PaymentCenterErrorCode.PAYMENT_IS_PROCESSING, "付款正在处理中"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayNotExistedException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayNotExistedException.java new file mode 100644 index 000000000..3407c7f76 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayNotExistedException.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.payment; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 付款记录不存在 + * + * @author xxm + * @date 2020/12/8 + */ +public class PayNotExistedException extends BizException { + + public PayNotExistedException() { + super(PaymentCenterErrorCode.PAYMENT_RECORD_NOT_EXISTED, "付款记录不存在"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayUnsupportedMethodException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayUnsupportedMethodException.java new file mode 100644 index 000000000..97ccd1bdd --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/payment/PayUnsupportedMethodException.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.payment; + +import cn.bootx.common.core.exception.FatalException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 付款方式不支持异常 + * + * @author xxm + * @date 2020/12/9 + */ +public class PayUnsupportedMethodException extends FatalException { + + public PayUnsupportedMethodException() { + super(PaymentCenterErrorCode.PAYMENT_METHOD_UNSUPPORT, "不支持的支付方式"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletBannedException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletBannedException.java new file mode 100644 index 000000000..c4e384859 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletBannedException.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.waller; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 钱包被禁用 + * + * @author xxm + * @date 2020/12/8 + */ +public class WalletBannedException extends BizException { + + public WalletBannedException() { + super(PaymentCenterErrorCode.WALLET_BANNED, "钱包被禁用"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletInfoNotExistException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletInfoNotExistException.java new file mode 100644 index 000000000..39fa56fc1 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletInfoNotExistException.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.waller; + +import cn.bootx.common.core.exception.FatalException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 钱包信息不存在 + * + * @author xxm + * @date 2020/12/8 + */ +public class WalletInfoNotExistException extends FatalException { + + public WalletInfoNotExistException() { + super(PaymentCenterErrorCode.WALLET_INFO_NOT_EXISTS, "钱包信息不存在"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletLackOfBalanceException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletLackOfBalanceException.java new file mode 100644 index 000000000..a33ca77e5 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletLackOfBalanceException.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.waller; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 余额不足异常 + * + * @author xxm + * @date 2020/12/8 + */ +public class WalletLackOfBalanceException extends BizException { + + public WalletLackOfBalanceException() { + super(PaymentCenterErrorCode.WALLET_BALANCE_NOT_ENOUGH, "余额不足异常"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletLogError.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletLogError.java new file mode 100644 index 000000000..9ea9b785d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletLogError.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.waller; + +import cn.bootx.common.core.exception.FatalException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 钱包日志错误 + * + * @author xxm + * @date 2020/12/8 + */ +public class WalletLogError extends FatalException { + + public WalletLogError() { + super(PaymentCenterErrorCode.WALLET_LOG_ERROR, "钱包日志错误"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletNotExistsException.java b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletNotExistsException.java new file mode 100644 index 000000000..2a6636fbf --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/exception/waller/WalletNotExistsException.java @@ -0,0 +1,18 @@ +package cn.bootx.daxpay.exception.waller; + +import cn.bootx.common.core.exception.BizException; +import cn.bootx.daxpay.code.PaymentCenterErrorCode; + +/** + * 钱包不存在 + * + * @author xxm + * @date 2020/12/8 + */ +public class WalletNotExistsException extends BizException { + + public WalletNotExistsException() { + super(PaymentCenterErrorCode.WALLET_NOT_EXISTS, "钱包不存在"); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/mq/PaymentEventSender.java b/dax-pay/src/main/java/cn/bootx/daxpay/mq/PaymentEventSender.java new file mode 100644 index 000000000..75bd7fb16 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/mq/PaymentEventSender.java @@ -0,0 +1,64 @@ +package cn.bootx.daxpay.mq; + +import cn.bootx.daxpay.code.PaymentEventCode; +import cn.bootx.daxpay.event.PayCancelEvent; +import cn.bootx.daxpay.event.PayCompleteEvent; +import cn.bootx.daxpay.event.PayRefundEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.retry.annotation.Retryable; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +/** + * 支付中心消息发送器 + * + * @author xxm + * @date 2021/4/22 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PaymentEventSender { + + private final RabbitTemplate rabbitTemplate; + + /** + * 支付完成 事件发布 + */ + @Async("bigExecutor") + @Retryable(value = Exception.class) + public void sendPayComplete(PayCompleteEvent event) { + rabbitTemplate.convertAndSend(PaymentEventCode.EXCHANGE_PAYMENT, PaymentEventCode.PAY_COMPLETE, event); + } + + /** + * 支付撤销/关闭 事件发布 + */ + @Async("bigExecutor") + @Retryable(value = Exception.class) + public void sendPayCancel(PayCancelEvent event) { + rabbitTemplate.convertAndSend(PaymentEventCode.EXCHANGE_PAYMENT, PaymentEventCode.PAY_CANCEL, event); + } + + /** + * 支付退款 事件发布 + */ + @Async("bigExecutor") + @Retryable(value = Exception.class) + public void sendPayRefund(PayRefundEvent event) { + rabbitTemplate.convertAndSend(PaymentEventCode.EXCHANGE_PAYMENT, PaymentEventCode.PAY_REFUND, event); + } + + /** + * 支付单超时 事件发布 + */ + @Async("bigExecutor") + @Retryable(value = Exception.class) + public void sendPaymentExpiredTime(Long paymentId) { + rabbitTemplate.convertAndSend(PaymentEventCode.EXCHANGE_PAYMENT, PaymentEventCode.PAYMENT_EXPIRED_TIME, + paymentId); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/mq/PaymentMessageListener.java b/dax-pay/src/main/java/cn/bootx/daxpay/mq/PaymentMessageListener.java new file mode 100644 index 000000000..294885ce5 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/mq/PaymentMessageListener.java @@ -0,0 +1,60 @@ +package cn.bootx.daxpay.mq; + +import cn.bootx.common.rabbit.conditional.ConditionalOnRabbit; +import cn.bootx.daxpay.code.PaymentEventCode; +import cn.bootx.daxpay.core.pay.service.PayExpiredTimeService; +import cn.bootx.daxpay.event.PayCancelEvent; +import cn.bootx.daxpay.event.PayCompleteEvent; +import cn.bootx.daxpay.event.PayRefundEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +/** + * 消息接收 + * + * @author xxm + * @date 2021/4/22 + */ +@Slf4j +@Component +@ConditionalOnRabbit +@RequiredArgsConstructor +public class PaymentMessageListener { + + private final PayExpiredTimeService payExpiredTimeService; + + /** + * 支付超时事件处理 + */ + @RabbitListener(queues = PaymentEventCode.PAYMENT_EXPIRED_TIME) + public void PaymentExpiredTime(Long paymentId) { + payExpiredTimeService.expiredTime(paymentId); + } + + /** + * 支付成功 + */ + @RabbitListener(queues = PaymentEventCode.PAY_COMPLETE) + public void payCancel(PayCompleteEvent event) { + log.info("支付完成事件:{}", event); + } + + /** + * 支付撤销/关闭 + */ + @RabbitListener(queues = PaymentEventCode.PAY_CANCEL) + public void payCancel(PayCancelEvent event) { + log.info("支付撤销/关闭事件:{}", event); + } + + /** + * 支付退款 + */ + @RabbitListener(queues = PaymentEventCode.PAY_REFUND) + public void payCancel(PayRefundEvent event) { + log.info("支付退款事件:{}", event); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/cashier/CashierCombinationPayParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/cashier/CashierCombinationPayParam.java new file mode 100644 index 000000000..b7f70dbd9 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/cashier/CashierCombinationPayParam.java @@ -0,0 +1,34 @@ +package cn.bootx.daxpay.param.cashier; + +import cn.bootx.daxpay.param.pay.PayModeParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.util.List; + +/** + * 结算台发起支付参数 + * + * @author xxm + * @date 2022/2/23 + */ +@Data +@Accessors(chain = true) +@Schema(title = "结算台组合支付参数") +public class CashierCombinationPayParam { + + @Schema(description = "标题") + private String title; + + @Schema(description = "金额") + private BigDecimal amount; + + @Schema(description = "业务id") + private String businessId; + + @Schema(description = "支付信息", required = true) + private List payModeList; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/cashier/CashierSinglePayParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/cashier/CashierSinglePayParam.java new file mode 100644 index 000000000..9fe2ced1b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/cashier/CashierSinglePayParam.java @@ -0,0 +1,44 @@ +package cn.bootx.daxpay.param.cashier; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 结算台发起支付参数 + * + * @author xxm + * @date 2022/2/23 + */ +@Data +@Accessors(chain = true) +@Schema(title = "结算台单支付参数") +public class CashierSinglePayParam { + + @Schema(description = "标题") + private String title; + + @Schema(description = "金额") + private BigDecimal amount; + + @Schema(description = "openId") + private String openId; + + @Schema(description = "支付通道") + private Integer payChannel; + + @Schema(description = "支付方式") + private Integer payWay; + + @Schema(description = "业务id") + private String businessId; + + @Schema(description = "付款码") + private String authCode; + + @Schema(description = "储值卡") + private String voucherNo; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/pay/PayModeParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/pay/PayModeParam.java new file mode 100644 index 000000000..1c0d1ee94 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/pay/PayModeParam.java @@ -0,0 +1,65 @@ +package cn.bootx.daxpay.param.pay; + +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayWayCode; +import cn.bootx.daxpay.core.pay.convert.PayConvert; +import cn.bootx.daxpay.dto.payment.PayChannelInfo; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import cn.bootx.daxpay.param.paymodel.alipay.AliPayParam; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherPayParam; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 不只是支付, 退款发起时也是用着参数 + * + * @author xxm + * @date 2020/12/8 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付方式参数") +public class PayModeParam implements Serializable { + + private static final long serialVersionUID = -46959864485463681L; + + /** + * @see PayChannelCode + */ + @Schema(description = "支付通道", required = true) + private int payChannel; + + /** + * @see PayWayCode + */ + @Schema(description = "支付通道下属支付方式", required = true) + private int payWay; + + @Schema(description = "支付金额", required = true) + private BigDecimal amount; + + /** + * @see AliPayParam + * @see WeChatPayParam + * @see VoucherPayParam + */ + @Schema(description = "扩展参数的json字符串") + private String extraParamsJson; + + public PayChannelInfo toPayTypeInfo() { + return PayConvert.CONVERT.convert(this); + } + + /** + * 转换为可退款信息 + */ + public RefundableInfo toRefundableInfo() { + return new RefundableInfo().setPayChannel(getPayChannel()).setAmount(getAmount()); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/pay/PayParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/pay/PayParam.java new file mode 100644 index 000000000..1a1c4ec60 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/pay/PayParam.java @@ -0,0 +1,42 @@ +package cn.bootx.daxpay.param.pay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.List; + +/** + * @author xxm + * @date 2020/12/9 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付参数") +public class PayParam implements Serializable { + + private static final long serialVersionUID = 3895679513150533566L; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "业务ID") + @NotEmpty(message = "业务ID不可为空") + private String businessId; + + @Schema(description = "支付标题") + @NotEmpty(message = "支付标题不可为空") + private String title; + + @Schema(description = "支付描述") + private String description; + + @Schema(description = "支付信息参数", required = true) + @NotEmpty(message = "支付信息参数不可为空") + @Valid + private List payModeList; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/payconfig/PayChannelParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/payconfig/PayChannelParam.java new file mode 100644 index 000000000..a1e061515 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/payconfig/PayChannelParam.java @@ -0,0 +1,41 @@ +package cn.bootx.daxpay.param.payconfig; + +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付通道 + * + * @author xxm + * @date 2021/6/30 + */ +@Data +@Accessors(chain = true) +public class PayChannelParam { + + /** 主键 */ + private Long id; + + /** + * 通道代码(唯一) + * @see PayChannelEnum + */ + private String code; + + /** 名称 */ + private String name; + + /** 页面展示:卡片-图标 */ + private String icon; + + /** 页面展示:卡片-背景色 */ + private String bgColor; + + /** 状态 */ + private Integer state; + + /** 备注 */ + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/payconfig/PayChannelWayParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/payconfig/PayChannelWayParam.java new file mode 100644 index 000000000..19ddc4e92 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/payconfig/PayChannelWayParam.java @@ -0,0 +1,28 @@ +package cn.bootx.daxpay.param.payconfig; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 支付通道支持的支付方式 + * + * @author xxm + * @date 2021/6/30 + */ +@Data +@Accessors(chain = true) +public class PayChannelWayParam { + + /** 通道id */ + private Long channelId; + + /** 支付方式代码 */ + private String code; + + /** 支付方式名称 */ + private String name; + + /** 备注 */ + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/payment/PaymentQuery.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/payment/PaymentQuery.java new file mode 100644 index 000000000..147c19df1 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/payment/PaymentQuery.java @@ -0,0 +1,29 @@ +package cn.bootx.daxpay.param.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @date 2021/7/21 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付记录查询参数") +public class PaymentQuery implements Serializable { + + private static final long serialVersionUID = 7071042101962400106L; + + @Schema(description = "支付单id") + private String paymentId; + + @Schema(description = "关联的业务id") + private String businessId; + + @Schema(description = "关联的业务id") + private String title; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AliPayParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AliPayParam.java new file mode 100644 index 000000000..9aa49c87b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AliPayParam.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.param.paymodel.alipay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @date 2021/2/27 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付宝支付参数") +public class AliPayParam implements Serializable { + + private static final long serialVersionUID = 7467373358780663978L; + + @Schema(description = "授权码(主动扫描用户的付款码)") + private String authCode; + + @Schema(description = "页面跳转同步通知页面路径") + private String returnUrl; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AlipayConfigParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AlipayConfigParam.java new file mode 100644 index 000000000..ad10354f5 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AlipayConfigParam.java @@ -0,0 +1,73 @@ +package cn.bootx.daxpay.param.paymodel.alipay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * @author xxm + * @date 2021/2/26 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付宝配置参数") +public class AlipayConfigParam implements Serializable { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "支付宝商户appId") + private String appId; + + @Schema(description = "服务器异步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问") + private String notifyUrl; + + @Schema(description = "页面跳转同步通知页面路径 需http://或者https://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问 商户可以自定义同步跳转地址") + private String returnUrl; + + @Schema(description = "请求网关地址") + private String serverUrl; + + @Schema(description = "认证类型 证书/公钥") + private Integer authType; + + @Schema(description = "签名类型") + public String signType; + + @Schema(description = "支付宝公钥") + public String alipayPublicKey; + + @Schema(description = "私钥") + private String privateKey; + + @Schema(description = "应用公钥证书") + private String appCert; + + @Schema(description = "支付宝公钥证书文件") + private String alipayCert; + + @Schema(description = "支付宝CA根证书文件") + private String alipayRootCert; + + @Schema(description = "超时配置") + private Integer expireTime; + + @Schema(description = "可用支付方式") + private List payWayList; + + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + @Schema(description = "状态") + private Integer state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AlipayConfigQuery.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AlipayConfigQuery.java new file mode 100644 index 000000000..98769749a --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/alipay/AlipayConfigQuery.java @@ -0,0 +1,29 @@ +package cn.bootx.daxpay.param.paymodel.alipay; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * @author xxm + * @date 2021/7/22 + */ +@Data +@Accessors(chain = true) +@Schema(title = "支付宝配置搜索参数") +public class AlipayConfigQuery implements Serializable { + + private static final long serialVersionUID = -173325268481050362L; + + /** 名称 */ + private String name; + + /** 状态 */ + private Integer state; + + /** 支付宝商户appId */ + private String appId; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherGenerationParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherGenerationParam.java new file mode 100644 index 000000000..816a7509b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherGenerationParam.java @@ -0,0 +1,45 @@ +package cn.bootx.daxpay.param.paymodel.voucher; + +import cn.bootx.daxpay.code.paymodel.VoucherCode; +import cn.hutool.core.date.DatePattern; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Data +@Accessors(chain = true) +@Schema(title = "储值卡生成参数") +public class VoucherGenerationParam { + + @Schema(description = "数量") + private Integer count; + + @Schema(description = "面值") + private BigDecimal faceValue; + + @Schema(description = "是否长期有效") + private Boolean enduring; + + @Schema(description = "开始时间") + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime startTime; + + @Schema(description = "结束时间") + @JsonFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime endTime; + + /** + * @see VoucherCode + */ + @Schema(description = "默认状态") + private Integer status; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherImportParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherImportParam.java new file mode 100644 index 000000000..05b64325b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherImportParam.java @@ -0,0 +1,16 @@ +package cn.bootx.daxpay.param.paymodel.voucher; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Data +@Accessors(chain = true) +@Schema(title = "储值卡导入参数") +public class VoucherImportParam { + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherParam.java new file mode 100644 index 000000000..a4890ed50 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherParam.java @@ -0,0 +1,54 @@ +package cn.bootx.daxpay.param.paymodel.voucher; + +import cn.bootx.daxpay.code.paymodel.VoucherCode; +import cn.hutool.core.date.DatePattern; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * @author xxm + * @date 2022/3/14 + */ +@Data +@Accessors(chain = true) +@Schema(title = "储值卡查询参数") +public class VoucherParam { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "卡号") + private String cardNo; + + @Schema(description = "生成批次号") + private Long batchNo; + + @Schema(description = "面值") + private BigDecimal faceValue; + + @Schema(description = "余额") + private BigDecimal balance; + + @Schema(description = "是否长期有效") + private Boolean enduring; + + @Schema(description = "开始时间") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime startTime; + + @Schema(description = "结束时间") + @DateTimeFormat(pattern = DatePattern.NORM_DATETIME_PATTERN) + private LocalDateTime endTime; + + /** + * @see VoucherCode + */ + @Schema(description = "状态") + private Integer status; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherPayParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherPayParam.java new file mode 100644 index 000000000..651914948 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/voucher/VoucherPayParam.java @@ -0,0 +1,23 @@ +package cn.bootx.daxpay.param.paymodel.voucher; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 储值卡支付参数 + * + * @author xxm + * @date 2022/3/14 + */ +@Data +@Accessors(chain = true) +@Schema(title = "储值卡支付参数") +public class VoucherPayParam { + + @Schema(description = "储值卡号") + private List cardNoList; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletLogQueryParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletLogQueryParam.java new file mode 100644 index 000000000..6a4287f38 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletLogQueryParam.java @@ -0,0 +1,37 @@ +package cn.bootx.daxpay.param.paymodel.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * @author xxm + * @date 2020/12/8 + */ +@Data +@Accessors(chain = true) +@Schema(title = "钱包日志查询参数") +public class WalletLogQueryParam implements Serializable { + + private static final long serialVersionUID = -4046664021959786637L; + + @Schema(description = "钱包ID (与userId至少存在一个)") + private Long walletId; + + @Schema(description = "用户ID (钱包至少存在一个)") + private Long userId; + + @Schema(description = "开始日期") + private LocalDateTime startDate; + + @Schema(description = "结束日期") + private LocalDateTime endDate; + + @Schema(description = "日志类型,不传则查询全部") + private List type; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletPayParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletPayParam.java new file mode 100644 index 000000000..717b2245b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletPayParam.java @@ -0,0 +1,35 @@ +package cn.bootx.daxpay.param.paymodel.wallet; + +import cn.bootx.daxpay.code.paymodel.WalletCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 钱包支付参数 + * + * @author xxm + * @date 2020/12/8 + */ +@Data +@Accessors(chain = true) +@Schema(title = "钱包支付参数") +public class WalletPayParam implements Serializable { + + private static final long serialVersionUID = 3255160458016870367L; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "操作人") + private Long operatorId; + + /** + * @see WalletCode + */ + @Schema(description = "操作源 1系统 2管理员 3用户") + private Integer operationSource; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletRechargeParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletRechargeParam.java new file mode 100644 index 000000000..b01db8f32 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wallet/WalletRechargeParam.java @@ -0,0 +1,42 @@ +package cn.bootx.daxpay.param.paymodel.wallet; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * @author xxm + * @date 2020/12/8 + */ +@Data +@Accessors(chain = true) +@Schema(title = "钱包充值参数") +public class WalletRechargeParam implements Serializable { + + private static final long serialVersionUID = 73058709379178254L; + + @Schema(description = "钱包ID") + private Long walletId; + + @Schema(description = "支付记录ID") + private Long paymentId; + + @Schema(description = "充值金额") + private BigDecimal amount; + + @Schema(description = "类型 2 主动充值 3 自动充值 4 admin充值") + private Integer type; + + @Schema(description = "业务ID,对应的充值订单ID等") + private String businessId; + + @Schema(description = "操作源") + private Integer operationSource; + + @Schema(description = "订单id") + private Long orderId; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wechat/WeChatPayConfigParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wechat/WeChatPayConfigParam.java new file mode 100644 index 000000000..02c779b47 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wechat/WeChatPayConfigParam.java @@ -0,0 +1,84 @@ +package cn.bootx.daxpay.param.paymodel.wechat; + +import cn.bootx.daxpay.code.paymodel.WeChatPayCode; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 微信支付配置参数 + * + * @author xxm + * @date 2022/7/7 + */ +@Data +@Accessors(chain = true) +@Schema(title = "微信支付配置参数") +public class WeChatPayConfigParam { + + @Schema(description = "主键") + private Long id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "微信商户号") + private String mchId; + + @Schema(description = "微信应用appId") + private String appId; + + /** + * @see WeChatPayCode#API_V2 + */ + @Schema(description = "api版本") + private String apiVersion; + + @Schema(description = "商户平台「API安全」中的 APIv2 密钥") + private String apiKeyV2; + + @Schema(description = "商户平台「API安全」中的 APIv3 密钥") + private String apiKeyV3; + + @Schema(description = "APPID对应的接口密码,用于获取接口调用凭证access_token时使用") + private String appSecret; + + @Schema(description = "API 证书中的 p12 文件id") + private Long p12; + + @Schema(description = "API 证书中的 cert.pem 证书") + private String certPem; + + @Schema(description = "API 证书中的 key.pem 私钥") + private String keyPem; + + @Schema(description = "应用域名,回调中会使用此参数") + private String domain; + + @Schema(description = "服务器异步通知页面路径 通知url必须为直接可访问的url,不能携带参数。公网域名必须为https ") + private String notifyUrl; + + @Schema(description = "页面跳转同步通知页面路径") + private String returnUrl; + + @Schema(description = "是否沙箱环境") + private boolean sandbox; + + @Schema(description = "超时时间(分钟)") + private Integer expireTime; + + @Schema(description = "可用支付方式") + private List payWayList; + + @Schema(description = "是否启用") + private Boolean activity; + + @Schema(description = "状态") + private Integer state; + + @Schema(description = "备注") + private String remark; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wechat/WeChatPayParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wechat/WeChatPayParam.java new file mode 100644 index 000000000..e49faea6d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/paymodel/wechat/WeChatPayParam.java @@ -0,0 +1,22 @@ +package cn.bootx.daxpay.param.paymodel.wechat; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * @author xxm + * @date 2021/6/21 + */ +@Data +@Accessors(chain = true) +@Schema(title = "微信支付参数") +public class WeChatPayParam { + + @Schema(description = "微信openId") + private String openId; + + @Schema(description = "授权码(主动扫描用户的付款码)") + private String authCode; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/refund/RefundModeParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/refund/RefundModeParam.java new file mode 100644 index 000000000..94502d069 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/refund/RefundModeParam.java @@ -0,0 +1,46 @@ +package cn.bootx.daxpay.param.refund; + +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.dto.payment.RefundableInfo; +import cn.bootx.daxpay.param.pay.PayModeParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.math.BigDecimal; + +/** + * 退款方式参数 + * + * @author xxm + * @date 2022/3/2 + */ +@Data +@Accessors(chain = true) +@Schema(title = "退款方式参数") +public class RefundModeParam { + + /** + * @see PayChannelCode + */ + @Schema(description = "支付通道", required = true) + private int payChannel; + + @Schema(description = "支付金额", required = true) + private BigDecimal amount; + + /** + * 转换成支付方式参数 + */ + public PayModeParam toPayModeParam() { + return new PayModeParam().setPayChannel(getPayChannel()).setAmount(getAmount()); + } + + /** + * 转换成退款方式记录对象 + */ + public RefundableInfo toRefundableInfo() { + return new RefundableInfo().setPayChannel(getPayChannel()).setAmount(getAmount()); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/param/refund/RefundParam.java b/dax-pay/src/main/java/cn/bootx/daxpay/param/refund/RefundParam.java new file mode 100644 index 000000000..de8611bd1 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/param/refund/RefundParam.java @@ -0,0 +1,26 @@ +package cn.bootx.daxpay.param.refund; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 退款参数 + * + * @author xxm + * @date 2020/12/10 + */ +@Data +@Accessors(chain = true) +@Schema(title = "退款参数") +public class RefundParam { + + @Schema(description = "业务id") + private String businessId; + + @Schema(description = "各通道退款参数") + private List refundModeParams; + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/task/PayExpiredTimeTask.java b/dax-pay/src/main/java/cn/bootx/daxpay/task/PayExpiredTimeTask.java new file mode 100644 index 000000000..921ccb12d --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/task/PayExpiredTimeTask.java @@ -0,0 +1,31 @@ +package cn.bootx.daxpay.task; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.PersistJobDataAfterExecution; +import org.springframework.stereotype.Component; + +/** + * 超时支付单任务撤销(2-5秒轮训一次) + * + * @author xxm + * @date 2022/7/12 + */ +@Slf4j +@Component +@DisallowConcurrentExecution +@PersistJobDataAfterExecution +@RequiredArgsConstructor +public class PayExpiredTimeTask implements Job { + + private final PayExpiredTimeTaskService payExpiredTimeTaskService; + + @Override + public void execute(JobExecutionContext context) { + payExpiredTimeTaskService.sync(); + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/task/PayExpiredTimeTaskService.java b/dax-pay/src/main/java/cn/bootx/daxpay/task/PayExpiredTimeTaskService.java new file mode 100644 index 000000000..65606f1c0 --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/task/PayExpiredTimeTaskService.java @@ -0,0 +1,43 @@ +package cn.bootx.daxpay.task; + +import cn.bootx.common.core.util.CollUtil; +import cn.bootx.daxpay.core.payment.dao.PaymentExpiredTimeRepository; +import cn.bootx.daxpay.mq.PaymentEventSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 支付超时任务撤销消息注册 + * + * @author xxm + * @date 2022/7/12 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PayExpiredTimeTaskService { + + private final PaymentExpiredTimeRepository expiredTimeRepository; + + private final PaymentEventSender paymentEventSender; + + /** + * 定时查询, 如果有过时的发送到消息队列 + */ + public void sync() { + List paymentIds = expiredTimeRepository.retrieveExpiredKeys(LocalDateTime.now()) + .stream() + .map(Long::valueOf) + .collect(Collectors.toList()); + if (CollUtil.isNotEmpty(paymentIds)) { + expiredTimeRepository.removeKeys(paymentIds.stream().map(String::valueOf).toArray(String[]::new)); + paymentIds.forEach(paymentEventSender::sendPaymentExpiredTime); + } + } + +} diff --git a/dax-pay/src/main/java/cn/bootx/daxpay/util/PayModelUtil.java b/dax-pay/src/main/java/cn/bootx/daxpay/util/PayModelUtil.java new file mode 100644 index 000000000..76b19d54b --- /dev/null +++ b/dax-pay/src/main/java/cn/bootx/daxpay/util/PayModelUtil.java @@ -0,0 +1,134 @@ +package cn.bootx.daxpay.util; + +import cn.bootx.common.core.util.BigDecimalUtil; +import cn.bootx.common.core.util.LocalDateTimeUtil; +import cn.bootx.daxpay.code.pay.PayChannelCode; +import cn.bootx.daxpay.code.pay.PayChannelEnum; +import cn.bootx.daxpay.code.pay.PayModelExtraCode; +import cn.bootx.daxpay.exception.payment.PayAmountAbnormalException; +import cn.bootx.daxpay.exception.payment.PayFailureException; +import cn.bootx.daxpay.param.pay.PayModeParam; +import cn.bootx.daxpay.param.pay.PayParam; +import cn.bootx.daxpay.param.paymodel.alipay.AliPayParam; +import cn.bootx.daxpay.param.paymodel.voucher.VoucherPayParam; +import cn.bootx.daxpay.param.paymodel.wechat.WeChatPayParam; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import lombok.experimental.UtilityClass; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 支付方式工具类 + * + * @author xxm + * @date 2022/7/12 + */ +@UtilityClass +public class PayModelUtil { + + /** + * 获取支付宝的过期时间 + */ + public String getAliExpiredTime(Integer minute) { + return minute + "m"; + } + + /** + * 获取微信的过期时间 + */ + public String getWxExpiredTime(Integer minute) { + LocalDateTime time = LocalDateTimeUtil.offset(LocalDateTime.now(), minute, ChronoUnit.MINUTES); + return LocalDateTimeUtil.format(time, DatePattern.PURE_DATETIME_PATTERN); + } + + /** + * 获取支付单的超时时间 + */ + public LocalDateTime getPaymentExpiredTime(Integer minute) { + return LocalDateTimeUtil.offset(LocalDateTime.now(), minute, ChronoUnit.MINUTES); + } + + /** + * 判断是否有异步支付 + */ + public boolean isNotSync(List payModeParams) { + return payModeParams.stream().map(PayModeParam::getPayChannel).noneMatch(PayChannelCode.ASYNC_TYPE::contains); + } + + /** + * 获取异步支付参数 + */ + public PayModeParam getAsyncPayModeParam(PayParam payParam) { + return payParam.getPayModeList() + .stream() + .filter(payMode -> PayChannelCode.ASYNC_TYPE.contains(payMode.getPayChannel())) + .findFirst() + .orElseThrow(() -> new PayFailureException("支付方式数据异常")); + } + + /** + * 构建扩展参数构建 + * @param payChannel 支付通道 + * @param map 支付方式扩展字段信息 key 为 PayModelExtraCode中定义的 + */ + public String buildExtraParamsJson(Integer payChannel, Map map) { + PayChannelEnum payChannelEnum = PayChannelEnum.findByNo(payChannel); + switch (payChannelEnum) { + case ALI: { + return JSONUtil.toJsonStr(new AliPayParam().setAuthCode(map.get(PayModelExtraCode.AUTH_CODE)) + .setReturnUrl(map.get(PayModelExtraCode.RETURN_URL))); + } + case WECHAT: { + return JSONUtil.toJsonStr(new WeChatPayParam().setOpenId(map.get(PayModelExtraCode.OPEN_ID)) + .setAuthCode(map.get(PayModelExtraCode.AUTH_CODE))); + } + case VOUCHER: { + String voucherNo = map.get(PayModelExtraCode.VOUCHER_NO); + List list = new ArrayList<>(); + if (StrUtil.isNotBlank(voucherNo)) { + list.add(voucherNo); + } + return JSONUtil.toJsonStr(new VoucherPayParam().setCardNoList(list)); + } + default: { + return null; + } + } + } + + /** + * 检查支付金额 + */ + public void validationAmount(List payModeList) { + for (PayModeParam payModeParam : payModeList) { + // 同时满足支付金额小于等于零 + if (BigDecimalUtil.compareTo(payModeParam.getAmount(), BigDecimal.ZERO) < 1) { + throw new PayAmountAbnormalException(); + } + } + } + + /** + * 检查异步支付方式 + */ + public void validationAsyncPayMode(PayParam payParam) { + // 组合支付时只允许有一个异步支付方式 + List payModeList = payParam.getPayModeList(); + + long asyncPayModeCount = payModeList.stream() + .map(PayModeParam::getPayChannel) + .filter(PayChannelCode.ASYNC_TYPE::contains) + .count(); + if (asyncPayModeCount > 1) { + throw new PayFailureException("组合支付时只允许有一个异步支付方式"); + } + } + +} diff --git a/dax-pay/src/main/resources/application-dev.yml b/dax-pay/src/main/resources/application-dev.yml new file mode 100644 index 000000000..19ffc39b7 --- /dev/null +++ b/dax-pay/src/main/resources/application-dev.yml @@ -0,0 +1,224 @@ +server: + port: 9898 +spring: + datasource: + dynamic: + primary: master + datasource: + # 默认数据源 + master: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/bootx-dax-pay?serverTimezone=GMT%2B8&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&nullCatalogMeansCurrent=true + username: root + password: root + hikari: + # 30 秒, 心跳检测 + keepalive-time: 30000 + # 300 分钟, 连接最大存活时间 + max-lifetime: 18000000 + redis: + host: 127.0.0.1 + port: 6379 + database: 1 + # 如果不使用MQ的话, 可以将项目中 @EnableRabbit 给注释掉, 就不会一直报错了 + rabbitmq: + # 虚拟主机 + virtual-host: dax-pay + host: 127.0.0.1 + port: 5672 + username: guest + password: guest + listener: + simple: + # 重试机制 + retry: + enabled: true + max-attempts: 5 + #静态资源加载配置 + mvc: + static-path-pattern: /** + web: + resources: + static-locations: classpath:/static/,classpath:/public/ + thymeleaf: + cache: false + # 数据库版本管理 + flyway: + # 如果 flyway_schema_history 不存在自动创建 + baseline-on-migrate: true + # 开发模式开启 + enabled: true + # 定时任务 + quartz: + # 持久化方式 + job-store-type: jdbc + #定时任务启动开关,true-开 false-关 + auto-startup: true + #启动时更新己存在的Job + overwrite-existing-jobs: true + properties: + org: + quartz: + scheduler: + instanceName: quartzScheduler + instanceId: AUTO + threadPool: + threadCount: 10 + threadPriority: 5 + threadsInheritContextClassLoaderOfInitializingThread: true + +# 开发时显示debug日志 +logging: + level: + cn.bootx.**: debug +# 项目配置 +bootx: + common: + # 接口文档 + swagger: + enabled: true + author: bootx + version: 1.2.3 + title: bootx开发平台单体版 + description: bootx-platform开发平台单体版 + # basic认证 + basic: + enable: true + # 多模块扫码 + base-packages: + "[基础API]": cn.bootx.baseapi + "[身份识别和管理]": cn.bootx.iam + "[消息服务]": cn.bootx.notice + "[办公服务]": + - cn.bootx.office + - cn.bootx.starter.flowable + "[支付服务]": cn.bootx.daxpay + "[演示模块]": cn.bootx.demo + "[可视化大屏]": cn.bootx.visualization + "[第三方对接]": + - cn.bootx.starter.dingtalk + - cn.bootx.starter.wecom + - cn.bootx.starter.wechat + "[认证与权限]": + - cn.bootx.starter.auth + - cn.bootx.starter.data.perm + "[其他starter组件]": + - cn.bootx.starter.audit.log + - cn.bootx.starter.file + - cn.bootx.starter.quartz + - cn.bootx.starter.monitor + # 缓存 + cache: + # 默认超时时间 30分钟 + default-ttl: 1800 + # 对Key设置超时间 (秒) + keys-ttl: + "[iam:user:path]" : 5200 + "[iam:ignore:path]" : 28800 + "[base:china:region]" : 28800 + # 序列生成器 + sequence: + type: jdbc + # 异常 + exception: + # 显示详细异常 + show-full-message: true + # 是否启用Rabbit, 并不是停用了Rabbit, 需配合 ConditionalOnRabbit 注解使用 + rabbit: + enable: false + # 表结构自动维护 + actable: + # 数据库类型 + database-type: mysql + # 更新类型 none为不进行任何操作 + update-type: update + # 扫描包路径, 可以用 ,和 ; 分隔 + scan-package: cn.bootx.**.entity + # 指定生成索引前缀 + prefix-index: idx_ + # 指定生成唯一约束前缀 + prefix-unique: uni_ + starter: + # 三方平台 + third: + # 钉钉 + ding-talk: + app-key: ?? + app-secret: ?? + #微信(公众平台) + wechat: + app-id: ?? + app-secret: ?? + token: ?? + encoding-aes-key: ?? + # 企业微信 + wecom: + agent-id: 1000003 + corp-id: ?? + corp-secret: ?? + token: ?? + encoding-aes-key: ?? + # 认证 + auth: + default-password: 123456 + ignore-urls: + - '/actuator/**' + - '/v3/api-docs/**' + - '/doc.html' + - '/swagger-resources/**' + - '/token/**' + - '/ws/**' + - '/demo/**' + - '/test/**' + - "/plumelog/**" + - '/webjars/**' + - '/css/**' + - '/error' + - '/favicon.ico' + - '/**' + # 第三方登录 + third-login: + ding-talk: + redirect-uri: http://127.0.0.1:9999/auth/third/callback/dingTalk + client-secret: ${bootx.starter.third.ding-talk.app-secret} + client-id: ${bootx.starter.third.ding-talk.app-key} + we-com: + redirect-uri: http://127.0.0.1:9999/auth/third/callback/weCom + client-secret: ${bootx.starter.third.wecom.corp-secret} + client-id: ${bootx.starter.third.wecom.corp-id} + agent-id: ${bootx.starter.third.wecom.agent-id} + # 微信开放平台 + we-chat-open: + redirect-uri: http://127.0.0.1:9999/auth/third/callback/weChat + client-id: ?? + client-secret: ?? + # 审计日志 + audit-log: + # 审计日志存储 + store: jdbc + # ip归属地 + ip2region: + # 数据文件路径 + file-path: /data/ip2region/ip2region.xdb + # 查询方式 + search-type: cache + # 文件上传 + file-upload: + upload-type: local + server-url: http://127.0.0.1:8080 + local: + local-path: /data/file + mongo: + bucket: fs + minio: + # 地址 + endpoint: http://127.0.0.1:9000 + # 账号 + access-key: root + # 密码 + access-secret: password + # 存储桶 需要至少三位 + bucket: bootx + # 数据权限 + data-perm: + field-decrypt-key: "UCrtxSCwYZNCIlav" diff --git a/dax-pay/src/main/resources/application.yml b/dax-pay/src/main/resources/application.yml new file mode 100644 index 000000000..cf82df7aa --- /dev/null +++ b/dax-pay/src/main/resources/application.yml @@ -0,0 +1,32 @@ +spring: + application: + name: bootx-platform + profiles: + active: dev + config: + use-legacy-processing: true + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB + enabled: true + thymeleaf: + mode: LEGACYHTML5 + flyway: + # 默认不启用 + enabled: false + # 对特殊占位符不进行替换 + placeholder-replacement: false +# mp配置 +mybatis-plus: + mapper-locations: classpath*:mapper/**/*Mapper.xml + global-config: + banner: false + db-config: + logic-delete-value: 1 + logic-not-delete-value: 0 + id-type: ASSIGN_ID +# 安全框架配置 +sa-token: + is-print: false + token-name: AccessToken diff --git a/dax-pay/src/main/resources/banner.txt b/dax-pay/src/main/resources/banner.txt new file mode 100644 index 000000000..dd4d6d0aa --- /dev/null +++ b/dax-pay/src/main/resources/banner.txt @@ -0,0 +1,12 @@ +${AnsiColor.CYAN} + _ _ +| | | | +| |__ ___ ___ | |_ __ __ +| '_ \ / _ \ / _ \ | __|\ \/ / +| |_) || (_) || (_) || |_ > < +|_.__/ \___/ \___/ \__|/_/\_\ +${AnsiColor.MAGENTA} + +Service: dax-pay 支付开发平台 +Spring Boot Version: ${spring-boot.version} +${AnsiColor.BLACK} diff --git a/dax-pay/src/main/resources/logback-spring.xml b/dax-pay/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..cf07c66f1 --- /dev/null +++ b/dax-pay/src/main/resources/logback-spring.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dax-pay/src/main/resources/mapper/wallet/WalletMapper.xml b/dax-pay/src/main/resources/mapper/wallet/WalletMapper.xml new file mode 100644 index 000000000..4b6c786e9 --- /dev/null +++ b/dax-pay/src/main/resources/mapper/wallet/WalletMapper.xml @@ -0,0 +1,47 @@ + + + + + + + update pay_wallet + set balance = (balance + #{amount}), + last_modifier = #{operator}, + last_modified_time = #{date}, + version = (version+1) + where id = #{walletId} + + + + update pay_wallet + set balance = (balance - #{amount}), + last_modifier = #{operator}, + last_modified_time = #{date}, + version = (version+1) + where id = #{walletId} and (balance- #{amount}) >= 0 + + + + update pay_wallet + set balance = (balance - #{amount}), + last_modifier = #{operator}, + last_modified_time = #{date}, + version = (version+1) + where id = #{walletId} + + + + + diff --git a/dax-pay/src/main/resources/templates/errorCashier.html b/dax-pay/src/main/resources/templates/errorCashier.html new file mode 100644 index 000000000..3e4433e5a --- /dev/null +++ b/dax-pay/src/main/resources/templates/errorCashier.html @@ -0,0 +1,10 @@ + + + + + 请使用微信或支付宝扫码打开 + + +

请使用微信或支付宝扫码打开!

+ + \ No newline at end of file diff --git a/dax-pay/src/main/resources/templates/wechatJsapiPay.html b/dax-pay/src/main/resources/templates/wechatJsapiPay.html new file mode 100644 index 000000000..6020e4145 --- /dev/null +++ b/dax-pay/src/main/resources/templates/wechatJsapiPay.html @@ -0,0 +1,47 @@ + + + + + 微信支付 + + +

+
+

+ + + \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..f6c09044c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3' +services: + platform: + image: bootx-platform:latest + restart: always + ports: + - "8080:8080" + volumes: + # 宿主机目录:容器目录 日志映射 + - /data/logs/bootx-platform:/logs + # 外部化配置文件 + - ./application.yml:/application.yml diff --git a/lombok.config b/lombok.config new file mode 100644 index 000000000..547f3950a --- /dev/null +++ b/lombok.config @@ -0,0 +1,4 @@ +# 将字段上的注解拷贝到对应构造参数上, 如果要新增别的注解, 需要在下方重新进行定义, +lombok.copyableAnnotations += org.springframework.context.annotation.Lazy +# 默认开启实体类的链式API 会导致一些反射相关类库出问题, 如 easyExcel +#lombok.accessors.chain = true diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..17ebbd12a --- /dev/null +++ b/pom.xml @@ -0,0 +1,161 @@ + + + + + org.springframework.boot + spring-boot-starter-parent + 2.7.10 + + + + 4.0.0 + + cn.bootx.platform + bootx-platform + pom + 1.2.3 + + + dax-pay + + + + UTF-8 + UTF-8 + 1.8 + + + 1.2.3 + + 5.8.15 + 6.4.0 + 2.12.3 + 3.11 + 4.4 + 1.10.0 + 2.11.0 + 31.1-jre + 4.0.1 + 0.0.38 + 1.7.30 + 2.14.2 + 7.15.0 + 1.4.01 + 6.8.0 + + 1.2.4 + + 3.5.2 + 3.2.1 + 2.3.1 + 2.9.7 + 1.34.0 + 1.16.5 + 8.5.2 + 3.5.3.1 + 3.6.1 + 1.6.15 + 3.20.0 + 4.0.0 + 1.5.3.Final + 0.2.0 + 1.6.2 + 7.3 + 1.2.83_noneautotype + 0.41 + V1.4-SNAPSHOT + 2.3 + 4.4.9.B + 1.3.81 + 2.2.4 + 2.7.0 + 1.5.1.beta2 + + + + + + + cn.bootx.platform + bootx-platform-parent + pom + import + ${bootx-platform.version} + + + + + + + + central + https://maven.aliyun.com/nexus/content/groups/public + + true + + + false + + + + snapshots + https://maven.aliyun.com/nexus/content/groups/public + + false + + + true + + + + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring.checkstyle.version} + + + + org.apache.maven.plugins + maven-resources-plugin + + UTF-8 + + xlsx + xls + docx + doc + + + + + + + + src/main/resources + + **/* + + true + + + + src/main/java + + **/*.xml + **/*.sql + **/*.flt + **/*.xlsx + **/*.xls + **/*.docx + **/*.doc + + + + + +