Compare commits

..

No commits in common. "setconfig" and "v1.0.0" have entirely different histories.

125 changed files with 2239 additions and 3392 deletions

12
!!.env Normal file
View File

@ -0,0 +1,12 @@
APP_DEBUG = true
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME = test
DB_USER = test
DB_PASS = 123456
DB_PORT = 3306
DB_CHARSET = utf8
DB_PREFIX = mpay_
DEFAULT_LANG = zh-cn

12
.env Normal file
View File

@ -0,0 +1,12 @@
APP_DEBUG = true
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME = mpay
DB_USER = admin
DB_PASS = Aa123456
DB_PORT = 3306
DB_CHARSET = utf8
DB_PREFIX = mpay_
DEFAULT_LANG = zh-cn

15
.gitignore vendored
View File

@ -1,13 +1,6 @@
.vscode /.idea
/.vscode
/.history
/.gitee
*.env *.env
extend/*
vendor/*
!vendor/vendor.zip
runtime/*
app/controller/TestController.php
config/extend/*

466
README.md
View File

@ -1,55 +1,29 @@
<p align="center">
<div align="center">
<a href="https://gitee.com/technical-laohu/mpay">
<img src="assets/20241129_120237_logo.jpg" alt="mpay" width=400 />
</a>
</div>
<div align="center">
<a href="https://gitee.com/technical-laohu/mpay" target="_blank">项目主页</a>
<a href="https://gitee.com/technical-laohu/mpay/releases" target="_blank">源码下载</a>
<a href="https://f0bmwzqjtq2.feishu.cn/docx/HBVrdrsACo36bzxUCSPcjOBNnyb" target="_blank">使用文档</a>
<a href="https://f0bmwzqjtq2.feishu.cn/docx/FtphdDA10oBfPyxNEEZc5mgJnqf" target="_blank">常见问题</a>
<a href="https://f0bmwzqjtq2.feishu.cn/docx/OjlwdPunLoGjL0xodMUcS0xFngX" target="_blank">学习交流</a>
<a href="https://f0bmwzqjtq2.feishu.cn/docx/PjwOdvBeZoQEHUxF2ZScTjHOnKb" target="_blank">赞赏作者</a>
</div>
<br />
<div align="center">
😎免签约、🧩多通道、🛜不掉线 - 专注于个人在线收款💴
</div>
</p>
## ✨ 项目介绍
# 码支付
![logo](assets/20241129_120237_logo.jpg)
**码支付[mpay]是一款便捷收款工具,专注于个人免签收款,通过普通收款码即可实现收款通知自动回调,支持绝大多数商城系统** **码支付[mpay]是一款便捷收款工具,专注于个人免签收款,通过普通收款码即可实现收款通知自动回调,支持绝大多数商城系统**
## 项目地址
| <p align="center">gitee</p> | <p align="center">github</p> | | gitee | github |
| :----------: | :------------: | | :----------: | :------------: |
| <a href="https://gitee.com/technical-laohu/mpay"><img src="assets/20241122_113208_gitee.png" alt="mpay" width=128 /></a>|<a href="https://github.com/techhaha/mpay"><img src="assets/20241122_113331_github.png" alt="mpay" width=128 /></a>| |[![](assets/20241122_113208_gitee.png)](https://gitee.com/technical-laohu/mpay)|[![](assets/20241122_113331_github.png)](https://github.com/techhaha/mpay)|
点以上图标进入项目页面,可查看最新发行版
## ⚙️ 工具特性 ## 项目说明
- 开源程序,个人免费使用,不断更新 ### 原因
- 支持第四方收款服务商聚合码收款,免挂机、不掉线
- 支持微信、支付宝个人账户收款,免签约
- 采用易支付接口标准开发,兼容市面上大部分商城系统
- 在H5环境中能正常长按识别扫码支付
- 支持多平台、多账号、多通道,灵活配置,收款轮询
## ✨ 演示站点
#### 访问 [在线演示](http://demo.stspwsc.com/) 快速体验。
## 📊 项目说明
### 🎈 原因
之前在工作之余,尝试过很多副业项目,基本都是跟互联网相关的。例如使用**Wordpress和zibll主题**建资源站点,用**微擎**和**微课堂V2**卖课程,使用**异次元发卡**和**独角数卡**程序卖代理的**微信营销软件**,其中遇到的让我最头痛的问题就是收款问题。 之前在工作之余,尝试过很多副业项目,基本都是跟互联网相关的。例如使用**Wordpress和zibll主题**建资源站点,用**微擎**和**微课堂V2**卖课程,使用**异次元发卡**和**独角数卡**程序卖代理的**微信营销软件**,其中遇到的让我最头痛的问题就是收款问题。
正规官方渠道,如微信支付宝申请相关支付接口,必须要签约,且有营业执照等相关资质,这对于只想搞想副业的我实在是没必要,也麻烦,后期的事情也多。于是在网上找到了一些解决方案来解决我的在线收款问题。 正规官方渠道,如微信支付宝申请相关支付接口,必须要签约,且有营业执照等相关资质,这对于只想搞想副业的我实在是没必要,也麻烦,后期的事情也多。于是在网上找到了一些解决方案来解决我的在线收款问题。
### 🪄 方案 ### 方案
市场也有很多针对此类需求的平台,也有不少合适的解决方案 市场也有很多针对此类需求的平台,也有不少合适的解决方案
@ -65,429 +39,133 @@
优点是审核不严,处理比较灵活,适合个人,技术支持比较好,注册可使用(有些需要注册费) 优点是审核不严,处理比较灵活,适合个人,技术支持比较好,注册可使用(有些需要注册费)
缺点就是平台容易卷款跑路,也没有什么有效监管,钱收不回来就亏大了(这也是最大的问题) 缺点就是平台容易卷款跑路,也没有什么有效监管,钱收不回来就亏大了(这也是最大的问题)
#### 源支付/V免签 #### 源支付
源支付也是一套收款程序,有个人版和商户版,市面上能搜到的大部分都是商户版,可以入驻,使用自己的个人微信支付宝二维码收款。 源支付也是一套收款程序,有个人版和商户版,市面上能搜到的大部分都是商户版,可以入驻,使用自己的个人微信支付宝二维码收款。
源支付程序的设计思路主要是通过在手机或电脑上安装消息监听软件,用来监听获取微信和支付宝的收款到账通知来实现的支付成功回调的。**方法很实用,本程序也添加了该功能插件,免费**。
只是这种思路,有一些小问题:
V免签是一款开源免费适用于个人收款使用的收款程序原理同源支付类似。 * 平台容易因为资质问题导致关站;
程序的设计思路主要是通过在手机或电脑上安装消息监听软件,用来监听获取微信和支付宝的收款到账通知来实现的支付成功回调的。**方法很实用,本程序也添加了该功能插件,免费**。 * 收取的手续费价格偏高;
* 个人码在微信H5环境无法长按识别付款能只技术PC端相机扫码付款。
#### 码支付(mpay)
## ✨ 开发思路 **本程序暂只提供个人版,开源免费使用。**
码支付是在源支付的设计思路基础上进行的改进,利用第四方**聚合收款码**来进行收款保证收款稳定和便捷。聚合收款码个人可以申请不需求相关资质不用申请API接口收银服务平台众多且实力雄厚如拉卡拉、收钱吧等不怕跑路。
特点如下:
### 🎯 服务商聚合码 * 免监听,不需要手机或电脑挂机监听消息,即可实现支付回调,只需要设置一个定时任务就行
* 支持微信、支付宝、云闪付的H5环境能正常长按识别扫码支付域名防红
* 个人搭建的收款系统,收款稳定,安全可控,不需要额外手续费
* 支持多平台(聚合码服务商),多账号(聚合码商户),多渠道(门店码/店员码/桌号码等),降低异地线上收款风控风险
<img src="assets/20241210_112301_6f2fef2a7aaee96790eb86f90e3b107.png" width=640 /> ### 思路
码支付说到底就是通过二维码来进行收款,日常使用的除了微信支付宝生成的二维码外,还有一类二维码是由收款服务商提供的,它能通过一张收款二维码,同时支持**微信**、**支付宝**、**云闪付**等多渠道付款,一般称为**聚合收款码**。 码支付说到底就是通过二维码来进行收款,日常使用的除了微信支付宝生成的二维码外,还有一类二维码是由收款服务商提供的,它能通过一张收款二维码,同时支持**微信**、**支付宝**、**云闪付**等多渠道付款,一般称为**聚合收款码**。
<img src="assets/20241128_164241_image.png" width=640 /> ![聚合收款码](assets/20241128_164241_image.png)
这类收款码扫码之后需要用户自己输入指定金额来进行付款,然后查看收款通知,确认是否到账,最后确认订单支付成功。 这类收款码扫码之后需要用户自己输入指定金额来进行付款,然后查看收款通知,确认是否到账,最后确认订单支付成功。
就像你去店子里买一瓶水,你扫二维码进去付款界面,就生成了一个订单,你付款成功之后,商店老板会去查看一下商户后台流水,确认订单是否支付成功,这是一个人工审核的过程。 就像你去店子里买一瓶水,你扫二维码进去付款界面,就生成了一个订单,你付款成功之后,商店老板会去查看一下商户后台流水,确认订单是否支付成功,这是一个人工审核的过程。
<img src="assets/20241128_164817_image.png" width=640 /> ![商户后台订单流水](assets/20241128_164817_image.png)
那么码支付的作用,就是让人工审核变成自动审核的,当用户通过网站购买商品的时候,码支付会生成一个订单并展示收银台界面,用户再扫码进入聚合码付款页面。 那么码支付的作用,就是让人工审核变成自动审核的,当用户通过网站购买商品的时候,码支付会生成一个订单并展示收银台界面,用户再扫码进入聚合码付款页面。
<img src="assets/20241128_164952_image.png" width=65% /> ![码支付收银台](assets/20241128_164952_image.png)
<img src="assets/20241128_171356_image.png" width=32% /> ![用户付款](assets/20241128_171356_image.png)
同一时间,**码支付后台会自动通过账号密码登陆聚合码服务平台的管理后台**并通过API接口循环查询最近的收款明细通过比对金额和时间确认是否付款到账最后确认成功收款。 同一时间,**码支付后台会自动通过账号密码登陆聚合码服务平台的管理后台**并通过API接口循环查询最近的收款明细通过比对金额和时间确认是否付款到账最后确认成功收款。
<img src="assets/20241128_165320_image.png" width=640 /> ![收款成功](assets/20241128_165320_image.png)
当用户付款成功,并且后台检测到收款成功消息后,收钱台就会提示收款成功,并最终确认收款。 当用户付款成功,并且后台检测到收款成功消息后,收钱台就会提示收款成功,并最终确认收款。
> 只有存在新订单时,且该订单与当前收款账号一致时,码支付后台才会主动登陆该账号,查询收款流水,减少频繁查询导致的可能风险 > 只有存在新订单时,且该订单与当前收款账号一致时,码支付后台才会主动登陆该账号,查询收款流水,减少频繁查询导致的可能风险
> 另外,在账号设置里也有两个模式可选,`单次监听`和`连续监听`,根据业务场景可以自行选择,具体使用,下面有介绍 > 另外,在账号设置里也有两个模式可选,`单次监听`和`连续监听`,根据业务场景可以自行选择,具体使用,下面有介绍
### 🪁 微信、支付宝收款码 ## 开源声明
<img src="assets/20241210_112331_e8d2c4043a3c57ad887aef92df1c253.png" width=640 /> ### 内置插件
普通的个人收款码,大多使用微信/支付宝来进行收款,依赖于微信/支付宝平台的推送服务和系统通知机制,就可以获取到收款金额的通知信息,从而可以实现支付回调,自动确认收款。 码支付收款管理系统开源免费使用,支付插件有**免费**和**付费**版本,按需使用购买
目前主流的方式也是这种,来实现自动收款,简单方便,不过有一些小问题:
- 微信/支付宝的收款通知推送,需要软件挂机进行监听,否则无法确认收款
- 监听软件因手机或PC系统的不同可能会出现各种监听不稳定的问题
- 微信个人码因升级在微信H5环境里无法长按识别付款
## 📦 技术架构 1. **微信**插件`wxpay`默认安装,支持`赞赏码`、`个人码|经营码|商家码`2个通道需挂机监听
2. **支付宝**插件`alipay`默认安装,支持`收钱码`、`经营码`2个通道需挂机监听
3. **收钱吧**聚合码插件`sqbpay`默认安装,无需挂机,设置定时任务即可
使用`Thinkphp8`框架PHP版本 > 8.0推荐8.2前端UI使用`Layui 2.9`+`PearAdmin`后台 ### 技术架构
## 🧰 内置插件 须用`Thinkphp8`框架前端UI使用`Layui 2.9`+`PearAdmin`后台
码支付收款工具开源免费使用,插件中心的插件列表有**免费版**和**付费版**版,按需使用购买 ## 安装和使用
1. **微信**插件`wxpay`默认安装,支持`赞赏码`、`个人码`、`经营码`、`商家码`,需挂机监听 ### 全新安装
2. **支付宝**插件`alipay`默认安装,支持`收钱码`、`经营码`,需挂机监听
3. **收钱吧**聚合码插件`sqbpay`默认安装,无需挂机,设置定时任务即可监听回调
--- #### 源码下载
## 📝 安装说明 #### 安装配置
以下演示基于**云服务器**环境+**宝塔面板**安装,云服务器购买可以去阿里云、腾讯云等平台,宝塔面板安装教程参考[宝塔面板安装教程](https://www.bt.cn/new/download.html)
### 安装配置
以**宝塔面板**示例,其他服务器管理面板可以参考 以**宝塔面板**示例,其他服务器管理面板可以参考
通过宝塔面板登陆管理后台新建PHP站点和数据库并确认创建 ##### 新建站点与数据库
![](assets/20241203_161723_image.png) #### 仿静态配置
在网站列表页面,点击创建的网站的根目录,进入文件管理 #### 安装步骤
![](assets/20241203_162102_image.png) ### 聚合码使用
文件夹里面有一些默认文件,不用管他 #### 申请收款码
![](assets/20241203_162231_image.png) #### 安装插件
点击上传文件,将源码压缩包上传到该文件夹,并解压到前文件夹
![](assets/20241203_162646_image.png) #### 添加账号
**将`mpay`文件夹里面的所有文件,复制到当前根目录下**,返回网站列表管理页面 #### 添加收款码
> 注意,压缩包文件打包的是一个名为`mpay`的文件夹,需要将代码文件夹里面的所有文件复制出来,放到创建的网站根目录下, #### 设置监听
### 运行目录&仿静态 配置 ##### 订单监听
点击网站名,进入网站配置设置页面 ##### 账号收款监听
![](assets/20241203_163259_image.png) #### 支付测试
选择**网站目录**,运行目录选择`public`,保存 #### 服务商支持
![](assets/20241203_164321_image.png) ### 微信/支付宝使用
选择**伪静态**,模版选择`thinkphp`,即可自动填写,保存 #### 添加账号
![](assets/20241203_163507_image.png) #### 添加收钱码
### 安装步骤 #### 挂机监听收款通知
在浏览器输入`http://你的域名/install`,进入程序安装界面,按照提示进行填写提交 ##### 软件下软(安卓)
![](assets/20241203_165327_image.png)
数据库配置相关信息,在服务器管理面板里查找
![](assets/20241203_165507_image.png)
---
## ❤️ 聚合码收款
如果本身就有聚合码收钱码最好,没有就需要提前去各收银服务商申请,申请也不复杂
### 申请收款码
以下列出一些常见收款服务平台,可以按需申请,个人直接申请小微商户即可
| 平台 | 官网 |
|--------|--------------------------------------|
| 收钱吧 | https://www.shouqianba.com/ |
| 小Y经营 | https://xym.ysepay.com/ |
| 码钱 | https://m.hkrt.cn/ |
| 拉卡拉 | https://customer.lakala.com/ |
| 盛付通 | https://b.shengpay.com/ |
> 申请可以去官方平台注册账号等客服电话,或者在社群里询问(有很多人有代办资质),实在找不到的,可以去淘宝上的官方店买个二维码卡牌贴纸,然后询问客服如何开通账号就行,会有专员联系你开通。
### 安装插件
程序默认安装有**微信支付**`wxpay`、**支付宝**`alipay`、**收钱吧**`sqbpay`三个插件,基本能满足大家的日常收款需求。
如果需要其他收银平台插件,可以在插件中心自行安装,插件中心没有的,也可以联系作者定制开发。
### 添加账号
除**微信支付**`wxpay`、**支付宝**`alipay`之外,所有的收款平台均为聚合码收款平台
以**收钱吧**`sqbpay`为例,添加账号时,需要填写**收钱吧**商户管理中心的**登陆账号**和**登陆密码**,需要使用时,插件会自动在后台登陆账号并查询相关订单流水信息。
![](assets/20241205_102856_image.png)
![](assets/20241205_103718_image.png)
**监听模式**说明,分为`单次监听`和`连续监听`
![](assets/20241205_104101_image.png)
`单次监听`是在收银台页面,用户扫码支付成功后,需要手动点击**确认支付**,后台才会登陆查询该账号的收款流水情况,**点击一次,查询一次,不点击,则不会查询**,降低密集查询可能导致的账户风控
![](assets/20241205_104306_image.png)
`连续监听`是在创建订单之后,**在订单有效期内且订单未完成状态时**,插件会一直连续不断的查询账户流水详情,直到过期或者成功收款,用户不需要主动点击确认
![](assets/20241205_105844_image.png)
> 使用哪种监听模式,视应用场景自行选择,一般选择**连续监听**即可,如果日常订单比较密集,可以选择**单次监听**,或多添加几个账号,减少密集查询风控
### 添加收款码
点击可以查看当前账号的所有收款码
![](assets/20241205_110432_image.png)
点击**添加收款码**后,可以正常填写二维码信息
**终端编号**需要填写当前收款码在收银服务商系统内的编码,有的可以直接在收款二维码解析的**链接里找到**,有的需要**登陆商户管理中心**,去订单详情里查询才能知道
🚀️ 具体各个平台的终端编号如何获取,可以去**程序后端控制台主页**的`项目文档`查看🚀️
![](assets/20241205_110508_image.png)
![](assets/20241205_111720_image.png)
收款样式有两个选项`付款链接`和`图片地址`,根据实际情况选择
**付款链接:** 将二维码的内容解析成字符串保存,使用时再生成二维码
**图片地址:** 上传二维码图片到服务器或引用http远程图片地址
> 一般的**聚合收款码都解析成文字保存**即可,前端展示时会重新生成二维码,无法解析的就上传图片
### 🚀️ 设置监听
#### 宝塔任务计划
等会儿需要在这里设置任务监听
![](assets/20241205_112415_image.png)
#### 新订单监听
一个站点只需要设置一个定时任务,每次访问都会查询一次数据库,并生成新订单数据缓存
![](assets/20241205_112033_image.png)
![](assets/20241205_112517_image.png)
#### 账号监听
**每个账号都需要单独设置一个定时任务**,每次访问都会检索新订单缓存数据,如果存在该账户的新订单,插件就会去查询服务商后台的订单流水,并通知相关收款处理程序
![](assets/20241205_112116_image.png)
![](assets/20241205_113629_image.png)
### 支付测试
只开启一个账号,然后点击`支付测试`,正常支付后,观察是否成功回调,如果不能回调请重新检查配置,或再次详细查看**程序后端控制台主页**的`项目文档`
![](assets/20241205_113835_image.png)
---
## ❤️ 微信/支付宝收款
微信/支付宝生成的收款码,需要挂机监听收款消息,基本情况上面有介绍,因为使用广泛,所以也单独添加进来,可以实现正常收款回调
### 添加账号
收款平台选择`微信支付`或`支付宝`,收款账号就填写`微信支付`或`支付宝`的账号,与**聚合码支付**不同,此处填写的账号主要用来做区分,不会登陆后台
![](assets/20241204_164128_image.png)
### 添加收钱码
点击可以查看当前账号的所有收款码
![](assets/20241204_164617_image.png)
点击**添加收款码**后,可以正常填写二维码信息
`微信支付`和`支付宝`的终端编号是自动生成的,不可手动填写,分别对应当前账号的不同收款通道,支持`个人码`和`赞赏码`
![](assets/20241204_164650_image.png)
收款样式有两个选项`付款链接`和`图片地址`,根据实际情况选择
**付款链接:** 将二维码的内容解析成字符串保存,使用时再生成二维码
**图片地址:** 上传二维码图片到服务器或引用http远程图片地址
> `赞赏码`等收款方式,采用的不是标准二维码编码格式,不能解析成文字保存,只能展示图片,建议一般**二维码都解析成文字保存**,前端展示时会根据内容自动再生成二维码,无法解析的就上传图片
---
### 挂机监听收款通知
收款通知信息需要挂机监听,有`手机监听`和`PC监听`两种手机可以同时监听微信和支付宝PC只能监听微信具体支持的通知和功能在设置账号页面有详细介绍。
#### 🚀️ 手机软件下软(安卓)
码支付使用的是开源工具**短信转发器**`SmsForwarder`来监听收款通知,`SmsForwarder`功能非常强大,喜欢*搞机* 的朋友可以多钻研,这是使用文档[短信转发器](https://gitee.com/pp/SmsForwarder/wikis/pages)
* **开源项目地址:[SmsForwarder](https://gitee.com/pp/SmsForwarder)**
* **发布地址,尽量下载最新版,兼职更多机型:[SmsForwarder](https://gitee.com/pp/SmsForwarder/releases)**
* **网盘地址,访问密码:`pppscn`[SmsForwarder](https://wws.lanzoui.com/b025yl86h)**
![](assets/20241204_171340_image.png)
---
##### 功能配置 ##### 功能配置
找一台不常用的安卓系统手机,下载安装好`SmsForwarder`之后,点开进行配置,同时打开码支付后台**用户中心**页面,查看相关配置参数 ##### 收款通知测试
![](assets/20241204_175759_image.png) ### 补充说明
##### **通用设置** ### 页面展示
1. 开启转发应用通知,只针对应用软件才会转发 ![](assets/20241128_171842_image.png)
2. 保活措施全部开启(实际情况看手机环境)
3. 通知栏文案可以自定义,方便自己运维,可参考图片
4. 其他默认即可,也可以参考实际情况自行设置
![](assets/20241204_172346_6c9c6f4d97d5850e7890633edd7e5d5.jpg) ![](assets/20241128_171854_image.png)
##### **发送通道** ![](assets/20241128_171931_image.png)
添加发送通道,选择`Webhook`类型 ![](assets/20241128_172022_image.png)
![](assets/20241204_172532_image.png) ![](assets/20241128_172101_image.png)
**编辑规则** ![](assets/20241128_172229_image.png)
1. 通道名称自行命名,方便区别就行 ![](assets/20241128_172353_image.png)
2. 请求方式选择`POST`方式
3. Webhook Server 地址**用户中心**查看
4. **消息模版**在**用户中心**查看
5. **Secert**密钥在**用户中心**查看
6. 应签关键字`200`
填写完成点击**保存**即可
![](assets/20241204_172612_image.png)
##### **转发规则**
添加应用转发规则,**微信**和**支付宝**需要分别设置,请注意选择**应用**规则
![](assets/20241204_172656_image.png)
**具体设置**
1. 选择发送通道
2. **匹配字段**选择**多重匹配****匹配的值**去**用户中心**复制,然后粘贴过来
3. 开启**启用自定义模版**,内容填写去码支付后台**账号列表**里复制,粘贴过来
**注意:** 微信支付规则里,第三行的`[空格]`需要替换成真实的` `空格
![](assets/20241204_172718_c313070e899cc93cc2fc9fe25a1ff17.jpg)
![](assets/20241204_175634_image.png)
---
##### **转发日志**
这里可以查看所有的转发记录,点击也可以查看消息详情
![](assets/20241204_172819_2d569802058d4d1b0f92135dcc30469.jpg)
##### **注意事项**
用户中心后台里的配置APP包名不一定与手机应用里的包名一致需要查看一下如果有区别请修改成实际本机显示的APP包名下面有图片演示操作
![](assets/20241204_180022_image.png)
![](assets/20241204_173010_image.png)
![](assets/20241204_173046_image.png)
#### 🚀️ PC软件下载
码支付微信PC监听是老胡使用python写的小工具可以监听电脑桌面打开的窗口和内容信息非侵入微信应用内部版本升级不影响监听。需要将被监听的微信聊天界面单独拖出来成独立窗口。
[PC监听软件下载wxmonitor_v1.2.zip](assets/wxmonitor_v1.4.zip)
##### 配置信息
1. 打开用户中心,添加接口密钥等信息
![](assets/20250217_104137_image.png)
2. 打开账号管理列表,填写通道信息,格式`pid#aid`
![](assets/20250217_104349_image.png)
3. 循环间隔是软件检索窗口通知信息的循环时间一般设置1-3秒均可看电脑性能
4. 监控窗口根据实际情况勾选,哪个窗口需要检索收款通知,就开启哪个,不使用的就不需要开启
![](assets/20250217_105144_image.png)
5. PC监听同时支持个人码、经营码、赞赏码、商户码**手机监听因无法区分个人码和经营码,添加时只能二选一,且只能在`个人码`通道添加**。
![](assets/20250217_105939_image.png)
7. PC监听和手机监听只需要开启一个即可不要同时开启会出现重复通知导致订单异常。
---
## 🔎 补充说明
- **码支付**是一个**个人免费使用**的收款工具,**码支付**的**源码**是开源的,你可以根据自己的实际情况进行修改,也可以自行开发,有任何问题,可以社区交流
- 该软件仅适用于个人线上免签收款,避免人工审核确认收款的繁琐步骤。实际使用中请勿用于诈骗、黑灰产业,如有此类行为,一经发现直接举报
- 插件中心目前还在开发中不支持在线安装插件如果有购买插件或定制其他收银平台插件的需求可以联系作者Wechat:**K103516**
## 📬 学习交流
社群答疑、插件定制、技术交流,添加微信拉群,请备注:**码支付**
微信:**K103516**
备用:**HU1094551889**
<img src="assets/wxqrcode.png" width=480 />
## 💰 赞赏作者
<img src="assets/20250217_111408_1739762042428.jpg" width=360 />
|姓名 | 金额 |
|---|---|
| 知汇学社 | 100 |
| exrock | 100 |
| A筱磊 | 6.66 |
| 云峰 | 8.88 |
| 零零 | 200 |
| 子染 | 88 |
| John | 100 |
| 辰起 | 8.88 |
## 🔗 友情链接
1. [腾飞博客专业WP子比主题美化定制](https://calfbxtoi72.feishu.cn/docx/DA3ddthzOolQ16xDgeOcHA36nrg)
## 📷 项目截图
![首页](assets/20241203_153935_image.png)
![安装](assets/20241203_154034_image.png)
![登陆](assets/20241203_154108_image.png)
![后端控制台](assets/20241203_154141_image.png)
![订单管理](assets/20241203_154218_image.png)
![账号管理](assets/20241203_154307_image.png)
![插件管理](assets/20241203_154420_image.png)
![用户中心](assets/20241203_154505_image.png)
![添加账号](assets/20241203_154755_image.png)
![收款码](assets/20241203_154918_image.png)
![订单详情](assets/20241203_155123_image.png)

View File

@ -1,496 +0,0 @@
# 码支付
![logo](assets/20241129_120237_logo.jpg)
**码支付[mpay]是一款便捷收款工具,专注于个人免签收款,通过普通收款码即可实现收款通知自动回调,支持绝大多数商城系统**
# 项目地址
| gitee | github |
| :----------: | :------------: |
|[![](assets/20241122_113208_gitee.png)](https://gitee.com/technical-laohu/mpay)|[![](assets/20241122_113331_github.png)](https://github.com/techhaha/mpay)|
点以上图标进入项目页面,可查看最新发行版
# 项目说明
## 原因
之前在工作之余,尝试过很多副业项目,基本都是跟互联网相关的。例如使用**Wordpress和zibll主题**建资源站点,用**微擎**和**微课堂V2**卖课程,使用**异次元发卡**和**独角数卡**程序卖代理的**微信营销软件**,其中遇到的让我最头痛的问题就是收款问题。
正规官方渠道,如微信支付宝申请相关支付接口,必须要签约,且有营业执照等相关资质,这对于只想搞想副业的我实在是没必要,也麻烦,后期的事情也多。于是在网上找到了一些解决方案来解决我的在线收款问题。
---
## 方案
市场也有很多针对此类需求的平台,也有不少合适的解决方案
### 虎皮椒/迅虎/蓝兔支付
这些平台都是微信支付宝等官方平台的支付服务商能签约个人商户实现在线收款一些API支付接口可以直接调用跟官方自己申请的几乎差不多。
优点是跟官方申请的支付接口差不多N+1到账
缺点就是审核比较严动不动投诉封商户号一些羊毛党就爱整你你是一点办法没有另外开户基本都要交开户费50-200不等且还需要额外收取手续费。
### 彩虹易支付
彩虹易支付是一套收款程序,有专门的公司或平台用这套程序搭建一个收款平台,使用自己的微信或支付宝等官方账户来进行收款,你自己注商户号,运营方提供代收款服务,然后给你打款结算。
优点是审核不严,处理比较灵活,适合个人,技术支持比较好,注册可使用(有些需要注册费)
缺点就是平台容易卷款跑路,也没有什么有效监管,钱收不回来就亏大了(这也是最大的问题)
### 源支付/V免签
源支付也是一套收款程序,有个人版和商户版,市面上能搜到的大部分都是商户版,可以入驻,使用自己的个人微信支付宝二维码收款。
V免签是一款开源免费适用于个人收款使用的收款程序原理同源支付类似。
程序的设计思路主要是通过在手机或电脑上安装消息监听软件,用来监听获取微信和支付宝的收款到账通知来实现的支付成功回调的。**方法很实用,本程序也添加了该功能插件,免费**。
![](assets/20241210_112331_e8d2c4043a3c57ad887aef92df1c253.png)
只是这种思路,有一些小问题:
* 平台容易因为资质问题导致关站;
* 收取的手续费价格偏高;
* 个人码在微信H5环境无法长按识别付款只能通过PC端相机扫码付款。
* 挂机监听容易掉线,导致收款通知无法回调
### 🚀️ 码支付(mpay)
**本程序暂只提供个人版,开源免费使用。**
码支付是在源支付的设计思路基础上进行的改进,利用第四方**聚合收款码**来进行收款,保证收款稳定和便捷不掉线。
聚合收款码个人可以申请不需求相关资质不用申请API接口收银服务平台众多且实力雄厚如拉卡拉、收钱吧等不怕跑路。
特点如下:
* 免监听,不需要手机或电脑挂机监听消息,即可实现支付回调,只需要设置一个定时任务就行
* 支持微信、支付宝、云闪付的H5环境能正常长按识别扫码支付域名防红
* 个人搭建的收款系统,收款稳定,安全可控,不需要额外手续费
* 支持多平台(聚合码服务商),多账号(聚合码商户),多渠道(门店码/店员码/桌号码等),降低异地线上收款风控风险
---
## 思路
码支付说到底就是通过二维码来进行收款,日常使用的除了微信支付宝生成的二维码外,还有一类二维码是由收款服务商提供的,它能通过一张收款二维码,同时支持**微信**、**支付宝**、**云闪付**等多渠道付款,一般称为**聚合收款码**。
![聚合收款码](assets/20241128_164241_image.png)
这类收款码扫码之后需要用户自己输入指定金额来进行付款,然后查看收款通知,确认是否到账,最后确认订单支付成功。
就像你去店子里买一瓶水,你扫二维码进去付款界面,就生成了一个订单,你付款成功之后,商店老板会去查看一下商户后台流水,确认订单是否支付成功,这是一个人工审核的过程。
![商户后台订单流水](assets/20241128_164817_image.png)
那么码支付的作用,就是让人工审核变成自动审核的,当用户通过网站购买商品的时候,码支付会生成一个订单并展示收银台界面,用户再扫码进入聚合码付款页面。
![码支付收银台](assets/20241128_164952_image.png)
![用户付款](assets/20241128_171356_image.png)
同一时间,**码支付后台会自动通过账号密码登陆聚合码服务平台的管理后台**并通过API接口循环查询最近的收款明细通过比对金额和时间确认是否付款到账最后确认成功收款。
![收款成功](assets/20241128_165320_image.png)
当用户付款成功,并且后台检测到收款成功消息后,收钱台就会提示收款成功,并最终确认收款。
![](assets/20241210_112301_6f2fef2a7aaee96790eb86f90e3b107.png)
> 只有存在新订单时,且该订单与当前收款账号一致时,码支付后台才会主动登陆该账号,查询收款流水,减少频繁查询导致的可能风险
> 另外,在账号设置里也有两个模式可选,`单次监听`和`连续监听`,根据业务场景可以自行选择,具体使用,下面有介绍
# 开源声明
## 演示站点
[码支付](http://demo.stspwsc.com/) http://demo.stspwsc.com/
## 内置插件
码支付收款管理系统开源免费使用,支付插件有**免费**和**付费**版本,按需使用购买
1. **微信**插件`wxpay`默认安装,支持`赞赏码`、`个人码|经营码|商家码`2个通道需挂机监听
2. **支付宝**插件`alipay`默认安装,支持`收钱码`、`经营码`2个通道需挂机监听
3. **收钱吧**聚合码插件`sqbpay`默认安装,无需挂机,设置定时任务即可
## 技术架构
使用`Thinkphp8`框架PHP版本 > 8.0推荐8.2前端UI使用`Layui 2.9`+`PearAdmin`后台
---
# 安装和使用
## 全新安装
以下演示基于**云服务器**环境+**宝塔面板**安装,云服务器购买可以去阿里云、腾讯云等平台,宝塔面板安装教程参考[宝塔面板安装教程](https://www.bt.cn/new/download.html)
### 🚀️ 源码下载
**点击下载 [码支付 v1版本](https://gitee.com/technical-laohu/mpay/releases)**
**更多版本请关注发行版更新记录**
### 安装配置
以**宝塔面板**示例,其他服务器管理面板可以参考
通过宝塔面板登陆管理后台新建PHP站点和数据库并确认创建
![](assets/20241203_161723_image.png)
在网站列表页面,点击创建的网站的根目录,进入文件管理
![](assets/20241203_162102_image.png)
文件夹里面有一些默认文件,不用管他
![](assets/20241203_162231_image.png)
点击上传文件,将源码压缩包上传到该文件夹,并解压到前文件夹
![](assets/20241203_162646_image.png)
**将`mpay`文件夹里面的所有文件,复制到当前根目录下**,返回网站列表管理页面
> 注意,压缩包文件打包的是一个名为`mpay`的文件夹,需要将代码文件夹里面的所有文件复制出来,放到创建的网站根目录下,
### 运行目录&仿静态 配置
点击网站名,进入网站配置设置页面
![](assets/20241203_163259_image.png)
选择**网站目录**,运行目录选择`public`,保存
![](assets/20241203_164321_image.png)
选择**伪静态**,模版选择`thinkphp`,即可自动填写,保存
![](assets/20241203_163507_image.png)
### 安装步骤
在浏览器输入`http://你的域名/install`,进入程序安装界面,按照提示进行填写提交
![](assets/20241203_165327_image.png)
数据库配置相关信息,在服务器管理面板里查找
![](assets/20241203_165507_image.png)
---
## ❤️ 聚合码使用
如果本身就有聚合码收钱码最好,没有就需要提前去各收银服务商申请,申请也不复杂
### 申请收款码
以下列出一些常见收款服务平台,可以按需申请,个人直接申请小微商户即可
| 平台 | 官网 |
|--------|--------------------------------------|
| 收钱吧 | https://www.shouqianba.com/ |
| 小Y经营 | https://xym.ysepay.com/ |
| 码钱 | https://m.hkrt.cn/ |
| 拉卡拉 | https://customer.lakala.com/ |
| 盛付通 | https://b.shengpay.com/ |
> 申请可以去官方平台注册账号等客服电话,或者在社群里询问(有很多人有代办资质),实在找不到的,可以去淘宝上的官方店买个二维码卡牌贴纸,然后询问客服如何开通账号就行,会有专员联系你开通。
### 安装插件
程序默认安装有**微信支付**`wxpay`、**支付宝**`alipay`、**收钱吧**`sqbpay`三个插件,基本能满足大家的日常收款需求。
如果需要其他收银平台插件,可以在插件中心自行安装,插件中心没有的,也可以联系作者定制开发。
### 添加账号
除**微信支付**`wxpay`、**支付宝**`alipay`之外,所有的收款平台均为聚合码收款平台
以**收钱吧**`sqbpay`为例,添加账号时,需要填写**收钱吧**商户管理中心的**登陆账号**和**登陆密码**,需要使用时,插件会自动在后台登陆账号并查询相关订单流水信息。
![](assets/20241205_102856_image.png)
![](assets/20241205_103718_image.png)
**监听模式**说明,分为`单次监听`和`连续监听`
![](assets/20241205_104101_image.png)
`单次监听`是在收银台页面,用户扫码支付成功后,需要手动点击**确认支付**,后台才会登陆查询该账号的收款流水情况,**点击一次,查询一次,不点击,则不会查询**,降低密集查询可能导致的账户风控
![](assets/20241205_104306_image.png)
`连续监听`是在创建订单之后,**在订单有效期内且订单未完成状态时**,插件会一直连续不断的查询账户流水详情,直到过期或者成功收款,用户不需要主动点击确认
![](assets/20241205_105844_image.png)
> 使用哪种监听模式,视应用场景自行选择,一般选择**连续监听**即可,如果日常订单比较密集,可以选择**单次监听**,或多添加几个账号,减少密集查询风控
### 添加收款码
点击可以查看当前账号的所有收款码
![](assets/20241205_110432_image.png)
点击**添加收款码**后,可以正常填写二维码信息
**终端编号**需要填写当前收款码在收银服务商系统内的编码,有的可以直接在收款二维码解析的**链接里找到**,有的需要**登陆商户管理中心**,去订单详情里查询才能知道
🚀️ 具体各个平台的终端编号如何获取,可以去**程序后端控制台主页**的`项目文档`查看🚀️
![](assets/20241205_110508_image.png)
![](assets/20241205_111720_image.png)
收款样式有两个选项`付款链接`和`图片地址`,根据实际情况选择
**付款链接:** 将二维码的内容解析成字符串保存,使用时再生成二维码
**图片地址:** 上传二维码图片到服务器或引用http远程图片地址
> 一般的**聚合收款码都解析成文字保存**即可,前端展示时会重新生成二维码,无法解析的就上传图片
### 🚀️ 设置监听
#### 宝塔任务计划
等会儿需要在这里设置任务监听
![](assets/20241205_112415_image.png)
#### 新订单监听
一个站点只需要设置一个定时任务,每次访问都会查询一次数据库,并生成新订单数据缓存
![](assets/20241205_112033_image.png)
![](assets/20241205_112517_image.png)
#### 账号监听
**每个账号都需要单独设置一个定时任务**,每次访问都会检索新订单缓存数据,如果存在该账户的新订单,插件就会去查询服务商后台的订单流水,并通知相关收款处理程序
![](assets/20241205_112116_image.png)
![](assets/20241205_113629_image.png)
### 支付测试
只开启一个账号,然后点击`支付测试`,正常支付后,观察是否成功回调,如果不能回调请重新检查配置,或再次详细查看**程序后端控制台主页**的`项目文档`
![](assets/20241205_113835_image.png)
---
## ❤️ 微信/支付宝使用
微信/支付宝生成的收款码,需要挂机监听收款消息,基本情况上面有介绍,因为使用广泛,所以也单独添加进来,可以实现正常收款回调
### 添加账号
收款平台选择`微信支付`或`支付宝`,收款账号就填写`微信支付`或`支付宝`的账号,与**聚合码支付**不同,此处填写的账号主要用来做区分,不会登陆后台
![](assets/20241204_164128_image.png)
### 添加收钱码
点击可以查看当前账号的所有收款码
![](assets/20241204_164617_image.png)
点击**添加收款码**后,可以正常填写二维码信息
`微信支付`和`支付宝`的终端编号是自动生成的,不可手动填写,分别对应当前账号的不同收款通道,支持`个人码`和`赞赏码`
![](assets/20241204_164650_image.png)
收款样式有两个选项`付款链接`和`图片地址`,根据实际情况选择
**付款链接:** 将二维码的内容解析成字符串保存,使用时再生成二维码
**图片地址:** 上传二维码图片到服务器或引用http远程图片地址
> `赞赏码`等收款方式,采用的不是标准二维码编码格式,不能解析成文字保存,只能展示图片,建议一般**二维码都解析成文字保存**,前端展示时会根据内容自动再生成二维码,无法解析的就上传图片
---
### 挂机监听收款通知
收款通知信息需要挂机监听,有`手机监听`和`PC监听`两种手机可以同时监听微信和支付宝PC只能监听微信具体支持的通知和功能在设置账号页面有详细介绍。
#### 🚀️ 手机软件下软(安卓)
码支付使用的是开源工具**短信转发器**`SmsForwarder`来监听收款通知,`SmsForwarder`功能非常强大,喜欢*搞机* 的朋友可以多钻研,这是使用文档[短信转发器](https://gitee.com/pp/SmsForwarder/wikis/pages)
* **开源项目地址:[SmsForwarder](https://gitee.com/pp/SmsForwarder)**
* **发布地址,尽量下载最新版,兼职更多机型:[SmsForwarder](https://gitee.com/pp/SmsForwarder/releases)**
* **网盘地址,访问密码:`pppscn`[SmsForwarder](https://wws.lanzoui.com/b025yl86h)**
![](assets/20241204_171340_image.png)
---
##### 功能配置
找一台不常用的安卓系统手机,下载安装好`SmsForwarder`之后,点开进行配置,同时打开码支付后台**用户中心**页面,查看相关配置参数
![](assets/20241204_175759_image.png)
##### **通用设置**
1. 开启转发应用通知,只针对应用软件才会转发
2. 保活措施全部开启(实际情况看手机环境)
3. 通知栏文案可以自定义,方便自己运维,可参考图片
4. 其他默认即可,也可以参考实际情况自行设置
![](assets/20241204_172346_6c9c6f4d97d5850e7890633edd7e5d5.jpg)
##### **发送通道**
添加发送通道,选择`Webhook`类型
![](assets/20241204_172532_image.png)
**编辑规则**
1. 通道名称自行命名,方便区别就行
2. 请求方式选择`POST`方式
3. Webhook Server 地址**用户中心**查看
4. **消息模版**在**用户中心**查看
5. **Secert**密钥在**用户中心**查看
6. 应签关键字`200`
填写完成点击**保存**即可
![](assets/20241204_172612_image.png)
##### **转发规则**
添加应用转发规则,**微信**和**支付宝**需要分别设置,请注意选择**应用**规则
![](assets/20241204_172656_image.png)
**具体设置**
1. 选择发送通道
2. **匹配字段**选择**多重匹配****匹配的值**去**用户中心**复制,然后粘贴过来
3. 开启**启用自定义模版**,内容填写去码支付后台**账号列表**里复制,粘贴过来
**注意:** 微信支付规则里,第三行的`[空格]`需要替换成真实的` `空格
![](assets/20241204_172718_c313070e899cc93cc2fc9fe25a1ff17.jpg)
![](assets/20241204_175634_image.png)
---
##### **转发日志**
这里可以查看所有的转发记录,点击也可以查看消息详情
![](assets/20241204_172819_2d569802058d4d1b0f92135dcc30469.jpg)
##### **注意事项**
用户中心后台里的配置APP包名不一定与手机应用里的包名一致需要查看一下如果有区别请修改成实际本机显示的APP包名下面有图片演示操作
![](assets/20241204_180022_image.png)
![](assets/20241204_173010_image.png)
![](assets/20241204_173046_image.png)
#### 🚀️ PC软件下载
码支付微信PC监听是老胡使用python写的小工具可以监听电脑桌面打开的窗口和内容信息非侵入微信应用内部版本升级不影响监听。需要将被监听的微信聊天界面单独拖出来成独立窗口。
[PC监听软件下载wxmonitor.zip](assets/20250217_103606_wxmonitor.zip)
##### 配置信息
1. 打开用户中心,添加接口密钥等信息
![](assets/20250217_104137_image.png)
2. 打开账号管理列表,填写通道信息,格式`pid#aid`
![](assets/20250217_104349_image.png)
3. 循环间隔是软件检索窗口通知信息的循环时间一般设置1-3秒均可看电脑性能
4. 监控窗口根据实际情况勾选,哪个窗口需要检索收款通知,就开启哪个,不使用的就不需要开启
![](assets/20250217_105144_image.png)
5. PC监听同时支持个人码、经营码、赞赏码、商户码**手机监听因无法区分个人码和经营码,添加时只能二选一,且只能在`个人码`通道添加**。
![](assets/20250217_105939_image.png)
7. PC监听和手机监听只需要开启一个即可不要同时开启会出现重复通知导致订单异常。
---
# 补充说明
* 该软件仅适用于个人线上免签收款,避免人工审核确认收款的繁琐步骤。请勿用于诈骗、黑灰产业,如有此类行为,后果自负。
* 插件中心目前还在开发中不支持在线安装插件如果有购买插件或定制其他收银平台插件的需求可以联系作者Wechat:**K103516**
# 页面展示
![首页](assets/20241203_153935_image.png)
![安装](assets/20241203_154034_image.png)
![登陆](assets/20241203_154108_image.png)
![后端控制台](assets/20241203_154141_image.png)
![订单管理](assets/20241203_154218_image.png)
![账号管理](assets/20241203_154307_image.png)
![插件管理](assets/20241203_154420_image.png)
![用户中心](assets/20241203_154505_image.png)
![添加账号](assets/20241203_154755_image.png)
![收款码](assets/20241203_154918_image.png)
![订单详情](assets/20241203_155123_image.png)
# 学习交流社群
社群答疑、插件定制、技术交流,添加微信拉群,请备注:**码支付**
微信:**K103516**
<img src="assets/wxqrcode.png" width=50% />
## 感谢赞助
<img src="assets/20250217_111408_1739762042428.jpg" width=35% />
|姓名 | 金额 |
|---|---|
| 知汇学社 | 100 |
| exrock | 100 |
| A筱磊 | 6.66 |
| 云峰 | 8.88 |
| 零零 | 200 |
| 子染 | 88 |
## 友情链接
1. [腾飞博客专业WP子比主题美化定制](https://calfbxtoi72.feishu.cn/docx/DA3ddthzOolQ16xDgeOcHA36nrg)

1
app/.htaccess Normal file
View File

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

Binary file not shown.

View File

@ -13,14 +13,13 @@ class ConsoleController extends BaseController
// 后台主页 // 后台主页
public function index() public function index()
{ {
View::assign('version', 'V1');
return View::fetch(); return View::fetch();
} }
// 管理菜单 // 管理菜单
public function menu() public function menu()
{ {
// 加载菜单配置 // 加载菜单配置
$menu = \think\facade\Config::load("extend/menu", 'extend'); $menu = \think\facade\Config::load("extendconfig/menu", 'extendconfig');
return json($menu); return json($menu);
} }
// 管理菜单 // 管理菜单
@ -28,12 +27,6 @@ class ConsoleController extends BaseController
{ {
// 加载菜单配置 // 加载菜单配置
$message = \Plugin::getNotifyMessage(); $message = \Plugin::getNotifyMessage();
if (empty($message)) {
$message = [
["id" => 1, "title" => "应用更新", "children" => []],
["id" => 2, "title" => "官方消息", "children" => []],
];
}
return json($message); return json($message);
} }
// 首页仪表盘 // 首页仪表盘

View File

@ -20,4 +20,9 @@ class IndexController
View::assign('domain', \request()->domain()); View::assign('domain', \request()->domain());
return View::fetch(); return View::fetch();
} }
public function test()
{
return app()->getBasePath();
}
} }

View File

@ -7,178 +7,53 @@ namespace app\controller;
use think\facade\Db; use think\facade\Db;
use think\Request; use think\Request;
use think\facade\View; use think\facade\View;
use think\facade\Log;
use think\exception\ValidateException;
use think\Validate;
class InstallController class InstallController
{ {
private const INSTALL_LOCK_FILE = 'install.lock';
/**
* 连接数据库
* @return \think\db\Connection
*/
private function connectDatabase()
{
return Db::connect();
}
/**
* 首页,检查是否已安装,若已安装则跳转到登录页,否则显示安装页面
* @return \think\response\Redirect|\think\response\View
*/
public function index() public function index()
{ {
// 检查是否已经安装过
if ($this->checkLock()) { if ($this->checkLock()) {
return redirect('User/login'); return redirect('User/login');
} };
return View::fetch(); return View::fetch();
} }
/**
* 安装操作,检查环境、保存数据库配置信息
* @param Request $request
* @return \think\response\Json
*/
public function install(Request $request) public function install(Request $request)
{ {
// 检查是否已经安装过
if ($this->checkLock()) { if ($this->checkLock()) {
return json(backMsg(1, '已经安装')); return backMsg(1, '已经安装');
} };
// 获取表单提交的数据库配置信息
$envCheck = $this->checkEnvironment();
if ($envCheck !== true) {
return json(backMsg(1, $envCheck));
}
$dbConfig = $request->post(); $dbConfig = $request->post();
try {
$this->validateDbConfig($dbConfig); // 保存数据库配置信息到配置文件
$this->saveDbConfig($dbConfig); $this->saveDbConfig($dbConfig);
return json(backMsg(0, '配置保存成功'));
} catch (ValidateException $e) {
return json(backMsg(1, $e->getMessage()));
} catch (\Exception $e) {
Log::error("保存数据库配置失败: " . $e->getMessage());
return json(backMsg(1, '配置保存失败'));
}
}
/** // 连接数据库并建表
* 初始化数据库,创建表并初始化数据 $is_succ_tb = $this->createTables();
* @param Request $request
* @return \think\response\Json // 初始化数据记录
*/ $is_succ_data = $this->initData($dbConfig);
public function init(Request $request)
{ // 安装检测
if ($this->checkLock()) { if (!$is_succ_tb) {
return json(backMsg(1, '已经安装')); return json(backMsg(1, '数据表创建失败'));
} }
if (!$is_succ_data) {
$dbConfig = $request->post(); return json(backMsg(1, '数据初始化失败'));
$startTime = microtime(true); }
// 安装成功,写入安装锁文件
try {
$this->validateInitData($dbConfig);
$this->connectDatabase()->transaction(function () use ($dbConfig) {
$this->createTables();
$this->initData($dbConfig);
});
$this->setLock(); $this->setLock();
$endTime = microtime(true);
Log::info("数据库初始化完成,耗时: " . ($endTime - $startTime) . "");
return json(backMsg(0, '安装成功')); return json(backMsg(0, '安装成功'));
} catch (ValidateException $e) {
return json(backMsg(1, $e->getMessage()));
} catch (\Exception $e) {
Log::error("数据库初始化失败: " . $e->getMessage());
return json(backMsg(1, '数据库初始化失败'));
}
} }
/** private function saveDbConfig($dbConfig)
* 检查环境,包括 PHP 版本、文件上传写入权限、Fileinfo 扩展
* @return bool|string
*/
private function checkEnvironment()
{
if (version_compare(PHP_VERSION, '8.0', '<')) {
return 'PHP 版本必须大于等于 8.0';
}
if (!is_writable(sys_get_temp_dir())) {
return '文件上传目录没有写入权限';
}
if (!extension_loaded('fileinfo')) {
return 'Fileinfo 扩展未安装';
}
return true;
}
/**
* 验证数据库配置信息
* @param array $dbConfig
* @throws ValidateException
*/
private function validateDbConfig(array $dbConfig)
{
$validate = new Validate();
$rule = [
'host' => 'require',
'name' => 'require',
'user' => 'require',
'pass' => 'require',
'port' => 'require|integer',
];
if (!$validate->rule($rule)->check($dbConfig)) {
throw new ValidateException($validate->getError());
}
}
/**
* 验证初始化数据信息
* @param array $dbConfig
* @throws ValidateException
*/
private function validateInitData(array $dbConfig)
{
$validate = new Validate();
$rule = [
'nickname' => 'require',
'username' => 'require',
'password' => 'require'
];
if (!$validate->rule($rule)->check($dbConfig)) {
throw new ValidateException($validate->getError());
}
}
/**
* 保存数据库配置信息到 .env 文件
* @param array $dbConfig
* @throws \Exception
*/
private function saveDbConfig(array $dbConfig)
{ {
$envPath = app()->getRootPath() . '.env'; $envPath = app()->getRootPath() . '.env';
$envContent = $this->generateEnvContent($dbConfig); $envContent = <<<EOT
if (file_put_contents($envPath, $envContent) === false) { APP_DEBUG = true
throw new \Exception("无法写入 .env 文件");
}
}
/**
* 生成 .env 文件内容
* @param array $dbConfig
* @return string
*/
private function generateEnvContent(array $dbConfig): string
{
return <<<EOT
APP_DEBUG = false
DB_TYPE = mysql DB_TYPE = mysql
DB_HOST = {$dbConfig['host']} DB_HOST = {$dbConfig['host']}
@ -186,150 +61,136 @@ DB_NAME = {$dbConfig['name']}
DB_USER = {$dbConfig['user']} DB_USER = {$dbConfig['user']}
DB_PASS = {$dbConfig['pass']} DB_PASS = {$dbConfig['pass']}
DB_PORT = {$dbConfig['port']} DB_PORT = {$dbConfig['port']}
DB_CHARSET = {$dbConfig['charset']}
DB_PREFIX = mpay_ DB_PREFIX = mpay_
DEFAULT_LANG = zh-cn DEFAULT_LANG = zh-cn
EOT; EOT;
file_put_contents($envPath, $envContent);
} }
/**
* 创建数据库表
* @throws \Exception
*/
private function createTables() private function createTables()
{ {
$db = $this->connectDatabase(); // 连接数据库
$tables = $this->getTableCreationSqls(); $db = Db::connect();
if ($db === false) {
return false;
}
// 创建order表的 SQL 语句
$sql = "CREATE TABLE `mpay_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL DEFAULT '0' COMMENT '商户ID',
`order_id` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '订单号',
`type` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '支付类型',
`out_trade_no` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '商户订单号',
`notify_url` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '异步通知地址',
`return_url` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '跳转通知地址',
`name` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '商品名称',
`really_price` float NOT NULL DEFAULT '0' COMMENT '实际支付金额',
`money` float NOT NULL DEFAULT '0' COMMENT '订单价格',
`clientip` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '用户IP地址',
`device` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '设备类型',
`param` varchar(720) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '扩展参数',
`state` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态',
`patt` tinyint(4) NOT NULL DEFAULT '0' COMMENT '开启回调监听',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '订单创建时间',
`close_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '订单关闭时间',
`pay_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '支付时间',
`platform_order` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '收款平台订单号',
`aid` int(11) NOT NULL DEFAULT '0' COMMENT '收款账号ID',
`cid` int(11) NOT NULL DEFAULT '0' COMMENT '收款码ID',
`delete_time` timestamp NULL DEFAULT NULL COMMENT '软删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;";
foreach ($tables as $tableName => $sql) { // 执行 SQL 语句创建表
try { $db->execute("DROP TABLE IF EXISTS `mpay_order`;");
$db->execute("DROP TABLE IF EXISTS `$tableName`;");
$db->execute($sql); $db->execute($sql);
Log::info("$tableName 表创建成功");
} catch (\Exception $e) { // 创建pay_account表的 SQL 语句
throw new \Exception("创建 $tableName 表失败: " . $e->getMessage()); $sql = "CREATE TABLE `mpay_pay_account` (
} `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '收款平台ID',
} `pid` int(11) NOT NULL DEFAULT '0' COMMENT '用户ID',
`platform` varchar(255) NOT NULL DEFAULT '' COMMENT '收款平台',
`account` varchar(255) NOT NULL DEFAULT '' COMMENT '账号',
`password` varchar(255) NOT NULL DEFAULT '' COMMENT '密码',
`state` tinyint(4) NOT NULL DEFAULT '1' COMMENT '启用',
`pattern` tinyint(4) NOT NULL DEFAULT '1' COMMENT '账号监听模式',
`params` varchar(255) NOT NULL DEFAULT '' COMMENT '自定义查询',
`delete_time` timestamp NULL DEFAULT NULL COMMENT '软删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;";
// 执行 SQL 语句创建表
$db->execute("DROP TABLE IF EXISTS `mpay_pay_account`;");
$db->execute($sql);
// 创建pay_channel表的 SQL 语句
$sql = "CREATE TABLE `mpay_pay_channel` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '渠道ID',
`account_id` int(11) NOT NULL DEFAULT '0' COMMENT '收款平台ID',
`channel` varchar(255) NOT NULL DEFAULT '' COMMENT '收款通道',
`type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '保存类型',
`qrcode` varchar(255) NOT NULL DEFAULT '' COMMENT '二维码',
`last_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最近使用',
`state` tinyint(4) NOT NULL DEFAULT '1' COMMENT '启用',
`delete_time` timestamp NULL DEFAULT NULL COMMENT '软删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;";
// 执行 SQL 语句创建表
$db->execute("DROP TABLE IF EXISTS `mpay_pay_channel`;");
$db->execute($sql);
// 创建user表的 SQL 语句
$sql = "CREATE TABLE `mpay_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL DEFAULT '0' COMMENT '商户ID',
`secret_key` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '商户秘钥',
`nickname` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '用户昵称',
`username` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '账号',
`password` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '密码',
`state` tinyint(4) NOT NULL DEFAULT '1' COMMENT '启用状态 0:禁用 1:启用',
`role` tinyint(4) NOT NULL DEFAULT '0' COMMENT '用户角色 0:普通用户 1:管理员',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`delete_time` timestamp NULL DEFAULT NULL COMMENT '软删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;";
// 执行 SQL 语句创建表
$db->execute("DROP TABLE IF EXISTS `mpay_user`;");
$db->execute($sql);
return true;
} }
/** private function initData($dbConfig)
* 获取表创建的 SQL 语句
* @return array
*/
private function getTableCreationSqls(): array
{ {
return [ // 连接数据库
'mpay_order' => "CREATE TABLE `mpay_order` ( $db = Db::connect();
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL DEFAULT 0,
`order_id` varchar(255) NOT NULL DEFAULT '',
`type` varchar(255) NOT NULL DEFAULT '',
`out_trade_no` varchar(255) NOT NULL DEFAULT '',
`notify_url` varchar(512) NOT NULL DEFAULT '',
`return_url` varchar(512) NOT NULL DEFAULT '',
`name` varchar(255) NOT NULL DEFAULT '',
`really_price` decimal(10, 2) NOT NULL DEFAULT 0.00,
`money` decimal(10, 2) NOT NULL DEFAULT 0.00,
`clientip` varchar(255) NOT NULL DEFAULT '',
`device` varchar(255) NOT NULL DEFAULT '',
`param` varchar(720) NOT NULL DEFAULT '',
`state` tinyint(4) NOT NULL DEFAULT 0,
`patt` tinyint(4) NOT NULL DEFAULT 0,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`close_time` datetime DEFAULT NULL,
`pay_time` datetime DEFAULT NULL,
`platform` varchar(15) NOT NULL DEFAULT '',
`platform_order` varchar(255) NOT NULL DEFAULT '',
`aid` int(11) NOT NULL DEFAULT 0,
`cid` int(11) NOT NULL DEFAULT 0,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
INDEX `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;",
'mpay_pay_account' => "CREATE TABLE `mpay_pay_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL DEFAULT 0,
`platform` varchar(255) NOT NULL DEFAULT '',
`account` varchar(255) NOT NULL DEFAULT '',
`password` varchar(255) NOT NULL DEFAULT '',
`state` tinyint(4) NOT NULL DEFAULT 1,
`pattern` tinyint(4) NOT NULL DEFAULT 1,
`params` text NOT NULL,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;",
'mpay_pay_channel' => "CREATE TABLE `mpay_pay_channel` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`account_id` int(11) NOT NULL DEFAULT 0,
`channel` varchar(255) NOT NULL DEFAULT '',
`type` tinyint(4) NOT NULL DEFAULT 0,
`qrcode` varchar(512) NOT NULL DEFAULT '',
`last_time` datetime DEFAULT CURRENT_TIMESTAMP,
`state` tinyint(4) NOT NULL DEFAULT 1,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;",
'mpay_user' => "CREATE TABLE `mpay_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL DEFAULT 0,
`secret_key` varchar(255) NOT NULL DEFAULT '',
`nickname` varchar(255) NOT NULL DEFAULT '',
`username` varchar(255) NOT NULL DEFAULT '',
`password` varchar(255) NOT NULL DEFAULT '',
`state` tinyint(4) NOT NULL DEFAULT 1,
`role` tinyint(4) NOT NULL DEFAULT 0,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`delete_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC;",
];
}
/**
* 初始化数据
* @param array $dbConfig
* @throws \Exception
*/
private function initData(array $dbConfig)
{
$db = $this->connectDatabase();
$info = [ $info = [
'secret_key' => md5(1000 . time() . mt_rand()), 'secret_key' => md5(1000 . time() . mt_rand()),
'nickname' => $dbConfig['nickname'], 'nickname' => $dbConfig['nickname'],
'username' => $dbConfig['username'], 'username' => $dbConfig['username'],
'password' => password_hash($dbConfig['password'], PASSWORD_DEFAULT), 'password' => password_hash($dbConfig['password'], PASSWORD_DEFAULT),
'create_time' => date('Y-m-d H:i:s'),
]; ];
// 初始化数据的 SQL 语句
$sql = "INSERT INTO `mpay_user` (`id`, `pid`, `secret_key`, `nickname`, `username`, `password`, `state`, `role`) VALUES (1, 1000, :secret_key, :nickname, :username, :password, 1, 1);";
$sql = "INSERT INTO `mpay_user` (`id`, `pid`, `secret_key`, `nickname`, `username`, `password`, `state`, `role`, `create_time`) VALUES (1, 1000, :secret_key, :nickname, :username, :password, 1, 1, :create_time);"; // 执行 SQL 语句插入初始数据
$is_succ = $db->execute($sql, $info);
try { if (!$is_succ) {
$db->execute($sql, $info); return false;
Log::info("mpay_user 表数据初始化成功");
} catch (\Exception $e) {
throw new \Exception("初始化 mpay_user 表数据失败: " . $e->getMessage());
} }
return true;
} }
/**
* 检查是否已安装
* @return bool
*/
private function checkLock() private function checkLock()
{ {
$path = runtime_path() . self::INSTALL_LOCK_FILE; $path = runtime_path() . 'install.lock';
return file_exists($path); return file_exists($path);
} }
/**
* 设置安装锁
* @throws \Exception
*/
private function setLock() private function setLock()
{ {
$path = runtime_path() . self::INSTALL_LOCK_FILE; $path = runtime_path() . 'install.lock';
if (file_put_contents($path, time()) === false) { file_put_contents($path, time());
throw new \Exception("无法写入安装锁文件");
}
} }
} }

View File

@ -6,7 +6,6 @@ use think\Request;
use think\facade\View; use think\facade\View;
use app\model\User; use app\model\User;
use app\model\Order; use app\model\Order;
use app\model\PayAccount;
use app\model\PayChannel; use app\model\PayChannel;
class PayController class PayController
@ -20,41 +19,70 @@ class PayController
'POST' => $request->post(), 'POST' => $request->post(),
default => [] default => []
}; };
if (!$req_data) return '参数错误'; if (!$req_data) {
// 验证签名 return '参数错误';
}
$key = User::where('pid', $req_data['pid'])->where('state', 1)->value('secret_key'); $key = User::where('pid', $req_data['pid'])->where('state', 1)->value('secret_key');
if (!$key) return '用户禁用或不存在'; if (!$key) {
return '用户禁用或不存在';
}
$sign_str = self::getSign($req_data, $key); $sign_str = self::getSign($req_data, $key);
if ($req_data['sign'] !== $sign_str) return '签名错误'; if ($req_data['sign'] === $sign_str) {
// 检查商户订单 // 检查商户订单
$out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no'); $out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no');
if ($out_trade_no) return '订单提交重复'; if (!$out_trade_no) {
// 创建新订单 // 创建新订单
$order_info = Order::createOrder($req_data); $order_id = Order::createOrder($req_data);
if ($order_info['code'] !== 0) return $order_info['msg']; if ($order_id) {
return redirect("/Pay/console/{$order_info['data']['order_id']}"); return redirect("/Pay/console/{$order_id}");
} else {
return '创建订单失败';
}
} else {
return '订单提交重复';
}
} else {
return '签名错误';
}
} }
// api提交订单 // api提交订单
public function mapi(Request $request) public function mapi(Request $request)
{ {
if (!$request->isPost()) return json(backMsg(0, '请求方式错误')); if ($request->isPost()) {
$req_data = $request->post(); $req_data = $request->post();
if (!$req_data) $req_data = $request->get(); if (!$req_data) {
if (!$req_data) return json(backMsg(0, '参数错误')); $req_data = $request->get();
// 验证签名 if (!$req_data) {
return '参数错误';
}
}
} else {
return '请使用POST方式提交';
}
$key = User::where('pid', $req_data['pid'])->where('state', 1)->value('secret_key'); $key = User::where('pid', $req_data['pid'])->where('state', 1)->value('secret_key');
if (!$key) return json(backMsg(0, '用户禁用或不存在')); if (!$key) {
return '用户禁用或不存在';
}
$sign_str = self::getSign($req_data, $key); $sign_str = self::getSign($req_data, $key);
if ($req_data['sign'] !== $sign_str) return json(backMsg(0, '签名错误')); if ($req_data['sign'] === $sign_str) {
// 检查商户订单 // 检查商户订单
$out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no'); $out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no');
if ($out_trade_no) return json(backMsg(0, '订单提交重复')); if (!$out_trade_no) {
// 创建新订单 // 创建新订单
$order_info = Order::createOrder($req_data); $order_id = Order::createOrder($req_data);
if ($order_info['code'] !== 0) return json(backMsg(0, $order_info['msg'])); if ($order_id) {
$payurl = $request->domain() . "/Pay/console/{$order_info['data']['order_id']}"; $payurl = $request->domain() . "/Pay/console/{$order_id}";
$info = ['code' => 1, 'msg' => '订单创建成功', 'trade_no' => $order_info['data']['order_id'], 'payurl' => $payurl]; $info = ['code' => 1, 'msg' => '订单创建成功', 'trade_no' => $order_id, 'qrcode' => $payurl];
return json($info); return json($info);
} else {
return '创建订单失败';
}
} else {
return '订单提交重复';
}
} else {
return '签名错误';
}
} }
// 收银台 // 收银台
public function console($order_id = '') public function console($order_id = '')
@ -66,20 +94,7 @@ class PayController
View::assign($act_order->toArray()); View::assign($act_order->toArray());
$passtime = strtotime($act_order->close_time) - time(); $passtime = strtotime($act_order->close_time) - time();
View::assign('passtime', $passtime > 0 ? $passtime : 0); View::assign('passtime', $passtime > 0 ? $passtime : 0);
// Alipay免输
if (preg_match('/^alipay4#\d+$/', $channel->channel)) {
$chan = request()->get('chan', '');
if ($chan && $chan == 'Alipayf') {
$payurl = \payclient\AliPayf::getPayUrl($act_order->order_id, $act_order->money, $channel->qrcode, 1);
View::assign('payUrl', $payurl['data'] ?? $payurl['msg']);
View::assign('payclient', 'Alipayf');
} else {
$payurl = \payclient\AliPayf::getPayUrl($act_order->order_id, $act_order->money, $channel->qrcode);
}
View::assign('payUrl', $payurl['data'] ?? $payurl['msg']);
} else {
View::assign('payUrl', $channel->qrcode); View::assign('payUrl', $channel->qrcode);
}
View::assign('code_type', $channel->type); View::assign('code_type', $channel->type);
return View::fetch(); return View::fetch();
} else { } else {
@ -111,7 +126,6 @@ class PayController
$notify['sign'] = $sign; $notify['sign'] = $sign;
// 跳转通知URL // 跳转通知URL
$res_return_url = $act_order->return_url . '?' . http_build_query($notify); $res_return_url = $act_order->return_url . '?' . http_build_query($notify);
if (strpos($act_order->return_url, '?')) $res_return_url = $act_order->return_url . '&' . http_build_query($notify);
// 响应消息 // 响应消息
$data['order_id'] = $act_order->order_id; $data['order_id'] = $act_order->order_id;
$data['passtime'] = $passtime > 0 ? $passtime : 0; $data['passtime'] = $passtime > 0 ? $passtime : 0;
@ -143,20 +157,31 @@ class PayController
} }
} }
// 处理收款通知 // 处理收款通知
private function payHeart(array $records, array $config) public function payHeart(Request $request)
{ {
$pid = $config['pid']; $pid = $request->get('pid');
$aid = $config['aid']; $aid = $request->get('aid');
$sign = $request->get('sign');
// 检测请求参数
if (!($pid && $aid && $sign)) {
return json(['code' => 0, 'msg' => '参数错误']);
}
// 检测收款通知 // 检测收款通知
if (!$records) { $payList = $request->post();
if (!$payList) {
return json(['code' => 0, 'msg' => '空收款通知']); return json(['code' => 0, 'msg' => '空收款通知']);
} }
// 签名验证
$is_user = User::checkUser($pid, $sign);
if (!$is_user) {
return json(['code' => 0, 'msg' => '签名错误']);
}
// 当前用户账号 // 当前用户账号
$query = ['pid' => $pid, 'aid' => $aid]; $query = ['pid' => $pid, 'aid' => $aid];
// 排除已支付订单 // 排除已支付订单
$doneOrders = Order::scope('dealOrder')->where($query)->column('platform_order'); $doneOrders = Order::scope('dealOrder')->where($query)->column('platform_order');
$new_orders = []; $new_orders = [];
foreach ($records as $order) { foreach ($payList as $order) {
if (!in_array($order['order_no'], $doneOrders)) $new_orders[] = $order; if (!in_array($order['order_no'], $doneOrders)) $new_orders[] = $order;
} }
if (!count($new_orders)) return json(['code' => 0, 'msg' => '收款通知无新消息']); if (!count($new_orders)) return json(['code' => 0, 'msg' => '收款通知无新消息']);
@ -172,24 +197,15 @@ class PayController
foreach ($activeOrders as $order) { foreach ($activeOrders as $order) {
// 支付方式核对 // 支付方式核对
$is_payway = $order->type == $new_order['payway']; $is_payway = $order->type == $new_order['payway'];
if ($new_order['payway'] == '') $is_payway = true;
// 支付渠道核对 // 支付渠道核对
$is_channel = $cids[$order->cid] == $new_order['channel']; $is_channel = $cids[$order->cid] == $new_order['channel'];
// 金额核对 // 金额核对
$is_money = $order->really_price == $new_order['price']; $is_money = $order->money == $new_order['price'];
// 订单核对 // 订单核对
if ($is_payway && $is_channel && $is_money) { if ($is_payway && $is_channel && $is_money) {
// 是否免输
if (isset($new_order['remark'])) {
if ($new_order['remark'] == $order->order_id) {
$res = $this->updateOrderState($order, $new_order['order_no']); $res = $this->updateOrderState($order, $new_order['order_no']);
$notify[] = $res; $notify[] = $res;
} }
} else {
$res = $this->updateOrderState($order, $new_order['order_no']);
$notify[] = $res;
}
}
} }
} }
if (!$notify) $notify = ['code' => 0, 'msg' => '收款通知无匹配订单']; if (!$notify) $notify = ['code' => 0, 'msg' => '收款通知无匹配订单'];
@ -210,85 +226,107 @@ class PayController
$sign = self::getSign($notify, $user_key); $sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign; $notify['sign'] = $sign;
// 异步通知 // 异步通知
$notify_url = $order->notify_url . '?' . http_build_query($notify); $res_notify = self::getHttpResponse($order->notify_url . '?' . http_build_query($notify));
if (strpos($order->notify_url, '?')) $notify_url = $order->notify_url . '&' . http_build_query($notify);
$res_notify = self::getHttpResponse($notify_url);
if ($res_notify === 'success') { if ($res_notify === 'success') {
return ['order' => $order->order_id, 'code' => 1, 'msg' => 'notify success']; return ['order' => $order->order_id, 'code' => 1, 'msg' => 'notify success'];
} else { } else {
return ['order' => $order->order_id, 'code' => 0, 'msg' => 'notify fail']; return ['order' => $order->order_id, 'code' => 0, 'msg' => 'notify fail'];
} }
} }
// [定时任务]获取收款明细,提交收款通知 // [定时任务]获取收款明细,提交收款通知[本地版]
public function checkPayResult(Request $request) public function checkPayResult(Request $request)
{ {
$req_info = $request->get(); $req_info = $request->get();
$req_pid = $req_info['pid']; $req_pid = $req_info['pid'];
$req_aid = $req_info['aid']; $req_aid = $req_info['aid'];
// 加载配置文件
$config = \think\facade\Config::load("payconfig/{$req_pid}_{$req_aid}", 'payconfig');
// 用户账号配置
$user_config = isset($config['user']) ? $config['user'] : [];
// 收款平台账号配置
$pay_config = isset($config['pay']) ? $config['pay'] : [];
// 配置检查
if ($user_config && $pay_config) {
// 账号配置信息
$pid = $user_config['pid'];
$aid = $pay_config['aid'];
if (!($req_pid == $pid && $req_aid == $aid)) {
return '监听收款配置不一致';
}
} else {
return '监听收款配置文件名错误';
}
// 当前站点
$user_config['host'] = \request()->domain();
// 实例化支付类
$Mpay = new \MpayClass($user_config);
// 获取订单 // 获取订单
$new_order = cache('order'); $res_new_order = $Mpay->orderHeart();
if (!$new_order) return json(['code' => 3, 'msg' => '没有找到新订单缓存']); $new_order = json_decode($res_new_order, true);
// 检测新订单 // 检测新订单
if ($new_order['code'] !== 1) return json($new_order); if ($new_order['code'] !== 1) return $res_new_order;
// 订单列表 // 订单列表
$order_list = $new_order['orders']; $order_list = $new_order['orders'];
// 检测本账号订单 // 检测本账号订单
$orders = []; $orders = [];
foreach ($order_list as $key => $val) { foreach ($order_list as $key => $val) {
if ($req_pid == $val['pid'] && $req_aid == $val['aid'] && $val['patt'] == 1) { if ($pid == $val['pid'] && $aid == $val['aid'] && $val['patt'] == 1) {
$orders[] = $order_list[$key]; $orders[] = $order_list[$key];
} }
} }
if (!$orders) return json(['code' => 0, 'msg' => '非本账号订单或监听模式不对']); if (!$orders) {
// 加载配置文件 return \json(['code' => 0, 'msg' => '非本账号订单或监听模式不对']);
$config = PayAccount::getAccountConfig($req_aid); }
if ($config === false) return json(['code' => 4, 'msg' => '监听收款配置错误']);
// 登陆账号 // 登陆账号
$pay_config = ['username' => $config['account'], 'password' => $config['password'], 'aid' => $config['aid']]; $config = ['username' => $pay_config['account'], 'password' => $pay_config['password']];
// 配置参数 // 收款查询
$params = $config['params']; $query = $pay_config['query'];
// 实例监听客户端 // 实例监听客户端
$payclient_name = $config['payclass']; $payclient_name = $pay_config['payclass'];
// 插件类文件是否存在
$payclient_path = root_path() . '/extend/payclient/' . $payclient_name . '.php';
if (!file_exists($payclient_path)) return json(['code' => 5, 'msg' => '监听客户端文件不存在']);
$payclient_path = "\\payclient\\{$payclient_name}"; $payclient_path = "\\payclient\\{$payclient_name}";
$Payclient = new $payclient_path($pay_config); $Payclient = new $payclient_path($config);
// 获取支付明细 // 获取支付明细
$records = $Payclient->getOrderInfo($params); $records = $Payclient->getOrderInfo($query);
if ($records['code'] === 0) { if ($records) {
// 提交收款记录 // 提交收款记录
$upres = $this->payHeart($records['data'], $config); $upres = $Mpay->upRecords($records, $aid);
return $upres; return $upres;
} else { } else {
return json(['code' => 0, 'msg' => $records['msg']], 320); return \json(['code' => 0, 'msg' => '查询空订单'], 320);
} }
} }
// [定时任务]监听新订单,生成缓存 // [定时任务]监听新订单,生成JSON文件信息
public function checkOrder($pid = '', $sign = '') public function checkOrder($pid = '', $sign = '')
{ {
if (!($pid && $sign)) return '参数错误'; if (!($pid && $sign)) {
return '参数错误';
}
$is_user = User::checkUser($pid, $sign); $is_user = User::checkUser($pid, $sign);
$path = runtime_path() . 'order.json';
if ($is_user) { if ($is_user) {
$orders = Order::scope('activeOrder')->field('id,pid,aid,cid,patt')->select(); $orders = Order::scope('activeOrder')->field('id,pid,aid,cid,patt')->select();
$old_info = cache('order'); if (!file_exists($path)) {
file_put_contents($path, '[]');
}
$old_info = file_get_contents($path);
$num = count($orders); $num = count($orders);
if ($num > 0) { if ($num > 0) {
$info = ['code' => 1, 'msg' => "{$num}个新订单"]; $info = ['code' => 1, 'msg' => "{$num}个新订单"];
$order_list = ['code' => 1, 'msg' => "{$num}个新订单", 'orders' => $orders]; $order_list = ['code' => 1, 'msg' => "{$num}个新订单", 'orders' => $orders];
if ($old_info !== $order_list) { if ($old_info !== json_encode($order_list)) {
cache('order', $order_list); file_put_contents($path, json_encode($order_list));
} }
return json($info); return json($info);
} else { } else {
$info = ['code' => 0, 'msg' => '没有新订单']; $info = ['code' => 0, 'msg' => '没有新订单'];
if ($old_info !== $info) { if ($old_info !== json_encode($info, 320)) {
cache('order', $info); file_put_contents($path, json_encode($info, 320));
} }
return json($info); return json($info);
} }
} else { } else {
$info = ['code' => 2, 'msg' => '签名错误']; $info = ['code' => 2, 'msg' => '签名错误'];
file_put_contents($path, json_encode($info, 320));
return json($info); return json($info);
} }
} }
@ -297,18 +335,16 @@ class PayController
{ {
$info = $request->post(); $info = $request->post();
$action = isset($info['action']) ? $info['action'] : ''; $action = isset($info['action']) ? $info['action'] : '';
if ($action !== 'mpay' && $action !== 'mpaypc') return '非mpay的访问请求'; if ($action === 'mpay') {
$data = json_decode($info['data'], true); $data = json_decode($info['data'], true);
if (!is_array($data)) return '通知数据为空'; $config = \think\facade\Config::load("payconfig/{$data['pid']}_{$data['aid']}", 'payconfig');
if (!isset($data['aid']) || !isset($data['pid'])) return 'aid和pid参数错误'; $payclient_path = "\\payclient\\{$config['pay']['payclass']}";
$config = PayAccount::getAccountConfig($data['aid'], $data['pid']); $Payclient = new $payclient_path($info, $config, $request->domain());
$payclient_path = "\\payclient\\{$config['payclass']}"; $Payclient->notify();
$Payclient = new $payclient_path($info, $config);
if ($action == 'mpay') $res = $Payclient->notify();
if ($action == 'mpaypc') $res = $Payclient->pcNotify();
if ($res['code'] !== 0) return $res['msg'];
$this->payHeart($res['data'], $config);
return 200; return 200;
} else {
return 202;
}
} }
// 签名 // 签名
private static function getSign(array $param = [], string $key = ''): string private static function getSign(array $param = [], string $key = ''): string
@ -340,12 +376,7 @@ class PayController
'sign_type' => 'MD5', 'sign_type' => 'MD5',
]; ];
// 添加扩展参数 // 添加扩展参数
// $notify = array_merge($notify, unserialize($param->param)); $notify = array_merge($notify, unserialize($param->param));
$notify['param'] = unserialize($param->param);
// 删除空值
foreach ($notify as $key => $val) {
if ($val === '') unset($notify[$key]);
}
return $notify; return $notify;
} }
// 请求外部资源 // 请求外部资源

View File

@ -74,9 +74,4 @@ class PayManageController extends BaseController
View::assign(['id' => $id]); View::assign(['id' => $id]);
return View::fetch(); return View::fetch();
} }
// 收款统计
public function payStatistics()
{
return View::fetch();
}
} }

View File

@ -0,0 +1,85 @@
<?php
declare (strict_types = 1);
namespace app\controller;
use think\Request;
class SystemController
{
/**
* 显示资源列表
*
* @return \think\Response
*/
public function index()
{
return '系统设置';
}
/**
* 显示创建资源表单页.
*
* @return \think\Response
*/
public function create()
{
//
}
/**
* 保存新建的资源
*
* @param \think\Request $request
* @return \think\Response
*/
public function save(Request $request)
{
//
}
/**
* 显示指定的资源
*
* @param int $id
* @return \think\Response
*/
public function read($id)
{
//
}
/**
* 显示编辑资源表单页.
*
* @param int $id
* @return \think\Response
*/
public function edit($id)
{
//
}
/**
* 保存更新的资源
*
* @param \think\Request $request
* @param int $id
* @return \think\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* 删除指定资源
*
* @param int $id
* @return \think\Response
*/
public function delete($id)
{
//
}
}

View File

@ -48,9 +48,7 @@ class OrderController extends BaseController
$sign = self::getSign($notify, $user_key); $sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign; $notify['sign'] = $sign;
// 异步通知 // 异步通知
$notify_url = $order->notify_url . '?' . http_build_query($notify); $res_notify = self::getHttpResponse($order->notify_url . '?' . http_build_query($notify));
if (strpos($order->notify_url, '?')) $notify_url = $order->notify_url . '&' . http_build_query($notify);
$res_notify = self::getHttpResponse($notify_url);
if ($res_notify === 'success') { if ($res_notify === 'success') {
return json(\backMsg(0, '订单通知成功')); return json(\backMsg(0, '订单通知成功'));
} else { } else {
@ -74,9 +72,7 @@ class OrderController extends BaseController
$sign = self::getSign($notify, $user_key); $sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign; $notify['sign'] = $sign;
// 异步通知 // 异步通知
$notify_url = $order->notify_url . '?' . http_build_query($notify); $res_notify = self::getHttpResponse($order->notify_url . '?' . http_build_query($notify));
if (strpos($order->notify_url, '?')) $notify_url = $order->notify_url . '&' . http_build_query($notify);
$res_notify = self::getHttpResponse($notify_url);
if ($res_notify === 'success') { if ($res_notify === 'success') {
return json(\backMsg(0, '订单通知成功')); return json(\backMsg(0, '订单通知成功'));
} else { } else {
@ -160,12 +156,7 @@ class OrderController extends BaseController
'sign_type' => 'MD5', 'sign_type' => 'MD5',
]; ];
// 添加扩展参数 // 添加扩展参数
// $notify = array_merge($notify, unserialize($param->param)); $notify = array_merge($notify, unserialize($param->param));
$notify['param'] = unserialize($param->param);
// 删除空值
foreach ($notify as $key => $val) {
if ($val === '') unset($notify[$key]);
}
return $notify; return $notify;
} }
// 请求外部资源 // 请求外部资源

View File

@ -7,9 +7,7 @@ namespace app\controller\api;
use app\BaseController; use app\BaseController;
use app\model\PayAccount; use app\model\PayAccount;
use app\model\PayChannel; use app\model\PayChannel;
use app\model\Order; use app\model\User;
use think\facade\Db;
use \think\facade\Log;
class PayManageController extends BaseController class PayManageController extends BaseController
{ {
@ -30,9 +28,9 @@ class PayManageController extends BaseController
$aid = $this->request->post('aid'); $aid = $this->request->post('aid');
$res = PayChannel::where(['account_id' => $aid])->order('last_time', 'desc')->select(); $res = PayChannel::where(['account_id' => $aid])->order('last_time', 'desc')->select();
if ($res) { if ($res) {
return json(backMsg(0, '获取成功', $res)); return \json(\backMsg(0, '获取成功', $res));
} else { } else {
return json(backMsg(1, '失败')); return \json(\backMsg(1, '失败'));
} }
} }
// 账号状态 // 账号状态
@ -41,9 +39,9 @@ class PayManageController extends BaseController
$info = $this->request->post(); $info = $this->request->post();
$up_res = PayAccount::update($info); $up_res = PayAccount::update($info);
if ($up_res) { if ($up_res) {
return json(backMsg(0, '成功')); return json(\backMsg(0, '成功'));
} else { } else {
return json(backMsg(1, '失败')); return json(\backMsg(1, '失败'));
} }
} }
// 添加账号 // 添加账号
@ -55,13 +53,17 @@ class PayManageController extends BaseController
$info['params'] = '{}'; $info['params'] = '{}';
$check_acc = PayAccount::where(['account' => $info['account'], 'platform' => $info['platform'], 'pid' => $pid])->find(); $check_acc = PayAccount::where(['account' => $info['account'], 'platform' => $info['platform'], 'pid' => $pid])->find();
if ($check_acc) { if ($check_acc) {
return json(backMsg(1, '账号已存在')); return \json(\backMsg(1, '账号已存在'));
} }
$acc = PayAccount::create($info); $acc = PayAccount::create($info);
if ($acc) { if ($acc) {
return json(backMsg(0, '添加成功')); $state = $this->createAccountConfig($acc);
if (!$state) {
return json(\backMsg(1, '自字义参数错误'));
}
return \json(\backMsg(0, '添加成功'));
} else { } else {
return json(backMsg(1, '添加失败')); return \json(\backMsg(1, '添加失败'));
} }
} }
// 编辑账号 // 编辑账号
@ -70,9 +72,14 @@ class PayManageController extends BaseController
$info = $this->request->post(); $info = $this->request->post();
$up_res = PayAccount::update($info); $up_res = PayAccount::update($info);
if ($up_res) { if ($up_res) {
return json(backMsg(0, '修改成功')); $acc = PayAccount::find($info['id']);
$state = $this->createAccountConfig($acc);
if (!$state) {
return json(\backMsg(1, '自字义参数错误'));
}
return json(\backMsg(0, '修改成功'));
} else { } else {
return json(backMsg(1, '修改失败')); return json(\backMsg(1, '修改失败'));
} }
} }
// 删除账号 // 删除账号
@ -82,9 +89,13 @@ class PayManageController extends BaseController
$res = PayAccount::destroy($ids); $res = PayAccount::destroy($ids);
$res2 = PayChannel::whereIn('account_id', $ids)->select()->delete(); $res2 = PayChannel::whereIn('account_id', $ids)->select()->delete();
if ($res && $res2) { if ($res && $res2) {
return json(backMsg(0, '已删除')); $accs = PayAccount::whereIn('id', $ids)->withTrashed()->select();
foreach ($accs as $acc) {
$this->delAccountConfig($acc);
}
return \json(\backMsg(0, '已删除'));
} else { } else {
return json(backMsg(1, '失败')); return \json(\backMsg(1, '失败'));
} }
} }
// 添加收款终端 // 添加收款终端
@ -93,14 +104,13 @@ class PayManageController extends BaseController
$info = $this->request->post(); $info = $this->request->post();
$check = PayChannel::where(['account_id' => $info['account_id'], 'channel' => $info['channel']])->count(); $check = PayChannel::where(['account_id' => $info['account_id'], 'channel' => $info['channel']])->count();
if ($check) { if ($check) {
return json(backMsg(1, '编号已存在')); return \json(\backMsg(1, '编号已存在'));
} }
$info['last_time'] = date('Y-m-d H:i:s');
$res = PayChannel::create($info); $res = PayChannel::create($info);
if ($res) { if ($res) {
return json(backMsg(0, '添加成功')); return \json(\backMsg(0, '添加成功'));
} else { } else {
return json(backMsg(1, '添加失败')); return \json(\backMsg(1, '添加失败'));
} }
} }
// 编辑收款终端 // 编辑收款终端
@ -109,9 +119,9 @@ class PayManageController extends BaseController
$info = $this->request->post(); $info = $this->request->post();
$up_res = PayChannel::update($info); $up_res = PayChannel::update($info);
if ($up_res) { if ($up_res) {
return json(backMsg(0, '修改成功')); return json(\backMsg(0, '修改成功'));
} else { } else {
return json(backMsg(1, '修改失败')); return json(\backMsg(1, '修改失败'));
} }
} }
// 删除收款终端 // 删除收款终端
@ -120,163 +130,64 @@ class PayManageController extends BaseController
$cid = $this->request->post('id'); $cid = $this->request->post('id');
$res = PayChannel::destroy($cid); $res = PayChannel::destroy($cid);
if ($res) { if ($res) {
return json(backMsg(0, '已删除')); return \json(\backMsg(0, '已删除'));
} else { } else {
return json(backMsg(1, '失败')); return \json(\backMsg(1, '失败'));
} }
} }
// 上传二维码图片 // 删除账号配置
public function uploadQrcode() public function delAccountConfig($acc)
{ {
try { $path = config_path() . "/payconfig/{$acc->pid}_{$acc->id}.php";
// 获取上传的文件 if (file_exists($path)) {
$img = $this->request->file('codeimg'); unlink($path);
if (!$img) {
return json(backMsg(1, '请选择要上传的文件'));
}
// 验证文件大小,防止大文件攻击
$maxSize = 2 * 1024 * 1024; // 2MB
if ($img->getSize() > $maxSize) {
return json(backMsg(1, '文件大小不能超过 2MB'));
}
// 验证文件类型,防止恶意文件上传
$allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
$fileMimeType = $img->getMime();
if (!in_array($fileMimeType, $allowedTypes)) {
return json(backMsg(1, '只允许上传 PNG、JPEG 或 GIF 格式的图片'));
}
// 二次验证文件类型,通过文件内容判断
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$realMimeType = finfo_file($finfo, $img->getRealPath());
finfo_close($finfo);
if (!in_array($realMimeType, $allowedTypes)) {
return json(backMsg(1, '文件类型验证失败,请上传有效的图片文件'));
}
// 生成唯一文件名,避免文件名冲突
$filename = 'img_' . time() . '_' . uniqid() . '.' . $img->getOriginalExtension();
// 过滤文件名,防止路径遍历攻击
$filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $filename);
// 设置文件保存路径
$path = public_path() . '/files/qrcode/';
if (!is_dir($path)) {
if (!mkdir($path, 0755, true)) {
return json(backMsg(1, '创建目录失败'));
} }
} }
// 移动文件到指定目录 // 生成账号配置
$info = $img->move($path, $filename); private function createAccountConfig($acc)
if ($info) {
$imgpath = '/files/qrcode/' . $filename;
return json(backMsg(0, '上传成功', ['imgpath' => $imgpath]));
} else {
return json(backMsg(1, '上传失败'));
}
} catch (\Exception $e) {
Log::error('上传过程中出现异常: ' . $e->getMessage());
return json(backMsg(1, '上传过程中出现异常,请稍后重试'));
}
}
// 获取账号交易流水
public function getAccountTrade()
{ {
$req_info = $this->request->get(); $params = \json_decode($acc->params, \true);
$req_pid = $req_info['pid']; if ($params === null) {
$req_aid = $req_info['aid']; return false; // 自定义参数错误
// 加载配置文件 }
$config = PayAccount::getAccountConfig($req_aid); $platform = \app\controller\api\PluginController::getPluginInfo($acc->getData('platform'));
if ($config === false) return json(backMsg(1, '账号配置文件错误')); $user = User::where('pid', $acc->pid)->find();
if ($req_aid != $config['aid'] || $req_pid != session('pid')) return json(backMsg(1, '监听收款配置不一致')); $query_tpl = $platform['query'];
// 登陆账号 $query = var_export(\array_merge($query_tpl, $params), \true);
$pay_config = ['username' => $config['account'], 'password' => $config['password'], 'aid' => $config['aid']]; $config = <<<EOF
// 收款查询 <?php
$params = $config['params']; // +----------------------------------------------------------------------
// 实例监听客户端 // | 支付监听配置,一个文件,一个账号
$payclient_name = $config['payclass']; // +----------------------------------------------------------------------
$payclient_path = "\\payclient\\{$payclient_name}";
$Payclient = new $payclient_path($pay_config); return [
// 获取支付明细 // 用户账号配置
$records = $Payclient->getOrderInfo($params); 'user' => [
if ($records['code'] === 0) { 'pid' => {$user->pid},
// 收款流水 'key' => '{$user->secret_key}'
return json(backMsg(0, '查询成功', $records['data'])); ],
} else { // 收款平台账号配置
return json(['code' => 1, 'msg' => $records['msg']]); 'pay' => [
// 账号id
'aid' => $acc->id,
// 收款平台
'platform' => '{$acc->getData('platform')}',
// 插件类名
'payclass' => '{$platform['class_name']}',
// 账号
'account' => '{$acc->account}',
// 密码
'password' => '{$acc->password}',
// 订单查询参数配置
'query' => {$query},
]
];
EOF;
$name = "{$user->pid}_{$acc->id}";
$path = config_path() . "/payconfig/{$name}.php";
\file_put_contents($path, $config);
return true;
} }
} }
public function payStatisticsList()
{
$query = $this->request->get();
$limit = $query['limit'] ?? 10;
$page = $query['page'] ?? 1;
$start_time = $query['time_start'] ?? date('Y-m-d H:i:s', strtotime('today'));
$end_time = $query['time_end'] ?? date('Y-m-d H:i:s', strtotime('tomorrow') - 1);
// 确保日期时间格式正确
$start_time = date('Y-m-d H:i:s', strtotime($start_time));
$end_time = date('Y-m-d H:i:s', strtotime($end_time));
$accounts = Db::table('mpay_pay_account', 'PayAccount')
->alias('PayAccount')
->join('mpay_order Order', 'PayAccount.id = Order.aid AND Order.delete_time IS NULL AND Order.state = 1', 'LEFT')
->field([
'PayAccount.*',
'SUM(CASE WHEN DATE(Order.pay_time) = CURDATE() THEN Order.really_price ELSE 0 END) as day',
'SUM(CASE WHEN DATE(Order.pay_time) = DATE_SUB(CURDATE(), INTERVAL 1 DAY) THEN Order.really_price ELSE 0 END) as yesterday',
'SUM(CASE WHEN YEARWEEK(Order.pay_time, 1) = YEARWEEK(CURDATE(), 1) THEN Order.really_price ELSE 0 END) as week',
'SUM(CASE WHEN DATE_FORMAT(Order.pay_time, "%Y-%m") = DATE_FORMAT(CURDATE(), "%Y-%m") THEN Order.really_price ELSE 0 END) as month',
'SUM(CASE WHEN YEAR(Order.pay_time) = YEAR(CURDATE()) THEN Order.really_price ELSE 0 END) as year',
'SUM(IFNULL(Order.really_price, 0)) as total',
"SUM(CASE WHEN Order.pay_time BETWEEN '$start_time' AND '$end_time' THEN Order.really_price ELSE 0 END) as income"
])
->where('PayAccount.delete_time IS NULL')
->group('PayAccount.id')
->order('PayAccount.id', 'DESC')
->paginate(['list_rows' => $limit, 'page' => $page]);
return json([
'code' => 0,
'msg' => 'OK',
'count' => $accounts->total(),
'data' => $accounts->items()
]);
}
// 收款统计
// public function payStatisticsList()
// {
// $query = $this->request->get();
// // 定义统计字段
// $fields = [
// "SUM(IF(DATE(pay_time) = CURDATE(), really_price, 0)) as day",
// "SUM(IF(DATE(pay_time) = CURDATE() - INTERVAL 1 DAY, really_price, 0)) as yesterday",
// "SUM(IF(YEARWEEK(pay_time, 1) = YEARWEEK(CURDATE(), 1), really_price, 0)) as week",
// "SUM(IF(DATE_FORMAT(pay_time, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m'), really_price, 0)) as month",
// "SUM(IF(YEAR(pay_time) = YEAR(CURDATE()), really_price, 0)) as year",
// "SUM(really_price) as total"
// ];
// $where = ['state', 1;
// // 合并 pay_account 表字段和统计字段
// $allFields = array_merge([PayAccount::getTable() . '.*'], $fields);
// $accounts = PayAccount::hasWhere('order', $where, '*', 'LEFT')
// ->field($allFields)
// ->group(PayAccount::getTable() . '.id')
// ->order('id', 'desc')
// ->paginate(['list_rows' => $query['limit'] ?? 10, 'page' => $query['page'] ?? 1]);
// if ($accounts) {
// return json(['code' => 0, 'msg' => PayAccount::getLastSql(), 'count' => $accounts->total(), 'data' => $accounts->items()]);
// } else {
// return json(['code' => 1, 'msg' => '无数据记录', 'count' => 0, 'data' => []]);
// }
// }
}

View File

@ -25,74 +25,42 @@ class PluginController extends BaseController
return json(['code' => 1, 'msg' => '无数据记录', 'count' => 0, 'data' => []]); return json(['code' => 1, 'msg' => '无数据记录', 'count' => 0, 'data' => []]);
} }
} }
// 安装插件
public function installPlugin()
{
$platform = $this->request->post('platform');
if (!$platform) return json(backMsg(1, '请选择插件'));
$intall_info = \Plugin::installPlugin($platform);
if ($intall_info['code'] !== 0) return json(backMsg(1, $intall_info['msg']));
// 需要授权
if ($intall_info['data']['status'] === 0) {
return json(['code' => 0, 'msg' => '请支付', 'state' => 0, 'data' => $intall_info['data']]);
}
$saved = $this->saveNewPluginConfig($intall_info['data']);
if ($saved['code'] !== 0) return json(backMsg(1, $saved['msg']));
return json(['code' => 0, 'msg' => '授权成功', 'state' => 1]);
}
// 更新插件
public function updatePlugin()
{
$platform = $this->request->post('platform');
if (!$platform) return json(backMsg(1, '请选择插件'));
$update_info = \Plugin::updatePlugin($platform);
if ($update_info['code'] !== 0) return json(backMsg(1, $update_info['msg']));
$saved = $this->saveNewPluginConfig($update_info['data']);
if ($saved['code'] !== 0) return json(backMsg(1, $saved['msg']));
return json(['code' => 0, 'msg' => '更新成功']);
}
// 保存全部插件信息
private function saveNewPluginConfig(array $config = [])
{
$plugin_config = $config['config'];
$plugin_auth = $config['authcode'];
$plugin_file = $config['file'];
if (!$this->savePluginFile($plugin_file, $plugin_config)) return backMsg(1, '保存插件文件失败');
if (!$this->saveAuthCode($plugin_auth, $plugin_config)) return backMsg(1, '保存插件授权码失败');
if (!$this->addPlugin($plugin_config)) return backMsg(1, '保存插件配置失败');
return backMsg(0, 'ok');
}
// 卸载插件 // 卸载插件
public function uninstallPlugin() public function uninstallPlugin()
{ {
$platform = $this->request->post('platform'); $platform = $this->request->post('platform');
if (!$platform) return json(backMsg(1, '请选择插件')); $classname = $this->request->post('classname');
$this->delPluginFile($platform); if (!$platform || !$classname) {
$this->delPlugin($platform); return json(backMsg(1, '请选择插件'));
return json(backMsg(0, '卸载成功'));
} }
// 添加或更新插件 $res1 = $this->delPlugin($platform);
$res2 = $this->delPluginFile($classname);
if ($res1 && $res2) {
return json(backMsg(0, '卸载成功'));
} else {
return json(backMsg(1, '插件不存在'));
}
}
// 添加插件
public function addPlugin(array $option = []) public function addPlugin(array $option = [])
{ {
$keys = ['platform', 'name', 'class_name', 'price', 'describe', 'website', 'helplink', 'version']; $keys = ['platform', 'name', 'class_name', 'price', 'describe', 'website', 'state', 'query'];
$config = []; $config = [];
foreach ($option as $key => $value) { foreach ($option as $key => $value) {
if (in_array($key, $keys)) $config[$key] = $value; if (in_array($key, $keys)) {
$config[$key] = $value;
}
} }
$config['state'] = 1;
$plugin_config = self::getPluginConfig(); $plugin_config = self::getPluginConfig();
$plugin_platform = $config['platform'] ?: ''; $plugin_platform = $config['platform'] ?: '';
foreach ($plugin_config as $i => $value) { foreach ($plugin_config as $value) {
if ($plugin_platform == $value['platform']) { if ($plugin_platform == $value['platform']) {
$plugin_config[$i] = $config; return 1; //'插件已存在'
$this->savePluginConfig($plugin_config, '支付插件列表');
return true;
} }
} }
$plugin_config[] = $config; $plugin_config[] = $config;
$this->savePluginConfig($plugin_config, '支付插件列表'); $this->savePluginConfig($plugin_config, '支付插件列表');
return true; return 0;
} }
// 删除插件配置 // 删除插件配置
private function delPlugin(string $plugin_name = '') private function delPlugin(string $plugin_name = '')
@ -102,31 +70,37 @@ class PluginController extends BaseController
foreach ($plugin_config as $i => $value) { foreach ($plugin_config as $i => $value) {
if ($value['platform'] == $plugin_name) { if ($value['platform'] == $plugin_name) {
$index = $i; $index = $i;
break;
} }
} }
if ($index === null) return false; if ($index === null) {
return false; // 插件不存在
}
unset($plugin_config[$index]); unset($plugin_config[$index]);
$config = array_values($plugin_config); $config = array_values($plugin_config);
$this->savePluginConfig($config, '支付插件列表'); $this->savePluginConfig($config, '支付插件列表');
return true; return true;
} }
// 删除插件类库文件 // 删除插件类库文件
private function delPluginFile(string $platform = '') private function delPluginFile(string $file_name = '')
{ {
$file_name = self::getPluginInfo($platform)['class_name'];
if (!$file_name) return false;
$plugin_path = root_path() . '/extend/payclient/' . $file_name . '.php'; $plugin_path = root_path() . '/extend/payclient/' . $file_name . '.php';
if (!file_exists($plugin_path)) return false; if (file_exists($plugin_path)) {
unlink($plugin_path); unlink($plugin_path);
return true; return true;
} else {
return false;
}
} }
// 修改插件 // 修改插件
public function setPlugin($platform = '', $option = []) public function setPlugin($platform = '', $option = [])
{ {
$config = self::getPluginConfig(); $config = self::getPluginConfig();
if (!$platform) return 1; if (!$platform) {
if (!$option) return 2; return 1; // 请选择插件
}
if (!$option) {
return 2; // 请添加插件配置
}
foreach ($config as $index => $options) { foreach ($config as $index => $options) {
if ($options['platform'] == $platform) { if ($options['platform'] == $platform) {
foreach ($options as $key => $value) { foreach ($options as $key => $value) {
@ -143,7 +117,9 @@ class PluginController extends BaseController
public function pluginEnable() public function pluginEnable()
{ {
$info = $this->request->post(); $info = $this->request->post();
if (!$this->isPluginInstall($info['platform'])) return json(backMsg(1, '插件未安装')); if ($this->isPluginInstall($info['platform']) == false) {
return json(backMsg(1, '插件未安装'));
}
$up_res = $this->setPlugin($info['platform'], ['state' => $info['state']]); $up_res = $this->setPlugin($info['platform'], ['state' => $info['state']]);
if ($up_res) { if ($up_res) {
return json(backMsg(1, '失败')); return json(backMsg(1, '失败'));
@ -192,30 +168,13 @@ class PluginController extends BaseController
} }
return $info; return $info;
} }
// 保存授权码
private function saveAuthCode(string $authcode = '', array $config = [])
{
$dir_path = runtime_path() . "auth/";
if (!is_dir($dir_path)) mkdir($dir_path, 755, true);
$auth_path = $dir_path . md5("{$config['platform']}payclient\\{$config['class_name']}") . '.json';
return file_put_contents($auth_path, json_encode(['authcode' => $authcode])) !== false ? true : false;
}
// 保存插件类库文件
private function savePluginFile($file_url = '', array $config = [])
{
if (empty($file_url)) return false;
$file_content = @file_get_contents($file_url);
if ($file_content === false) return false;
$save_dir = root_path() . 'extend/payclient/';
if (!is_dir($save_dir)) mkdir($save_dir, 0755, true);
$save_path = $save_dir . $config['class_name'] . '.php';
return file_put_contents($save_path, $file_content) !== false ? true : false;
}
// 获取插件配置 // 获取插件配置
private static function getPluginConfig(): array private static function getPluginConfig(): array
{ {
$payplugin_path = config_path() . '/extend/payplugin.php'; $payplugin_path = config_path() . '/extendconfig/payplugin.php';
if (!file_exists($payplugin_path)) return []; if (!file_exists($payplugin_path)) {
return [];
}
// 加载插件配置 // 加载插件配置
$payplugin_config = require $payplugin_path; $payplugin_config = require $payplugin_path;
return $payplugin_config; return $payplugin_config;
@ -223,7 +182,7 @@ class PluginController extends BaseController
// 保存插件配置 // 保存插件配置
private function savePluginConfig(array $config, string $note = '说明') private function savePluginConfig(array $config, string $note = '说明')
{ {
$payplugin_path = config_path() . '/extend/payplugin.php'; $payplugin_path = config_path() . '/extendconfig/payplugin.php';
$note_tpl = <<<EOF $note_tpl = <<<EOF
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
// | $note // | $note

View File

@ -10,7 +10,7 @@ use app\model\User;
class UserController extends BaseController class UserController extends BaseController
{ {
protected $middleware = ['Auth' => ['except' => ['login']]]; protected $middleware = ['Auth' => ['except' => ['login', 'resetKey']]];
public function login() public function login()
{ {

View File

@ -13,12 +13,10 @@ class Order extends BaseModel
// 订单有效期 // 订单有效期
private static $activity_time = 180; private static $activity_time = 180;
// 新建订单 // 新建订单
public static function createOrder($data): array public static function createOrder($data)
{ {
$my_time = time(); $my_time = time();
$channel = self::setChannel($data['pid'], $data['type']); $channel = self::setChannel($data['pid'], $data['type']);
if ($channel['code'] !== 0) return $channel;
$channel = $channel['data'];
$new_order = [ $new_order = [
// 订单号 // 订单号
'order_id' => self::createOrderID('H'), 'order_id' => self::createOrderID('H'),
@ -37,19 +35,17 @@ class Order extends BaseModel
// 商品金额 // 商品金额
'money' => $data['money'], 'money' => $data['money'],
// 实际成交金额 // 实际成交金额
'really_price' => self::checkMoney($data['money'], $data['type'], $channel['aid'], $channel['cid'], $channel['chan']), 'really_price' => self::checkMoney($data['money'], $data['type'], $channel['aid'], $channel['cid']),
// 用户IP // 用户IP
'clientip' => isset($data['clientip']) ? $data['clientip'] : '', 'clientip' => isset($data['clientip']) ? $data['clientip'] : '',
// 设备类型 // 设备类型
'device' => isset($data['device']) ? $data['device'] : '', 'device' => isset($data['device']) ? $data['device'] : '',
// 业务扩展参数 // 业务扩展参数
'param' => serialize(isset($data['param']) ? $data['param'] : ''), 'param' => serialize(self::getParams($data)),
// 等待/过期0, 支付成功1 // 等待/过期0, 支付成功1
'state' => 0, 'state' => 0,
// 开启监听1, 关闭监听0 // 开启监听1, 关闭监听0
'patt' => $channel['patt'], 'patt' => $channel['patt'],
// 平台
'platform' => $channel['platform'],
// 订单创建时间 // 订单创建时间
'create_time' => self::getFormatTime($my_time), 'create_time' => self::getFormatTime($my_time),
// 订单关闭时间 // 订单关闭时间
@ -63,49 +59,56 @@ class Order extends BaseModel
]; ];
$res = self::create($new_order); $res = self::create($new_order);
if ($res->order_id) { if ($res->order_id) {
return backMsg(0, 'ok', ['order_id' => $res->order_id]); return $res->order_id;
} else { } else {
return backMsg(4, '创建订单记录失败'); return false;
} }
} }
// 查询订单列表 // 查询订单列表
public static function serchOrders($query) public static function serchOrders($query)
{ {
$select = []; $select = [];
$allow_field = ['id', 'order_id', 'pid', 'type', 'out_trade_no', 'name', 'really_price', 'money', 'state', 'create_time_start', 'create_time_end', 'close_time', 'pay_time', 'platform', 'platform_order', 'aid', 'cid',]; $_select = [];
$allow_field = ['id', 'order_id', 'pid', 'type', 'out_trade_no', 'notify_url', 'return_url', 'name', 'really_price', 'money', 'clientip', 'device', 'state', 'create_time_start', 'create_time_end', 'close_time', 'pay_time', 'platform', 'platform_order', 'aid', 'cid',];
foreach ($query as $key => $value) { foreach ($query as $key => $value) {
if (in_array($key, $allow_field) && isset($value)) { if (in_array($key, $allow_field) && isset($value)) {
if ($key === 'name') { if ($key === 'name') {
$select[] = [$key, 'like', '%' . $value . '%']; $select[] = ['Order.' . $key, 'like', '%' . $value . '%'];
continue; continue;
} }
if ($key === 'create_time_start') { if ($key === 'create_time_start') {
$select[] = ['create_time', '>', $value]; $select[] = ['Order.' . 'create_time', '>', $value];
continue; continue;
} }
if ($key === 'create_time_end') { if ($key === 'create_time_end') {
$select[] = ['create_time', '<', $value]; $select[] = ['Order.' . 'create_time', '<', $value];
continue; continue;
} }
$select[] = [$key, '=', $value]; if ($key === 'platform') {
$_select['platform'] = $value;
continue;
}
$select[] = ['Order.' . $key, '=', $value];
} }
} }
return self::where($select); return self::with('payAccount')
->hasWhere('payAccount', function ($query) use ($_select) {
$query->where($_select);
})
->where($select);
} }
// 查询订单详细 // 查询订单详细
public static function showOrderDetail($id) public static function showOrderDetail($id)
{ {
$order = self::find($id); $order = self::find($id);
$a_list = PayAccount::withTrashed()->find($order->aid); $a_list = PayAccount::with('payChannel')->hasWhere('payChannel', ['id' => $order->cid])->where('PayAccount.id', $order->aid)->find();
$c_list = PayChannel::withTrashed()->find($order->cid);
if (!$order) { if (!$order) {
return []; return [];
} }
$order->platform = $a_list['platform'] ?? '···'; $order->platform = $a_list->platform ?? '···';
$order->account = $a_list['account'] ?? '···'; $order->account = $a_list->account ?? '···';
$order->channel = $c_list['channel'] ?? '···'; $order->channel = $a_list->payChannel[0]->channel ?? '···';
$order->qrcode = $c_list['qrcode'] ?? '···'; $order->qrcode = $a_list->payChannel[0]->qrcode ?? '···';
$order->url_type = $c_list['type'] ?? '···';
return $order->toArray(); return $order->toArray();
} }
// 选择收款通道 // 选择收款通道
@ -113,11 +116,9 @@ class Order extends BaseModel
{ {
// 查询有效收款账户及通道 // 查询有效收款账户及通道
$aids = PayAccount::where('pid', $pid)->where('state', 1)->column('id'); $aids = PayAccount::where('pid', $pid)->where('state', 1)->column('id');
if (!$aids) return backMsg(1, '用户无可用收款账户');
$channel_infos = PayChannel::whereIn('account_id', $aids)->where('state', 1)->order('last_time', 'asc')->select(); $channel_infos = PayChannel::whereIn('account_id', $aids)->where('state', 1)->order('last_time', 'asc')->select();
if ($channel_infos->isEmpty()) return backMsg(2, '用户账户无可用收款码'); if (!$channel_infos || !$aids) return [];
// 微信/支付宝收款处理 // 微信/支付宝收款处理
$channel_info = null;
foreach ($channel_infos as $key => $value) { foreach ($channel_infos as $key => $value) {
$check_wx = preg_match('/^wxpay\d+#/i', $value->channel); $check_wx = preg_match('/^wxpay\d+#/i', $value->channel);
$check_ali = preg_match('/^alipay\d+#/i', $value->channel); $check_ali = preg_match('/^alipay\d+#/i', $value->channel);
@ -135,33 +136,28 @@ class Order extends BaseModel
break; break;
} }
} }
if (!$channel_info) return backMsg(3, '用户账户无可用收款通道');
// 选取收款通道 // 选取收款通道
$patt = PayAccount::find($channel_info->account_id); $patt = PayAccount::find($channel_info->account_id);
$channel = ['aid' => $channel_info->account_id, 'cid' => $channel_info->id, 'patt' => $patt->getData('pattern'), 'chan' => $channel_info->channel, 'platform' => $patt->getData('platform')]; $channel = ['aid' => $channel_info->account_id, 'cid' => $channel_info->id, 'patt' => $patt->getData('pattern')];
PayChannel::update(['last_time' => self::getFormatTime(), 'id' => $channel['cid']]); PayChannel::update(['last_time' => self::getFormatTime(), 'id' => $channel['cid']]);
return backMsg(0, 'ok', $channel); return $channel;
} }
// 获取扩展参数数组 // 获取扩展参数数组
// private static function getParams(array $data): array private static function getParams(array $data): array
// { {
// $keys = ['pid', 'type', 'out_trade_no', 'notify_url', 'return_url', 'name', 'money', 'sign', 'sign_type']; $keys = ['pid', 'type', 'out_trade_no', 'notify_url', 'return_url', 'name', 'money', 'sign', 'sign_type'];
// $params = []; $params = [];
// foreach ($data as $key => $value) { foreach ($data as $key => $value) {
// if (!in_array($key, $keys)) { if (!in_array($key, $keys)) {
// $params[$key] = $value; $params[$key] = $value;
// } }
// } }
// return $params; return $params;
// } }
// 检查金额 // 检查金额
private static function checkMoney($money, $type, $aid, $cid, $chan): float private static function checkMoney($money, $type, $aid, $cid): float
{ {
$money = (float) $money; $money = (float) $money;
// Alipay免输
if (preg_match('/^alipay4#\d+$/', $chan)) {
return $money;
}
// 查询有效订单 // 查询有效订单
$query = self::scope('activeOrder')->where(['type' => $type, 'aid' => $aid, 'cid' => $cid]); $query = self::scope('activeOrder')->where(['type' => $type, 'aid' => $aid, 'cid' => $cid]);
$activeOrders = $query->column('really_price'); $activeOrders = $query->column('really_price');
@ -188,9 +184,7 @@ class Order extends BaseModel
// 生成订单号 // 生成订单号
private static function createOrderID(string $prefix = ''): string private static function createOrderID(string $prefix = ''): string
{ {
$date = date('YmdHis'); return $prefix . date('Ymd') . substr(implode('', array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
$rand = rand(1000, 9999);
return $prefix . $date . $rand;
} }
// 查询有效期内的未支付订单 // 查询有效期内的未支付订单
public function scopeActiveOrder($query) public function scopeActiveOrder($query)
@ -210,6 +204,6 @@ class Order extends BaseModel
// 模型多对一关联 // 模型多对一关联
public function payAccount() public function payAccount()
{ {
return $this->belongsTo(PayAccount::class, 'id', 'aid'); return $this->belongsTo(PayAccount::class, 'aid', 'id');
} }
} }

View File

@ -5,8 +5,6 @@ declare(strict_types=1);
namespace app\model; namespace app\model;
use app\BaseModel; use app\BaseModel;
use app\model\User;
use app\controller\api\PluginController;
class PayAccount extends BaseModel class PayAccount extends BaseModel
{ {
@ -24,64 +22,12 @@ class PayAccount extends BaseModel
$select[] = [$key, '=', $value]; $select[] = [$key, '=', $value];
} }
} }
return self::withCount(['payChannel' => 'channel_num'])->withSum(['order' => function ($query, &$alias) { return self::withCount(['payChannel' => 'channel'])->where($select);
$query->whereDay('pay_time')->where('state', 1);
$alias = 'income';
}], 'really_price')->where($select);
}
public static function findAccount($query)
{
$select = [];
$allow_field = ['state', 'platform', 'account', 'pattern'];
foreach ($query as $key => $value) {
if (in_array($key, $allow_field) && isset($value)) {
if ($key === 'account') {
$select[] = [$key, 'like', '%' . $value . '%'];
continue;
}
$select[] = [$key, '=', $value];
}
}
return self::where($select);
}
// 获取账号配置
public static function getAccountConfig($aid, $pid = null): array|bool
{
$aid_info = self::find($aid);
if (!$aid_info) return false;
// 插件配置
$platform = PluginController::getPluginInfo($aid_info->getData('platform'));
// 查询参数
$params = json_decode($aid_info->params, true);
if ($aid_info && $platform) {
$config = [
'pid' => $aid_info->pid,
// 账号id
'aid' => $aid_info->id,
// 收款平台
'platform' => $aid_info->getData('platform'),
// 插件类名
'payclass' => $platform['class_name'],
// 账号
'account' => $aid_info->account,
// 密码
'password' => $aid_info->password,
// 配置参数
'params' => $params,
];
if ($pid !== null) {
$pid_info = User::where('pid', $pid)->find();
$config['key'] = $pid_info->secret_key;
}
return $config;
} else {
return false;
}
} }
// 获取器 // 获取器
public function getPlatformAttr($value) public function getPlatformAttr($value)
{ {
$payplugin_path = config_path() . '/extend/payplugin.php'; $payplugin_path = config_path() . '/extendconfig/payplugin.php';
if (!file_exists($payplugin_path)) { if (!file_exists($payplugin_path)) {
return []; return [];
} }
@ -91,7 +37,7 @@ class PayAccount extends BaseModel
foreach ($payplugin_config as $config) { foreach ($payplugin_config as $config) {
$option[$config['platform']] = $config['name']; $option[$config['platform']] = $config['name'];
} }
return isset($option[$value]) ? $option[$value] : '[已卸载,请停用]'; return $option[$value];
} }
public function getPatternAttr($value) public function getPatternAttr($value)
{ {
@ -104,9 +50,4 @@ class PayAccount extends BaseModel
{ {
return $this->hasMany(PayChannel::class, 'account_id', 'id'); return $this->hasMany(PayChannel::class, 'account_id', 'id');
} }
// 一对多关联
public function order()
{
return $this->hasMany(Order::class, 'aid', 'id');
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

View File

@ -0,0 +1,72 @@
<?php
// +----------------------------------------------------------------------
// | 后台菜单配置
// +----------------------------------------------------------------------
return [
[
'id' => 'console',
'title' => '平台首页',
'icon' => 'icon pear-icon pear-icon-home',
'type' => 1,
'openType' => '_iframe',
'href' => 'Console/console',
],
[
'id' => 'order',
'title' => '订单管理',
'icon' => 'icon pear-icon pear-icon-survey',
'type' => 1,
'openType' => '_iframe',
'href' => '/Order/index',
],
[
'id' => 'payManage',
'title' => '账号管理',
'icon' => 'icon pear-icon pear-icon-security',
'type' => 1,
'openType' => '_iframe',
'href' => '/PayManage/index',
],
[
'id' => 'pluginManage',
'title' => '插件管理',
'icon' => 'icon pear-icon pear-icon-modular',
'type' => 1,
'openType' => '_iframe',
'href' => '/Plugin/index',
],
[
'id' => 'userCenter',
'title' => '用户中心',
'icon' => 'icon pear-icon pear-icon-user',
'type' => 1,
'openType' => '_iframe',
'href' => '/User/index',
],
// [
// 'id' => 'system',
// 'title' => '系统设置',
// 'icon' => 'icon pear-icon pear-icon-import',
// 'type' => 1,
// 'openType' => '_iframe',
// 'href' => '/System/index',
// ],
// [
// 'id' => 'pay',
// 'title' => '支付管理',
// 'icon' => 'icon pear-icon pear-icon-import',
// 'type' => 0,
// 'href' => '',
// 'children' => [
// [
// 'id' => 'pay_qrcode_list',
// 'title' => '收款账户',
// 'icon' => 'icon pear-icon pear-icon-import',
// 'type' => 1,
// 'openType' => '_iframe',
// 'href' => '/PayQrcode/index',
// ],
// ],
// ],
];

View File

@ -0,0 +1,54 @@
<?php
// +----------------------------------------------------------------------
// | 支付插件列表
// +----------------------------------------------------------------------
return array (
0 =>
array (
'platform' => 'wxpay',
'name' => '微信支付',
'class_name' => 'WxPay',
'price' => '0.00',
'describe' => '支持微信个人收款码、赞赏码、经营码、商家码收款,监听回调',
'website' => 'https://weixin.qq.com/',
'state' => 1,
'query' =>
array (
),
),
1 =>
array (
'platform' => 'alipay',
'name' => '支付宝',
'class_name' => 'AliPay',
'price' => '0.00',
'describe' => '支持支付宝个人收款码、经营码收款,监听回调',
'website' => 'https://www.alipay.com/',
'state' => 1,
'query' =>
array (
),
),
2 =>
array (
'platform' => 'sqbpay',
'name' => '收钱吧',
'class_name' => 'ShouQianBa',
'price' => '49.00',
'describe' => '主流移动支付全能收 信用卡,花呗都能用,生意帮手收钱吧,移动收款就用它!',
'website' => 'https://www.shouqianba.com/',
'state' => 1,
'query' =>
array (
'date_end' => NULL,
'date_start' => NULL,
'page' => 1,
'page_size' => 10,
'upayQueryType' => 0,
'status' => '2000',
'store_sn' => '',
'type' => '30',
),
),
);

View File

@ -23,7 +23,7 @@ class ImgCaptcha
return $captcha; return $captcha;
} }
// 请求外部资源 // 请求外部资源
private static function getHttpResponse($url, $header = [], $post = null, $timeout = 10) private function getHttpResponse($url, $header = [], $post = null, $timeout = 10)
{ {
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);

66
extend/MpayClass.php Normal file
View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
class MpayClass
{
private $pid;
private $key;
private $host;
private $check_neworder_url;
private $submit_records_url;
function __construct($config)
{
$this->pid = $config['pid'];
$this->key = $config['key'];
$this->host = $config['host'];
$this->check_neworder_url = $this->host . '/order.php';
$this->submit_records_url = $this->host . '/payHeart';
}
// 查询新订单
public function orderHeart()
{
$url = $this->check_neworder_url . "?pid={$this->pid}&sign={$this->getSign()}";
$res = $this->getHttpResponse($url);
return $res;
}
// 提交收款明细
public function upRecords($records, $aid)
{
$header = ['Content-Type: application/json;charset=UTF-8'];
$url = $this->submit_records_url . "?pid={$this->pid}&aid={$aid}&sign={$this->getSign()}";
$res = $this->getHttpResponse($url, $header, json_encode($records));
return $res;
}
// 签名方法
private function getSign()
{
return md5($this->pid . $this->key);
}
// 请求外部资源
private function getHttpResponse($url, $header = [], $post = null, $timeout = 10)
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
if ($header) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
} else {
$httpheader[] = "Accept: */*";
$httpheader[] = "Accept-Language: zh-CN,zh;q=0.8";
$httpheader[] = "Connection: close";
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
}
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if ($post) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
}
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
}

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
class Plugin class Plugin
{ {
private static $siteUrl = 'https://api.qcjy.cc'; private static $siteUrl = 'https://api.zhaidashi.cn';
// 获取全部插件(含本地) // 获取全部插件(含本地)
public static function getAllPlugins(array $local_plugin = []): array public static function getAllPlugins(array $local_plugin = []): array
{ {
@ -23,7 +23,6 @@ class Plugin
// 获取已安装插件 // 获取已安装插件
public static function getInstall(array $local_plugin = []): array public static function getInstall(array $local_plugin = []): array
{ {
if (empty($local_plugin)) return [];
foreach ($local_plugin as $key => $value) { foreach ($local_plugin as $key => $value) {
$local_plugin[$key]['install'] = true; $local_plugin[$key]['install'] = true;
} }
@ -34,11 +33,9 @@ class Plugin
{ {
$uninstall_plugin = []; $uninstall_plugin = [];
$install = []; $install = [];
if (!empty($local_plugin)) {
foreach ($local_plugin as $e_val) { foreach ($local_plugin as $e_val) {
$install[] = $e_val['platform']; $install[] = $e_val['platform'];
} }
}
foreach ($app_plugin as $i_val) { foreach ($app_plugin as $i_val) {
if (in_array($i_val['platform'], $install)) { if (in_array($i_val['platform'], $install)) {
continue; continue;
@ -52,68 +49,37 @@ class Plugin
// 获取平台所有支持插件 // 获取平台所有支持插件
public static function getAllPlugin(): array public static function getAllPlugin(): array
{ {
$app_plugin = cache('app_plugin'); $app_plugin = self::getHttpResponse(self::$siteUrl . '/mpay/getplugins');
if ($app_plugin) return $app_plugin; return json_decode($app_plugin, true);
$app_plugin = self::getHttpResponse(self::$siteUrl . '/MpayApi', ['action' => 'getPluginList']);
$info = json_decode($app_plugin, true);
if ($info['code'] === 0) cache('app_plugin', $info['data'], 36000);
return $info['data'];
} }
// 获取通知消息 // 获取通知消息
public static function getNotifyMessage(): array public static function getNotifyMessage(): array
{ {
$message = cache('message'); $message = self::getHttpResponse(self::$siteUrl . '/mpay/message');
if ($message) return $message; return json_decode($message, true);
$message = self::getHttpResponse(self::$siteUrl . '/MpayApi', ['action' => 'message'], [], 3);
$info = json_decode($message, true);
if ($info === null) return [];
if ($info['code'] === 0) cache('message', $info['data'], 36000);
return $info['data'];
}
// 安装插件
public static function installPlugin($platform): array
{
$res = self::getHttpResponse(self::$siteUrl . '/MpayApi', ['action' => 'installPlugin', 'platform' => $platform, 'host' => parse_url(request()->domain(), PHP_URL_HOST)]);
// halt($res);
return json_decode($res, true);
}
// 更新插件
public static function updatePlugin($platform): array
{
$res = self::getHttpResponse(self::$siteUrl . '/MpayApi', ['action' => 'updatePlugin', 'platform' => $platform, 'host' => parse_url(request()->domain(), PHP_URL_HOST)]);
return json_decode($res, true);
} }
// 请求外部资源 // 请求外部资源
private static function getHttpResponse($url, $post = null, $header = [], $timeout = 10) private static function getHttpResponse($url, $header = [], $post = null, $timeout = 10)
{ {
$ch = curl_init($url); $ch = curl_init($url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$httpheader = [ if ($header) {
"Accept: application/json", curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
"Accept-Language: zh-CN,zh;q=0.9", } else {
"Connection: close", $httpheader[] = "Accept: */*";
"mpayAgent: your_mpay_agent_identifier" $httpheader[] = "Accept-Language: zh-CN,zh;q=0.9";
]; $httpheader[] = "Connection: close";
$httpheader = array_merge($httpheader, $header); curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
}
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if ($post) { if ($post) {
if (!is_string($post)) $post = json_encode($post);
$httpheader[] = "Content-Type: application/json";
$httpheader[] = "Content-Length: " . strlen($post);
curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post); curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
} }
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch); $response = curl_exec($ch);
// 检查 cURL 请求是否出错
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new \RuntimeException("cURL error: $error");
}
curl_close($ch); curl_close($ch);
return $response; return $response;
} }

BIN
extend/old_payclient.zip Normal file

Binary file not shown.

BIN
extend/payclient.zip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

9
public/order.php Normal file
View File

@ -0,0 +1,9 @@
<?php
header('content-type: application/json; charset=utf-8');
$path = '../runtime/order.json';
if (!file_exists($path)) {
exit('{"code":3,"msg":"文件不存在"}');
} else {
exit(file_get_contents($path));
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,78 +0,0 @@
// 登陆配置
const logins = [
{
name: '拉卡拉',
host: 'm2.lakala.com',
method: 'POST',
orderQuery: '/m/lamsmerdash/account/pwdLogin',
accPath: 'account',
pswPath: 'pwd',
},
{
name: '收钱吧',
host: 'web-platforms-msp.shouqianba.com',
method: 'POST',
orderQuery: '/api/login/ucUser/login',
accPath: 'username',
pswPath: 'password',
}
];
// 提取登陆信息
function extractLoginInfo(request, logins) {
logins.forEach((login) => {
const urlObj = isHttp(request.url, login.host);
if (login.host.toLowerCase() === urlObj.hostname.toLowerCase() &&
login.orderQuery.toLowerCase() === urlObj.pathname.toLowerCase() &&
login.method.toLowerCase() === request.method.toLowerCase()) {
const jsonData = JSON.parse(request.request);
const acc = eval(`jsonData.${login.accPath}`);
const psw = eval(`jsonData.${login.pswPath}`);
const data = {
'账号': acc,
'密码': psw
};
console.log('----- ' + login.name + ' -----');
console.table(data);
alert('账号:' + acc + '\n密码' + psw);
}
})
}
function isHttp(url, host) {
if (url.startsWith('http') || url.startsWith('https')) {
return new URL(url);
} else {
url = 'https://' + host + url;
return new URL(url);
}
}
var oldOpen = XMLHttpRequest.prototype.open;
var oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (method, url) {
this._url = url;
this._method = method;
return oldOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
this._body = body;
const res = {
url: this._url,
method: this._method,
request: this._body
};
extractLoginInfo(res, logins);
return oldSend.apply(this, arguments);
};
window.au_fetch = window.fetch;
window.fetch = function (url, options) {
const res = {
url: url,
method: options.method,
request: options.body
};
extractLoginInfo(res, logins);
return window.au_fetch.apply(window, [url, options]).then((response) => {
return response;
})
};

Some files were not shown because too many files have changed in this diff Show More