Compare commits

...

87 Commits

Author SHA1 Message Date
技术老胡
872afd10cb 更新文档 2025-09-16 11:07:11 +08:00
技术老胡
924042aa5e 更新支付宝插件 2025-06-28 10:09:45 +08:00
技术老胡
5b69aded08 修复一些交互问题 2025-06-03 11:41:16 +08:00
技术老胡
5826a7f6e1 1、更新通道统计,可以查看每个通道的收入情况,可以查询指定时间段的通道收入情况
2、账号管理列表交互优化,添加操作下拉框
3、修复二维码图片上传功能可能导致的漏洞
2025-06-03 11:15:12 +08:00
技术老胡
98c90dffcf 更新图片上传可能的漏洞 2025-05-09 09:06:48 +08:00
技术老胡
e524424628 更新文件 2025-04-23 23:51:21 +08:00
技术老胡
a6b5133cc3 Update .gitignore rules 2025-04-18 18:10:02 +08:00
技术老胡
748356cc6b 更新店员码收款 2025-04-16 15:39:14 +08:00
技术老胡
e4952f6389 安装程序优化,移除无用参数 2025-03-31 12:41:19 +08:00
技术老胡
ba8df721d7 更新文件 2025-03-31 12:29:46 +08:00
技术老胡
a3d15ee7e2 1. 订单列表添加平台流水号查询
2. 修临订单查询功能异常问题
3. 优化订单数据表结构
2025-03-31 12:21:00 +08:00
技术老胡
2cd473019b 修复非HTTP环境无法点击复制的问题 2025-03-31 10:34:57 +08:00
技术老胡
a8bb0d7513 更新数据库 2025-03-29 08:47:31 +08:00
技术老胡
ee49c2a0d0 1. 开发文档加载速度优化
2. 浏览器跳转支付宝异常修复
2025-03-19 23:17:58 +08:00
技术老胡
374fc75423 1. 更新收款通知处理逻辑
2. 数据库字段长度调整512
2025-03-19 19:45:03 +08:00
技术老胡
fa75d580b8 修复手机监听通知配置aid信息错误导致的报错 2025-03-18 18:07:18 +08:00
技术老胡
1ac1c2fd13 修改用户中心移动端排版问题 2025-03-18 13:53:07 +08:00
技术老胡
d570aa88b3 更新接口文档 2025-03-18 12:47:04 +08:00
技术老胡
a05a5710af 1. 优化数据库结构,增强兼容性
2. 添加手机浏览器跳转支付宝收银
3. 添加手机浏览器跳转微信支付提示信息
2025-03-18 11:49:00 +08:00
技术老胡
b3dd6ba3f0 Merge branch 'setconfig' of https://gitee.com/technical-laohu/mpay into setconfig 2025-03-14 16:06:50 +08:00
技术老胡
d0dd5a3f9b 更新文档 2025-03-14 16:06:46 +08:00
技术老胡
ccb94768d3
update README.md.
Signed-off-by: 技术老胡 <1094551889@qq.com>
2025-03-10 11:32:08 +00:00
技术老胡
7c35519e65 更新 2025-03-07 21:22:19 +08:00
技术老胡
d0fe56bade 优化安装检测程序,增强报错提示 2025-03-07 21:16:58 +08:00
技术老胡
0a270541c7 更新文档 2025-03-07 15:17:28 +08:00
技术老胡
62181cf33d 更新文档 2025-03-06 21:05:10 +08:00
技术老胡
1f98974d5a 新增支付宝免输插件,修复一些小问题 2025-03-05 11:28:44 +08:00
技术老胡
a59a22fe82 修复异次元发卡通知回调问题 2025-03-04 17:27:05 +08:00
技术老胡
e3d7efa60f 更新PC端监听软件 2025-03-03 11:45:39 +08:00
技术老胡
aff1c51227 更新插件管理 2025-03-01 12:54:53 +08:00
技术老胡
6e42b0359e 更新插件管理 2025-02-28 15:43:18 +08:00
技术老胡
d5c43e28af 更新插件中心功能 2025-02-28 14:23:39 +08:00
技术老胡
3b02869b5d 更新文档 2025-02-25 11:40:56 +08:00
技术老胡
92a5ce6edb 更新插件管理 2025-02-25 11:37:33 +08:00
技术老胡
1d55442388 优化插件管理逻辑 2025-02-24 13:07:07 +08:00
技术老胡
c469680ae4 修改支付宝部分情况不回调问题 2025-02-22 20:18:58 +08:00
技术老胡
f247f54daf 更新订单生成逻辑提醒 2025-02-22 20:12:55 +08:00
技术老胡
d92daaa1ae 更新代码结构 2025-02-21 18:53:21 +08:00
技术老胡
6f6355e57f 更新文档 2025-02-19 12:26:23 +08:00
技术老胡
43932ec0d9 更新文档 2025-02-19 12:18:20 +08:00
技术老胡
aaf0fd96e7 更新文档 2025-02-19 11:19:19 +08:00
技术老胡
2f0408a463 更新 2025-02-19 11:01:52 +08:00
技术老胡
d53b31cb1e 更新 2025-02-19 11:01:25 +08:00
技术老胡
53a0a75d93 更新 2025-02-19 10:59:27 +08:00
技术老胡
795e84bd96 更新文件 2025-02-19 10:54:55 +08:00
技术老胡
d3e8749e29 更新文档 2025-02-19 10:47:43 +08:00
技术老胡
049ab97702 更新文档 2025-02-19 10:35:59 +08:00
技术老胡
5576a606e4 更新文档 2025-02-18 14:01:10 +08:00
技术老胡
dc1ee1a226 新增支付宝账单查询插件 2025-02-18 13:22:26 +08:00
技术老胡
67153084c8 更新 2025-02-17 11:32:33 +08:00
技术老胡
686227eeba 更新 2025-02-17 11:22:12 +08:00
技术老胡
9791b0c017 更新 2025-02-17 11:16:45 +08:00
技术老胡
023c05e5e3 更新文档 2025-02-17 11:15:15 +08:00
技术老胡
fc35815ae4 更新文档 2025-02-17 11:10:42 +08:00
技术老胡
df6b7f7c22 更新文档 2025-02-17 11:02:43 +08:00
技术老胡
06a062546c 更新文档 2025-02-13 21:10:56 +08:00
技术老胡
f03d3de109 添加收款码设置说明 2025-02-08 11:37:14 +08:00
技术老胡
b546977011 更新PC端监控逻辑 2025-02-08 10:53:26 +08:00
技术老胡
9080543df4 更新文档 2025-02-06 18:34:12 +08:00
技术老胡
a4b8381395 更新文档 2025-01-24 15:51:58 +08:00
技术老胡
020398b34f 更新 2025-01-24 15:44:53 +08:00
技术老胡
a7ca3d2db5 更新文档 2025-01-24 15:42:30 +08:00
技术老胡
5d42d2ef70 更新 2025-01-22 17:08:30 +08:00
技术老胡
9fbedc8cfc 更新文件 2025-01-20 10:30:53 +08:00
技术老胡
b56156d33a 更新手动补单问题 2025-01-20 10:00:07 +08:00
技术老胡
f9c804c1e5 更新订单回调通知逻辑 2025-01-18 10:57:16 +08:00
技术老胡
2515515182 更新插件 2025-01-04 10:21:32 +08:00
技术老胡
87563d01d7 更新收银台付款环境检测 2025-01-02 11:13:40 +08:00
技术老胡
16f4438a16 更新 2024-12-30 10:57:33 +08:00
技术老胡
29c894faae 新增安装环境检测 2024-12-30 10:56:14 +08:00
技术老胡
a5d09d0a26 新订单缓存更新,更利于维护 2024-12-30 10:03:46 +08:00
技术老胡
504aa6c284 添加插件远程访问缓存 2024-12-30 09:28:59 +08:00
技术老胡
e65c0e9173 修改金额核对值对象,防止漏单 2024-12-28 16:05:36 +08:00
技术老胡
2c1ece3eb2 更新插件 2024-12-28 11:46:51 +08:00
技术老胡
ebcac8a9aa 更新文件 2024-12-23 16:47:59 +08:00
技术老胡
4eef7f0797 更新二维码通道添加指引 2024-12-23 15:24:57 +08:00
技术老胡
dfb8edff22 修复通道测试服务器报500错误 2024-12-21 20:16:06 +08:00
技术老胡
05c1b08e95 修复图片上传文件漏洞 2024-12-20 11:16:57 +08:00
技术老胡
de848ab32b 更新一些细节 2024-12-18 17:56:40 +08:00
技术老胡
3f5866ea16 更新文档 2024-12-18 09:21:21 +08:00
技术老胡
fb2f8f2f2c 优化代码结构 2024-12-17 14:43:59 +08:00
技术老胡
62989e4100 更新支持云闪付通道 2024-12-17 11:53:03 +08:00
技术老胡
eeb877bd87 修改没有收款码,程序报BUG问题 2024-12-16 15:36:55 +08:00
技术老胡
e9f64988a9 更新文件 2024-12-16 14:15:27 +08:00
技术老胡
f62d4cd103 更新结构 2024-12-16 14:14:02 +08:00
技术老胡
331b0b2d56 更新逻辑 2024-12-16 12:52:52 +08:00
技术老胡
e5871d91c5 加添微信支付商业版通道 2024-12-16 11:00:27 +08:00
52 changed files with 2640 additions and 1836 deletions

12
.env
View File

