Bläddra i källkod

增加支付通道配置

Anyon 4 år sedan
förälder
incheckning
6aa47275bc

+ 18 - 2
admin_v6.sql

@@ -104,8 +104,23 @@ CREATE TABLE `data_news_x_comment`  (
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '数据-文章-评论' ROW_FORMAT = COMPACT;
 
 -- ----------------------------
--- Records of data_news_x_comment
--- ----------------------------
+-- Table structure for data_payment
+-- ----------------------------
+DROP TABLE IF EXISTS `data_payment`;
+CREATE TABLE `data_payment`  (
+ `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '支付类型',
+ `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '支付名称',
+ `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '支付参数',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '支付说明',
+ `sort` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '排序权重',
+ `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '支付状态(1使用,0禁用)',
+ `deleted` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '删除状态',
+ `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `idx_data_news_mark_status`(`status`) USING BTREE,
+ INDEX `idx_data_news_mark_deleted`(`deleted`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '数据-支付-通道' ROW_FORMAT = COMPACT;
 
 -- ----------------------------
 -- Table structure for data_user
@@ -4506,6 +4521,7 @@ INSERT INTO `system_menu` VALUES (83, 73, '配送区域管理', 'layui-icon layu
 INSERT INTO `system_menu` VALUES (84, 68, '微信小程序配置', 'layui-icon layui-icon-set', 'data/config/wxapp', 'data/config/wxapp', '', '_self', 0, 1, '2020-09-21 16:34:08');
 INSERT INTO `system_menu` VALUES (85, 68, '会员服务协议', 'layui-icon layui-icon-template-1', 'data/config/agreement', 'data/config/agreement', '', '_self', 30, 1, '2020-09-22 16:00:10');
 INSERT INTO `system_menu` VALUES (86, 68, '关于我们描述', 'layui-icon layui-icon-app', 'data/config/about', 'data/config/about', '', '_self', 40, 1, '2020-09-22 16:12:44');
+INSERT INTO `system_menu` VALUES (87, 68, '商城支付配置', 'layui-icon layui-icon-set-sm', 'data/payment/index', 'data/payment/index', '', '_self', 0, 1, '2020-12-12 09:08:09');
 
 -- ----------------------------
 -- Table structure for system_oplog

+ 116 - 0
app/data/controller/Payment.php

@@ -0,0 +1,116 @@
+<?php
+
+namespace app\data\controller;
+
+use app\data\service\PaymentService;
+use think\admin\Controller;
+
+/**
+ * 支付通道通道
+ * Class Payment
+ * @package app\data\controller
+ */
+class Payment extends Controller
+{
+    /**
+     * 绑定数据表
+     * @var string
+     */
+    private $table = 'DataPayment';
+
+    /**
+     * 支付通道类型
+     * @var array
+     */
+    protected $types = PaymentService::TYPES;
+
+    /**
+     * 支付通道管理
+     * @auth true
+     * @menu true
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function index()
+    {
+        $this->title = '支付通道管理';
+        $query = $this->_query($this->table);
+        $query->where(['deleted' => 0])->order('sort desc,id desc');
+        $query->equal('type,status')->like('name')->dateBetween('create_at')->page();
+    }
+
+    /**
+     * 添加支付通道
+     * @auth true
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function add()
+    {
+        $this->title = '添加支付通道';
+        $this->_form($this->table, 'form');
+    }
+
+    /**
+     * 编辑支付通道
+     * @auth true
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function edit()
+    {
+        $this->title = '编辑支付通道';
+        $this->_form($this->table, 'form');
+    }
+
+    /**
+     * 数据表单处理
+     * @param array $data
+     */
+    protected function _form_filter(array &$data)
+    {
+        if ($this->request->isGet()) {
+            $data['content'] = json_decode($data['content'] ?? '[]', true) ?: [];
+        } else {
+            $data['content'] = json_encode($this->request->post() ?: [], JSON_UNESCAPED_UNICODE);
+        }
+    }
+
+    /**
+     * 表单结果处理
+     * @param boolean $state
+     */
+    protected function _form_result(bool $state)
+    {
+        if ($state) {
+            $this->success('支付通道保存成功!', 'javascript:history.back()');
+        }
+    }
+
+    /**
+     * 修改支付通道状态
+     * @auth true
+     * @throws \think\db\exception\DbException
+     */
+    public function state()
+    {
+        $this->_save($this->table, $this->_vali([
+            'status.in:0,1'  => '状态值范围异常!',
+            'status.require' => '状态值不能为空!',
+        ]));
+    }
+
+    /**
+     * 删除支付通道
+     * @auth true
+     * @throws \think\db\exception\DbException
+     */
+    public function remove()
+    {
+        $this->_delete($this->table);
+    }
+
+}

+ 14 - 0
app/data/controller/api/Data.php

@@ -23,4 +23,18 @@ class Data extends Controller
         $this->success('获取轮播图片数据', $data);
     }
 
+    /**
+     * 获取支付通道数据
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function getPayment()
+    {
+        $map = ['status' => 1, 'deleted' => 0];
+        $query = $this->app->db->name('DataPayment')->where($map);
+        $result = $query->order('sort desc,id desc')->field('id,name,type')->select();
+        $this->success('获取支付通道数据', $result->toArray());
+    }
+
 }

+ 16 - 35
app/data/controller/api/Notify.php

@@ -2,8 +2,8 @@
 
 namespace app\data\controller\api;
 
-use app\data\service\OrderService;
-use app\wechat\service\WechatService;
+use app\data\service\payment\JoinPayService;
+use app\data\service\payment\WechatPayService;
 use think\admin\Controller;
 
 /**
@@ -14,7 +14,7 @@ use think\admin\Controller;
 class Notify extends Controller
 {
     /**
-     * 微信支付通知处理
+     * 微信支付通知
      * @param string $scene 支付场景
      * @return string
      * @throws \WeChat\Exceptions\InvalidResponseException
@@ -24,46 +24,27 @@ class Notify extends Controller
      */
     public function wxpay(string $scene = 'order'): string
     {
-        $notify = ($payment = WechatService::WePayOrder())->getNotify();
-        if ($notify['result_code'] == 'SUCCESS' && $notify['return_code'] == 'SUCCESS') {
-            if ($scene === 'order') {
-                if ($this->setOrder($notify['out_trade_no'], $notify['cash_fee'] / 100, $notify['transaction_id'], 'wxpay')) {
-                    return $payment->getNotifySuccessReply();
-                }
-            }
-            // ... 其他支付场景
+        if (strtolower($scene) === 'order') {
+            return WechatPayService::instance()->notify();
+        } else {
+            return 'success';
         }
-        return $payment->getNotifySuccessReply();
     }
 
     /**
-     * 订单状态更新
-     * @param string $code 订单单号
-     * @param string $amount 交易金额
-     * @param string $paycode 交易单号
-     * @param string $paytype 支付类型
-     * @return boolean
+     * 汇聚支付通知
+     * @param string $scene 支付场景
+     * @return string
      * @throws \think\db\exception\DataNotFoundException
      * @throws \think\db\exception\DbException
      * @throws \think\db\exception\ModelNotFoundException
      */
-    private function setOrder(string $code, string $amount, string $paycode, string $paytype = 'wxpay'): bool
+    public function joinpay(string $scene = 'order'): string
     {
-        // 检查订单支付状态
-        $map = ['order_no' => $code, 'payment_status' => 0, 'status' => 2];
-        $order = $this->app->db->name('StoreOrder')->where($map)->find();
-        if (empty($order)) return false;
-        // 更新订单支付状态
-        $this->app->db->name('StoreOrder')->where($map)->update([
-            'status'           => 3,
-            'payment_type'     => $paytype,
-            'payment_code'     => $paycode,
-            'payment_status'   => 1,
-            'payment_amount'   => $amount,
-            'payment_remark'   => '微信在线支付',
-            'payment_datetime' => date('Y-m-d H:i:s'),
-        ]);
-        // 调用用户升级机制
-        return OrderService::instance()->syncAmount($order['order_no']);
+        if (strtolower($scene) === 'order') {
+            return JoinPayService::instance()->notify();
+        } else {
+            return 'success';
+        }
     }
 }

+ 8 - 28
app/data/controller/api/auth/Order.php

@@ -5,8 +5,8 @@ namespace app\data\controller\api\auth;
 use app\data\controller\api\Auth;
 use app\data\service\GoodsService;
 use app\data\service\OrderService;
+use app\data\service\PaymentService;
 use app\data\service\TruckService;
-use app\wechat\service\WechatService;
 use think\admin\extend\CodeExtend;
 use think\exception\HttpResponseException;
 
@@ -169,7 +169,7 @@ class Order extends Auth
         $update = ['status' => 2, 'amount_express' => $express['template_amount']];
         $update['amount_total'] = $order['amount_goods'] + $amount - $order['amount_reduct'] - $order['amount_discount'];
         if ($this->app->db->name('ShopOrder')->where($map)->update($update) !== false) {
-            $this->success('订单确认成功!', $this->_getPaymentParams($order['order_no'], $order['amount_total']));
+            $this->success('订单确认成功!', ['order_no' => $order['order_no']]);
         } else {
             $this->error('订单确认失败,请稍候再试!');
         }
@@ -183,13 +183,17 @@ class Order extends Auth
      */
     public function payment()
     {
-        $map = $this->_vali(['order_no.require' => '订单单号不能为空!']);
+        $data = $this->_vali([
+            'payid.require'    => '支付通道不能为空!',
+            'order_no.require' => '订单单号不能为空!',
+        ]);
+        $map = ['order_no' => $data['order_no']];
         $order = $this->app->db->name('ShopOrder')->where($map)->find();
         if (empty($order)) $this->error('获取订单数据失败,请稍候再试!');
         if ($order['status'] != 2) $this->error('该订单不能发起支付哦!');
         if ($order['payment_status']) $this->error('订单已经支付,不需要再次支付哦!');
         try {
-            $params = $this->_getPaymentParams($order['order_no'], $order['amount_total']);
+            $params = PaymentService::build($data['payid'])->create($this->user['openid'], $order['order_no'], $order['amount_total'], '商城订单支付', '');
             $this->success('获取支付参数成功!', $params);
         } catch (HttpResponseException $exception) {
             throw  $exception;
@@ -199,30 +203,6 @@ class Order extends Auth
     }
 
     /**
-     * 获取订单支付参数
-     * @param string $code 订单单号
-     * @param string $amount 支付金额
-     * @return array
-     */
-    private function _getPaymentParams(string $code, string $amount): array
-    {
-        try {
-            return WechatService::WePayOrder()->create([
-                'body'             => '商城订单支付',
-                'openid'           => $this->user['openid'],
-                'out_trade_no'     => $code,
-                'total_fee'        => $amount * 100,
-                'trade_type'       => 'JSAPI',
-                'notify_url'       => sysuri('@data/api.notify/wxpay/type/order', [], false, true),
-                'spbill_create_ip' => $this->app->request->ip(),
-            ]);
-        } catch (\Exception $exception) {
-            $this->error("创建支付参数失败,{$exception->getMessage()}");
-        }
-    }
-
-
-    /**
      * 主动取消未支付的订单
      * @throws \think\db\exception\DataNotFoundException
      * @throws \think\db\exception\DbException

+ 139 - 0
app/data/service/PaymentService.php

@@ -0,0 +1,139 @@
+<?php
+
+namespace app\data\service;
+
+use app\data\service\payment\JoinPayService;
+use app\data\service\payment\WechatPayService;
+use think\admin\Service;
+
+/**
+ * 支付基础服务
+ * Class PaymentService
+ * @package app\data\service
+ */
+abstract class PaymentService extends Service
+{
+    // 微信支付类型
+    const PAYMENT_WECHAT_GZH = 'wechat_gzh';
+    const PAYMENT_WECHAT_XCX = 'wechat_xcx';
+
+    // 汇聚支付类型
+    const PAYMENT_JOINPAY_GZH = 'joinpay_gzh';
+    const PAYMENT_JOINPAY_XCX = 'joinpay_xcx';
+
+    // 支付通道描述
+    const TYPES = [
+        PaymentService::PAYMENT_WECHAT_XCX  => '微信小程序支付',
+        PaymentService::PAYMENT_WECHAT_GZH  => '微信服务号支付',
+        PaymentService::PAYMENT_JOINPAY_XCX => '汇聚小程序支付',
+        PaymentService::PAYMENT_JOINPAY_GZH => '汇聚服务号支付',
+    ];
+
+    /**
+     * 默认支付类型
+     * @var string
+     */
+    protected static $type;
+
+    /**
+     * 当前支付通道
+     * @var array
+     */
+    protected static $config;
+
+    /**
+     * 支付服务对象
+     * @var JoinPayService|WechatPayService
+     */
+    protected static $driver = [];
+
+
+    /**
+     * 根据配置实例支付服务
+     * @param string $payid 支付通道编号
+     * @return JoinPayService|WechatPayService
+     * @throws \think\Exception
+     */
+    public static function build(string $payid): PaymentService
+    {
+        if (isset(static::$driver[$payid])) {
+            return static::$driver[$payid];
+        }
+        // 支付通道配置验证
+        $map = ['id' => $payid, 'status' => 1, 'deleted' => 0];
+        $payment = app()->db->name('DataPayment')->where($map)->find();
+        if (empty($payment)) {
+            throw new \think\Exception("支付通道[#{$payid}]已关闭");
+        }
+        static::$config = json_decode(static::$config['content'], true);
+        if (empty(static::$config)) {
+            throw new \think\Exception("支付通道[#{$payid}]配置无效");
+        }
+        // 支付通道类型验证
+        if (empty(static::TYPES[$payment['type']])) {
+            throw new \think\Exception("支付通道[{$payment['type']}]未定义");
+        }
+        // 实例化具体支付通道类型
+        static::$type = $payment['type'];
+        if (stripos(static::$type, 'wechat_') === 0) {
+            return static::$driver[$payid] = WechatPayService::instance();
+        } else {
+            return static::$driver[$payid] = JoinPayService::instance();
+        }
+    }
+
+    /**
+     * 订单更新操作
+     * @param string $code 订单单号
+     * @param string $payno 交易单号
+     * @param string $amount 支付金额
+     * @param string $paytype 支付类型
+     * @return bool
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function updateOrder(string $code, string $payno, string $amount, string $paytype): bool
+    {
+        // 检查订单支付状态
+        $map = ['order_no' => $code, 'payment_status' => 0, 'status' => 2];
+        $order = $this->app->db->name('StoreOrder')->where($map)->find();
+        if (empty($order)) return false;
+        // 更新订单支付状态
+        $this->app->db->name('StoreOrder')->where($map)->update([
+            'status'           => 3,
+            'payment_type'     => $paytype,
+            'payment_code'     => $payno,
+            'payment_status'   => 1,
+            'payment_amount'   => $amount,
+            'payment_remark'   => '微信在线支付',
+            'payment_datetime' => date('Y-m-d H:i:s'),
+        ]);
+        // 调用用户升级机制
+        return OrderService::instance()->syncAmount($order['order_no']);
+    }
+
+    /**
+     * 支付通知处理
+     * @return string
+     */
+    abstract public function notify(): string;
+
+    /**
+     * 订单主动查询
+     * @param string $orderNo
+     * @return array
+     */
+    abstract public function query(string $orderNo): array;
+
+    /**
+     * 创建支付订单
+     * @param string $openid 会员OPENID
+     * @param string $orderNo 交易订单单号
+     * @param string $payAmount 交易订单金额(元)
+     * @param string $payTitle 交易订单名称
+     * @param string $payDescription 订单订单描述
+     * @return array
+     */
+    abstract public function create(string $openid, string $orderNo, string $payAmount, string $payTitle, string $payDescription): array;
+}

+ 164 - 0
app/data/service/payment/JoinPayService.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace app\data\service\payment;
+
+use app\data\service\PaymentService;
+use think\admin\extend\HttpExtend;
+
+/**
+ * 汇聚支付基础服务
+ * Class JoinPayService
+ * @package app\store\service\payment
+ */
+class JoinPayService extends PaymentService
+{
+    /**
+     * 请求地址
+     * @var string
+     */
+    protected $uri;
+
+    /**
+     * 应用编号
+     * @var string
+     */
+    protected $appid;
+
+    /**
+     * 报备商户号
+     * @var string
+     */
+    protected $trade;
+
+    /**
+     * 平台商户号
+     * @var string
+     */
+    protected $mchid;
+
+    /**
+     * 平台商户密钥
+     * @var string
+     */
+    protected $mchkey;
+
+    /**
+     * 汇聚支付服务初始化
+     * @return JoinPayService
+     */
+    protected function initialize(): JoinPayService
+    {
+        $this->appid = static::$config['joinpay_appid'];
+        $this->trade = static::$config['joinpay_trade'];;
+        $this->mchid = static::$config['joinpay_mch_id'];
+        $this->mchkey = static::$config['joinpay_mch_key'];
+        return $this;
+    }
+
+    /**
+     * 创建订单支付参数
+     * @param string $openid 会员OPENID
+     * @param string $orderNo 交易订单单号
+     * @param string $payAmount 交易订单金额(元)
+     * @param string $payTitle 交易订单名称
+     * @param string $payDescription 订单订单描述
+     * @return array
+     * @throws \think\Exception
+     */
+    public function create(string $openid, string $orderNo, string $payAmount, string $payTitle, string $payDescription): array
+    {
+        $types = [
+            static::PAYMENT_JOINPAY_GZH => 'WEIXIN_GZH',
+            static::PAYMENT_JOINPAY_XCX => 'WEIXIN_XCX',
+        ];
+        if (isset($types[static::$type])) {
+            $type = $types[static::$type];
+        } else {
+            throw new \think\Exception('支付类型[' . static::$type . ']未配置定义!');
+        }
+        try {
+            $data = [
+                'p0_Version'         => '1.0',
+                'p1_MerchantNo'      => $this->mchid,
+                'p2_OrderNo'         => $orderNo,
+                'p3_Amount'          => $payAmount * 100,
+                'p4_Cur'             => '1',
+                'p5_ProductName'     => $payTitle,
+                'p6_ProductDesc'     => $payDescription,
+                'p9_NotifyUrl'       => sysuri('@data/api.notify/joinpay/scene/order', [], false, true),
+                'q1_FrpCode'         => $type ?? '',
+                'q5_OpenId'          => $openid,
+                'q7_AppId'           => $this->appid,
+                'qa_TradeMerchantNo' => $this->trade,
+            ];
+            if (empty($data['q5_OpenId'])) unset($data['q5_OpenId']);
+            $this->uri = 'https://www.joinpay.com/trade/uniPayApi.action';
+            $result = $this->_doReuest($data);
+            if (is_array($result) && isset($result['ra_Code']) && intval($result['ra_Code']) === 100) {
+                return json_decode($result['rc_Result'], true);
+            } elseif (is_array($result) && isset($result['rb_CodeMsg'])) {
+                throw new \think\Exception($result['rb_CodeMsg']);
+            } else {
+                throw new \think\Exception('获取预支付码失败!');
+            }
+        } catch (\Exception $exception) {
+            throw new \think\Exception($exception->getMessage(), $exception->getCode());
+        }
+    }
+
+    /**
+     * 查询订单数据
+     * @param string $orderNo
+     * @return array
+     */
+    public function query(string $orderNo): array
+    {
+        $this->uri = 'https://www.joinpay.com/trade/queryOrder.action';
+        return $this->_doReuest(['p1_MerchantNo' => $this->mchid, 'p2_OrderNo' => $orderNo]);
+    }
+
+    /**
+     * 支付结果处理
+     * @return string
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function notify(): string
+    {
+        $notify = $this->app->request->get();
+        foreach ($notify as &$item) $item = urldecode($item);
+        if (empty($notify['hmac']) || $notify['hmac'] !== $this->_doSign($notify)) {
+            return 'error';
+        }
+        if (isset($notify['r6_Status']) && intval($notify['r6_Status']) === 100) {
+            if ($this->updateOrder($notify['r2_OrderNo'], $notify['r9_BankTrxNo'], $notify['r3_Amount'], 'joinpay')) {
+                return 'success';
+            }
+        }
+        return 'error';
+    }
+
+    /**
+     * 请求数据签名
+     * @param array $data
+     * @return string
+     */
+    private function _doSign(array $data): string
+    {
+        ksort($data);
+        unset($data['hmac']);
+        return md5(join('', $data) . $this->mchkey);
+    }
+
+    /**
+     * 执行数据请求
+     * @param array $data
+     * @return array
+     */
+    private function _doReuest($data = []): array
+    {
+        $data['hmac'] = $this->_doSign($data);
+        return json_decode(HttpExtend::post($this->uri, $data), true);
+    }
+}

+ 105 - 0
app/data/service/payment/WechatPayService.php

@@ -0,0 +1,105 @@
+<?php
+
+namespace app\data\service\payment;
+
+use app\data\service\PaymentService;
+use WePay\Order;
+
+/**
+ * 微信官方公众号支持
+ * Class WechatPayService
+ * @package app\store\service\payment
+ */
+class WechatPayService extends PaymentService
+{
+    /**
+     * 微信对象对象
+     * @var Order
+     */
+    protected $payment;
+
+    /**
+     * 微信支付服务初始化
+     * @return WechatPayService
+     */
+    protected function initialize(): WechatPayService
+    {
+        $this->payment = Order::instance([
+            'appid'      => static::$config['wechat_appid'],
+            'mch_id'     => static::$config['wechat_mch_id'],
+            'mch_key'    => static::$config['wechat_mch_key'],
+            'cache_path' => $this->app->getRuntimePath() . 'wechat',
+        ]);
+        return $this;
+    }
+
+    /**
+     * 查询微信支付订单
+     * @param string $orderNo
+     * @return array
+     * @throws \WeChat\Exceptions\InvalidResponseException
+     * @throws \WeChat\Exceptions\LocalCacheException
+     */
+    public function query(string $orderNo): array
+    {
+        return $this->payment->query(['out_trade_no' => $orderNo]);
+    }
+
+    /**
+     * 创建微信支付订单
+     * @param string $openid 会员OPENID
+     * @param string $orderNo 交易订单单号
+     * @param string $payAmount 交易订单金额(元)
+     * @param string $payTitle 交易订单名称
+     * @param string $payDescription 订单订单描述
+     * @return array
+     * @throws \think\Exception
+     */
+    public function create(string $openid, string $orderNo, string $payAmount, string $payTitle, string $payDescription): array
+    {
+        try {
+            $body = empty($payDescription) ? $payTitle : ($payTitle . '-' . $payDescription);
+            $data = [
+                'body'             => $body,
+                'openid'           => $openid,
+                'out_trade_no'     => $orderNo,
+                'total_fee'        => $payAmount * 100,
+                'trade_type'       => 'JSAPI',
+                'notify_url'       => sysuri('@data/api.notify/wxpay/scene/order', [], false, true),
+                'spbill_create_ip' => $this->app->request->ip(),
+            ];
+            if (empty($data['openid'])) unset($data['openid']);
+            $info = $this->payment->create($data);
+            if ($info['return_code'] === 'SUCCESS' && $info['result_code'] === 'SUCCESS') {
+                return $this->payment->jsapiParams($info['prepay_id']);
+            }
+            if (isset($info['err_code_des'])) {
+                throw new \think\Exception($info['err_code_des']);
+            } else {
+                throw new \think\Exception('获取预支付码失败!');
+            }
+        } catch (\Exception $exception) {
+            throw new \think\Exception($exception->getMessage(), $exception->getCode());
+        }
+    }
+
+    /**
+     * 支付结果处理
+     * @return string
+     * @throws \WeChat\Exceptions\InvalidResponseException
+     * @throws \think\db\exception\DataNotFoundException
+     * @throws \think\db\exception\DbException
+     * @throws \think\db\exception\ModelNotFoundException
+     */
+    public function notify(): string
+    {
+        $notify = $this->payment->getNotify();
+        if ($notify['result_code'] == 'SUCCESS' && $notify['return_code'] == 'SUCCESS') {
+            if ($this->updateOrder($notify['out_trade_no'], $notify['transaction_id'], $notify['cash_fee'] / 100, 'wechat')) {
+                return $this->payment->getNotifySuccessReply();
+            }
+        } else {
+            return $this->payment->getNotifySuccessReply();
+        }
+    }
+}

+ 66 - 0
app/data/view/payment/form.html

@@ -0,0 +1,66 @@
+{extend name="../../admin/view/main"}
+
+{block name='content'}
+<form class="layui-form layui-card" action="{:request()->url()}" data-auto="true" method="post" autocomplete="off">
+    <div class="layui-card-body padding-40">
+        <label class="layui-form-item relative block">
+            <span class="color-green font-w7">支付名称</span>
+            <span class="color-desc margin-left-5">Payment Name</span>
+            <input class="layui-input" required placeholder="请输入支付名称" name="name" value="{$vo.name|default=''}"/>
+            <span class="help-block"><b>必填,</b>请填写分类名称(如:微信小程序支付),建议字符不要太长,一般4-6个汉字</span>
+        </label>
+        <div class="layui-form-item">
+            <span class="color-green font-w7 label-required-prev">支付通道</span>
+            <span class="color-desc margin-left-5">Payment Channel</span>
+            <div class="layui-input">
+                {empty name='vo.type'}{php}$vo['type'] = 'wechat_xcx';{/php}{/empty}
+                {foreach $types as $k => $v}
+                <label class="think-radio notselect">
+                    {if $vo.type eq $k}
+                    <input data-payment-type checked type="radio" name="type" value="{$k}" lay-ignore> {$v}
+                    {else}
+                    <input data-payment-type type="radio" name="type" value="{$k}" lay-ignore> {$v}
+                    {/if}
+                </label>
+                {/foreach}
+            </div>
+        </div>
+
+        <div data-payment-type="wechat">{include file='payment/form_wechat'}</div>
+        <div data-payment-type="joinpay" class="layui-hide">{include file='payment/form_joinpay'}</div>
+
+        <div class="layui-form-item relative block">
+            <span class="color-green font-w7">支付描述</span>
+            <span class="color-desc margin-left-5">Payment Remark</span>
+            <label class="relative block">
+                <textarea class="layui-textarea" placeholder="请输入支付描述" name="desc">{$vo.desc|default=''}</textarea>
+            </label>
+        </div>
+
+        <div class="hr-line-dashed"></div>
+        {notempty name='vo.id'}<input type='hidden' value='{$vo.id}' name='id'>{/notempty}
+
+        <div class="layui-form-item text-center">
+            <button class="layui-btn" type='submit'>保存数据</button>
+            <button class="layui-btn layui-btn-danger" type='button' data-confirm="确定要取消编辑吗?" data-history-back>取消编辑</button>
+        </div>
+
+    </div>
+
+</form>
+{/block}
+
+{block name='script'}
+<script>
+    $('input[data-payment-type]').on('change', function () {
+        var type = $('input[data-payment-type]:checked').val();
+        if (type.indexOf('wechat') > -1) {
+            $('[data-payment-type="wechat"]').removeClass('layui-hide')
+            $('[data-payment-type="joinpay"]').addClass('layui-hide');
+        } else {
+            $('[data-payment-type="wechat"]').addClass('layui-hide')
+            $('[data-payment-type="joinpay"]').removeClass('layui-hide');
+        }
+    }).trigger('change');
+</script>
+{/block}

+ 23 - 0
app/data/view/payment/form_joinpay.html

@@ -0,0 +1,23 @@
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7 margin-right-10">商户绑定的公众号</span>
+    <input name="joinpay_appid" required placeholder="请输入商户绑定的公众号(必填)" value="{$vo.content.joinpay_appid|default=''}" class="layui-input">
+    <span class="help-block">商户绑定的公众号,授权给汇聚支付平台的公众号APPID</span>
+</label>
+
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7">汇聚支付报备商户号</span>
+    <input name="joinpay_trade" required maxlength="15" placeholder="请输入汇聚支付报备商户号(必填)" value="{$vo.content.joinpay_trade|default=''}" class="layui-input">
+    <span class="help-block">汇聚支付报备商户号,需要联系汇聚支付平台的客服获取,通常以 777 开头的15位数字!</span>
+</label>
+
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7 margin-right-10">汇聚支付的商户编号</span>
+    <input name="joinpay_mch_id" required maxlength="15" placeholder="请输入汇聚支付的商户编号(必填)" value="{$vo.content.joinpay_mch_id|default=''}" class="layui-input">
+    <span class="help-block">汇聚支付的商户编号,需要在汇聚支付平台商户中心获取,通常是以 888 开头的15位数字!</span>
+</label>
+
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7 margin-right-10">汇聚支付的商户密钥</span>
+    <input name="joinpay_mch_key" required maxlength="32" placeholder="请输入汇聚支付的商户密钥(必填)" value="{$vo.content.joinpay_mch_key|default=''}" class="layui-input">
+    <span class="help-block">汇聚支付的商户密钥,需要在汇聚支付平台商户中心的密钥管理处获取,通常为32位字符串!</span>
+</label>

+ 29 - 0
app/data/view/payment/form_wechat.html

@@ -0,0 +1,29 @@
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7 margin-right-10">绑定公众号</span>
+    <input name="wechat_appid" required placeholder="请输入绑定公众号(必填)" value="{$vo.content.wechat_appid|default=''}" class="layui-input">
+    <span class="help-block">商户绑定的公众号,授权给汇聚支付平台的公众号APPID</span>
+</label>
+
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7 margin-right-10">微信商户号</span>
+    <input name="wechat_mch_id" required placeholder="请输入微信商户ID(必填)" value="{$vo.content.wechat_mch_id|default=''}" class="layui-input">
+    <span class="help-block">微信商户编号,需要在微信商户平台获取,微信商户号 与 公众号APPID 匹配</span>
+</label>
+
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7 margin-right-10">微信商户密钥</span>
+    <input name="wechat_mch_key" required maxlength="32" placeholder="请输入微信商户密钥(必填)" value="{$vo.content.wechat_mch_key|default=''}" class="layui-input">
+    <span class="help-block">微信商户密钥,需要在微信商户平台操作设置密码并获取密钥,建议定期更换密钥</span>
+</label>
+
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7">微信商户密钥文件内容</span><span class="nowrap color-desc">( 需要填写文件的全部内容 )</span>
+    <textarea name="wechat_mch_key_text" placeholder="请输入微信KEY密钥内容" class="layui-textarea">{$vo.content.wechat_mch_key_text|default=''}</textarea>
+    <span class="help-block">从商户平台上下载支付证书,解压并取得其中的 apiclient_key.pem 用记事本打开并复制文件内容填至此处</span>
+</label>
+
+<label class="layui-form-item margin-bottom-20 block relative">
+    <span class="color-green font-s14 font-w7">微信商户证书文件内容</span><span class="nowrap color-desc">( 需要填写文件的全部内容 )</span>
+    <textarea name="wechat_mch_cert_text" placeholder="请输入微信CERT证书内容" class="layui-textarea">{$vo.content.wechat_mch_cert_text|default=''}</textarea>
+    <span class="help-block">从商户平台上下载支付证书,解压并取得其中的 apiclient_cert.pem 用记事本打开并复制文件内容填至此处</span>
+</label>

+ 66 - 0
app/data/view/payment/index.html

@@ -0,0 +1,66 @@
+{extend name="../../admin/view/main"}
+
+{block name="button"}
+<!--{if auth("add")}-->
+<button data-open='{:url("add")}' class='layui-btn layui-btn-sm layui-btn-primary'>添加支付</button>
+<!--{/if}-->
+<!--{if auth("remove")}-->
+<button data-action='{:url("remove")}' data-rule="id#{key}" data-confirm="确定要删除这些支付吗?" class='layui-btn layui-btn-sm layui-btn-primary'>删除支付</button>
+<!--{/if}-->
+{/block}
+
+{block name='content'}
+<div class="think-box-shadow table-block">
+    {include file='payment/index_search'}
+    <table class="layui-table margin-top-10" lay-skin="line">
+        {notempty name='list'}
+        <thead>
+        <tr>
+            <th class='list-table-check-td think-checkbox'>
+                <label><input data-auto-none data-check-target='.list-check-box' type='checkbox'/></label>
+            </th>
+            <th class='list-table-sort-td'>
+                <button type="button" data-reload class="layui-btn layui-btn-xs">刷 新</button>
+            </th>
+            <th class="text-left nowrap">支付名称</th>
+            <th class="text-left nowrap">支付通道</th>
+            <th class="text-left nowrap">支付状态</th>
+            <th class="text-left nowrap">创建时间</th>
+            <th class="text-left nowrap"></th>
+        </tr>
+        </thead>
+        {/notempty}
+        <tbody>
+        {foreach $list as $key=>$vo}
+        <tr data-dbclick>
+            <td class='list-table-check-td think-checkbox'>
+                <label><input class="list-check-box" value='{$vo.id}' type='checkbox'/></label>
+            </td>
+            <td class='list-table-sort-td'>
+                <label><input data-action-blur="{:request()->url()}" data-value="id#{$vo.id};action#sort;sort#{value}" data-loading="false" value="{$vo.sort}" class="list-sort-input"></label>
+            </td>
+            <td class="text-left nowrap">{$vo.name|default=''}</td>
+            <td class="text-left nowrap">{$types[$vo.type]??$vo.type}</td>
+            <td>{if $vo.status eq 0}<span class="color-red">已禁用</span>{elseif $vo.status eq 1}<span class="color-green">已激活</span>{/if}</td>
+            <td class="text-left nowrap">{$vo.create_at|format_datetime}</td>
+            <td class='text-left nowrap'>
+                {if auth("edit")}
+                <a data-dbclick class="layui-btn layui-btn-xs" data-open="{:url('edit')}?id={$vo.id}">编 辑</a>
+                {/if}
+                {if auth("state") and $vo.status eq 1}
+                <a class="layui-btn layui-btn-xs layui-btn-warm" data-action="{:url('state')}" data-value="id#{$vo.id};status#0">禁 用</a>
+                {/if}
+                {if auth("state") and $vo.status eq 0}
+                <a class="layui-btn layui-btn-xs layui-btn-warm" data-action="{:url('state')}" data-value="id#{$vo.id};status#1">激 活</a>
+                {/if}
+                {if auth("remove")}
+                <a class="layui-btn layui-btn-xs layui-btn-danger" data-confirm="确定要删除该支付吗?" data-action="{:url('remove')}" data-value="id#{$vo.id}">删 除</a>
+                {/if}
+            </td>
+        </tr>
+        {/foreach}
+        </tbody>
+    </table>
+    {empty name='list'}<span class="notdata">没有记录哦</span>{else}{$pagehtml|raw|default=''}{/empty}
+</div>
+{/block}

+ 49 - 0
app/data/view/payment/index_search.html

@@ -0,0 +1,49 @@
+<fieldset>
+    <legend>条件搜索</legend>
+    <form class="layui-form layui-form-pane form-search" action="{:request()->url()}" onsubmit="return false" method="get" autocomplete="off">
+        <div class="layui-form-item layui-inline">
+            <label class="layui-form-label">支付名称</label>
+            <label class="layui-input-inline">
+                <input name="name" value="{:input('name','')}" placeholder="请输入支付名称" class="layui-input">
+            </label>
+        </div>
+        <div class="layui-form-item layui-inline">
+            <label class="layui-form-label">支付通道</label>
+            <div class="layui-input-inline">
+                <select class="layui-select" name="type">
+                    <option value="">-- 全部 --</option>
+                    {foreach $types as $k=>$v}
+                    {if $k eq input('type')}
+                    <option selected value="{$k}">{$v}</option>
+                    {else}
+                    <option value="{$k}">{$v}</option>
+                    {/if}{/foreach}
+                </select>
+            </div>
+        </div>
+        <div class="layui-form-item layui-inline">
+            <label class="layui-form-label">使用状态</label>
+            <div class="layui-input-inline">
+                <select class="layui-select" name="status">
+                    {foreach [''=>'-- 全部 --','0'=>'已禁用的记录','1'=>'已激活的记录'] as $k=>$v}
+                    {if $k.'' eq input('status')}
+                    <option selected value="{$k}">{$v}</option>
+                    {else}
+                    <option value="{$k}">{$v}</option>
+                    {/if}{/foreach}
+                </select>
+            </div>
+        </div>
+        <div class="layui-form-item layui-inline">
+            <label class="layui-form-label">创建时间</label>
+            <label class="layui-input-inline">
+                <input data-date-range name="create_at" value="{:input('create_at','')}" placeholder="请选择创建时间" class="layui-input">
+            </label>
+        </div>
+        <div class="layui-form-item layui-inline">
+            <button class="layui-btn layui-btn-primary"><i class="layui-icon">&#xe615;</i> 搜 索</button>
+        </div>
+    </form>
+</fieldset>
+
+<script>window.form.render()</script>