@ -1,12 +0,0 @@
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,6 +1,13 @@
/.idea
/.vscode
/.history
/.gitee
.vscode
*.env
extend/*
vendor/*
!vendor/vendor.zip
runtime/*
app/controller/TestController.php
config/extend/*

213
README.md
View File

@ -1,138 +1,138 @@
# 码支付
<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]是一款便捷收款工具,专注于个人免签收款,通过普通收款码即可实现收款通知自动回调,支持绝大多数商城系统**
# 项目地址
| gitee | github |
| <p align="center">gitee</p> | <p align="center">github</p> |
| :----------: | :------------: |
|[![](assets/20241122_113208_gitee.png)](https://gitee.com/technical-laohu/mpay)|[![](assets/20241122_113331_github.png)](https://github.com/techhaha/mpay)|
| <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>|
点以上图标进入项目页面,可查看最新发行版
# 项目说明
## ⚙️ 工具特性
## 原因
- 开源程序,个人免费使用,不断更新
- 支持第四方收款服务商聚合码收款,免挂机、不掉线
- 支持微信、支付宝个人账户收款,免签约
- 采用易支付接口标准开发,兼容市面上大部分商城系统
- 在H5环境中能正常长按识别扫码支付
- 支持多平台、多账号、多通道,灵活配置,收款轮询
## ✨ 演示站点
#### 访问 [在线演示](http://demo.stspwsc.com/) 快速体验。
## 📊 项目说明
### 🎈 原因
之前在工作之余,尝试过很多副业项目,基本都是跟互联网相关的。例如使用**Wordpress和zibll主题**建资源站点,用**微擎**和**微课堂V2**卖课程,使用**异次元发卡**和**独角数卡**程序卖代理的**微信营销软件**,其中遇到的让我最头痛的问题就是收款问题。
正规官方渠道,如微信支付宝申请相关支付接口,必须要签约,且有营业执照等相关资质,这对于只想搞想副业的我实在是没必要,也麻烦,后期的事情也多。于是在网上找到了一些解决方案来解决我的在线收款问题。
---
## 方案
### 🪄 方案
市场也有很多针对此类需求的平台,也有不少合适的解决方案
### 虎皮椒/迅虎/蓝兔支付
#### 虎皮椒/迅虎/蓝兔支付
这些平台都是微信支付宝等官方平台的支付服务商能签约个人商户实现在线收款一些API支付接口可以直接调用跟官方自己申请的几乎差不多。
优点是跟官方申请的支付接口差不多N+1到账
缺点就是审核比较严动不动投诉封商户号一些羊毛党就爱整你你是一点办法没有另外开户基本都要交开户费50-200不等且还需要额外收取手续费。
### 彩虹易支付
#### 彩虹易支付
彩虹易支付是一套收款程序,有专门的公司或平台用这套程序搭建一个收款平台,使用自己的微信或支付宝等官方账户来进行收款,你自己注商户号,运营方提供代收款服务,然后给你打款结算。
优点是审核不严,处理比较灵活,适合个人,技术支持比较好,注册可使用(有些需要注册费)
缺点就是平台容易卷款跑路,也没有什么有效监管,钱收不回来就亏大了(这也是最大的问题)
### 源支付/V免签
#### 源支付/V免签
源支付也是一套收款程序,有个人版和商户版,市面上能搜到的大部分都是商户版,可以入驻,使用自己的个人微信支付宝二维码收款。
V免签是一款开源免费适用于个人收款使用的收款程序原理同源支付类似。
程序的设计思路主要是通过在手机或电脑上安装消息监听软件,用来监听获取微信和支付宝的收款到账通知来实现的支付成功回调的。**方法很实用,本程序也添加了该功能插件,免费**。
![](assets/20241210_112331_e8d2c4043a3c57ad887aef92df1c253.png)
只是这种思路,有一些小问题:
## ✨ 开发思路
* 平台容易因为资质问题导致关站;
* 收取的手续费价格偏高;
* 个人码在微信H5环境无法长按识别付款只能通过PC端相机扫码付款。
* 挂机监听容易掉线,导致收款通知无法回调
### 🎯 服务商聚合码
### 🚀️ 码支付(mpay)
**本程序暂只提供个人版,开源免费使用。**
码支付是在源支付的设计思路基础上进行的改进,利用第四方**聚合收款码**来进行收款,保证收款稳定和便捷不掉线。
聚合收款码个人可以申请不需求相关资质不用申请API接口收银服务平台众多且实力雄厚如拉卡拉、收钱吧等不怕跑路。
特点如下:
* 免监听,不需要手机或电脑挂机监听消息,即可实现支付回调,只需要设置一个定时任务就行
* 支持微信、支付宝、云闪付的H5环境能正常长按识别扫码支付域名防红
* 个人搭建的收款系统,收款稳定,安全可控,不需要额外手续费
* 支持多平台(聚合码服务商),多账号(聚合码商户),多渠道(门店码/店员码/桌号码等),降低异地线上收款风控风险
---
## 思路
<img src="assets/20241210_112301_6f2fef2a7aaee96790eb86f90e3b107.png" width=640 />
码支付说到底就是通过二维码来进行收款,日常使用的除了微信支付宝生成的二维码外,还有一类二维码是由收款服务商提供的,它能通过一张收款二维码,同时支持**微信**、**支付宝**、**云闪付**等多渠道付款,一般称为**聚合收款码**。
![聚合收款码](assets/20241128_164241_image.png)
<img src="assets/20241128_164241_image.png" width=640 />
这类收款码扫码之后需要用户自己输入指定金额来进行付款,然后查看收款通知,确认是否到账,最后确认订单支付成功。
就像你去店子里买一瓶水,你扫二维码进去付款界面,就生成了一个订单,你付款成功之后,商店老板会去查看一下商户后台流水,确认订单是否支付成功,这是一个人工审核的过程。
![商户后台订单流水](assets/20241128_164817_image.png)
<img src="assets/20241128_164817_image.png" width=640 />
那么码支付的作用,就是让人工审核变成自动审核的,当用户通过网站购买商品的时候,码支付会生成一个订单并展示收银台界面,用户再扫码进入聚合码付款页面。
![码支付收银台](assets/20241128_164952_image.png)
<img src="assets/20241128_164952_image.png" width=65% />
![用户付款](assets/20241128_171356_image.png)
<img src="assets/20241128_171356_image.png" width=32% />
同一时间,**码支付后台会自动通过账号密码登陆聚合码服务平台的管理后台**并通过API接口循环查询最近的收款明细通过比对金额和时间确认是否付款到账最后确认成功收款。
![收款成功](assets/20241128_165320_image.png)
<img src="assets/20241128_165320_image.png" width=640 />
当用户付款成功,并且后台检测到收款成功消息后,收钱台就会提示收款成功,并最终确认收款。
![](assets/20241210_112301_6f2fef2a7aaee96790eb86f90e3b107.png)
> 只有存在新订单时,且该订单与当前收款账号一致时,码支付后台才会主动登陆该账号,查询收款流水,减少频繁查询导致的可能风险
> 另外,在账号设置里也有两个模式可选,`单次监听`和`连续监听`,根据业务场景可以自行选择,具体使用,下面有介绍
# 开源声明
### 🪁 微信、支付宝收款码
## 演示站点
<img src="assets/20241210_112331_e8d2c4043a3c57ad887aef92df1c253.png" width=640 />
[码支付](http://demo.stspwsc.com/) http://demo.stspwsc.com/
普通的个人收款码,大多使用微信/支付宝来进行收款,依赖于微信/支付宝平台的推送服务和系统通知机制,就可以获取到收款金额的通知信息,从而可以实现支付回调,自动确认收款。
目前主流的方式也是这种,来实现自动收款,简单方便,不过有一些小问题:
- 微信/支付宝的收款通知推送,需要软件挂机进行监听,否则无法确认收款
- 监听软件因手机或PC系统的不同可能会出现各种监听不稳定的问题
- 微信个人码因升级在微信H5环境里无法长按识别付款
## 内置插件
码支付收款管理系统开源免费使用,支付插件有**免费**和**付费**版本,按需使用购买
1. **微信**插件`wxpay`默认安装,支持`赞赏码`、`个人码|经营码|商家码`2个通道需挂机监听
2. **支付宝**插件`alipay`默认安装,支持`收钱码`、`经营码`2个通道需挂机监听
3. **收钱吧**聚合码插件`sqbpay`默认安装,无需挂机,设置定时任务即可
## 技术架构
## 📦 技术架构
使用`Thinkphp8`框架PHP版本 > 8.0推荐8.2前端UI使用`Layui 2.9`+`PearAdmin`后台
## 🧰 内置插件
码支付收款工具开源免费使用,插件中心的插件列表有**免费版**和**付费版**版,按需使用购买
1. **微信**插件`wxpay`默认安装,支持`赞赏码`、`个人码`、`经营码`、`商家码`,需挂机监听
2. **支付宝**插件`alipay`默认安装,支持`收钱码`、`经营码`,需挂机监听
3. **收钱吧**聚合码插件`sqbpay`默认安装,无需挂机,设置定时任务即可监听回调
---
# 安装和使用
## 全新安装
## 📝 安装说明
以下演示基于**云服务器**环境+**宝塔面板**安装,云服务器购买可以去阿里云、腾讯云等平台,宝塔面板安装教程参考[宝塔面板安装教程](https://www.bt.cn/new/download.html)
### 🚀️ 源码下载
**点击下载 [码支付 v1版本](https://gitee.com/technical-laohu/mpay/releases)**
**更多版本请关注发行版更新记录**
### 安装配置
以**宝塔面板**示例,其他服务器管理面板可以参考
@ -182,7 +182,8 @@ V免签是一款开源免费适用于个人收款使用的收款程序原理
---
## ❤️ 聚合码使用
## ❤️ 聚合码收款
如果本身就有聚合码收钱码最好,没有就需要提前去各收银服务商申请,申请也不复杂
@ -285,7 +286,7 @@ V免签是一款开源免费适用于个人收款使用的收款程序原理
---
## ❤️ 微信/支付宝使用
## ❤️ 微信/支付宝收款
微信/支付宝生成的收款码,需要挂机监听收款消息,基本情况上面有介绍,因为使用广泛,所以也单独添加进来,可以实现正常收款回调
@ -319,9 +320,9 @@ V免签是一款开源免费适用于个人收款使用的收款程序原理
### 挂机监听收款通知
`微信支付`和`支付宝`需要手机挂机监听收款通知,并通知服务器收款信息,也是最常见的解决方案
收款通知信息需要挂机监听,有`手机监听`和`PC监听`两种手机可以同时监听微信和支付宝PC只能监听微信具体支持的通知和功能在设置账号页面有详细介绍。
#### 软件下软(安卓)
#### 🚀️ 手机软件下软(安卓)
码支付使用的是开源工具**短信转发器**`SmsForwarder`来监听收款通知,`SmsForwarder`功能非常强大,喜欢*搞机* 的朋友可以多钻研,这是使用文档[短信转发器](https://gitee.com/pp/SmsForwarder/wikis/pages)
@ -333,7 +334,7 @@ V免签是一款开源免费适用于个人收款使用的收款程序原理
---
#### 🚀️ 功能配置
##### 功能配置
找一台不常用的安卓系统手机,下载安装好`SmsForwarder`之后,点开进行配置,同时打开码支付后台**用户中心**页面,查看相关配置参数
@ -369,7 +370,7 @@ V免签是一款开源免费适用于个人收款使用的收款程序原理
##### **转发规则**
添加应用转发规则,**微信**和**支付宝**需要分别设置
添加应用转发规则,**微信**和**支付宝**需要分别设置,请注意选择**应用**规则
![](assets/20241204_172656_image.png)
@ -403,16 +404,71 @@ V免签是一款开源免费适用于个人收款使用的收款程序原理
![](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**
- **码支付**是一个**个人免费使用**的收款工具,**码支付**的**源码**是开源的,你可以根据自己的实际情况进行修改,也可以自行开发,有任何问题,可以社区交流
- 该软件仅适用于个人线上免签收款,避免人工审核确认收款的繁琐步骤。实际使用中请勿用于诈骗、黑灰产业,如有此类行为,一经发现直接举报
- 插件中心目前还在开发中不支持在线安装插件如果有购买插件或定制其他收银平台插件的需求可以联系作者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)
@ -434,5 +490,4 @@ V免签是一款开源免费适用于个人收款使用的收款程序原理
![收款码](assets/20241203_154918_image.png)
![订单详情](assets/20241203_155123_image.png)
![订单详情](assets/20241203_155123_image.png)

496
README_old.md Normal file
View File

@ -0,0 +1,496 @@
# 码支付
![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)

BIN
app/app.zip Normal file

Binary file not shown.

View File

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

View File

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

View File

@ -7,66 +7,177 @@ namespace app\controller;
use think\facade\Db;
use think\Request;
use think\facade\View;
use think\facade\Log;
use think\exception\ValidateException;
use think\Validate;
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()
{
// 检查是否已经安装过
if ($this->checkLock()) {
return redirect('User/login');
};
}
return View::fetch();
}
/**
* 安装操作,检查环境、保存数据库配置信息
* @param Request $request
* @return \think\response\Json
*/
public function install(Request $request)
{
// 检查是否已经安装过
if ($this->checkLock()) {
return backMsg(1, '已经安装');
};
// 获取表单提交的数据库配置信息
$dbConfig = $request->post();
return json(backMsg(1, '已经安装'));
}
// 保存数据库配置信息到配置文件
if ($this->saveDbConfig($dbConfig) === false) {
return json(backMsg(1, '配置保存失败'));
} else {
$envCheck = $this->checkEnvironment();
if ($envCheck !== true) {
return json(backMsg(1, $envCheck));
}
$dbConfig = $request->post();
try {
$this->validateDbConfig($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, '配置保存失败'));
}
}
// 初始化数据库
/**
* 初始化数据库,创建表并初始化数据
* @param Request $request
* @return \think\response\Json
*/
public function init(Request $request)
{
// 检查是否已经安装过
if ($this->checkLock()) {
return backMsg(1, '已经安装');
};
// 获取表单提交的数据库配置信息
return json(backMsg(1, '已经安装'));
}
$dbConfig = $request->post();
$startTime = microtime(true);
// 连接数据库并建表
$is_succ_tb = $this->createTables();
// 初始化数据记录
$is_succ_data = $this->initData($dbConfig);
// 安装检测
if (!$is_succ_tb) {
return json(backMsg(1, '数据表创建失败'));
try {
$this->validateInitData($dbConfig);
$this->connectDatabase()->transaction(function () use ($dbConfig) {
$this->createTables();
$this->initData($dbConfig);
});
$this->setLock();
$endTime = microtime(true);
Log::info("数据库初始化完成,耗时: " . ($endTime - $startTime) . "");
return json(backMsg(0, '安装成功'));
} catch (ValidateException $e) {
return json(backMsg(1, $e->getMessage()));
} catch (\Exception $e) {
Log::error("数据库初始化失败: " . $e->getMessage());
return json(backMsg(1, '数据库初始化失败'));
}
if (!$is_succ_data) {
return json(backMsg(1, '数据初始化失败'));
}
// 安装成功,写入安装锁文件
$this->setLock();
return json(backMsg(0, '安装成功'));
}
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';
$envContent = <<<EOT
$envContent = $this->generateEnvContent($dbConfig);
if (file_put_contents($envPath, $envContent) === false) {
throw new \Exception("无法写入 .env 文件");
}
}
/**
* 生成 .env 文件内容
* @param array $dbConfig
* @return string
*/
private function generateEnvContent(array $dbConfig): string
{
return <<<EOT
APP_DEBUG = false
DB_TYPE = mysql
@ -75,136 +186,150 @@ DB_NAME = {$dbConfig['name']}
DB_USER = {$dbConfig['user']}
DB_PASS = {$dbConfig['pass']}
DB_PORT = {$dbConfig['port']}
DB_CHARSET = {$dbConfig['charset']}
DB_PREFIX = mpay_
DEFAULT_LANG = zh-cn
EOT;
return file_put_contents($envPath, $envContent);
}
/**
* 创建数据库表
* @throws \Exception
*/
private function createTables()
{
// 连接数据库
$db = Db::connect();
if ($db === false) {
return false;
$db = $this->connectDatabase();
$tables = $this->getTableCreationSqls();
foreach ($tables as $tableName => $sql) {
try {
$db->execute("DROP TABLE IF EXISTS `$tableName`;");
$db->execute($sql);
Log::info("$tableName 表创建成功");
} catch (\Exception $e) {
throw new \Exception("创建 $tableName 表失败: " . $e->getMessage());
}
}
// 创建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;";
// 执行 SQL 语句创建表
$db->execute("DROP TABLE IF EXISTS `mpay_order`;");
$db->execute($sql);
// 创建pay_account表的 SQL 语句
$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
{
// 连接数据库
$db = Db::connect();
return [
'mpay_order' => "CREATE TABLE `mpay_order` (
`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 = [
'secret_key' => md5(1000 . time() . mt_rand()),
'nickname' => $dbConfig['nickname'],
'username' => $dbConfig['username'],
'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 语句插入初始数据
$is_succ = $db->execute($sql, $info);
if (!$is_succ) {
return false;
$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);";
try {
$db->execute($sql, $info);
Log::info("mpay_user 表数据初始化成功");
} catch (\Exception $e) {
throw new \Exception("初始化 mpay_user 表数据失败: " . $e->getMessage());
}
return true;
}
/**
* 检查是否已安装
* @return bool
*/
private function checkLock()
{
$path = runtime_path() . 'install.lock';
$path = runtime_path() . self::INSTALL_LOCK_FILE;
return file_exists($path);
}
/**
* 设置安装锁
* @throws \Exception
*/
private function setLock()
{
$path = runtime_path() . 'install.lock';
file_put_contents($path, time());
$path = runtime_path() . self::INSTALL_LOCK_FILE;
if (file_put_contents($path, time()) === false) {
throw new \Exception("无法写入安装锁文件");
}
}
}

View File

@ -20,70 +20,41 @@ class PayController
'POST' => $request->post(),
default => []
};
if (!$req_data) {
return '参数错误';
}
if (!$req_data) return '参数错误';
// 验证签名
$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);
if ($req_data['sign'] === $sign_str) {
// 检查商户订单
$out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no');
if (!$out_trade_no) {
// 创建新订单
$order_id = Order::createOrder($req_data);
if ($order_id) {
return redirect("/Pay/console/{$order_id}");
} else {
return '创建订单失败';
}
} else {
return '订单提交重复';
}
} else {
return '签名错误';
}
if ($req_data['sign'] !== $sign_str) return '签名错误';
// 检查商户订单
$out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no');
if ($out_trade_no) return '订单提交重复';
// 创建新订单
$order_info = Order::createOrder($req_data);
if ($order_info['code'] !== 0) return $order_info['msg'];
return redirect("/Pay/console/{$order_info['data']['order_id']}");
}
// api提交订单
public function mapi(Request $request)
{
if ($request->isPost()) {
$req_data = $request->post();
if (!$req_data) {
$req_data = $request->get();
if (!$req_data) {
return '参数错误';
}
}
} else {
return '请使用POST方式提交';
}
if (!$request->isPost()) return json(backMsg(0, '请求方式错误'));
$req_data = $request->post();
if (!$req_data) $req_data = $request->get();
if (!$req_data) return json(backMsg(0, '参数错误'));
// 验证签名
$key = User::where('pid', $req_data['pid'])->where('state', 1)->value('secret_key');
if (!$key) {
return '用户禁用或不存在';
}
if (!$key) return json(backMsg(0, '用户禁用或不存在'));
$sign_str = self::getSign($req_data, $key);
if ($req_data['sign'] === $sign_str) {
// 检查商户订单
$out_trade_no = Order::where('out_trade_no', $req_data['out_trade_no'])->value('out_trade_no');
if (!$out_trade_no) {
// 创建新订单
$order_id = Order::createOrder($req_data);
if ($order_id) {
$payurl = $request->domain() . "/Pay/console/{$order_id}";
$info = ['code' => 1, 'msg' => '订单创建成功', 'trade_no' => $order_id, 'qrcode' => $payurl];
return json($info);
} else {
return '创建订单失败';
}
} else {
return '订单提交重复';
}
} else {
return '签名错误';
}
if ($req_data['sign'] !== $sign_str) return json(backMsg(0, '签名错误'));
// 检查商户订单
$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, '订单提交重复'));
// 创建新订单
$order_info = Order::createOrder($req_data);
if ($order_info['code'] !== 0) return json(backMsg(0, $order_info['msg']));
$payurl = $request->domain() . "/Pay/console/{$order_info['data']['order_id']}";
$info = ['code' => 1, 'msg' => '订单创建成功', 'trade_no' => $order_info['data']['order_id'], 'payurl' => $payurl];
return json($info);
}
// 收银台
public function console($order_id = '')
@ -95,7 +66,20 @@ class PayController
View::assign($act_order->toArray());
$passtime = strtotime($act_order->close_time) - time();
View::assign('passtime', $passtime > 0 ? $passtime : 0);
View::assign('payUrl', $channel->qrcode);
// 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('code_type', $channel->type);
return View::fetch();
} else {
@ -127,6 +111,7 @@ class PayController
$notify['sign'] = $sign;
// 跳转通知URL
$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['passtime'] = $passtime > 0 ? $passtime : 0;
@ -187,14 +172,23 @@ class PayController
foreach ($activeOrders as $order) {
// 支付方式核对
$is_payway = $order->type == $new_order['payway'];
if ($new_order['payway'] == '') $is_payway = true;
// 支付渠道核对
$is_channel = $cids[$order->cid] == $new_order['channel'];
// 金额核对
$is_money = $order->money == $new_order['price'];
$is_money = $order->really_price == $new_order['price'];
// 订单核对
if ($is_payway && $is_channel && $is_money) {
$res = $this->updateOrderState($order, $new_order['order_no']);
$notify[] = $res;
// 是否免输
if (isset($new_order['remark'])) {
if ($new_order['remark'] == $order->order_id) {
$res = $this->updateOrderState($order, $new_order['order_no']);
$notify[] = $res;
}
} else {
$res = $this->updateOrderState($order, $new_order['order_no']);
$notify[] = $res;
}
}
}
}
@ -216,7 +210,9 @@ class PayController
$sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign;
// 异步通知
$res_notify = self::getHttpResponse($order->notify_url . '?' . http_build_query($notify));
$notify_url = $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') {
return ['order' => $order->order_id, 'code' => 1, 'msg' => 'notify success'];
} else {
@ -230,9 +226,8 @@ class PayController
$req_pid = $req_info['pid'];
$req_aid = $req_info['aid'];
// 获取订单
$order_path = runtime_path() . '/order.json';
if (!file_exists($order_path)) return json(['code' => 3, 'msg' => '订单文件不存在']);
$new_order = json_decode(file_get_contents($order_path), true);
$new_order = cache('order');
if (!$new_order) return json(['code' => 3, 'msg' => '没有找到新订单缓存']);
// 检测新订单
if ($new_order['code'] !== 1) return json($new_order);
// 订单列表
@ -249,55 +244,51 @@ class PayController
$config = PayAccount::getAccountConfig($req_aid);
if ($config === false) return json(['code' => 4, 'msg' => '监听收款配置错误']);
// 登陆账号
$pay_config = ['username' => $config['account'], 'password' => $config['password']];
// 收款查询
$query = $config['query'];
$pay_config = ['username' => $config['account'], 'password' => $config['password'], 'aid' => $config['aid']];
// 配置参数
$params = $config['params'];
// 实例监听客户端
$payclient_name = $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 = new $payclient_path($pay_config);
// 获取支付明细
$records = $Payclient->getOrderInfo($query);
if ($records) {
$records = $Payclient->getOrderInfo($params);
if ($records['code'] === 0) {
// 提交收款记录
$upres = $this->payHeart($records, $config);
$upres = $this->payHeart($records['data'], $config);
return $upres;
} else {
return json(['code' => 0, 'msg' => '查询空订单'], 320);
return json(['code' => 0, 'msg' => $records['msg']], 320);
}
}
// [定时任务]监听新订单,生成JSON文件信息
// [定时任务]监听新订单,生成缓存
public function checkOrder($pid = '', $sign = '')
{
if (!($pid && $sign)) {
return '参数错误';
}
if (!($pid && $sign)) return '参数错误';
$is_user = User::checkUser($pid, $sign);
$path = runtime_path() . 'order.json';
if ($is_user) {
$orders = Order::scope('activeOrder')->field('id,pid,aid,cid,patt')->select();
if (!file_exists($path)) {
file_put_contents($path, '[]');
}
$old_info = file_get_contents($path);
$old_info = cache('order');
$num = count($orders);
if ($num > 0) {
$info = ['code' => 1, 'msg' => "{$num}个新订单"];
$order_list = ['code' => 1, 'msg' => "{$num}个新订单", 'orders' => $orders];
if ($old_info !== json_encode($order_list)) {
file_put_contents($path, json_encode($order_list));
if ($old_info !== $order_list) {
cache('order', $order_list);
}
return json($info);
} else {
$info = ['code' => 0, 'msg' => '没有新订单'];
if ($old_info !== json_encode($info, 320)) {
file_put_contents($path, json_encode($info, 320));
if ($old_info !== $info) {
cache('order', $info);
}
return json($info);
}
} else {
$info = ['code' => 2, 'msg' => '签名错误'];
file_put_contents($path, json_encode($info, 320));
return json($info);
}
}
@ -306,17 +297,18 @@ class PayController
{
$info = $request->post();
$action = isset($info['action']) ? $info['action'] : '';
if ($action === 'mpay') {
$data = json_decode($info['data'], true);
$config = PayAccount::getAccountConfig($data['aid'], $data['pid']);
$payclient_path = "\\payclient\\{$config['payclass']}";
$Payclient = new $payclient_path($info, $config);
$res = $Payclient->notify();
$this->payHeart($res, $config);
return 200;
} else {
return 202;
}
if ($action !== 'mpay' && $action !== 'mpaypc') return '非mpay的访问请求';
$data = json_decode($info['data'], true);
if (!is_array($data)) return '通知数据为空';
if (!isset($data['aid']) || !isset($data['pid'])) return 'aid和pid参数错误';
$config = PayAccount::getAccountConfig($data['aid'], $data['pid']);
$payclient_path = "\\payclient\\{$config['payclass']}";
$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;
}
// 签名
private static function getSign(array $param = [], string $key = ''): string
@ -348,7 +340,12 @@ class PayController
'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;
}
// 请求外部资源

View File

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

View File

@ -48,7 +48,9 @@ class OrderController extends BaseController
$sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign;
// 异步通知
$res_notify = self::getHttpResponse($order->notify_url . '?' . http_build_query($notify));
$notify_url = $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') {
return json(\backMsg(0, '订单通知成功'));
} else {
@ -72,7 +74,9 @@ class OrderController extends BaseController
$sign = self::getSign($notify, $user_key);
$notify['sign'] = $sign;
// 异步通知
$res_notify = self::getHttpResponse($order->notify_url . '?' . http_build_query($notify));
$notify_url = $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') {
return json(\backMsg(0, '订单通知成功'));
} else {
@ -156,7 +160,12 @@ class OrderController extends BaseController
'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;
}
// 请求外部资源

View File

@ -7,6 +7,9 @@ namespace app\controller\api;
use app\BaseController;
use app\model\PayAccount;
use app\model\PayChannel;
use app\model\Order;
use think\facade\Db;
use \think\facade\Log;
class PayManageController extends BaseController
{
@ -92,6 +95,7 @@ class PayManageController extends BaseController
if ($check) {
return json(backMsg(1, '编号已存在'));
}
$info['last_time'] = date('Y-m-d H:i:s');
$res = PayChannel::create($info);
if ($res) {
return json(backMsg(0, '添加成功'));
@ -124,17 +128,59 @@ class PayManageController extends BaseController
// 上传二维码图片
public function uploadQrcode()
{
$img = $this->request->file('codeimg');
$path = public_path() . '/files/qrcode/';
if (!is_dir($path)) {
mkdir($path, 0777, true);
}
$info = $img->move($path, 'img' . time() . '.' . $img->getOriginalExtension());
if ($info) {
$imgpath = '/files/qrcode/';
return json(backMsg(0, '上传成功', ['imgpath' => $imgpath . $info->getFilename()]));
} else {
return json(backMsg(1, '上传失败'));
try {
// 获取上传的文件
$img = $this->request->file('codeimg');
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);
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, '上传过程中出现异常,请稍后重试'));
}
}
// 获取账号交易流水
@ -148,20 +194,89 @@ class PayManageController extends BaseController
if ($config === false) return json(backMsg(1, '账号配置文件错误'));
if ($req_aid != $config['aid'] || $req_pid != session('pid')) return json(backMsg(1, '监听收款配置不一致'));
// 登陆账号
$pay_config = ['username' => $config['account'], 'password' => $config['password']];
$pay_config = ['username' => $config['account'], 'password' => $config['password'], 'aid' => $config['aid']];
// 收款查询
$query = $config['query'];
$params = $config['params'];
// 实例监听客户端
$payclient_name = $config['payclass'];
$payclient_path = "\\payclient\\{$payclient_name}";
$Payclient = new $payclient_path($pay_config);
// 获取支付明细
$records = $Payclient->getOrderInfo($query);
if ($records) {
$records = $Payclient->getOrderInfo($params);
if ($records['code'] === 0) {
// 收款流水
return json(backMsg(0, '查询成功', $records));
return json(backMsg(0, '查询成功', $records['data']));
} else {
return json(backMsg(1, '查询空订单'));
return json(['code' => 1, 'msg' => $records['msg']]);
}
}
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,42 +25,74 @@ class PluginController extends BaseController
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()
{
$platform = $this->request->post('platform');
$classname = $this->request->post('classname');
if (!$platform || !$classname) {
return json(backMsg(1, '请选择插件'));
}
$res1 = $this->delPlugin($platform);
$res2 = $this->delPluginFile($classname);
if ($res1 && $res2) {
return json(backMsg(0, '卸载成功'));
} else {
return json(backMsg(1, '插件不存在'));
}
if (!$platform) return json(backMsg(1, '请选择插件'));
$this->delPluginFile($platform);
$this->delPlugin($platform);
return json(backMsg(0, '卸载成功'));
}
// 添加插件
// 添加或更新插件
public function addPlugin(array $option = [])
{
$keys = ['platform', 'name', 'class_name', 'price', 'describe', 'website', 'state', 'query'];
$keys = ['platform', 'name', 'class_name', 'price', 'describe', 'website', 'helplink', 'version'];
$config = [];
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_platform = $config['platform'] ?: '';
foreach ($plugin_config as $value) {
foreach ($plugin_config as $i => $value) {
if ($plugin_platform == $value['platform']) {
return 1; //'插件已存在'
$plugin_config[$i] = $config;
$this->savePluginConfig($plugin_config, '支付插件列表');
return true;
}
}
$plugin_config[] = $config;
$this->savePluginConfig($plugin_config, '支付插件列表');
return 0;
return true;
}
// 删除插件配置
private function delPlugin(string $plugin_name = '')
@ -70,37 +102,31 @@ class PluginController extends BaseController
foreach ($plugin_config as $i => $value) {
if ($value['platform'] == $plugin_name) {
$index = $i;
break;
}
}
if ($index === null) {
return false; // 插件不存在
}
if ($index === null) return false;
unset($plugin_config[$index]);
$config = array_values($plugin_config);
$this->savePluginConfig($config, '支付插件列表');
return true;
}
// 删除插件类库文件
private function delPluginFile(string $file_name = '')
private function delPluginFile(string $platform = '')
{
$file_name = self::getPluginInfo($platform)['class_name'];
if (!$file_name) return false;
$plugin_path = root_path() . '/extend/payclient/' . $file_name . '.php';
if (file_exists($plugin_path)) {
unlink($plugin_path);
return true;
} else {
return false;
}
if (!file_exists($plugin_path)) return false;
unlink($plugin_path);
return true;
}
// 修改插件
public function setPlugin($platform = '', $option = [])
{
$config = self::getPluginConfig();
if (!$platform) {
return 1; // 请选择插件
}
if (!$option) {
return 2; // 请添加插件配置
}
if (!$platform) return 1;
if (!$option) return 2;
foreach ($config as $index => $options) {
if ($options['platform'] == $platform) {
foreach ($options as $key => $value) {
@ -117,9 +143,7 @@ class PluginController extends BaseController
public function pluginEnable()
{
$info = $this->request->post();
if ($this->isPluginInstall($info['platform']) == false) {
return json(backMsg(1, '插件未安装'));
}
if (!$this->isPluginInstall($info['platform'])) return json(backMsg(1, '插件未安装'));
$up_res = $this->setPlugin($info['platform'], ['state' => $info['state']]);
if ($up_res) {
return json(backMsg(1, '失败'));
@ -168,13 +192,30 @@ class PluginController extends BaseController
}
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
{
$payplugin_path = config_path() . '/extend/payplugin.php';
if (!file_exists($payplugin_path)) {
return [];
}
if (!file_exists($payplugin_path)) return [];
// 加载插件配置
$payplugin_config = require $payplugin_path;
return $payplugin_config;

View File

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

View File

@ -24,17 +24,35 @@ class PayAccount extends BaseModel
$select[] = [$key, '=', $value];
}
}
return self::withCount(['payChannel' => 'channel'])->where($select);
return self::withCount(['payChannel' => 'channel_num'])->withSum(['order' => function ($query, &$alias) {
$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);
$query = array_merge($platform['query'], $params);
if ($aid_info && $platform) {
$config = [
'pid' => $aid_info->pid,
@ -48,8 +66,8 @@ class PayAccount extends BaseModel
'account' => $aid_info->account,
// 密码
'password' => $aid_info->password,
// 订单查询参数配置
'query' => $query,
// 配置参数
'params' => $params,
];
if ($pid !== null) {
$pid_info = User::where('pid', $pid)->find();
@ -86,4 +104,9 @@ class PayAccount extends BaseModel
{
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: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
assets/wxmonitor_v1.4.zip Normal file

Binary file not shown.

BIN
assets/wxqrcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

View File

@ -1,72 +0,0 @@
<?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

@ -1,54 +0,0 @@
<?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' => '0.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',
),
),
);

1
extend/.gitignore vendored
View File

@ -1 +0,0 @@
/payclient

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
class Plugin
{
private static $siteUrl = 'https://api.zhaidashi.cn';
private static $siteUrl = 'https://api.qcjy.cc';
// 获取全部插件(含本地)
public static function getAllPlugins(array $local_plugin = []): array
{
@ -23,6 +23,7 @@ class Plugin
// 获取已安装插件
public static function getInstall(array $local_plugin = []): array
{
if (empty($local_plugin)) return [];
foreach ($local_plugin as $key => $value) {
$local_plugin[$key]['install'] = true;
}
@ -33,8 +34,10 @@ class Plugin
{
$uninstall_plugin = [];
$install = [];
foreach ($local_plugin as $e_val) {
$install[] = $e_val['platform'];
if (!empty($local_plugin)) {
foreach ($local_plugin as $e_val) {
$install[] = $e_val['platform'];
}
}
foreach ($app_plugin as $i_val) {
if (in_array($i_val['platform'], $install)) {
@ -49,37 +52,68 @@ class Plugin
// 获取平台所有支持插件
public static function getAllPlugin(): array
{
$app_plugin = self::getHttpResponse(self::$siteUrl . '/mpay/getplugins');
return json_decode($app_plugin, true);
$app_plugin = cache('app_plugin');
if ($app_plugin) return $app_plugin;
$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
{
$message = self::getHttpResponse(self::$siteUrl . '/mpay/message');
return json_decode($message, true);
$message = cache('message');
if ($message) return $message;
$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, $header = [], $post = null, $timeout = 10)
private static function getHttpResponse($url, $post = null, $header = [], $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.9";
$httpheader[] = "Connection: close";
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
}
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$httpheader = [
"Accept: application/json",
"Accept-Language: zh-CN,zh;q=0.9",
"Connection: close",
"mpayAgent: your_mpay_agent_identifier"
];
$httpheader = array_merge($httpheader, $header);
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_POSTFIELDS, $post);
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
// 检查 cURL 请求是否出错
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new \RuntimeException("cURL error: $error");
}
curl_close($ch);
return $response;
}

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -1,9 +0,0 @@
<?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.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

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,14 +0,0 @@
<?php
function encrypt($string, $key) {
$method = "AES-256-CBC";
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($method));
$encrypted = openssl_encrypt($string, $method, $key, 0, $iv);
// 将iv和加密字符串拼接起来
return base64_encode($iv . $encrypted);
}
$key = "your-encryption-key"; // 这里应该是一个安全的密钥
$string = "Hello, World!";
$encryptedString = encrypt($string, $key);
echo $encryptedString; // 输出加密字符串

2
runtime/.gitignore vendored
View File

@ -1,2 +0,0 @@
*
!.gitignore

3
vendor/.gitignore vendored
View File

@ -1,3 +0,0 @@
*
!.gitignore
!vendor.zip

View File

@ -82,7 +82,7 @@
<div class="layui-side-scroll">
<div id="sideMenu"></div>
</div>
<div id="version"><p>版本号:<?php echo $version ?></p><p>by techhaha</p></div>
<div id="version"><p>版本号:<?php echo $version ?></p><p>by 技术老胡</p></div>
</div>
<!-- 视 图 页 面 -->
<div class="layui-body">

File diff suppressed because it is too large Load Diff

View File

@ -113,7 +113,7 @@
</a></li>
<?php }else{ ?>
<li><a href="/User/login" class="a-head-btn bdr-5 bg-color-white">登录</a></li>
<li><a href="/" class="a-head-btn bdr-5 bg-color-0055ff">注册</a></li>
<!-- <li><a href="/" class="a-head-btn bdr-5 bg-color-0055ff">注册</a></li> -->
<?php } ?>
</ul>
</div>
@ -127,7 +127,7 @@
<div class="title-heading mt-4">
<h1 class="heading mb-3">码支付<small class="text-success" style="font-size: 50%">
[ˈheɪloʊ]</small></h1>
<p class="para-desc text-muted">源支付,扫码支付,免签支付,聚合支付,码支付免签约,聚合支付平台</p>
<p class="para-desc text-muted">基于易支付接口开发的免签收款工具,方便个人在线收款</p>
<div class="mt-4 pt-2">
<a href="/User/login" class="btn btn-primary mr-2"><svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img"
@ -322,7 +322,7 @@
document.write(new Date().getFullYear());
</script>2024 <a href="/">码支付</a> - All
rights reserved<span class="sep"> | </span><a href="https://beian.miit.gov.cn/"
target="_blank" rel="noreferrer nofollow">湘ICP备2023031541号-2</a>
target="_blank" rel="noreferrer nofollow">湘ICP备********号</a>
</p>
</div>
</div>

View File

@ -54,17 +54,10 @@
<div class="layui-form-item">
<label class="layui-form-label">端口</label>
<div class="layui-input-block">
<input type="text" name="port" required lay-verify="required" placeholder="请输入数据库名称"
<input type="text" name="port" required lay-verify="required" placeholder="请输入端口号"
autocomplete="off" class="layui-input" value="3306">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">编码</label>
<div class="layui-input-block">
<input type="text" name="charset" required lay-verify="required" placeholder="请输入数据库名称"
autocomplete="off" class="layui-input" value="utf8mb4">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">数据库</label>
<div class="layui-input-block">
@ -76,7 +69,7 @@
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="user" required lay-verify="required" placeholder="请输入数据库用户名"
autocomplete="off" class="layui-input" value="root">
autocomplete="off" class="layui-input" value="">
</div>
</div>
<div class="layui-form-item">
@ -90,21 +83,21 @@
<div class="layui-form-item">
<label class="layui-form-label">管理员昵称</label>
<div class="layui-input-block">
<input type="text" name="nickname" required lay-verify="required" placeholder="请输入管理员昵称"
autocomplete="off" class="layui-input" value="勇敢牛牛">
<input type="text" name="nickname" required lay-verify="required" placeholder="设置管理员昵称"
autocomplete="off" class="layui-input" value="Mpay">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">管理员账号</label>
<div class="layui-input-block">
<input type="text" name="username" required lay-verify="required" placeholder="请输入管理员用户名"
<input type="text" name="username" required lay-verify="required" placeholder="设置管理员用户名"
autocomplete="off" class="layui-input" value="admin">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">管理员密码</label>
<div class="layui-input-block">
<input type="password" name="password" required lay-verify="required" placeholder="请输入管理员密码"
<input type="password" name="password" required lay-verify="required" placeholder="设置登陆密码"
autocomplete="off" class="layui-input">
</div>
</div>
@ -126,10 +119,11 @@
const field = data.field;
(async () => {
const saveRes = await fetch('/install/install', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(field) });
if (saveRes.status != 200) { layer.msg('保存数据库配置失败'); return; }
const saveJson = await saveRes.json();
if (saveJson.code == 0) {
const initRes = await fetch('/install/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(field) });
if (initRes.status != 200) { layer.msg('初始化失败,请检查配置'); return; }
if (initRes.status != 200) { layer.msg('初始化数据失败,请检查配置'); return; }
const initJson = await initRes.json();
if (initJson.code == 0) {
layer.msg(initJson.msg, {

View File

@ -5,6 +5,19 @@
<meta charset="utf-8">
<title>订单明细</title>
<link rel="stylesheet" href="/component/pear/css/pear.css" />
<style>
.paytype {
display: flex;
align-items: center;
justify-content: center;
height: 34px;
}
.paytype>span {
margin-left: 3px;
line-height: normal;
}
</style>
</head>
<body class="pear-container">
@ -26,9 +39,10 @@
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">用户ID</label>
<label class="layui-form-label">平台流水号</label>
<div class="layui-input-inline">
<input type="text" name="pid" autocomplete="off" class="layui-input" placeholder="请输入">
<input type="text" name="platform_order" autocomplete="off" class="layui-input"
placeholder="请输入">
</div>
</div>
<div class="layui-inline">
@ -56,6 +70,7 @@
<option value="">请选择</option>
<option value="wxpay">微信支付</option>
<option value="alipay">支付宝</option>
<option value="unionpay">云闪付</option>
</select>
</div>
</div>
@ -189,60 +204,74 @@
(async () => {
const data = await fetch('/api/Plugin/pluginOption').then(res => res.json());
let option_str = `<option value="">收款平台</option>`;
let platforms = {};
data.forEach(val => {
option_str += `<option value="${val.platform}">${val.name}</option>`;
platforms[val.platform] = val.name;
});
const select = document.querySelector('select[name="platform"]');
select.innerHTML = option_str;
form.render('select');
// 会话存储
sessionStorage.setItem('platforms', JSON.stringify(platforms));
let platformTpl = (d) => {
let platforms = JSON.parse(sessionStorage.getItem('platforms')) || {};
let platformName = platforms[d.platform] || '已卸载';
return `${platformName} [${d.aid}:${d.cid}]`;
}
const urlParams = new URLSearchParams(window.location.search);
const where = {};
urlParams.forEach((value, key) => {
where[key] = value;
});
// 表格列参数
let cols = [[
{ type: 'checkbox' },
{ title: '订单号', field: 'order_id', align: 'center', minWidth: 165, templet: '<div><a href="javascript:;" class="layui-font-blue" lay-event="showOrder">{{= d.order_id }}</a></div>' },
// { title: '用户', field: 'pid', align: 'center', width: 85 },
{ title: '商家订单号', field: 'out_trade_no', align: 'center', minWidth: 180 },
{ title: '商品', field: 'name', align: 'center', minWidth: 180 },
{ title: '订单金额', field: 'money', align: 'center', minWidth: 85 },
{ title: '成交金额', field: 'really_price', align: 'center', minWidth: 85 },
{ title: '支付状态', field: 'state', align: 'center', minWidth: 85, templet: '<div>{{# if(d.state==1){return`<span class="layui-badge layui-bg-green">成功</span>`}else{if(new Date(d.close_time)>new Date("<?php echo $servertime ?>")){return`<span class="layui-badge layui-bg-orange">等待</span>`}else{return`<span class="layui-badge layui-bg-gray">过期</span>`} } }}</div>' },
{ title: '支付时间', field: 'pay_time', align: 'center', minWidth: 160, templet: '<div>{{= d.pay_time == d.create_time ? "- -" : d.pay_time}}</div>' },
{ title: '支付平台', field: 'type', align: 'center', width: 120, templet: '<div>{{# if(d.type=="wxpay"){return`<div class="paytype"><img src="/static/img/wxpay.ico"width="15"><span>微信支付</span></div>`}if(d.type=="alipay"){return`<div class="paytype"><img src="/static/img/alipay.ico"width="15"><span>支付宝</span></div>`}if(d.type=="unionpay"){return`<div class="paytype"><img src="/static/img/unionpay.ico"width="15"><span>云闪付</span></div>`} }}</div>' },
{ title: '收款平台[账号:终端]', field: 'platform', align: 'center', minWidth: 160, templet: platformTpl },
{ title: '操作', align: 'center', width: 120, fixed: 'right', templet: '<div><strong><a href="javascript:;" data-id="{{= d.id }}" class="layui-font-green {{= d.state==1 ? "orderSet-paid" : "orderSet-paying" }}">设置</a></strong></div>' }
]]
// 表格渲染
table.render({
id: 'orders-table',
elem: '#orders-table',
url: '/api/Order/getOrders',
where: where,
page: true,
cols: cols,
skin: 'line',
toolbar: '#order-toolbar',
defaultToolbar: [{
title: '刷新',
layEvent: 'refresh',
icon: 'layui-icon-refresh',
}, 'filter', 'print', 'exports'],
done: function () {
dropdown.render({
elem: '.orderSet-paying',
data: [{ title: '改已支付', type: 1 }, { type: '-' }, { title: '手动补单', type: 3 }, { title: '删除订单', type: 0 }],
click: function (data, othis) {
order.setOrder(data, this.elem);
}
});
dropdown.render({
elem: '.orderSet-paid',
data: [{ title: '改未支付', type: 2 }, { type: '-' }, { title: '重新通知', type: 4 }, { title: '删除订单', type: 0 }],
click: function (data, othis) {
order.setOrder(data, this.elem);
}
});
},
});
})()
// 表格列参数
let cols = [[
{ type: 'checkbox' },
{ title: '订单号', field: 'order_id', align: 'center', minWidth: 165, templet: '<div><a href="javascript:;" class="layui-font-blue" lay-event="showOrder">{{= d.order_id }}</a></div>' },
// { title: '用户', field: 'pid', align: 'center', width: 85 },
{ title: '商家订单号', field: 'out_trade_no', align: 'center', minWidth: 180 },
{ title: '商品', field: 'name', align: 'center', minWidth: 180 },
{ title: '订单金额', field: 'money', align: 'center', minWidth: 85 },
{ title: '成交金额', field: 'really_price', align: 'center', minWidth: 85 },
{ title: '支付状态', field: 'state', align: 'center', minWidth: 85, templet: '<div>{{# if(d.state==1){return`<span class="layui-badge layui-bg-green">成功</span>`}else{if(new Date(d.close_time)>new Date("<?php echo $servertime ?>")){return`<span class="layui-badge layui-bg-orange">等待</span>`}else{return`<span class="layui-badge layui-bg-gray">过期</span>`} } }}</div>' },
{ title: '支付时间', field: 'pay_time', align: 'center', minWidth: 160, templet: '<div>{{= d.pay_time == d.create_time ? "- -" : d.pay_time}}</div>' },
{ title: '支付平台', field: 'type', align: 'center', width: 120, templet: '<div>{{# if(d.type=="wxpay"){return`<img src="/static/img/wxpay.ico"width="15"><span>微信支付</span>`}if(d.type=="alipay"){return`<img src="/static/img/alipay.ico"width="15"><span>支付宝</span>`} }}</div>' },
{ title: '收款平台[账号:终端]', field: 'platform', align: 'center', minWidth: 160, templet: '<div>{{# return`${d.payAccount.platform} [${d.aid}:${d.cid}]` }}</div>' },
{ title: '操作', align: 'center', width: 120, fixed: 'right', templet: '<div><strong><a href="javascript:;" data-id="{{= d.id }}" class="layui-font-green {{= d.state==1 ? "orderSet-paid" : "orderSet-paying" }}">设置</a></strong></div>' }
]]
// 表格渲染
table.render({
id: 'orders-table',
elem: '#orders-table',
url: '/api/Order/getOrders',
page: true,
cols: cols,
skin: 'line',
toolbar: '#order-toolbar',
defaultToolbar: [{
title: '刷新',
layEvent: 'refresh',
icon: 'layui-icon-refresh',
}, 'filter', 'print', 'exports'],
done: function () {
dropdown.render({
elem: '.orderSet-paying',
data: [{ title: '改已支付', type: 1 }, { type: '-' }, { title: '手动补单', type: 3 }, { title: '删除订单', type: 0 }],
click: function (data, othis) {
order.setOrder(data, this.elem);
}
});
dropdown.render({
elem: '.orderSet-paid',
data: [{ title: '改未支付', type: 2 }, { type: '-' }, { title: '重新通知', type: 4 }, { title: '删除订单', type: 0 }],
click: function (data, othis) {
order.setOrder(data, this.elem);
}
});
},
});
// 事件处理
// 表格单元格事件
@ -335,7 +364,8 @@
type: 2,
title: '订单详细',
shade: 0.1,
area: [common.isModile() ? '100%' : '500px', common.isModile() ? '100%' : '800px'],
offset: '10px',
area: [common.isModile() ? '100%' : '500px', common.isModile() ? '100%' : '640px'],
content: `/Order/showOrder?id=${id}`,
});
}

View File

@ -23,6 +23,15 @@
margin-left: 20px;
}
.paytype {
display: flex;
align-items: center;
}
.paytype>strong {
margin-left: 3px;
}
.alipay {
color: #1677ff;
}
@ -30,6 +39,10 @@
.wxpay {
color: #1AAD19;
}
.unionpay {
color: #d81e06;
}
</style>
</head>
@ -54,8 +67,8 @@
<div class="layui-row">
<div class="layui-col-xs3"><label class="layui-form-label"><strong>收款方式</strong></label></div>
<div class="layui-col-xs9">
<div class="list">
<?php $payway=['alipay'=>'支付宝','wxpay'=>'微信支付'];$payway_img=['alipay'=>'/static/img/alipay.ico','wxpay'=>'/static/img/wxpay.ico'];echo "<img src='{$payway_img[$type]}'width='16'><strong class='{$type}'>{$payway[$type]}</strong>" ?>
<div class="list paytype">
<?php $payway=['alipay'=>'支付宝','wxpay'=>'微信支付','unionpay'=>'云闪付'];$payway_img=['alipay'=>'/static/img/alipay.ico','wxpay'=>'/static/img/wxpay.ico','unionpay'=>'/static/img/unionpay.ico'];echo "<img src='{$payway_img[$type]}'width='16'><strong class='{$type}'>{$payway[$type]}</strong>" ?>
</div>
</div>
</div>

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>订单明细</title>
<title>支付测试</title>
<link rel="stylesheet" href="/component/pear/css/pear.css" />
<style>
.paybtn {
@ -60,6 +60,10 @@
<img src="/static/img/alipay.ico" width="16">
<span>支付宝</span>
</button>
<button class="pear-btn unionpay">
<img src="/static/img/unionpay.ico" width="16">
<span>云闪付</span>
</button>
</div>
</div>
</div>
@ -91,6 +95,9 @@
document.querySelector('.alipay').addEventListener('click', function () {
crateOrder('alipay');
});
document.querySelector('.unionpay').addEventListener('click', function () {
crateOrder('unionpay');
});
// 创建订单
function crateOrder(paytype) {
form.submit('paytest', function (data) {

View File

@ -6,6 +6,44 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>收银台</title>
<link rel="stylesheet" href="/component/pear/css/pear.css" />
<script>
function detectBrowserEnvironment() {
const userAgent = navigator.userAgent.toLowerCase();
let environment = 'other';
if (userAgent.includes('micromessenger')) {
if (userAgent.includes('miniprogram')) {
environment = 'wxapp'; // 微信小程序
} else if (userAgent.includes('android') || userAgent.includes('iphone') || userAgent.includes('ipad')) {
environment = 'wxphone'; // 手机微信
} else {
environment = 'wxpc'; // PC微信
}
} else if (userAgent.includes('aliapp') || userAgent.includes('alipayclient')) {
if (userAgent.includes('android') || userAgent.includes('iphone') || userAgent.includes('ipad')) {
environment = 'aliphone'; // 手机支付宝
} else {
environment = 'alipc'; // PC支付宝
}
} else if (userAgent.includes('android') || userAgent.includes('iphone') || userAgent.includes('ipad')) {
// 先判断是否是已知的手机APP内置浏览器如果不是则认为是手机浏览器
if (!userAgent.includes('micromessenger') && !userAgent.includes('aliapp') && !userAgent.includes('qq')) {
environment = 'phone'; // 手机浏览器
}
} else {
environment = 'pc'; // 剩下的情况认为是PC浏览器
}
return environment;
}
const payCode = '<?php echo $payUrl; ?>';
const codeType = '<?php echo htmlentities($code_type); ?>';
const payType = '<?php echo htmlentities($type); ?>';
const order = '<?php echo htmlentities($order_id); ?>';
const environment = detectBrowserEnvironment();
const payclient = '<?php echo htmlentities($payclient ?? ""); ?>';
if (payclient == 'Alipayf' && environment == 'aliphone') {
window.location.href = payCode;
}
</script>
<style>
body {
background: #f7f7f7;
@ -134,6 +172,12 @@
</span><span></span></div>
<div class="qrcode"><img id="qrcode" src="/static/img/loading.gif">
</div>
<!-- 添加跳转按钮 -->
<div style="margin: 16px auto;display: none;">
<button id="openApp" class="layui-btn layui-btn-normal">
打开支付应用
</button>
</div>
<div class="msg">
<p>请付款 <span class="note">
<?php echo htmlentities($really_price); ?>
@ -151,10 +195,6 @@
<script src="/component/layui/layui.js"></script>
<script src="/static/js/awesome-qr.min.js"></script>
<script>
const payCode = '<?php echo htmlentities($payUrl); ?>';
const codeType = '<?php echo htmlentities($code_type); ?>';
const payType = '<?php echo htmlentities($type); ?>';
const order = '<?php echo htmlentities($order_id); ?>';
const QR = AwesomeQR.AwesomeQR;
(async () => {
// 支付类型
@ -166,10 +206,22 @@
} else if (payType === 'alipay') {
payTpeyImg.src = '/static/img/alipay.jpg';
payTypeText.innerText = '请使用支付宝扫码支付'
} else if (payType === 'unionpay') {
payTpeyImg.src = '/static/img/unionpay.jpg';
payTypeText.innerText = '请使用云闪付扫码支付'
}
// 生成二维码
if (codeType == 0) {
document.getElementById('qrcode').src = await getQrcode(payCode, QR);
if (payType === 'alipay' && environment === 'pc') {
// 解析网址
const url = new URL(payCode);
const queryParams = new URLSearchParams(url.search);
const qrcode = queryParams.get('qrcode');
const decodedQrcode = decodeURIComponent(qrcode);
document.getElementById('qrcode').src = await getQrcode(decodedQrcode, QR);
} else {
document.getElementById('qrcode').src = await getQrcode(payCode, QR);
}
} else {
document.getElementById('qrcode').src = payCode;
}
@ -244,6 +296,37 @@
return info;
}
/* <?php } ?> */
// 环境判断
if (payType === 'wxpay' && environment === 'aliphone') {
layer.alert('请使用微信打开此页面');
} else if (payType === 'alipay' && environment === 'wxphone') {
layer.alert('请使用支付宝打开此页面');
}
// 添加按钮控制逻辑
const openAppBtn = document.getElementById('openApp');
if (environment === 'phone') {
openAppBtn.parentNode.style.display = 'block';
if (payType === 'wxpay') {
openAppBtn.innerText = '截图并打开微信';
openAppBtn.className = 'layui-btn layui-btn-green';
openAppBtn.onclick = function () {
window.location.href = 'weixin://';
};
} else if (payType === 'alipay') {
openAppBtn.innerText = '打开支付宝付款';
openAppBtn.className = 'layui-btn layui-btn-normal';
openAppBtn.onclick = function () {
if (codeType == 0) {
const payUrl = encodeURIComponent(payCode);
window.location.href = 'alipays://platformapi/startapp?appId=20000067&&url=' + payUrl;
} else {
const currentUrl = encodeURIComponent(window.location.href);
window.location.href = 'alipays://platformapi/startapp?appId=20000067&&url=' + currentUrl;
}
};
}
}
// 生成二维码
async function getQrcode(text, QR) {
const qrcodeUrl = await new Promise((resolve) => {

View File

@ -39,10 +39,10 @@
lay-filter="scanning" class="layui-input" data-type="">
</div>
</div>
</div>
<div class="main-container">
<div class="layui-text">
<p>终端编号如何填写,<strong><a href="https://f0bmwzqjtq2.feishu.cn/docx/HBVrdrsACo36bzxUCSPcjOBNnyb?from=from_copylink" target="_blank"><strong>请查看文档</strong></a></p>
<div class="layui-form-item">
<blockquote class="layui-elem-quote">
<p>首次添加,不知道终端编号可以先空着,后面再查询填写。查询终端编号,<strong><a class="layui-font-blue" href="https://f0bmwzqjtq2.feishu.cn/docx/HBVrdrsACo36bzxUCSPcjOBNnyb?from=from_copylink" target="_blank"><strong>请查看文档</strong></a></p>
</blockquote>
</div>
</div>
</div>

View File

@ -30,12 +30,13 @@
<!-- <?php if ($platform == 'wxpay') { ?> -->
<option value="wxpay1">个人码</option>
<option value="wxpay2">赞赏码</option>
<!-- <option value="wxpay3">经营码</option>
<option value="wxpay4">商家码</option> -->
<option value="wxpay3">经营码</option>
<option value="wxpay4">商家码</option>
<option value="wxpay5">店员码</option>
<!-- <?php } ?> -->
<!-- <?php if ($platform == 'alipay') { ?> -->
<option value="alipay1">收钱码</option>
<!-- <option value="alipay2">经营码</option> -->
<option value="alipay2">经营码</option>
<!-- <?php } ?> -->
</select>
</div>
@ -57,6 +58,18 @@
lay-filter="scanning" class="layui-input" data-type="">
</div>
</div>
<div class="layui-form-item">
<blockquote class="layui-elem-quote">
<!-- <?php if ($platform == 'wxpay') { ?> -->
<p><strong>手机监控:</strong>微信个人码与经营码只能二选一不能同时添加且经营码只能通过个人码通道1添加成功不然不回调商家码监听需要关注“微信收款商业版”公众号赞赏码和店员码正常添加。</p>
<p><strong>电脑监控:</strong>所有收款码均可监控,需要将收款通知聊天窗口单独拖出来</p>
<!-- <?php } ?> -->
<!-- <?php if ($platform == 'alipay') { ?> -->
<p><strong>手机监控:</strong>支付宝收钱码与经营码通知无法做区分,只能二选一,且只能通过收钱码选项添加</p>
<p><strong>电脑监控:</strong>暂不支持PC端监控可以选择支付宝账单方式监听收款</p>
<!-- <?php } ?> -->
</blockquote>
</div>
</div>
</div>
<div class="bottom">

View File

@ -106,10 +106,11 @@
<script src="/component/layui/layui.js"></script>
<script src="/component/pear/pear.js"></script>
<script>
layui.use(['table', 'form', 'common', 'util'], function () {
layui.use(['table', 'form', 'common', 'dropdown', 'util'], function () {
let table = layui.table;
let form = layui.form;
let common = layui.common;
let dropdown = layui.dropdown;
let util = layui.util;
// 渲染插件选项
@ -130,10 +131,11 @@
{ title: '账 号', field: 'account', align: 'center' },
{ title: '启用状态', field: 'state', align: 'center', templet: '#account-state' },
{ title: '监听模式', field: 'pattern', align: 'center' },
{ title: '监听地址 / 自定义模版', field: 'checkUrl', align: 'center', minWidth: 300, event: 'copy', templet: '#account-checkUrl' },
{ title: '监听地址 / 自定义模版', field: 'checkUrl', align: 'center', minWidth: 240, event: 'copy', templet: '#account-checkUrl' },
{ title: '收款平台流水', field: 'trade', align: 'center', templet: '#account-trade' },
{ title: '收款码数量', field: 'channel', align: 'center', templet: '<div><a href="javascript:;" lay-event="channelList"><span class="layui-badge layui-bg-green">{{= d.channel }}</span></a></div>' },
{ title: '操作', align: 'center', fixed: 'right', templet: '<div><a href="javascript:;" class="layui-font-green" lay-event="edit"><strong>编辑</strong></a></div>' }
{ title: '收款码数量', field: 'channel_num', align: 'center', templet: '<div><a href="javascript:;" lay-event="channelList"><span class="layui-badge layui-bg-green">{{= d.channel_num }}</span></a></div>' },
{ title: '今日收款', field: 'income', align: 'center', templet: '<div><strong>{{# return d.income ?? 0 }}</strong></div>' },
{ title: '操作', align: 'center', fixed: 'right', templet: '<div><a href="javascript:;" class="layui-font-green edit" data-aid="{{= d.id }}"><strong>编辑</strong></a></div>' }
]]
table.render({
@ -148,7 +150,17 @@
title: '刷新',
layEvent: 'refresh',
icon: 'layui-icon-refresh',
}, 'filter', 'print', 'exports']
}, 'filter', 'print', 'exports'],
done: function () {
dropdown.render({
elem: '.edit',
align: 'center',
data: [{ title: '编辑', id: 1 }, { type: '-' }, { title: '二维码', id: 2 }, { title: '收款统计', id: 3 }, { title: '收款明细', id: 4 }],
click: function (data, othis) {
account.doEdit(data, this.elem);
}
});
}
});
// 事件处理
@ -168,13 +180,7 @@
table.on('toolDouble(account-table)', function (obj) {
if (obj.event === 'copy') {
const text = obj.tr[0].querySelector('td[data-field="checkUrl"]>div').innerText;
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => {
layer.msg('复制成功');
});
} else {
layer.msg('复制失败,请手动复制');
}
copyText(text);
}
});
// 表格头部按钮事件
@ -232,6 +238,27 @@
// 操作方法
let account = {};
// 编辑操作
account.doEdit = function (data, elem) {
const type = data.id;
const id = elem.attr('data-aid');
switch (type) {
case 1:
account.editAccount(id);
break;
case 2:
account.channelList(id);
break;
case 3:
account.tradeList(id);
break;
case 4:
account.orderList(id);
break;
}
}
// 编辑
account.editAccount = function (id) {
layer.open({
@ -243,6 +270,29 @@
content: `/PayManage/editAccount?id=${id}`,
});
}
// 收款统计
account.tradeList = function (aid) {
// 先尝试关闭已存在的标签
if (parent.layui.tab) {
parent.layui.tab.delTabByElem('content', 'payStatistics');
}
// 重新打开标签
parent.layui.tab.addTabOnlyByElem('content',
{ id: 'payStatistics', title: '收款统计', url: `/PayManage/payStatistics`, close: true }
)
}
// 终端列表
account.orderList = function (aid) {
// 先尝试关闭已存在的标签
if (parent.layui.tab) {
parent.layui.tab.delTabByElem('content', 'order');
}
// 重新打开标签
parent.layui.tab.addTabOnlyByElem('content',
{ id: 'order', title: '订单管理', url: `/Order/index?aid=${aid}`, close: true }
)
parent.layui.tab.changeTabTitleById('content', 'order', `【${aid}】订单明细`);
}
// 终端列表
account.channelList = function (id) {
layer.open({
@ -331,6 +381,34 @@
}
}
})
// 复制文本到剪贴板
function copyText(text) {
if (navigator.clipboard && window.isSecureContext === false) {
navigator.clipboard.writeText(text).then(() => {
layer.msg('复制成功');
}).catch(err => {
copyToClipboardFallback(text);
});
} else {
copyToClipboardFallback(text);
}
}
// 传统复制文本到剪贴板
function copyToClipboardFallback(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
layer.msg('复制成功');
} catch (err) {
layer.msg('复制失败,请手动复制');
} finally {
document.body.removeChild(textarea);
}
}
</script>
</body>

View File

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>收款管理</title>
<link rel="stylesheet" href="/component/pear/css/pear.css" />
<style>
.account-trade {
padding: 15px;
}
</style>
</head>
<body class="pear-container">
<div class="layui-card">
<div class="layui-card-body">
<form class="layui-form layui-form-pane" action="" id="serch-form">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">时间段</label>
<div class="layui-inline" id="create_time">
<div class="layui-input-inline">
<input type="text" name="time_start" autocomplete="off" placeholder="开始"
class="layui-input">
</div>
<div class="layui-form-mid">-</div>
<div class="layui-input-inline">
<input type="text" name="time_end" autocomplete="off" placeholder="结束"
class="layui-input">
</div>
</div>
</div>
<div class="layui-inline">
<button type="submit" class="pear-btn pear-btn-md pear-btn-primary" lay-submit
lay-filter="query">
<i class="layui-icon layui-icon-search"></i>
查询
</button>
<button type="button" lay-on="reset" class="pear-btn pear-btn-md">
<i class="layui-icon layui-icon-refresh"></i>
重置
</button>
</div>
</div>
</form>
</div>
</div>
<div class="layui-card">
<div class="layui-card-body">
<table id="account-table" lay-filter="account-table"></table>
</div>
</div>
<script src="/component/layui/layui.js"></script>
<script src="/component/pear/pear.js"></script>
<script>
layui.use(['table', 'form', 'common', 'dropdown', 'util'], function () {
let table = layui.table;
let form = layui.form;
let common = layui.common;
let dropdown = layui.dropdown;
let util = layui.util;
let laydate = layui.laydate;
const now = new Date();
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
laydate.render({
elem: '#create_time',
range: ['input[name="time_start"]', 'input[name="time_end"]'],
rangeLinked: true,
type: 'datetime',
fullPanel: true,
weekStart: 1,
// 当天开始日期 - 当天结束日期
value: util.toDateString(startOfDay, 'yyyy-MM-dd HH:mm:ss') + ' - ' + util.toDateString(endOfDay, 'yyyy-MM-dd HH:mm:ss')
});
let cols = [[
{ type: 'checkbox' },
{ title: '平 台', field: 'platform', align: 'center', templet: '' },
{ title: '账 号', field: 'account', align: 'center' },
{ title: '时段查询收款', field: 'income', align: 'center', templet: '<div><strong>{{# return d.income ?? 0 }}</strong></div>' },
{ title: '今日收款', field: 'day', align: 'center', templet: '<div><strong>{{# return d.day ?? 0 }}</strong></div>' },
{ title: '昨日收款', field: 'yesterday', align: 'center', templet: '<div><strong>{{# return d.yesterday ?? 0 }}</strong></div>' },
{ title: '本周收款', field: 'week', align: 'center', templet: '<div><strong>{{# return d.week ?? 0 }}</strong></div>' },
{ title: '本月收款', field: 'month', align: 'center', templet: '<div><strong>{{# return d.month ?? 0 }}</strong></div>' },
{ title: '当年收款', field: 'year', align: 'center', templet: '<div><strong>{{# return d.year ?? 0 }}</strong></div>' },
{ title: '总计收款', field: 'total', align: 'center', templet: '<div><strong>{{# return d.total ?? 0 }}</strong></div>' },
]]
table.render({
id: 'account-table',
elem: '#account-table',
url: '/api/PayManage/payStatisticsList',
page: true,
cols: cols,
skin: 'line',
defaultToolbar: [{
title: '刷新',
layEvent: 'refresh',
icon: 'layui-icon-refresh',
}, 'filter', 'print', 'exports'],
});
// 表单自定义事件监听
form.on('submit(query)', function (obj) {
const field = obj.field;
let new_field = {};
for (const key in field) {
if (field.hasOwnProperty.call(field, key)) {
const value = field[key];
if (value) {
new_field[key] = value;
}
}
}
table.reload('account-table', { where: new_field });
return false;
});
// 监听重置按钮
util.on({
reset: function () {
document.querySelector('#serch-form').reset();
table.reload('account-table', { where: {} });
}
});
})
</script>
</body>
</html>

View File

@ -57,23 +57,45 @@
<a href="javascript:;" class="layui-font-green" lay-event="pluginInstall">立即安装</a>
{{# } }}
</script>
<script type="text/html" id="plugin-pay">
<div class="layui-bg-gray layui-padding-3" style="height: 100%;">
<div class="layui-card" style="max-width: 320px;min-width: 240px;margin: 0 auto;text-align: center;">
<div class="layui-card-header">
<strong>请使用{{- d.paytype==='alipay'?'<span class="layui-font-blue">支付宝</span>':'<span class="layui-font-green">微信支付</span>' }}付款</strong>
</div>
<div class="layui-card-body">
<div class="price"><p><span class="layui-font-red layui-font-18">{{= d.money }}</span></p><p>商品:{{= d.name }}</p><p>订单:{{= d.orderid }}</p></div>
<div class="layui-padding-3 paycode"><img id="qrcode" src="/static/img/loading.gif" width="100%" /></div>
<div class="note">
<p>支付<span class="layui-font-red">{{= d.money }}</span>元,不要多付或少付</p>
<p><span class="layui-font-blue">{{= d.closetime }}</span> 前支付</p>
<p>支付完成点击确认支付,过期请不要支付</p>
</div>
</div>
</div>
</div>
</script>
<script src="/component/layui/layui.js"></script>
<script src="/component/pear/pear.js"></script>
<script src="/static/js/awesome-qr.min.js"></script>
<script>
layui.use(['layer', 'form', 'common', 'layer'], function () {
const QR = AwesomeQR.AwesomeQR;
layui.use(['layer', 'form', 'common', 'layer', 'laytpl'], function () {
let table = layui.table;
let layer = layui.layer;
let form = layui.form;
let common = layui.common;
let util = layui.util;
let laytpl = layui.laytpl;
const cols = [[
{ title: '收款平台名称', field: 'name', align: 'center', maxWidth: 180 },
{ title: '官网', field: 'name', align: 'left', maxWidth: 220, templet: `<div><a href="{{= d.website }}" class="layui-font-green" target="_blank">{{= d.website }}</a></div>` },
{ title: '说明', field: 'describe', align: 'left' },
{ title: '价格', field: 'price', align: 'center', maxWidth: 180, templet: `<div>{{= d.price==0 ? '免费' : d.price }}</div>` },
{ title: '状态', field: 'state', align: 'center', maxWidth: 180, templet: `#plugin-state` },
{ title: '操作', align: 'center', maxWidth: 180, fixed: 'right', templet: `#plugin-action` },
{ title: '价格', field: 'price', align: 'center', width: 120, templet: `<div>{{= d.price==0 ? '免费' : d.price }}</div>` },
{ title: '状态', field: 'state', align: 'center', width: 120, templet: `#plugin-state` },
{ title: '教程', field: 'helplink', align: 'center', width: 120, templet: `<div><a href="{{= d.helplink }}" class="layui-font-green" target="_blank">使用文档</a></div>` },
{ title: '操作', align: 'center', width: 180, fixed: 'right', templet: `#plugin-action` },
]]
table.render({
@ -101,17 +123,13 @@
table.on('tool(plugin-table)', function (obj) {
const id = obj.data.id;
if (obj.event === 'pluginInstall') {
layer.msg('功能开发中,有需要请联系作者');
plugin.install({ platform: obj.data.platform });
} else if (obj.event === 'pluginUpdate') {
layer.msg('功能开发中,有需要请联系作者');
plugin.update({ platform: obj.data.platform });
} else if (obj.event === 'pluginUninstall') {
layer.confirm('卸载之后无法恢复,确认吗?', { icon: 3, title: '卸载插件' }, function (index) {
layer.close(index);
config = {
platform: obj.data.platform,
classname: obj.data.class_name
}
plugin.uninstall(config);
plugin.uninstall({ platform: obj.data.platform });
});
}
});
@ -155,19 +173,125 @@
}
// 卸载插件
plugin.uninstall = async (config) => {
const load = layer.load(2);
const res = await fetch('/api/Plugin/uninstallPlugin', { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) });
if (res.status !== 200) {
layer.msg('请求失败,请重试!', { tips: 2, time: 1200 });
layer.close(load);
return false;
}
const rec_info = await res.json();
if (rec_info.code === 0) {
layer.msg(rec_info.msg, { icon: 1, time: 1200 }, () => { table.reload('plugin-table'); });
layer.close(load);
return false;
} else {
layer.msg(rec_info.msg, { icon: 2, time: 1200 });
layer.close(load);
return false;
}
}
// 安装插件
plugin.install = async (config, step = 0) => {
// 加载中
const load = layer.load(2);
const res = await fetch('/api/Plugin/installPlugin', { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) });
if (res.status !== 200) {
layer.close(load);
layer.msg('请求失败,请重试!', { tips: 2, time: 1200 });
return false;
}
const rec_info = await res.json();
// 错误信息
if (rec_info.code !== 0) {
layer.close(load);
layer.msg(rec_info.msg, { icon: 2, time: 1200 });
return false;
}
const info = rec_info.data;
if (rec_info.state === 0) {
plugin.pay(info, config.platform, step);
layer.close(load);
return false;
} else {
// 安装成功
layer.close(load);
layer.msg(rec_info.msg, { icon: 1, time: 1200 }, () => { table.reload('plugin-table'); });
return false;
}
}
// 更新插件
plugin.update = async (config) => {
const load = layer.load(2);
const res = await fetch('/api/Plugin/updatePlugin', { method: 'post', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) });
if (res.status !== 200) {
layer.msg('请求失败,请重试!', { tips: 2, time: 1200 });
layer.close(load);
return false;
}
const rec_info = await res.json();
if (rec_info.code === 0) {
layer.close(load);
layer.msg(rec_info.msg, { icon: 1, time: 1200 }, () => { table.reload('plugin-table'); });
return false;
} else {
layer.close(load);
layer.msg(rec_info.msg, { icon: 2, time: 1200 });
return false;
}
}
// 支付费用
plugin.pay = (config, platform, step) => {
if (step == 0) {
plugin.payPage(config, platform);
} else {
layer.alert('支付失败,请重试!', { icon: 0, title: '提示' }, function (index) {
layer.close(index);
plugin.payPage(config, platform);
});
}
}
plugin.payPage = async (config, platform) => {
layer.open({
type: 1,
title: '支付费用',
area: [common.isModile() ? '100%' : '360px', common.isModile() ? '100%' : 'auto'],
content: laytpl(document.getElementById('plugin-pay').innerHTML).render(config),
offset: '16px',
anim: 1,
btn: ['确认支付', '取消'],
success: function () {
plugin.setQrcode(config.codetype, config.payurl);
},
yes: (index) => {
layer.close(index);
layer.load(2);
plugin.install({ platform: platform }, 1);
},
btn2: (index) => {
layer.close(index);
}
})
}
// 设置二维码
plugin.setQrcode = async (codeType, payCode) => {
if (codeType == 0) {
document.getElementById('qrcode').src = await getQrcode(payCode);
} else {
document.getElementById('qrcode').src = payCode;
}
}
});
// 生成二维码
async function getQrcode(text) {
const qrcodeUrl = await new Promise((resolve) => {
new QR({
text: text,
size: 500,
}).draw().then((dataURL) => { resolve(dataURL); });
})
return qrcodeUrl;
}
</script>
</body>

View File

@ -13,12 +13,29 @@
top: 20px;
z-index: 999;
}
label {
display: flex !important;
align-items: center;
justify-content: center;
}
.infomsg {
display: flex;
align-items: center;
justify-content: space-between;
margin-left: 10px;
width: calc(100% - 20px);
}
.infomsg div:first-child {
width: 100%;
word-break: break-all;
}
.infomsg div:last-child {
text-align: center;
}
</style>
</head>
@ -47,7 +64,7 @@
<span id="yiyan"></span>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">支付API配置<span class="layui-font-gray"> · 易支付接口标准</span></div>
<div class="layui-card-body">
@ -55,22 +72,26 @@
<div class="layui-form-item" pane>
<label class="layui-form-label">API 接口</label>
<div class="layui-input-block">
<div class="layui-form-mid" style="margin-left: 10px;color: #5f5f5f;float: none;">
<?php echo $url ?>
<a href="javascript:;" lay-on="copyinfo" data-info="<?php echo $url ?>"
style="float: right;" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
<div class="layui-form-mid infomsg">
<div>
<?php echo $url ?>
</div>
<div><a href="javascript:;" lay-on="copyinfo" data-info="<?php echo $url ?>"
title="复制"><span class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
<div class="layui-form-item" pane>
<label class="layui-form-label">商户 PID</label>
<div class="layui-input-block">
<div class="layui-form-mid" style="margin-left: 10px;color: #5f5f5f;float: none;">
<?php echo $pid ?>
<a href="javascript:;" lay-on="copyinfo" data-info="<?php echo $pid ?>"
style="float: right;" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
<div class="layui-form-mid infomsg">
<div>
<?php echo $pid ?>
</div>
<div><a href="javascript:;" lay-on="copyinfo" data-info="<?php echo $pid ?>"
title="复制"><span class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
@ -78,19 +99,22 @@
<label class="layui-form-label">商户密钥 <a href="javascript:;" lay-on="resetKey"
title="重置密钥"><span class="icon pear-icon pear-icon-refresh"></span></a></label>
<div class="layui-input-block">
<div class="layui-form-mid layui-elip"
style="margin-left: 10px;color: #5f5f5f;float: none;">
<?php echo $secret_key ?>
<a href="javascript:;" lay-on="copyinfo" data-info="<?php echo $secret_key ?>"
style="float: right;" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
<div class="layui-form-mid infomsg">
<div>
<?php echo $secret_key ?>
</div>
<div>
<a href="javascript:;" lay-on="copyinfo"
data-info="<?php echo $secret_key ?>" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">聚合码定时任务<span class="layui-font-gray"> · 监控有效期订单</span></div>
<div class="layui-card-body">
@ -98,12 +122,14 @@
<div class="layui-form-item" pane>
<label class="layui-form-label">订单监控</label>
<div class="layui-input-block">
<div class="layui-form-mid layui-elip"
style="margin-left: 10px;color: #5f5f5f;float: none;">
<?php echo $orderurl ?>
<a href="javascript:;" lay-on="copyinfo" data-info="<?php echo $orderurl ?>"
style="float: right;" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
<div class="layui-form-mid infomsg">
<div>
<?php echo $orderurl ?>
</div>
<div><a href="javascript:;" lay-on="copyinfo"
data-info="<?php echo $orderurl ?>" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
@ -112,7 +138,7 @@
</div>
</div>
<div class="layui-col-md6">
<div class="layui-card">
<div class="layui-card-header">收款码监控配置<span class="layui-font-gray"> · SmsForwarder</span></div>
<div class="layui-card-body">
@ -123,94 +149,103 @@
<div class="layui-form-item" pane>
<label class="layui-form-label">Webhook</label>
<div class="layui-input-block">
<div class="layui-form-mid layui-elip"
style="margin-left: 10px;color: #5f5f5f;float: none;">
<?php echo $url . 'mpayNotify' ?>
<a href="javascript:;" lay-on="copyinfo"
data-info="<?php echo $url . 'mpayNotify' ?>" style="float: right;"
title="复制"><span class="icon pear-icon pear-icon-survey"></span></a>
<div class="layui-form-mid infomsg">
<div>
<?php echo $url . 'mpayNotify' ?>
</div>
<div><a href="javascript:;" lay-on="copyinfo"
data-info="<?php echo $url . 'mpayNotify' ?>" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
<div class="layui-form-item" pane>
<label class="layui-form-label">消息模版</label>
<div class="layui-input-block">
<div class="layui-form-mid layui-elip"
style="margin-left: 10px;color: #5f5f5f;float: none;">
{"action": "mpay","data": "[msg]","time":"[timestamp]","sign": "[sign]"}
<a href="javascript:;" lay-on="copyinfo"
data-info='{"action": "mpay","data": "[msg]","time":"[timestamp]","sign": "[sign]"}'
style="float: right;" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
<div class="layui-form-mid infomsg">
<div>{"action": "mpay","data": "[msg]","time":"[timestamp]","sign":
"[sign]"}
</div>
<div><a href="javascript:;" lay-on="copyinfo"
data-info='{"action": "mpay","data": "[msg]","time":"[timestamp]","sign": "[sign]"}'
title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
<div class="layui-form-item" pane>
<label class="layui-form-label">Secret</label>
<div class="layui-input-block">
<div class="layui-form-mid layui-elip"
style="margin-left: 10px;color: #5f5f5f;float: none;">
<?php echo $secret_key ?>
<a href="javascript:;" lay-on="copyinfo"
data-info="<?php echo $secret_key ?>" style="float: right;"
title="复制"><span class="icon pear-icon pear-icon-survey"></span></a>
<div class="layui-form-mid infomsg">
<div>
<?php echo $secret_key ?>
</div>
<div><a href="javascript:;" lay-on="copyinfo"
data-info="<?php echo $secret_key ?>" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
<div class="layui-form-item" pane>
<label class="layui-form-label">成功关键字</label>
<div class="layui-input-block">
<div class="layui-form-mid layui-elip"
style="margin-left: 10px;color: #5f5f5f;float: none;">
200
<a href="javascript:;" lay-on="copyinfo" data-info="200"
style="float: right;" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
<div class="layui-form-mid infomsg">
<div>200</div>
<div><a href="javascript:;" lay-on="copyinfo" data-info="200"
title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
</div>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend style="font-size: 14px;">应用转发规则·多重匹配</legend>
<div class="layui-field-box">
<div class="layui-form layui-form-pane">
<div class="layui-form-item" pane>
<label class="layui-form-label">微信支付</label>
<div class="layui-input-block">
<div class="layui-form-mid layui-elip"
style="margin-left: 10px;color: #5f5f5f;float: none;">
<?php echo '并且 是 APP包名 相等 com.tencent.mm<br />并且 是 通知标题 相等 微信支付<br />[空格]或者 是 通知标题 相等 微信收款助手' ?>
<a href="javascript:;" lay-on="copyinfo"
data-info='<?php echo "并且 是 APP包名 相等 com.tencent.mm\n并且 是 通知标题 相等 微信支付\n[空格]或者 是 通知标题 相等 微信收款助手" ?>'
style="float: right;" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
<div class="layui-form-item" pane>
<label class="layui-form-label">支付宝</label>
<div class="layui-input-block">
<div class="layui-form-mid layui-elip"
style="margin-left: 10px;color: #5f5f5f;float: none;">
<?php echo '并且 是 APP包名 相等 com.eg.android.AlipayGphone<br />并且 是 通知标题 包含 成功收款' ?>
<a href="javascript:;" lay-on="copyinfo"
data-info='<?php echo "并且 是 APP包名 相等 com.eg.android.AlipayGphone\n并且 是 通知标题 包含 成功收款" ?>'
style="float: right;" title="复制"><span
class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
</fieldset>
<fieldset class="layui-elem-field">
<legend style="font-size: 14px;">应用转发规则·多重匹配</legend>
<div class="layui-field-box">
<div class="layui-form layui-form-pane">
<div class="layui-form-item" pane>
<label class="layui-form-label">微信支付</label>
<div class="layui-input-block">
<div class="layui-form-mid infomsg">
<div>
<?php echo '并且 是 APP包名 相等 com.tencent.mm<br />并且 是 通知标题 相等 微信支付<br />[空格]或者 是 通知标题 相等 微信收款助手<br />[空格]或者 是 通知标题 相等 微信收款商业版' ?>
</div>
<div><a href="javascript:;" lay-on="copyinfo"
data-info='<?php echo "并且 是 APP包名 相等 com.tencent.mm\n并且 是 通知标题 相等 微信支付\n[空格]或者 是 通知标题 相等 微信收款助手\n[空格]或者 是 通知标题 相等 微信收款商业版" ?>'
title="复制"><span class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
<div class="layui-form-item" pane>
<label class="layui-form-label">支付宝</label>
<div class="layui-input-block">
<div class="layui-form-mid infomsg">
<div>
<?php echo '并且 是 APP包名 相等 com.eg.android.AlipayGphone<br />并且 是 通知标题 包含 元<br />[空格]或者 是 通知内容 包含 元' ?>
</div>
<div><a href="javascript:;" lay-on="copyinfo"
data-info='<?php echo "并且 是 APP包名 相等 com.eg.android.AlipayGphone\n并且 是 通知标题 包含 元\n[空格]或者 是 通知内容 包含 元" ?>'
title="复制"><span class="icon pear-icon pear-icon-survey"></span></a>
</div>
</div>
</div>
</div>
</div>
</div>
</fieldset>
</div>
</div>
</div>
</div>
</div>
<script src="/component/layui/layui.js"></script>
<script src="/component/pear/pear.js"></script>
<script>
@ -221,13 +256,7 @@
util.on({
'copyinfo': (ele) => {
const info = ele.attr('data-info');
if (navigator.clipboard) {
navigator.clipboard.writeText(info).then(() => {
layer.msg('复制成功');
});
} else {
layer.msg('复制失败,请手动复制');
}
copyText(info);
},
'resetKey': (ele) => {
layer.confirm('重置密钥后,将无法使用原密钥,是否继续?', { icon: 3, title: '重置密钥' }, function (index) {
@ -262,8 +291,34 @@
});
fetch('https://v1.hitokoto.cn?c=d&c=i&c=k&encode=text').then(res => res.text()).then(data => { document.getElementById('yiyan').innerHTML = data || '人无横财不富,马无夜草不肥'; })
// 复制文本到剪贴板
function copyText(text) {
if (navigator.clipboard && window.isSecureContext === false) {
navigator.clipboard.writeText(text).then(() => {
layer.msg('复制成功');
}).catch(err => {
copyToClipboardFallback(text);
});
} else {
copyToClipboardFallback(text);
}
}
// 传统复制文本到剪贴板
function copyToClipboardFallback(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
layer.msg('复制成功');
} catch (err) {
layer.msg('复制失败,请手动复制');
} finally {
document.body.removeChild(textarea);
}
}
</script>
</body>

BIN
view/view.zip Normal file

Binary file not shown.