From ef6cd16d1b910bca59a50b140af8c9a977667b6c Mon Sep 17 00:00:00 2001
From: liuxiaoquan <q8197264@126.com>
Date: Fri, 17 Mar 2023 16:43:27 +0800
Subject: [PATCH] =?UTF-8?q?=E5=BA=97=E9=93=BA=E4=BF=9D=E8=AF=81=E9=87=91?=
 =?UTF-8?q?=E9=80=80=E5=9B=9E=E4=BF=AE=E5=A4=8Dbug=EF=BC=8C=E6=9C=AA?=
 =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E5=BE=AE=E4=BF=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../merchant/system/financial/Financial.php   |  60 +-
 .../system/merchant/MerchantMargin.php        |   2 +-
 app/admin/route/merchant.php                  |  12 +-
 .../merchant/system/merchant/margin/list.html | 105 ++--
 .../merchant/system/merchant/margin/mark.html |  56 ++
 .../system/merchant/margin/status.html        | 147 +++++
 .../system/merchant/merchant/lst.html         |   2 +-
 app/common/jobs/interfaces/JobInterface.php   |  17 +
 .../jobs/merchant/ChangeMerchantStatusJob.php |  40 ++
 .../model/merchant/store/product/Product.php  | 550 ++++++++++++++++++
 .../store/product/ProductCopy copy.php        | 110 ++++
 .../merchant/system/financial/Financial.php   | 251 ++++++--
 .../merchant/system/merchant/Merchant.php     |  18 +
 .../merchant/system/serve/ServeOrder.php      |   7 +-
 app/common/service/easywechat/BaseClient.php  | 297 ++++++++++
 .../service/easywechat/broadcast/Client.php   | 462 +++++++++++++++
 .../easywechat/broadcast/ServiceProvider.php  |  29 +
 .../service/easywechat/certficates/Client.php |  50 ++
 .../certficates/ServiceProvider.php           |  33 ++
 .../service/easywechat/combinePay/Client.php  | 265 +++++++++
 .../easywechat/combinePay/ServiceProvider.php |  33 ++
 .../service/easywechat/merchant/Client.php    | 217 +++++++
 .../easywechat/merchant/ServiceProvider.php   |  33 ++
 .../service/easywechat/storePay/Client.php    |  59 ++
 .../easywechat/storePay/ServiceProvider.php   |  33 ++
 .../easywechat/subscribe/ProgramProvider.php  |  40 ++
 .../easywechat/subscribe/ProgramSubscribe.php | 285 +++++++++
 app/common/service/merchant/WechatService.php | 167 ++++++
 composer.json                                 |   9 +-
 config/queue.php                              |  38 ++
 30 files changed, 3316 insertions(+), 111 deletions(-)
 create mode 100644 app/admin/view/merchant/system/merchant/margin/mark.html
 create mode 100644 app/admin/view/merchant/system/merchant/margin/status.html
 create mode 100644 app/common/jobs/interfaces/JobInterface.php
 create mode 100644 app/common/jobs/merchant/ChangeMerchantStatusJob.php
 create mode 100644 app/common/model/merchant/store/product/Product.php
 create mode 100644 app/common/model/merchant/store/product/ProductCopy copy.php
 create mode 100644 app/common/service/easywechat/BaseClient.php
 create mode 100644 app/common/service/easywechat/broadcast/Client.php
 create mode 100644 app/common/service/easywechat/broadcast/ServiceProvider.php
 create mode 100644 app/common/service/easywechat/certficates/Client.php
 create mode 100644 app/common/service/easywechat/certficates/ServiceProvider.php
 create mode 100644 app/common/service/easywechat/combinePay/Client.php
 create mode 100644 app/common/service/easywechat/combinePay/ServiceProvider.php
 create mode 100644 app/common/service/easywechat/merchant/Client.php
 create mode 100644 app/common/service/easywechat/merchant/ServiceProvider.php
 create mode 100644 app/common/service/easywechat/storePay/Client.php
 create mode 100644 app/common/service/easywechat/storePay/ServiceProvider.php
 create mode 100644 app/common/service/easywechat/subscribe/ProgramProvider.php
 create mode 100644 app/common/service/easywechat/subscribe/ProgramSubscribe.php
 create mode 100644 app/common/service/merchant/WechatService.php
 create mode 100644 config/queue.php

diff --git a/app/admin/controller/merchant/system/financial/Financial.php b/app/admin/controller/merchant/system/financial/Financial.php
index b6ee414..c08c42d 100644
--- a/app/admin/controller/merchant/system/financial/Financial.php
+++ b/app/admin/controller/merchant/system/financial/Financial.php
@@ -1,6 +1,6 @@
 <?php
 /**
- *  保证金退款处理
+ *  财务提现处理
  * 说明: 店铺类型 相关(不同类型需要不同的保证金)
  * @author:刘孝全
  * @email:q8197264@126.com
@@ -14,13 +14,15 @@ use think\App;
 use think\Request;
 use app\admin\BaseController;
 use app\common\model\merchant\system\financial\Financial as FinancialModel;
+use think\exception\ValidateException;
 use think\facade\View;
 
 class Financial extends BaseController
 {
     protected $model;
     protected $path = [
-
+        'status' => 'merchant/system/merchant/margin/status',
+        'mark' => 'merchant/system/merchant/margin/mark'
     ];
 
     public function __construct(App $app, FinancialModel $model)
@@ -35,7 +37,10 @@ class Financial extends BaseController
      */
     public function markForm()
     {
-        return View();
+        $id = (int)get_params('id');
+        $info = $this->model->get($id);
+        View::assign('mark', $info['admin_mark']);
+        return View($this->path['mark'],['id'=>$info['financial_id']]);
     }
 
     /**
@@ -43,7 +48,17 @@ class Financial extends BaseController
      */
     public function statusForm()
     {
-        return View();
+        $id = (int)get_params('id');
+        try{
+            $detail = $this->model->getDetail($id);
+            $detail['sub'] = bcsub($detail['merchant']['marginOrder']['pay_price'], $detail['extract_money'], 2);//扣费金额=已支付金额-提现金额
+            View::assign('detail', $detail);
+        }catch(ValidateException $e){
+            View::assign('errmsg', $e->getError()); 
+            View::assign('error', true); 
+        }
+        
+        return View($this->path['status']);
     }
 
 
@@ -73,9 +88,9 @@ class Financial extends BaseController
         $page = (int)get_params('page');
         $limit = (int)get_params('limit');
 
-        $where = get_params(['date','status','financial_type','financial_status','keyword','is_trader','mer_id']);
+        $where = get_params(['date','status','financial_type','financial_status','keyword','is_trader','mer_id','start_date','end_date', 'category_id','type_id']);
         $where['type'] = 1;//保证金
-
+       
         // 获取记录
         $data = $this->model->getAdminList($where, $page, $limit);
 
@@ -90,17 +105,25 @@ class Financial extends BaseController
      */
     public function switchStatus()
     {
-        $data = $this->request->params([['status',0], 'refusal']);
-        $type = $this->request->param('type',0);
+        $id = (int) get_params('id');
+        $data = get_params(['status','refusal']);
+        $data['status'] = empty($data['status'])?0:$data['status'];
         $data['status_time'] = date('Y-m-d H:i:s');
+        $type = get_params('type');
+
         if (!in_array($data['status'], [0,1,-1])) {
-            return app('json')->fail('审核状态错误');
+            return to_assign(1, '审核状态错误');
         }
         if (($data['status'] == -1) && empty($data['refusal'])) {
-            return app('json')->fail('请输入拒绝理由');
+            return to_assign(1, '请输入拒绝理由');
         }
-        $this->repository->switchStatus($id, $type, $data);
-        return app('json')->success('审核完成');
+        try{
+            $this->model->switchStatus($id, $type, $data);
+        }catch(ValidateException $e){
+            return to_assign(1, '审核失败:'.$e->getError());
+        }
+
+        return to_assign(0, '审核完成');
     }
 
     /**
@@ -108,13 +131,16 @@ class Financial extends BaseController
      */
     public function mark()
     {
-        $ret = $this->repository->getWhere([$this->repository->getPk() => $id]);
+        $id = (int)get_params('id');
+        var_dump($id);
+        $ret = $this->model->get($id);
+        if(!$ret) 
+            return to_assign(1,'数据不存在');
 
-        if(!$ret) return app('json')->fail('数据不存在');
-        $data = $this->request->params(['admin_mark']);
-        $this->repository->update($id,$data);
+        $data = get_params(['admin_mark']);
+        $this->model->modify($id,$data);
 
-        return app('json')->success('备注成功');
+        return to_assign(0, '备注成功');
     }
 
 
diff --git a/app/admin/controller/merchant/system/merchant/MerchantMargin.php b/app/admin/controller/merchant/system/merchant/MerchantMargin.php
index 68e859b..ca8a0c9 100644
--- a/app/admin/controller/merchant/system/merchant/MerchantMargin.php
+++ b/app/admin/controller/merchant/system/merchant/MerchantMargin.php
@@ -65,7 +65,7 @@ class MerchantMargin extends BaseController
         $params = get_params();
         $page = empty($params['page'])? 1 : (int)$params['page'];
         $limit = empty($params['limit'])? (int)get_config('app . page_size') : (int)$params['limit'];
-        $where = get_params(['date','keyword','is_trader','category_id','is_margin','type_id']);
+        $where = get_params(['date','keyword','is_trader','category_id','is_margin','type_id', 'start_date', 'end_date']);
         $where['type'] = 10;//10==保证金
 
         $data = $order->GetList($where, $page, $limit);
diff --git a/app/admin/route/merchant.php b/app/admin/route/merchant.php
index d1eb832..4b6a3ec 100644
--- a/app/admin/route/merchant.php
+++ b/app/admin/route/merchant.php
@@ -238,27 +238,27 @@ Route::group(function(){
         Route::get('lst', '/getMarginLst')->name('systemMarginRefundList')->option([
                 '_alias' => '退款申请列表',
             ]);
-        Route::get('refund/show/:id', '/refundShow')->name('systemMarginRefundShow')->option([
+        Route::get('show', '/refundShow')->name('systemMarginRefundShow')->option([
                 '_alias' => '退款申请详情',
             ]);
 
         // //审核
-        Route::get('refund/status/:id/form', '/statusForm')->name('systemMarginRefundSwitchStatusForm')->option([
+        Route::get('status', '/statusForm')->name('systemMarginRefundSwitchStatusForm')->option([
             '_alias' => '审核表单',
             '_auth' => false,
             '_form' => 'systemMarginRefundSwitchStatus',
         ]);
-        Route::post('refund/status/:id', '/switchStatus')->name('systemMarginRefundSwitchStatus')->append(['type' => 1])->option([
-                '_alias' => '审核',
+        Route::post('status', '/switchStatus')->name('systemMarginRefundSwitchStatus')->append(['type' => 1])->option([
+                '_alias' => '审核',//type=1 保证金
             ]);
 
         //备注
-        Route::get('refund/mark/:id/form', '/markMarginForm')->name('systemMarginRefundMarkForm')->option([
+        Route::get('mark', '/markForm')->name('systemMarginRefundMarkForm')->option([
             '_alias' => '备注表单',
             '_auth' => false,
             '_form' => 'systemMarginRefundMark',
         ]);
-        Route::post('refund/mark/:id', '/mark')->name('systemMarginRefundMark')->option([
+        Route::post('mark', '/mark')->name('systemMarginRefundMark')->option([
             '_alias' => '备注',
         ]);
     })->prefix('merchant.system.financial.Financial')->option([
diff --git a/app/admin/view/merchant/system/merchant/margin/list.html b/app/admin/view/merchant/system/merchant/margin/list.html
index 0fe994e..8d36fd3 100644
--- a/app/admin/view/merchant/system/merchant/margin/list.html
+++ b/app/admin/view/merchant/system/merchant/margin/list.html
@@ -62,7 +62,7 @@
                     <div class="layui-form-item">
                         <label class="layui-form-label">商户类别</label>
                         <div class="layui-input-block">
-                            <select name="is_trader" lay-filter="searchform">
+                            <select name="is_trader" lay-filter="seleform">
                                 <option value=""></option>
                                 <option value="1">自营</option>
                                 <option value="0">非自营</option>
@@ -147,7 +147,7 @@
 
                         <label class="layui-form-label">退回状态</label>
                         <div class="layui-input-block">
-                            <select name="refund" lay-filter="seleform">
+                            <select name="financial_type" lay-filter="seleform">
                                 <option value=""></option>
                                 <option value="0">未退回</option>
                                 <option value="1">已退回</option>
@@ -208,7 +208,9 @@
 <script type="text/html" id="refundBar">
     <div class="layui-btn-group">
         <a class="layui-btn layui-btn-xs" lay-event="mark">备注</a>
-        <a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="status">审核</a>
+        {{#  if(d.status == 0 ){ }}
+            <a class="layui-btn layui-btn-normal layui-btn-xs" lay-event="status">审核</a>
+        {{#  } }}
         <a class="layui-btn layui-btn-xs" lay-event="record">扣费记录</a>
     </div>
 </script>
@@ -235,7 +237,7 @@
                 [
                     {
                         fixed: 'ID',
-                        field: 'mer_id',
+                        field: 'order_id',
                         title: 'ID',
                         align: 'center',
                         width: 80
@@ -328,7 +330,7 @@
                 [
                     {
                         fixed: 'ID',
-                        field: 'mer_id',
+                        field: 'financial_id',
                         title: 'ID',
                         align: 'center',
                         width: 80
@@ -369,7 +371,7 @@
                                 case 1:
                                     return '通过';
                                 case -1:
-                                    return '未通过';
+                                    return '<div>未通过 原因:'+d.refusal+'</div>';
                             }
                         }
                     }, {
@@ -391,10 +393,10 @@
                             switch(d.status) {
                                 case 0:
                                     return '未退';
-                                // case 1:
-                                //     return '通过';
-                                // case -1:
-                                //     return '未通过';
+                                case 1:
+                                    return '通过';
+                                case -1:
+                                    return '未通过';
                             }
                         }
                     },
@@ -427,29 +429,28 @@
 
 
 
-        //监听表格行工具事件
+        //监听 缴存保证金 表格行工具事件
         table.on('tool(pay_list)', function (obj) {
             var data = obj.data;
             // console.log(data);
             if (obj.event === 'reduct') {
-                tool.side('/admin/margin/form?id=' + obj.data.mer_id);
+                tool.side('/admin/margin/form?id=' + obj.data.order_id);
             } else if (obj.event === 'record') {
-                tool.side('/admin/margin/read?id=' + obj.data.mer_id);
+                tool.side('/admin/margin/read?id=' + obj.data.order_id);
             }
             
             return false;
         });
 
-        //监听表格行工具事件
+        //监听 退回保证金 表格行工具事件
         table.on('tool(refund_list)', function (obj) {
             var data = obj.data;
             if (obj.event === 'status') {
-                alert("审核");
-                tool.post('/admin/margin/status?id=' + obj.data.mer_id, obj.data);
+                tool.side('/admin/margin/refund/status?id=' + data.financial_id, data);
             } else if (obj.event === 'mark') {
-                tool.side('/admin/margin/form?id=' + obj.data.mer_id);
+                tool.side('/admin/margin/refund/mark?id=' + data.financial_id);
             } else if (obj.event === 'record') {
-                tool.side('/admin/margin/read?id=' + obj.data.mer_id);
+                tool.side('/admin/margin/read?id=' + data.financial_id);
             }
             
             return false;
@@ -518,7 +519,12 @@
 
                         $('#both').removeClass('layui-btn-primary')
                         $('#both').siblings().addClass('layui-btn-primary')
-                        active['reload'] ? active['reload'].call(this) : '';
+
+                        if ('11' == active.TAGID) {
+                            active['reload'] ? active['reload'].call(this) : '';
+                        }else{
+                            active['refund_reload'] ? active['refund_reload'].call(this, othis) : '';
+                        }
                     }
                 });
 
@@ -551,7 +557,8 @@
                             }
                         });
                     }else{
-                        refundList(data.field)
+                        //refundList(data.field)
+                        active['refund_reload'] ? active['refund_reload'].call(this, othis) : '';
                     }
                     
                     return false;
@@ -560,17 +567,8 @@
                 // 监听select 提交
                 form.on('select(searchform)', function(e) {
                     let data = getformdata();
-
                     if ('11' == active.TAGID) {
                         active['reload'] ? active['reload'].call(this, othis) : '';
-                        // layui.payTable.reload({
-                        //     where: {
-                        //         ...data
-                        //     },
-                        //     page: {
-                        //         curr: 1
-                        //     }
-                        // });
                     }else{
                         active['refund_reload'] ? active['refund_reload'].call(this, othis) : '';
                     }
@@ -610,7 +608,11 @@
                     $('#chonse_start_date').val(start_date);
                     $('#chonse_end_date').val(end_date);
 
-                    active['reload'] ? active['reload'].call(this) : '';
+                    if (active.TAGID=='11') {
+                        active['reload'] ? active['reload'].call(this) : '';
+                    }else{
+                        active['refund_reload'] ? active['refund_reload'].call(this) : '';
+                    }
 
                     return false;
                 })
@@ -618,30 +620,37 @@
 
                 // 商户审核
                 form.on('submit(statusform)', function(data) {
-                    let name = data.elem.name
-                    let status = 0;
-                    
-                    if (name=='wait') {
-                        status = 0;
-                    }else if(name=='success'){
-                        status = 1;
-                    }else if(name=='failed'){
-                        status = 2;
+                    if (active.TAGID!='11') {
+                        let name = data.elem.name
+                        let status = 0;
+                        
+                        if (name=='wait') {
+                            status = 0;
+                        }else if(name=='success'){
+                            status = 1;
+                        }else if(name=='failed'){
+                            status = -1;
+                        }
+                        if (name=='both'){
+                            $('#status').attr('disabled', true);
+                        }else{
+                            $('#status').attr('disabled', false);
+                        }
+                        switchClass(this)
+                        $('#status').val(status);
+                        active['refund_reload'] ? active['refund_reload'].call(this) : '';
+                        
                     }
-                    if (name=='both'){
-                        $('#status').attr('disabled', true);
-                    }else{
-                        $('#status').attr('disabled', false);
-                    }
-                    switchClass(this)
-                    $('#status').val(status);
-                    active['reload'] ? active['reload'].call(this) : '';
                     return false;
                 });
 
                 //监听select提交
                 form.on('select(seleform)', function(data) {
-                    active['reload'] ? active['reload'].call(this) : '';
+                    if ('11' == active.TAGID) {
+                        active['reload'] ? active['reload'].call(this) : '';
+                    }else{
+                        active['refund_reload'] ? active['refund_reload'].call(this) : '';
+                    }
 
                     return false;
                 });
diff --git a/app/admin/view/merchant/system/merchant/margin/mark.html b/app/admin/view/merchant/system/merchant/margin/mark.html
new file mode 100644
index 0000000..c27c2ff
--- /dev/null
+++ b/app/admin/view/merchant/system/merchant/margin/mark.html
@@ -0,0 +1,56 @@
+{extend name="common/base"/}
+{block name="style"}
+
+{/block}
+<!-- 主体 -->
+{block name="body"}
+<form class="layui-form p-4">
+    <h3 class="pb-3">备注</h3>
+    <table class="layui-table layui-table-form">
+        <tr>
+            <td colspan="6">
+                <textarea class="layui-textarea" name="admin_mark">{$mark}</textarea>
+                <input type="hidden" name="id" value="{$id}">
+            </td>
+        </tr>
+    </table>
+    <div class="pt-3">
+        <button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="webform" type="button">立即提交</button>
+        <button type="reset" class="layui-btn layui-btn-primary">重置</button>
+    </div>
+</form>
+{/block}
+<!-- /主体 -->
+
+<!-- 脚本 -->
+{block name="script"}
+<script src="/static/assets/js/xm-select.js"></script>
+<script>
+    var moduleInit = ['tool', 'tagpicker', 'tinymce'];
+    var group_access = "{:session('gougu_admin')['group_access']}"
+
+
+    function gouguInit() {
+        var form = layui.form, tool = layui.tool, tagspicker = layui.tagpicker;
+       
+        //监听提交
+        form.on('submit(webform)', function (data) {
+            if (data.field == '') {
+                layer.msg('请先完善商品详情');
+                return false;
+            }
+            let callback = function (e) {
+                layer.msg(e.msg);
+                if (e.code == 0) {
+                    tool.tabRefresh(71);
+                    tool.sideClose(1000);
+                }
+            }
+            tool.post('/admin/margin/refund/mark', data.field, callback);
+            return false;
+        });
+
+    }
+</script>
+{/block}
+<!-- /脚本 -->
\ No newline at end of file
diff --git a/app/admin/view/merchant/system/merchant/margin/status.html b/app/admin/view/merchant/system/merchant/margin/status.html
new file mode 100644
index 0000000..cf80f26
--- /dev/null
+++ b/app/admin/view/merchant/system/merchant/margin/status.html
@@ -0,0 +1,147 @@
+{extend name="common/base"/}
+{block name="style"}
+<style type="text/css">
+    .editormd-code-toolbar select {
+        display: inline-block
+    }
+
+    .editormd li {
+        list-style: inherit;
+    }
+</style>
+{/block}
+<!-- 主体 -->
+{block name="body"}
+{if $error}
+<div>{$errmsg}</div>
+{else}
+<form class="layui-form p-4">
+    <h3 class="pb-3">保证金扣费</h3>
+    <table class="layui-table layui-table-form">
+
+        <tr>
+            <td  colspan="2"   class="layui-td-gray">商户名称</td>
+            <td colspan="6">
+                <input type="text" name="mer_name" lay-verify="required" 
+                lay-reqText="商户名称" disabled autocomplete="off" placeholder="商户名称" class="layui-input" value="{$detail.merchant.mer_name}">
+            </td>
+        </tr>
+        
+        <tr>
+            <td  colspan="2" class="layui-td-gray">商户ID</td>
+            <td colspan="6">
+                <input type="number" name="mer_id" lay-verify="required" lay-reqText="0" autocomplete="off" disabled placeholder="0" class="layui-input" value="{$detail.merchant.mer_id}">
+            </td>
+        </tr>
+
+        <tr>
+            <td  colspan="2"   class="layui-td-gray">店铺类型</td>
+            <td colspan="6">
+                <input type="text" name="mer_name" lay-verify="required" 
+                lay-reqText="店铺类型" disabled  class="layui-input" value="{$detail.merchant.merchantType.type_name}">
+            </td>
+        </tr>
+
+        <tr>
+            <td colspan="2" class="layui-td-gray">保证金额度</td>
+            <td >
+                <input type="number" name="margin" lay-verify="required" lay-reqText="0" autocomplete="off" disabled placeholder="0" class="layui-input" value="{$detail.merchant.marginOrder.pay_price}">
+                
+            </td>
+            <td >单位:元</td>
+        </tr>
+
+        <tr>
+            <td colspan="2" class="layui-td-gray">扣费金额</td>
+            <td >
+                <input type="number" name="number" lay-reqText="0" disabled autocomplete="off" placeholder="0" class="layui-input" value="{$detail.sub}">
+            </td>
+            <td >单位:元</td>
+        </tr>
+
+        <tr>
+            <td colspan="2" class="layui-td-gray">退回金额</td>
+            <td >
+                <input type="number" disabled name="number" lay-verify="required" lay-reqText="0" autocomplete="off" placeholder="0" class="layui-input" value="{$detail.extract_money}">
+            </td>
+            <td >单位:元</td>
+        </tr>
+        
+        <tr>
+            <td colspan="2" class="layui-td-gray">审核</td>
+            <td >
+                
+                <input type="radio" name="status" id="yes" lay-filter="status"  class="layui-input" value="1">
+                <label for="yes">同意</label>
+                
+                <input type="radio" name="status" id="no" class="layui-input" lay-filter="status" value="-1" checked>
+                <label for="no">拒绝</label>
+            </td>
+            <td >单位:元</td>
+        </tr>
+
+        <tr>
+            <td  colspan="2"  class="layui-td-gray">拒绝原因<font>*</font></td>
+            <td colspan="6">
+                <textarea class="layui-textarea" lay-verify="required"  name="refusal" ></textarea>
+            </td>
+        </tr>
+    </table>
+
+    <div class="pt-3">
+        <input type="hidden" name="id" value="{$detail.financial_id}">
+        <button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="webform" type='button'>确定</button>
+        <button type="reset" class="layui-btn layui-btn-primary">重置</button>
+    </div>
+</form>
+{/if}
+{/block}
+<!-- /主体 -->
+
+<!-- 脚本 -->
+{block name="script"}
+<script src="/static/assets/js/xm-select.js"></script>
+
+<script>
+    var moduleInit = ['tool','treeGrid', 'tagpicker', 'tinymce', 'admin'];
+    var group_access = "{:session('gougu_admin')['group_access']}"
+
+    function gouguInit() {
+        var treeGrid = layui.treeGrid,table = layui.table
+
+        var tool = layui.tool;
+        var form = layui.form, tool = layui.tool, tagspicker = layui.tagpicker;
+
+        var editor = layui.tinymce;
+        var edit = editor.render({
+            selector: "#container_content",
+            height: 500,
+        });
+        
+        //监听提交
+        form.on('submit(webform)', function (data) {
+            // console.log(data.field);
+            // data.field.content = tinyMCE.editors['container_content'].getContent();
+            if (data.field == '') {
+                layer.msg('请先完善表单输入');
+                return false;
+            }
+            let callback = function (e) {
+                layer.msg(e.msg);
+                if (e.code == 0) {
+                    tool.tabRefresh(71);
+                    tool.sideClose(1000);
+                }
+            }
+
+            
+            tool.post('/admin/margin/refund/status', data.field, callback);
+
+            return true;
+        });
+    }
+
+
+</script>
+{/block}
+<!-- /脚本 -->
diff --git a/app/admin/view/merchant/system/merchant/merchant/lst.html b/app/admin/view/merchant/system/merchant/merchant/lst.html
index 0d77a94..8ff9c9a 100644
--- a/app/admin/view/merchant/system/merchant/merchant/lst.html
+++ b/app/admin/view/merchant/system/merchant/merchant/lst.html
@@ -62,7 +62,7 @@
                     <div class="layui-form-item">
                         <label class="layui-form-label">商户类别</label>
                         <div class="layui-input-block">
-                            <select name="is_trader" lay-filter="searchform">
+                            <select name="is_trader" lay-filter="seleform">
                                 <option value=""></option>
                                 <option value="1">自营</option>
                                 <option value="0">非自营</option>
diff --git a/app/common/jobs/interfaces/JobInterface.php b/app/common/jobs/interfaces/JobInterface.php
new file mode 100644
index 0000000..644bc00
--- /dev/null
+++ b/app/common/jobs/interfaces/JobInterface.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * 清息队列任务 异步触发 接口
+ * 
+ * @author:刘孝全
+ * @email:q8197264@126.com
+ * @date :2023年03月17日
+ */
+namespace app\common\jobs\interfaces;
+
+
+interface JobInterface
+{
+    public function fire($job, $data);
+
+    public function failed($data);
+}
diff --git a/app/common/jobs/merchant/ChangeMerchantStatusJob.php b/app/common/jobs/merchant/ChangeMerchantStatusJob.php
new file mode 100644
index 0000000..9ffb2f1
--- /dev/null
+++ b/app/common/jobs/merchant/ChangeMerchantStatusJob.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * 商户状态修改
+ *  说明:来自商户保证金退还-->退返保证金审核通过的消息
+ * 
+ * @author:刘孝全
+ * @email:q8197264@126.com
+ * @date :2023年03月17日
+ */
+namespace app\common\jobs\merchant;
+
+
+use app\common\model\merchant\store\product\Product;
+use app\common\model\merchant\system\merchant\Merchant;
+use app\common\jobs\interfaces\JobInterface;
+use think\facade\Log;
+use think\queue\Job;
+
+class ChangeMerchantStatusJob implements JobInterface
+{
+
+    public function fire($job, $merId)
+    {
+        $rows=0;
+        $merchant = app()->make(Merchant::class)->get($merId);
+        if ($merchant) {
+            $where = [
+                'mer_status' => ($merchant['is_del'] || !$merchant['mer_state'] || !$merchant['status']) ? 0 : 1
+            ];
+            $rows = app()->make(Product::class)->changeMerchantProduct($merId, $where);
+        }
+        $job->delete();
+        return $rows;
+    }
+
+    public function failed($data)
+    {
+        // TODO: Implement failed() method.
+    }
+}
diff --git a/app/common/model/merchant/store/product/Product.php b/app/common/model/merchant/store/product/Product.php
new file mode 100644
index 0000000..81bc30e
--- /dev/null
+++ b/app/common/model/merchant/store/product/Product.php
@@ -0,0 +1,550 @@
+<?php
+
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+namespace app\common\model\merchant\store\product;
+
+use app\common\dao\store\StoreSeckillActiveDao;
+use app\common\model\BaseModel;
+use app\common\model\store\coupon\StoreCouponProduct;
+use app\common\model\store\Guarantee;
+use app\common\model\store\GuaranteeTemplate;
+use app\common\model\store\GuaranteeValue;
+use app\common\model\store\parameter\ParameterValue;
+use app\common\model\store\shipping\ShippingTemplate;
+use app\common\model\store\StoreBrand;
+use app\common\model\store\StoreCategory;
+use app\common\model\store\StoreSeckillActive;
+use app\common\model\system\merchant\Merchant;
+use app\common\repositories\store\StoreCategoryRepository;
+use crmeb\services\VicWordService;
+use Darabonba\GatewaySpi\Models\InterceptorContext\request;
+use think\db\BaseQuery;
+use think\facade\Db;
+use think\model\concern\SoftDelete;
+
+/**
+ * TODO: 
+ */
+class Product extends BaseModel
+{
+    use SoftDelete;
+
+
+    protected $deleteTime = 'is_del';
+    protected $defaultSoftDelete = 0;
+
+
+    /**
+     * @Author:Qinii
+     * @Date: 2020/5/8
+     * @return string
+     */
+    public static function tablePk(): string
+    {
+        return 'product_id';
+    }
+
+    /**
+     * @Author:Qinii
+     * @Date: 2020/5/8
+     * @return string
+     */
+    public static function tableName(): string
+    {
+        return 'store_product';
+    }
+
+    /*
+     * -----------------------------------------------------------------------------------------------------------------
+     * 属性
+     * -----------------------------------------------------------------------------------------------------------------
+    */
+    public function getSliderImageAttr($value)
+    {
+        return $value ? explode(',',$value) : [];
+    }
+    public function getGiveCouponIdsAttr($value)
+    {
+        return $value ? explode(',',$value) : [];
+    }
+    public function getMaxExtensionAttr($value)
+    {
+        if($this->extension_type){
+            $org_extension =  ($this->attrValue()->order('extension_two DESC')->value('extension_one'));
+        } else {
+            $org_extension = bcmul(($this->attrValue()->order('price DESC')->value('price')) , systemConfig('extension_one_rate'),2);
+        }
+        $spreadUser = (request()->isLogin() &&  request()->userType() == 1  ) ? request()->userInfo() : null;
+        if ($spreadUser && $spreadUser->brokerage_level > 0 && $spreadUser->brokerage && $spreadUser->brokerage->extension_one_rate > 0) {
+            $org_extension = bcmul($org_extension, 1 + $spreadUser->brokerage->extension_one_rate, 2);
+        }
+        return $org_extension;
+    }
+    public function getMinExtensionAttr($value)
+    {
+        if($this->extension_type){
+            $org_extension = ($this->attrValue()->order('extension_two ASC')->value('extension_two'));
+        } else {
+            $org_extension = bcmul(($this->attrValue()->order('price ASC')->value('price')) , systemConfig('extension_one_rate'),2);
+        }
+        $spreadUser = (request()->isLogin() &&  request()->userType() == 1 ) ? request()->userInfo() : null;
+        if ($spreadUser && $spreadUser->brokerage_level > 0 && $spreadUser->brokerage && $spreadUser->brokerage->extension_one_rate > 0) {
+            $org_extension = bcmul($org_extension, 1 + $spreadUser->brokerage->extension_one_rate, 2);
+        }
+        return $org_extension;
+    }
+
+    public function check()
+    {
+        if(!$this || !$this->is_show || !$this->is_used || !$this->status || $this->is_del || !$this->mer_status) return false;
+        return true;
+    }
+
+    /**
+     * TODO 秒杀商品结束时间
+     * @return false|int
+     * @author Qinii
+     * @day 2020-08-15
+     */
+    public function getEndTimeAttr()
+    {
+        if($this->product_type !== 1) return true;
+        $day = date('Y-m-d',time());
+        $_day = strtotime($day);
+        $end_day = strtotime($this->seckillActive['end_day']);
+        if($end_day >= $_day)
+            return strtotime($day.$this->seckillActive['end_time'].':00:00');
+        if($end_day < strtotime($day))
+            return strtotime(date('Y-m-d',$end_day).$this->seckillActive['end_time'].':00:00');
+    }
+
+    /**
+     * TODO 秒杀商品状态
+     * @return array|int
+     * @author Qinii
+     * @day 2020-08-19
+     */
+    public function getSeckillStatusAttr()
+    {
+        if($this->product_type !== 1) return true;
+        $day = strtotime(date('Y-m-d',time()));
+        $_h = date('H',time());
+        $start_day = strtotime($this->seckillActive['start_day']);
+        $end_day = strtotime($this->seckillActive['end_day']);
+        if(!$this->seckillActive) return '';
+        if($this->seckillActive['status'] !== -1){
+            //还未开始
+            if($start_day > time() || $this->is_show !== 1)return 0;
+            //已结束
+            if($end_day < $day) return -1;
+            //开始 - 结束
+            if($start_day <= $day && $day <= $end_day){
+                //未开始
+                if($this->seckillActive['start_time'] > $_h) return 0;
+                //已结束
+                if($this->seckillActive['end_time'] <= $_h) return -1;
+                //进行中
+                if($this->seckillActive['start_time'] <= $_h && $this->seckillActive['end_time'] > $_h) return 1;
+            }
+        }
+        //已结束
+        return -1;
+
+    }
+
+    public function getImageAttr($value)
+    {
+        if (is_int(strpos($value, 'http'))){
+            return $value;
+        }else{
+            return rtrim(systemConfig('site_url'),'/') .$value;
+        }
+    }
+
+    public function getTopReplyAttr()
+    {
+        $res = ProductReply::where('product_id',$this->product_id)->where('is_del',0)->with(['orderProduct'])->field('reply_id,uid,nickname,merchant_reply_content,avatar,order_product_id,product_id,product_score,service_score,postage_score,comment,pics,rate,create_time')
+            ->order('sort DESC,create_time DESC')->limit(1)->find();
+        if(!$res) return null;
+        if ($res['orderProduct'])
+            $res['sku'] = $res['orderProduct']['cart_info']['productAttr']['sku'];
+        unset($res['orderProduct']);
+        if (strlen($res['nickname']) > 1) {
+            $str = mb_substr($res['nickname'],0,1) . '*';
+            if (strlen($res['nickname']) > 2) {
+                $str .= mb_substr($res['nickname'], -1,1);
+            }
+            $res['nickname'] = $str;
+        }
+
+        return $res;
+    }
+
+    public function getUsStatusAttr()
+    {
+        return ($this->status == 1) ? ($this->is_used == 1 ? ( $this->is_show ? 1 : 0 ) : -1) : -1;
+    }
+
+    public function getGuaranteeTemplateAttr()
+    {
+        $gua = GuaranteeTemplate::where('guarantee_template_id',$this->guarantee_template_id)->where('status',1)->where('is_del',0)->find();
+        if(!$gua) return [];
+        $guarantee_id = GuaranteeValue::where('guarantee_template_id',$this->guarantee_template_id)->column('guarantee_id');
+        return Guarantee::where('guarantee_id','in',$guarantee_id)->where('status',1)->where('is_del',0)->select();
+    }
+
+    public function getMaxIntegralAttr()
+    {
+        if(systemConfig('integral_status') && merchantConfig($this->mer_id,'mer_integral_status')){
+            $price = ($this->attrValue()->order('price DESC')->value('price'));
+            $rate = ($this->integral_rate < 0) ? merchantConfig($this->mer_id,'mer_integral_rate') : $this->integral_rate;
+            $rate = $rate < 0 ? $rate / 100 : 0;
+            return bcmul($price ,$rate,2);
+        }
+        return '0';
+    }
+
+    public function getHotRankingAttr()
+    {
+        if ($this->product_type == 0) {
+            $where = [
+                'is_show'       => 1,
+                'status'        => 1,
+                'is_used'       => 1,
+                'product_type'  => 0,
+                'mer_status'    => 1,
+                'is_gift_bag'   => 0,
+                'cate_id' => $this->cate_id
+            ];
+            self::where($where)->order('sales DESC');
+        }
+    }
+
+    /**
+     * TODO 商品参数
+     * @author Qinii
+     * @day 2022/11/24
+     */
+    public function getParamsAttr()
+    {
+        if(in_array($this->product_type,[0,2])) {
+            $product_id = $this->product_id;
+        } else {
+            $product_id = $this->old_product_id;
+        }
+        return ParameterValue::where('product_id',$product_id)->order('parameter_value_id ASC')->select();
+    }
+
+    public function getParamTempIdAttr($value)
+    {
+        return $value ? explode(',',$value) : $value;
+    }
+
+    /*
+     * -----------------------------------------------------------------------------------------------------------------
+     *  关联模型
+     * -----------------------------------------------------------------------------------------------------------------
+    */
+    public function merCateId()
+    {
+        return $this->hasMany(ProductCate::class,'product_id','product_id')->field('product_id,mer_cate_id');
+    }
+    public function attr()
+    {
+        return $this->hasMany(ProductAttr::class,'product_id','product_id');
+    }
+    public function attrValue()
+    {
+        return $this->hasMany(ProductAttrValue::class,'product_id','product_id');
+    }
+    public function oldAttrValue()
+    {
+        return $this->hasMany(ProductAttrValue::class,'product_id','old_product_id');
+    }
+    public function content()
+    {
+        return $this->hasOne(ProductContent::class,'product_id','product_id');
+    }
+    protected function temp()
+    {
+        return $this->hasOne(ShippingTemplate::class,'shipping_template_id','temp_id');
+    }
+    public function storeCategory()
+    {
+        return $this->hasOne(StoreCategory::class,'store_category_id','cate_id')->field('store_category_id,cate_name');
+    }
+    public function merchant()
+    {
+        return $this->hasOne(Merchant::class,'mer_id','mer_id')->field('is_trader,type_id,mer_id,mer_name,mer_avatar,product_score,service_score,postage_score,service_phone,care_count');
+    }
+    public function reply()
+    {
+        return $this->hasMany(ProductReply::class,'product_id','product_id')->order('create_time DESC');
+    }
+    public function brand()
+    {
+        return $this->hasOne(StoreBrand::class,'brand_id','brand_id')->field('brand_id,brand_name');
+    }
+    public function seckillActive()
+    {
+        return $this->hasOne(StoreSeckillActive::class,'product_id','product_id');
+    }
+    public function issetCoupon()
+    {
+        return $this->hasOne(StoreCouponProduct::class, 'product_id', 'product_id')->alias('A')
+            ->rightJoin('StoreCoupon B', 'A.coupon_id = B.coupon_id')->where(function (BaseQuery $query) {
+                $query->where('B.is_limited', 0)->whereOr(function (BaseQuery $query) {
+                    $query->where('B.is_limited', 1)->where('B.remain_count', '>', 0);
+                });
+            })->where(function (BaseQuery $query) {
+                $query->where('B.is_timeout', 0)->whereOr(function (BaseQuery $query) {
+                    $time = date('Y-m-d H:i:s');
+                    $query->where('B.is_timeout', 1)->where('B.start_time', '<', $time)->where('B.end_time', '>', $time);
+                });
+            })->field('A.product_id,B.*')->where('status', 1)->where('type', 1)->where('send_type', 0)->where('is_del', 0)
+            ->order('sort DESC,coupon_id DESC')->hidden(['is_del', 'status']);
+    }
+    public function assist()
+    {
+        return $this->hasOne(ProductAssist::class,'product_id','product_id');
+    }
+    public function productGroup()
+    {
+        return $this->hasOne(ProductGroup::class,'product_id','product_id');
+    }
+    public function guarantee()
+    {
+        return $this->hasOne(GuaranteeTemplate::class,'guarantee_template_id','guarantee_template_id')->where('status',1)->where('is_del',0);
+    }
+
+
+    /**
+     * 更新商户商品
+     */
+    public function changeMerchantProduct($merId, $data)
+    {
+        return self::where('mer_id', $merId)->update($data);
+    }
+
+    /**
+     * TODO 是否是会员
+     * @return bool
+     * @author Qinii
+     * @day 2023/1/4
+     */
+    public function getIsVipAttr()
+    {
+        if (request()->isLogin()) {
+            if (request()->userType() == 1) {
+                $userInfo = request()->userInfo();
+                return $userInfo->is_svip ? true : false;
+            } else {
+                return true;
+            }
+        }
+        return  false;
+    }
+    /**
+     * TODO 是否展示会员价
+     * @return bool
+     * @author Qinii
+     * @day 2023/1/4
+     */
+    public function getShowSvipPriceAttr()
+    {
+        if ($this->mer_svip_status != 0 && (systemConfig('svip_show_price') != 1 || $this->is_vip) && $this->svip_price_type > 0 ) {
+            return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * TODO 是否显示会员价等信息
+     * @return array
+     * @author Qinii
+     * @day 2022/11/24
+     */
+    public function getShowSvipInfoAttr()
+    {
+        $res = [
+            'show_svip' => true, //是否展示会员入口
+            'is_svip' => false,  //当前用户是否是会员
+            'show_svip_price' => false, //是否展示会员价
+            'save_money' => 0, //当前商品会员优化多少钱
+        ];
+        if ($this->product_type == 0) {
+            if (!systemConfig('svip_switch_status')) {
+                $res['show_svip'] = false;
+            } else {
+                $res['is_svip'] = $this->is_vip;
+                if ($this->show_svip_price) {
+                    $res['show_svip_price'] = true;
+                    $res['save_money'] = bcsub($this->price, $this->svip_price, 2);
+                }
+            }
+        }
+        return  $res;
+    }
+
+    /**
+     * TODO 获取会员价
+     * @return int|string
+     * @author Qinii
+     * @day 2023/1/4
+     */
+    public function getSvipPriceAttr()
+    {
+        if ($this->product_type == 0 && $this->mer_svip_status != 0 && $this->show_svip_price) {
+            //默认比例
+            if ($this->svip_price_type == 1) {
+                $rate = merchantConfig($this->mer_id,'svip_store_rate');
+                $svip_store_rate = $rate > 0 ? bcdiv($rate,100,2) : 0;
+                $price = $this->attrValue()->order('price ASC')->value('price');
+                return bcmul($price,$svip_store_rate,2);
+            }
+            //自定义
+            if ($this->svip_price_type == 2) {
+                return $this->getData('svip_price');
+            }
+        }
+        return 0;
+    }
+
+
+    /*
+     * -----------------------------------------------------------------------------------------------------------------
+     * 搜索器
+     * -----------------------------------------------------------------------------------------------------------------
+     */
+    public function searchMerCateIdAttr($query, $value)
+    {
+        $cate_ids = (StoreCategory::where('path','like','%/'.$value.'/%'))->column('store_category_id');
+        $cate_ids[] = intval($value);
+        $product_id = ProductCate::whereIn('mer_cate_id',$cate_ids)->column('product_id');
+        $query->whereIn('Product.product_id',$product_id);
+    }
+    public function searchKeywordAttr($query, $value)
+    {
+        if (!$value) return;
+        if (is_numeric($value)) {
+            $query->whereLike("Product.store_name|Product.keyword|bar_code|Product.product_id", "%{$value}%");
+        } else {
+            $word = app()->make(VicWordService::class)->getWord($value);
+            $query->where(function ($query) use ($word, $value) {
+                foreach ($word as $item) {
+                    $query->whereOr('Product.store_name|Product.keyword', 'LIKE', "%$item%");
+                }
+                $query->order(Db::raw('REPLACE(Product.store_name,\'' . $value . '\',\'\')'));
+            });
+        }
+    }
+    public function searchStatusAttr($query, $value)
+    {
+        if($value === -1){
+            $query->where('Product.status', 'in',[-1,-2]);
+        }else {
+            $query->where('Product.status',$value);
+        }
+    }
+    public function searchCateIdAttr($query, $value)
+    {
+        $query->where('cate_id',$value);
+    }
+    public function searchCateIdsAttr($query, $value)
+    {
+        $query->whereIn('cate_id',$value);
+    }
+    public function searchIsShowAttr($query, $value)
+    {
+        $query->where('is_show',$value);
+    }
+    public function searchPidAttr($query, $value)
+    {
+        $cateId = app()->make(StoreCategoryRepository::class)->allChildren(intval($value));
+        $query->whereIn('cate_id', $cateId);
+    }
+    public function searchStockAttr($query, $value)
+    {
+        $value ? $query->where('stock','<=', $value) :  $query->where('stock', $value);
+    }
+    public function searchIsNewAttr($query, $value)
+    {
+        $query->where('is_new',$value);
+    }
+    public function searchPriceAttr($query, $value)
+    {
+        if(empty($value[0]) && !empty($value[1]))
+            $query->where('price','<',$value[1]);
+        if(!empty($value[0]) && empty($value[1]))
+            $query->where('price','>',$value[0]);
+        if(!empty($value[0]) && !empty($value[1]))
+            $query->whereBetween('price',[$value[0],$value[1]]);
+    }
+    public function searchBrandIdAttr($query, $value)
+    {
+        $query->whereIn('brand_id',$value);
+    }
+    public function searchIsGiftBagAttr($query, $value)
+    {
+        $query->where('is_gift_bag',$value);
+    }
+    public function searchIsGoodAttr($query, $value)
+    {
+        $query->where('is_good',$value);
+    }
+    public function searchIsUsedAttr($query, $value)
+    {
+        $query->where('is_used',$value);
+    }
+    public function searchProductTypeAttr($query, $value)
+    {
+        $query->where('Product.product_type',$value);
+    }
+    public function searchSeckillStatusAttr($query, $value)
+    {
+        $product_id = (new StoreSeckillActiveDao())->getStatus($value)->column('product_id');
+        $query->whereIn('Product.product_id',$product_id);
+    }
+    public function searchStoreNameAttr($query, $value)
+    {
+        $query->where('Product.store_name','like','%'.$value.'%');
+    }
+    public function searchMerStatusAttr($query, $value)
+    {
+        $query->where('mer_status',$value);
+    }
+    public function searchProductIdAttr($query, $value)
+    {
+        $query->where('Product.product_id',$value);
+    }
+    public function searchPriceOnAttr($query, $value)
+    {
+        $query->where('price','>=',$value);
+    }
+    public function searchPriceOffAttr($query, $value)
+    {
+        $query->where('price','<=',$value);
+    }
+    public function searchisFictiAttr($query, $value)
+    {
+        $query->where('type',$value);
+    }
+    public function searchGuaranteeTemplateIdAttr($query, $value)
+    {
+        $query->whereIn('guarantee_template_id',$value);
+    }
+    public function searchTempIdAttr($query, $value)
+    {
+        $query->whereIn('Product.temp_id',$value);
+    }
+}
diff --git a/app/common/model/merchant/store/product/ProductCopy copy.php b/app/common/model/merchant/store/product/ProductCopy copy.php
new file mode 100644
index 0000000..c10c1ff
--- /dev/null
+++ b/app/common/model/merchant/store/product/ProductCopy copy.php	
@@ -0,0 +1,110 @@
+<?php
+declare (strict_types = 1);
+
+namespace app\common\model\merchant\store\product;
+
+use think\Model;
+use think\facade\Db;
+use think\exception\ValidateException;
+use app\common\model\merchant\system\merchant\Merchant;
+use app\common\model\merchant\Common;
+
+/**
+ * @mixin \think\Model
+ */
+class ProductCopy extends Model
+{
+
+    protected $connection = 'shop';
+    protected $table = 'eb_store_product_copy';
+    protected $pk = 'store_product_copy_id';
+
+    
+       /**
+     * TODO 默认赠送复制次数
+     * @param $merId
+     * @author Qinii
+     * @day 2020-08-06
+     */
+    public function defaulCopyNum($merId)
+    {
+        if(Common::systemConfig('copy_product_status')){
+            $data = [
+                'type' => 'sys',
+                'num' => Common::systemConfig('copy_product_defaul'),
+                'message' => '赠送次数',
+            ];
+            $this->add($data,$merId);
+        }
+    }
+
+
+        /**
+     * TODO 添加记录并修改数据
+     * @param $data
+     * @param $merId
+     * @author Qinii
+     * @day 2020-08-06
+     */
+    public function add($data,$merId)
+    {
+        $make = app()->make(Merchant::class);
+        $getOne = $make->get($merId);
+
+        switch ($data['type']) {
+            case 'mer_dump':
+                //nobreak;
+            case 'pay_dump':
+                $field = 'export_dump_num';
+                break;
+            case 'sys':
+                //nobreak;
+                //nobreak;
+            case 'pay_copy':
+                //nobreak;
+            case 'copy':
+                //nobreak;
+                $field = 'copy_product_num';
+                break;
+            default:
+                $field = 'copy_product_num';
+                break;
+        }
+
+
+        $number = $getOne[$field] + $data['num'];
+        $arr = [
+            'type'  => $data['type'],
+            'num'   => $data['num'],
+            'info'   => $data['info']??'' ,
+            'mer_id'=> $merId,
+            'message' => $data['message'] ?? '',
+            'number' => ($number < 0) ? 0 : $number,
+        ];
+        Db::transaction(function()use($arr,$make,$field){
+            self::create($arr);
+            if ($arr['num'] < 0) {
+                $make->sumFieldNum($arr['mer_id'],$arr['num'],$field);
+            } else {
+                $make->addFieldNum($arr['mer_id'],$arr['num'],$field);
+            }
+        });
+    }
+
+    public function search(array $where)
+    {
+        return $this->getModel()::getDB()
+            ->when(isset($where['mer_id']) && $where['mer_id'] !== '',function($query)use($where){
+                $query->where('mer_id',$where['mer_id']);
+            })
+            ->when(isset($where['type']) && $where['type'] !== '',function($query)use($where){
+                if($where['type'] == 'copy'){
+                    $query->where('type','in',['taobao','jd','copy']);
+                } else {
+                    $query->where('type',$where['type']);
+                }
+            })
+            ->order('create_time DESC');
+    }
+
+}
diff --git a/app/common/model/merchant/system/financial/Financial.php b/app/common/model/merchant/system/financial/Financial.php
index c6ca38a..b723f17 100644
--- a/app/common/model/merchant/system/financial/Financial.php
+++ b/app/common/model/merchant/system/financial/Financial.php
@@ -11,9 +11,17 @@ declare (strict_types = 1);
 namespace app\common\model\merchant\system\financial;
 
 use think\Model;
+use think\facade\Db;
+use think\facade\Queue;
+use think\exception\ValidateException;
+
 use app\common\model\merchant\system\admin\Admin;
 use app\common\model\merchant\system\merchant\Merchant;
 use app\common\model\merchant\system\merchant\MerchantAdmin;
+use app\common\model\merchant\system\serve\ServeOrder;
+use app\common\service\merchant\WechatService;
+use app\common\jobs\merchant\ChangeMerchantStatusJob;
+use think\db\exception\DbException;
 
 /**
  * 商户财务申请提现 model
@@ -25,7 +33,7 @@ class Financial extends Model
     protected $pk = 'financial_id';
 
     
-    // --------------
+    // -------------- depend function -----------------
 
     public function getFinancialAccountAttr($value)
     {
@@ -86,6 +94,39 @@ class Financial extends Model
         return $this->hasOne(Merchant::class,'mer_id','mer_id');
     }
 
+    /** ---------------------- depend func end */
+
+    /**
+     * 获取指定退还信息
+     */
+    public function get(int $id)
+    {
+        return self::where($this->getPk(), $id)->find();
+    }
+
+    public function modify($id, $data)
+    {
+        return self::where($this->getPk(), $id)->update($data);
+    }
+
+    /**
+     * 获取指定退款订单相关信息
+     * 此处依赖: getFinancialAccountAttr, merchant.marginOrder
+     * 
+     */
+    public function getDetail($id)
+    {
+        $data = $this->get($id);
+        if (!$data['merchant']->marginOrder) 
+            throw new ValidateException('未查询到缴费记录');
+        if ($data['status'] !== 0) 
+            throw new ValidateException('请勿重复审核');
+        if (!$data['merchant']->merchantType)
+            throw new ValidateException('末查询到店铺类型');
+
+        return $data;
+    }
+
     /**
     * TODO 商户列表
     * @param array $where
@@ -114,6 +155,56 @@ class Financial extends Model
         return compact('count','list');
     }
 
+    /**
+     * 退还保证金审核
+     */
+    public function switchStatus($id, $type, $data)
+    {
+        $where = [
+            'financial_id' => $id,
+            'is_del' => 0,
+            'status' => 0,
+            'type'   => $type
+        ];
+
+        $res = self::where($where)->find();
+        if(!$res) throw new ValidateException('数据不存在');
+        
+        switch ($type) {
+            case 0:
+                //
+                if ($data['status'] == -1) 
+                    $this->cancel(null,$id,$data);
+                break;
+            case 1:
+                //保证金
+                if ($data['status'] == 1) {
+                    // 同意退回
+                    $this->agree($res);
+                    $data['financial_status'] = 1;
+                    $tempId = 'REFUND_MARGIN_SUCCESS';
+                }else if ($data['status'] == -1) {
+                    // 拒绝退回
+//                    $res->merchant->margin = $res['extract_money'];
+                    $res->merchant->is_margin = -10;
+                    $res->merchant->save();
+                    $tempId = 'REFUND_MARGIN_FAIL';
+                }
+                // 发送模板消息
+                // Queue::push(SendSmsJob::class, [
+                //     'tempId' => $tempId,
+                //     'id' => [
+                //         'name' => $res->merchant->mer_name,
+                //         'time' => $res->create_time,
+                //         'phone' => $res->merchant->mer_phone
+                //     ]]);
+                break;
+        }
+
+       return self::where($this->getPk(), $id)->update($data);
+    }
+
+
     /**
      * 组合sql条件
      * @param array $where
@@ -135,42 +226,132 @@ class Financial extends Model
         });
 
         $query->when(isset($where['status']) && $where['status'] !=='',
-            function($query) use($where){
-                $query->where('Financial.status',$where['status']);
-            })
-            ->when(isset($where['financial_type']) && $where['financial_type'] !=='',function($query) use($where){
-                $query->where('Financial.financial_type',$where['financial_type']);
-            })
-            ->when(isset($where['mer_id']) && $where['mer_id'] !=='',function($query) use($where){
-                $query->where('Financial.mer_id',$where['mer_id']);
-            })
-            ->when(isset($where['financial_status']) && $where['financial_status'] !=='',function($query) use($where){
-                $query->where('Financial.financial_status',$where['financial_status']);
-            })
-            ->when(isset($where['keyword']) && $where['keyword'] !=='',function($query) use($where){
-                $query->join('SystemAdmin A','Financial.admin_id = A.admin_id')
-                    ->field('A.real_name,A.admin_id,A.account')
-                    ->whereLike('A.real_name|A.account',"%{$where['keyword']}%");
-            })
-            ->when(isset($where['keywords_']) && $where['keywords_'] !=='',function($query) use($where){
-                $query->join('MerchantAdmin M','Financial.mer_admin_id = M.merchant_admin_id')
-                    ->field('M.real_name,M.account,M.merchant_admin_id')
-                    ->whereLike('M.real_name|M.account',"%{$where['keywords_']}%");
-            })
-            ->when(isset($where['financial_id']) && $where['financial_id'] !=='',function($query) use($where){
-                $query->where('Financial.financial_id',$where['financial_id']);
-            })
-            ->when(isset($where['date']) && $where['date'] !=='',function($query) use($where){
-                getModelTime($query,$where['date'],'Financial.create_time');
-            })
-            ->when(isset($where['is_del']) && $where['is_del'] !=='',function($query) use($where){
-                $query->where('Financial.is_del',$where['is_del']);
-            })
-            ->when(isset($where['type']) && $where['type'] !=='',function($query) use($where){
-                $query->where('Financial.type',$where['type']);
-            });;
+        function($query) use($where){
+            $query->where('Financial.status',$where['status']);
+        })
+        ->when(isset($where['financial_type']) && $where['financial_type'] !=='',function($query) use($where){
+            $query->where('Financial.financial_type',$where['financial_type']);
+        })
+        ->when(isset($where['mer_id']) && $where['mer_id'] !=='',function($query) use($where){
+            $query->where('Financial.mer_id',$where['mer_id']);
+        })
+        ->when(isset($where['financial_status']) && $where['financial_status'] !=='',function($query) use($where){
+            $query->where('Financial.financial_status',$where['financial_status']);
+        })
+        ->when(isset($where['keyword']) && $where['keyword'] !=='',function($query) use($where){
+            $query->join('SystemAdmin A','Financial.admin_id = A.admin_id')
+                ->field('A.real_name,A.admin_id,A.account')
+                ->whereLike('A.real_name|A.account',"%{$where['keyword']}%");
+        })
+        ->when(isset($where['keywords_']) && $where['keywords_'] !=='',function($query) use($where){
+            $query->join('MerchantAdmin M','Financial.mer_admin_id = M.merchant_admin_id')
+                ->field('M.real_name,M.account,M.merchant_admin_id')
+                ->whereLike('M.real_name|M.account',"%{$where['keywords_']}%");
+        })
+        ->when(isset($where['financial_id']) && $where['financial_id'] !=='',function($query) use($where){
+            $query->where('Financial.financial_id',$where['financial_id']);
+        })
+        ->when(isset($where['date']) && $where['date'] !=='',function($query) use($where){
+            getModelTime($query,$where['date'],'Financial.create_time');
+        })
+        ->when(isset($where['is_del']) && $where['is_del'] !=='',function($query) use($where){
+            $query->where('Financial.is_del',$where['is_del']);
+        })
+        ->when(isset($where['type']) && $where['type'] !=='',function($query) use($where){
+            $query->where('Financial.type',$where['type']);
+        })
+        ->when(isset($where['start_date'])&&isset($where['end_date'])&&$where['start_date']!==''&&$where['end_date']!=='',
+        function($query)use($where){
+            $query->where('Financial.create_time','between',[$where['start_date'], $where['end_date']]);
+        });
         $query->order('Financial.create_time DESC');
 
         return $query;
     }
+
+
+    /**
+     * TODO 取消/拒绝 变更状态返还余额
+     * @param $merId
+     * @param $id
+     * @param $data
+     */
+    protected function cancel(?int $merId,int $id,array $data)
+    {
+        $where = [
+            'financial_id' => $id,
+            'is_del' => 0,
+            'status' => 0
+        ];
+        if($merId) $where['mer_id'] = $merId;
+        $res = self::where($where)->find();
+        if(!$res) throw new ValidateException('数据不存在');
+        if($res['financial_status'] == 1) throw new ValidateException('当前状态无法完成此操作');
+        $merId = $merId?? $res['mer_id'];
+        Db::transaction(function()use($merId,$res,$id,$data) {
+            self::where($this->getPk(), $id)->update($data);
+            app()->make(Merchant::class)->addMoney($merId, (float)$res['extract_money']);
+        });
+    }
+
+    /**
+     * TODO 同意退保证金
+     * @param $res
+     * @author Qinii
+     * @day 1/27/22
+     */
+    protected function agree($res)
+    {
+        //验证
+        $comp = bccomp($res['financial_account']->pay_price, $res['extract_money'], 2);
+        if ($comp  == -1)
+            throw new ValidateException('申请退款金额:'.$res['extract_money'].',大于支付保证金:'.$res['financial_account']->pay_price);
+
+    //    if (bccomp($res['merchant']['margin'], $res['extract_money'],2) == -1)
+    //        throw new ValidateException('申请退款金额:'.$res['extract_money'].',大于剩余保证金:'.$res['merchant']['margin']);
+
+        Db::startTrans();
+        try{
+            //退款
+            $data = [
+                'refund_id' => $res['financial_account']->order_sn,
+                'pay_price' => $res['financial_account']->pay_price,
+                'refund_price' => $res['extract_money']
+            ];
+            //退回微信
+            // WechatService::create()->payOrderRefund($res['financial_account']->order_sn, $data);
+
+            //改订单
+            $order = app()->make(ServeOrder::class)->get($res['financial_account']->order_id);
+            $order->status = 20;
+            $b1 = $order->save();
+
+            //关店 同时更新金额
+            $res->merchant->is_margin = $res['merchant']['merchantType']['is_margin'];
+            $res->merchant->margin    = $res['merchant']['merchantType']['margin'];
+            if ($res['merchant']['merchantType']['is_margin'] == 1) {
+                $res->merchant->mer_state = 0;//商户关闭
+
+                //改变商户商品状态
+                // Queue::push(ChangeMerchantStatusJob::class, $res['mer_id']);
+                $b3 = app()->make(ChangeMerchantStatusJob::class)->fire([],$res['mer_id']);
+
+            }
+            $b2 = $res->merchant->save();
+            if (empty($b1)) {
+                Db::rollback();
+                throw new DbException('修改订单表失败');
+            }else if(empty($b2)) {
+                Db::rollback();
+                throw new DbException('更新商户表失败');
+            }else if(empty($b3)) {
+                Db::rollback();
+                throw new DbException('修改商户商品表失败');
+            }
+            Db::commit();
+        }catch(DbException $e){
+            Db::rollback();
+            throw new ValidateException($e->getMessage());
+        }
+    }
 }
diff --git a/app/common/model/merchant/system/merchant/Merchant.php b/app/common/model/merchant/system/merchant/Merchant.php
index 1004fc7..f8948f3 100644
--- a/app/common/model/merchant/system/merchant/Merchant.php
+++ b/app/common/model/merchant/system/merchant/Merchant.php
@@ -71,6 +71,24 @@ class Merchant extends Model
             ->order('is_good DESC,sort DESC');
     }
 
+    /**
+     * TODO 增加商户余额
+     * @param int $merId
+     * @param float $num
+     * @author Qinii
+     * @day 3/19/21
+     */
+    public function addMoney(int $merId, float $num)
+    {
+        $field = 'mer_money';
+        $merchant = self::where('mer_id', $merId)->find();
+        if ($merchant) {
+            $mer_money = bcadd($merchant[$field], (string)$num, 2);
+            $merchant[$field] = $mer_money;
+            $merchant->save();
+        }
+    }
+
     /**
      * TODO 商户列表下的推荐
      * @return \think\Collection
diff --git a/app/common/model/merchant/system/serve/ServeOrder.php b/app/common/model/merchant/system/serve/ServeOrder.php
index 998de56..e52dcfb 100644
--- a/app/common/model/merchant/system/serve/ServeOrder.php
+++ b/app/common/model/merchant/system/serve/ServeOrder.php
@@ -36,6 +36,11 @@ class ServeOrder extends Model
     {
         return $this->hasOne(User::class,'mer_id','ud');
     }
+
+    public function get(int $id)
+    {
+        return self::where($this->getPk(), $id)->find();
+    }
     
     /**
      * 获取所有商户的保证金数据
@@ -109,7 +114,7 @@ class ServeOrder extends Model
         )
         ->when(isset($where['start_date'])&&isset($where['end_date'])&&$where['start_date']!==''&&$where['end_date']!=='',
             function($query)use($where){
-                $query->where('create_time','between',[$where['start_date'], $where['end_date']]);
+                $query->where('ServeOrder.create_time','between',[$where['start_date'], $where['end_date']]);
             }
         )
         ->when(isset($where['mer_id']) && $where['mer_id'] !== '', 
diff --git a/app/common/service/easywechat/BaseClient.php b/app/common/service/easywechat/BaseClient.php
new file mode 100644
index 0000000..aeebf2c
--- /dev/null
+++ b/app/common/service/easywechat/BaseClient.php
@@ -0,0 +1,297 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+
+namespace app\common\services\easywechat;
+
+
+use EasyWeChat\Core\AbstractAPI;
+use EasyWeChat\Core\AccessToken;
+use EasyWeChat\Core\Exceptions\HttpException;
+use EasyWeChat\Core\Exceptions\InvalidConfigException;
+use EasyWeChat\Core\Http;
+use EasyWeChat\Encryption\EncryptionException;
+use think\exception\InvalidArgumentException;
+
+class BaseClient extends AbstractAPI
+{
+    protected $app;
+
+    const KEY_LENGTH_BYTE = 32;
+    const AUTH_TAG_LENGTH_BYTE = 16;
+
+    public function __construct(AccessToken $accessToken, $app)
+    {
+        parent::__construct($accessToken);
+        $this->app = $app;
+    }
+
+
+    /**
+     * @param $api
+     * @param $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    protected function httpPostJson($api, $params)
+    {
+        try {
+            return $this->parseJSON('json', [$api, $params]);
+        } catch (HttpException $e) {
+            $code = $e->getCode();
+            throw new HttpException("接口异常[$code]" . ($e->getMessage()), $code);
+        }
+    }
+
+    /**
+     * @param $api
+     * @param $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    protected function httpPost($api, $params)
+    {
+        try {
+            return $this->parseJSON('post', [$api, $params]);
+        } catch (HttpException $e) {
+            $code = $e->getCode();
+            throw new HttpException("接口异常[$code]" . ($e->getMessage()), $code);
+        }
+    }
+
+
+    /**
+     * @param $api
+     * @param $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    protected function httpGet($api, $params)
+    {
+        try {
+            return $this->parseJSON('get', [$api, $params]);
+        } catch (HttpException $e) {
+            $code = $e->getCode();
+            throw new HttpException("接口异常[$code]" . ($e->getMessage()), $code);
+        }
+    }
+
+    /**
+     * request.
+     *
+     * @param string $endpoint
+     * @param string $method
+     * @param array $options
+     * @param bool $returnResponse
+     */
+    public function request(string $endpoint, string $method = 'POST', array $options = [], $serial = true)
+    {
+        $sign_body = $options['sign_body'] ?? '';
+        $headers = [
+            'Content-Type' => 'application/json',
+            'User-Agent' => 'curl',
+            'Accept' => 'application/json',
+            'Authorization' => $this->getAuthorization($endpoint, $method, $sign_body),
+//            'Wechatpay-Serial' => $this->app['config']['payment']['serial_no']
+        ];
+        $options['headers'] = array_merge($headers, ($options['headers'] ?? []));
+
+        if ($serial) $options['headers']['Wechatpay-Serial'] = $this->app->certficates->get()['serial_no'];
+
+        Http::setDefaultOptions($options);
+        return $this->_doRequestCurl($method, 'https://api.mch.weixin.qq.com' . $endpoint, $options);
+    }
+
+
+    private function _doRequestCurl($method, $location, $options = [])
+    {
+        $curl = curl_init();
+        // POST数据设置
+        if (strtolower($method) === 'post') {
+            curl_setopt($curl, CURLOPT_POST, true);
+            curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data'] ?? $options['sign_body'] ?? '');
+        }
+        // CURL头信息设置
+        if (!empty($options['headers'])) {
+            $headers = [];
+            foreach ($options['headers'] as $k => $v) {
+                $headers[] = "$k: $v";
+            }
+            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
+        }
+        curl_setopt($curl, CURLOPT_URL, $location);
+        curl_setopt($curl, CURLOPT_HEADER, true);
+        curl_setopt($curl, CURLOPT_TIMEOUT, 60);
+        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
+        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
+        $content = curl_exec($curl);
+        $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
+        curl_close($curl);
+        return json_decode(substr($content, $headerSize), true);
+    }
+
+
+    /**
+     * get sensitive fields name.
+     *
+     * @return array
+     */
+    protected function getSensitiveFieldsName()
+    {
+        return [
+            'contact_name',
+            'contact_id_number',
+            'mobile_phone',
+            'contact_email',
+            'id_card_name',
+            'id_card_number',
+            'id_card_address',
+            'id_doc_name',
+            'id_doc_number',
+            'id_doc_address',
+            'name',
+            'id_number',
+            'account_name',
+            'account_number',
+            'contact_id_card_number',
+            'contact_email',
+            'openid',
+            'ubo_id_doc_name',
+            'ubo_id_doc_number',
+            'ubo_id_doc_address',
+            'bank_address_code',
+        ];
+    }
+
+    /**
+     * To id card, mobile phone number and other fields sensitive information encryption.
+     *
+     * @param string $string
+     *
+     * @return string
+     */
+    protected function encryptSensitiveInformation(string $string)
+    {
+        $certificates = $this->app->certficates->get()['certificates'];
+        if (null === $certificates) {
+            throw new InvalidConfigException('config certificate connot be empty.');
+        }
+        $encrypted = '';
+        if (openssl_public_encrypt($string, $encrypted, $certificates, OPENSSL_PKCS1_OAEP_PADDING)) {
+            //base64编码
+            $sign = base64_encode($encrypted);
+        } else {
+            throw new EncryptionException('Encryption of sensitive information failed');
+        }
+        return $sign;
+    }
+
+    /**
+     * processing parameters contain fields that require sensitive information encryption.
+     *
+     * @param array $params
+     *
+     * @return array
+     */
+    protected function processParams(array $params)
+    {
+
+        $sensitive_fields = $this->getSensitiveFieldsName();
+        foreach ($params as $k => $v) {
+            if (is_array($v)) {
+                $params[$k] = $this->processParams($v);
+            } else {
+                if (in_array($k, $sensitive_fields, true)) {
+                    $params[$k] = $this->encryptSensitiveInformation($v);
+                }
+            }
+        }
+
+        return $params;
+    }
+
+    /**
+     * @param string $url
+     * @param string $method
+     * @param string $body
+     * @return string
+     */
+    protected function getAuthorization(string $url, string $method, string $body)
+    {
+        $nonce_str = uniqid();
+        $timestamp = time();
+        $message = $method . "\n" .
+            $url . "\n" .
+            $timestamp . "\n" .
+            $nonce_str . "\n" .
+            $body . "\n";
+        openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
+        $sign = base64_encode($raw_sign);
+        $schema = 'WECHATPAY2-SHA256-RSA2048 ';
+        $token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
+            $this->app['config']['service_payment']['merchant_id'], $nonce_str, $timestamp, $this->app['config']['service_payment']['serial_no'], $sign);
+
+        return $schema . $token;
+    }
+
+    /**
+     * 获取商户私钥
+     * @return bool|resource
+     */
+    protected function getPrivateKey()
+    {
+        $key_path = $this->app['config']['service_payment']['key_path'];
+        if (!file_exists($key_path)) {
+            throw new \InvalidArgumentException(
+                "SSL certificate not found: {$key_path}"
+            );
+        }
+        return openssl_pkey_get_private(file_get_contents($key_path));
+    }
+
+    /**
+     * decrypt ciphertext.
+     *
+     * @param array $encryptCertificate
+     *
+     * @return string
+     */
+    public function decrypt(array $encryptCertificate)
+    {
+        $ciphertext = base64_decode($encryptCertificate['ciphertext'], true);
+        $associatedData = $encryptCertificate['associated_data'];
+        $nonceStr = $encryptCertificate['nonce'];
+        $aesKey = $this->app['config']['service_payment']['apiv3_key'];
+
+        try {
+            // ext-sodium (default installed on >= PHP 7.2)
+            if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()) {
+                return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
+            }
+            // ext-libsodium (need install libsodium-php 1.x via pecl)
+            if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()) {
+                return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
+            }
+            // openssl (PHP >= 7.1 support AEAD)
+            if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
+                $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
+                $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
+                return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
+            }
+        } catch (\Exception $exception) {
+            throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
+        } catch (\SodiumException $exception) {
+            throw new InvalidArgumentException($exception->getMessage(), $exception->getCode());
+        }
+        throw new InvalidArgumentException('AEAD_AES_256_GCM 需要 PHP 7.1 以上或者安装 libsodium-php');
+    }
+}
diff --git a/app/common/service/easywechat/broadcast/Client.php b/app/common/service/easywechat/broadcast/Client.php
new file mode 100644
index 0000000..ca35139
--- /dev/null
+++ b/app/common/service/easywechat/broadcast/Client.php
@@ -0,0 +1,462 @@
+<?php
+
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+
+namespace crmeb\services\easywechat\broadcast;
+
+
+use EasyWeChat\Core\Exceptions\HttpException;
+use EasyWeChat\MiniProgram\Core\AbstractMiniProgram;
+
+/**
+ * Class Client.
+ *
+ * @author Abbotton <uctoo@foxmail.com>
+ */
+class Client extends AbstractMiniProgram
+{
+    const MSG_CODE = [
+        '1' => '未创建直播间',
+        '1003' => '商品id不存在',
+        '47001' => '入参格式不符合规范',
+        '200002' => '入参错误',
+        '300001' => '禁止创建/更新商品 或 禁止编辑&更新房间',
+        '300002' => '名称长度不符合规则',
+        '300006' => '图片上传失败',
+        '300022' => '此房间号不存在',
+        '300023' => '房间状态 拦截',
+        '300024' => '商品不存在',
+        '300025' => '商品审核未通过',
+        '300026' => '房间商品数量已经满额',
+        '300027' => '导入商品失败',
+        '300028' => '房间名称违规',
+        '300029' => '主播昵称违规',
+        '300030' => '主播微信号不合法',
+        '300031' => '直播间封面图不合规',
+        '300032' => '直播间分享图违规',
+        '300033' => '添加商品超过直播间上限',
+        '300034' => '主播微信昵称长度不符合要求',
+        '300035' => '主播微信号不存在',
+        '300003' => '价格输入不合规',
+        '300004' => '商品名称存在违规违法内容',
+        '300005' => '商品图片存在违规违法内容',
+        '300007' => '线上小程序版本不存在该链接',
+        '300008' => '添加商品失败',
+        '300009' => '商品审核撤回失败',
+        '300010' => '商品审核状态不对',
+        '300011' => '操作非法',
+        '300012' => '没有提审额度',
+        '300013' => '提审失败',
+        '300014' => '审核中,无法删除',
+        '300017' => '商品未提审',
+        '300018' => '图片尺寸不符合要求',
+        '300021' => '商品添加成功,审核失败',
+        '300036' => '请先在微信直播小程序中实名认证',
+        '300038' => '请先在小程序后台配置直播客服',
+        '-1' => '系统错误',
+    ];
+
+    const API = 'https://api.weixin.qq.com/';
+
+    /**
+     * @param $api
+     * @param $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    protected function httpPostJson($api, $params)
+    {
+        try {
+            return $this->parseJSON('json', [self::API . $api, $params]);
+        } catch (HttpException $e) {
+            $code = $e->getCode();
+            throw new HttpException("接口异常[$code]" . (self::MSG_CODE[$code] ?? $e->getMessage()), $code);
+        }
+    }
+
+    /**
+     * @param $api
+     * @param $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    protected function httpPost($api, $params)
+    {
+        try {
+            return $this->parseJSON('post', [self::API . $api, $params]);
+        } catch (HttpException $e) {
+            $code = $e->getCode();
+            throw new HttpException("接口异常[$code]" . (self::MSG_CODE[$code] ?? $e->getMessage()), $code);
+        }
+    }
+
+
+    /**
+     * @param $api
+     * @param $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    protected function httpGet($api, $params)
+    {
+        try {
+            return $this->parseJSON('get', [self::API . $api, $params]);
+        } catch (HttpException $e) {
+            $code = $e->getCode();
+            throw new HttpException("接口异常[$code]" . (self::MSG_CODE[$code] ?? $e->getMessage()), $code);
+        }
+    }
+
+    /**
+     * Add broadcast goods.
+     *
+     * @param array $goodsInfo
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function create(array $goodsInfo)
+    {
+        $params = [
+            'goodsInfo' => $goodsInfo,
+        ];
+
+        return $this->httpPostJson('wxaapi/broadcast/goods/add', $params);
+    }
+
+    /**
+     * Reset audit.
+     *
+     * @param int $auditId
+     * @param int $goodsId
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function resetAudit(int $auditId, int $goodsId)
+    {
+        $params = [
+            'auditId' => $auditId,
+            'goodsId' => $goodsId,
+        ];
+
+        return $this->httpPostJson('wxaapi/broadcast/goods/resetaudit', $params);
+    }
+
+    /**
+     * Resubmit audit goods.
+     *
+     * @param int $goodsId
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function resubmitAudit(int $goodsId)
+    {
+        $params = [
+            'goodsId' => $goodsId,
+        ];
+
+        return $this->httpPostJson('wxaapi/broadcast/goods/audit', $params);
+    }
+
+    /**
+     * Delete broadcast goods.
+     *
+     * @param int $goodsId
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function delete(int $goodsId)
+    {
+        $params = [
+            'goodsId' => $goodsId,
+        ];
+        try{
+            return $this->httpPostJson('wxaapi/broadcast/goods/delete', $params);
+        } catch (HttpException $exception) {
+           if ($exception->getCode() ==  300015) return ;
+        }
+    }
+
+    /**
+     * Update goods info.
+     *
+     * @param array $goodsInfo
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function update(array $goodsInfo)
+    {
+        $params = [
+            'goodsInfo' => $goodsInfo,
+        ];
+
+        return $this->httpPostJson('wxaapi/broadcast/goods/update', $params);
+    }
+
+    /**
+     * Get goods information and review status.
+     *
+     * @param array $goodsIdArray
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function getGoodsWarehouse(array $goodsIdArray)
+    {
+        $params = [
+            'goods_ids' => $goodsIdArray,
+        ];
+
+        return $this->httpPostJson('wxa/business/getgoodswarehouse', $params);
+    }
+
+    /**
+     * Get goods list based on status
+     *
+     * @param array $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function getApproved(array $params)
+    {
+        return $this->httpGet('wxaapi/broadcast/goods/getapproved', $params);
+    }
+
+    /**
+     * Add goods to the designated live room.
+     *
+     * @param array $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function addGoods(array $params)
+    {
+        return $this->httpPost('wxaapi/broadcast/room/addgoods', $params);
+    }
+
+    /**
+     * Get Room List.
+     *
+     * @param int $start
+     * @param int $limit
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function getRooms(int $start = 0, int $limit = 10)
+    {
+        $params = [
+            'start' => $start,
+            'limit' => $limit,
+        ];
+
+        return $this->httpPostJson('wxa/business/getliveinfo', $params);
+    }
+
+    /**
+     * Get Playback List.
+     *
+     * @param int $roomId
+     * @param int $start
+     * @param int $limit
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function getPlaybacks(int $roomId, int $start = 0, int $limit = 10)
+    {
+        $params = [
+            'action' => 'get_replay',
+            'room_id' => $roomId,
+            'start' => $start,
+            'limit' => $limit,
+        ];
+
+        return $this->httpPostJson('wxa/business/getliveinfo', $params);
+    }
+
+    /**
+     * Create a live room.
+     *
+     * @param array $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function createLiveRoom(array $params)
+    {
+        return $this->httpPostJson('wxaapi/broadcast/room/create', $params);
+    }
+
+    /**
+     * TODO
+     * @param int $roomId
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/22/21
+     */
+    public function getPushUrl(int $roomId)
+    {
+        $params = [
+            'roomId' => $roomId,
+        ];
+        return $this->httpGet('wxaapi/broadcast/room/getpushurl', $params);
+    }
+
+    /**
+     * TODO 是否关闭客服 【0:开启,1:关闭】
+     * @param int $roomId
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/22/21
+     */
+    public function closeKf(int $roomId,int $status)
+    {
+        $params = [
+            'roomId' => $roomId,
+            'closeKf' => $status ? 1 : 0,
+        ];
+        return $this->httpPostJson('wxaapi/broadcast/room/updatekf', $params);
+    }
+
+    /**
+     * TODO 1-禁言,0-取消禁言
+     * @param int $roomId
+     * @param int $type
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/22/21
+     */
+    public function banComment(int $roomId, int $status)
+    {
+        $params = [
+            'roomId' => $roomId,
+            'banComment' => $status ? 1 : 0,
+        ];
+        return $this->httpPostJson('wxaapi/broadcast/room/updatecomment', $params);
+    }
+
+    /**
+     * TODO 添加助手
+     * @param array $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/25/21
+     */
+    public function addAssistant(array $params)
+    {
+        return $this->httpPostJson('wxaapi/broadcast/room/addassistant', $params);
+    }
+
+    /**
+     * TODO 删除助手
+     * @param array $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/25/21
+     */
+    public function removeAssistant(int $roomId, string $username)
+    {
+        $params = [
+            'roomId' => $roomId,
+            'username' => $username,
+        ];
+        return $this->httpPostJson('wxaapi/broadcast/room/removeassistant', $params);
+    }
+
+    /**
+     * TODO 修改小助手
+     * @param array $params
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/25/21
+     */
+    public function modifyAssistant(array $params)
+    {
+        return $this->httpPostJson('wxaapi/broadcast/room/modifyassistant', $params);
+    }
+
+    /**
+     * TODO 助手列表
+     * @param int $roomId
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/25/21
+     * wxa/business/get_wxa_followers?access_token=
+
+
+     */
+    public function getAssistantList(int $roomId)
+    {
+        $params = [
+            'roomId' => $roomId,
+        ];
+        return $this->httpGet('wxaapi/broadcast/room/getassistantlist', $params);
+    }
+
+    /**
+     * TODO 获取长期订阅用户
+     * @param int $roomId
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/25/21
+     */
+    public function getFollowers(string $page, int $limit = 2000)
+    {
+        $params['limit'] = $limit;
+        if ($page) $params['page_break'] = $page;
+        return $this->httpPostJson('wxa/business/get_wxa_followers', $params);
+    }
+
+    /**
+     * TODO 群发发送订阅
+     * @param int $roomId
+     * @param array $data
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/25/21
+     */
+    public function pushMessage(int $roomId, array $data)
+    {
+        $params = [
+            'room_id' => $roomId,
+            'user_openid' => $data,
+        ];
+
+        return $this->httpPostJson('wxa/business/push_message', $params);
+    }
+
+
+    /**
+     * TODO 更新官方收录
+     * @param int $roomId
+     * @param int $status
+     * @return \EasyWeChat\Support\Collection|null
+     * @author Qinii
+     * @day 10/30/21
+     */
+    public function updateFeedPublic(int $roomId, int $status)
+    {
+        $params = [
+            'roomId' => $roomId,
+            'isFeedsPublic' => $status ? 1 : 0,
+        ];
+
+        return $this->httpPostJson('wxaapi/broadcast/room/updatefeedpublic', $params);
+    }
+
+    public function goodsOnsale(int $roomId, int $goodsId, int $status)
+    {
+        $params = [
+            'roomId' => $roomId,
+            'goodsId' => $goodsId,
+            'onSale' => $status ? 1 : 0,
+        ];
+
+        return $this->httpPostJson('wxaapi/broadcast/goods/onsale', $params);
+    }
+}
diff --git a/app/common/service/easywechat/broadcast/ServiceProvider.php b/app/common/service/easywechat/broadcast/ServiceProvider.php
new file mode 100644
index 0000000..8a8a36d
--- /dev/null
+++ b/app/common/service/easywechat/broadcast/ServiceProvider.php
@@ -0,0 +1,29 @@
+<?php
+
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+
+namespace crmeb\services\easywechat\broadcast;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+class ServiceProvider implements ServiceProviderInterface
+{
+
+    public function register(Container $pimple)
+    {
+        $pimple['miniBroadcast'] = function ($pimple) {
+            return new Client($pimple['mini_program.access_token'], $pimple['config']['mini_program']);
+        };
+        \EasyWeChat\Core\Http::setDefaultOptions(['timeout' => 0]);
+    }
+}
diff --git a/app/common/service/easywechat/certficates/Client.php b/app/common/service/easywechat/certficates/Client.php
new file mode 100644
index 0000000..2d56de7
--- /dev/null
+++ b/app/common/service/easywechat/certficates/Client.php
@@ -0,0 +1,50 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+
+namespace crmeb\services\easywechat\certficates;
+
+
+use crmeb\exceptions\WechatException;
+use crmeb\services\easywechat\BaseClient;
+use EasyWeChat\Core\AbstractAPI;
+use think\exception\InvalidArgumentException;
+use think\facade\Cache;
+
+class Client extends BaseClient
+{
+    public function get()
+    {
+        $driver = Cache::store('file');
+        $cacheKey = '_wx_v3' . $this->app['config']['service_payment']['serial_no'];
+        if ($driver->has($cacheKey)) {
+            return $driver->get($cacheKey);
+        }
+        $certficates = $this->getCertficates();
+        $driver->set($cacheKey, $certficates, 3600 * 24 * 30);
+        return $certficates;
+    }
+
+    /**
+     * get certficates.
+     *
+     * @return array
+     */
+    public function getCertficates()
+    {
+        $response = $this->request('/v3/certificates', 'GET', [], false);
+        if (isset($response['code']))  throw new WechatException($response['message']);
+        $certificates = $response['data'][0];
+        $certificates['certificates'] = $this->decrypt($certificates['encrypt_certificate']);
+        unset($certificates['encrypt_certificate']);
+        return $certificates;
+    }
+}
diff --git a/app/common/service/easywechat/certficates/ServiceProvider.php b/app/common/service/easywechat/certficates/ServiceProvider.php
new file mode 100644
index 0000000..9d8bf58
--- /dev/null
+++ b/app/common/service/easywechat/certficates/ServiceProvider.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of the overtrue/wechat.
+ *
+ * (c) overtrue <i@overtrue.me>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace crmeb\services\easywechat\certficates;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Class ServiceProvider.
+ *
+ * @author ClouderSky <clouder.flow@gmail.com>
+ */
+class ServiceProvider implements ServiceProviderInterface
+{
+    /**
+     * {@inheritdoc}.
+     */
+    public function register(Container $pimple)
+    {
+        $pimple['certficates'] = function ($pimple) {
+            return new Client($pimple['access_token'], $pimple);
+        };
+    }
+}
diff --git a/app/common/service/easywechat/combinePay/Client.php b/app/common/service/easywechat/combinePay/Client.php
new file mode 100644
index 0000000..d42ac3f
--- /dev/null
+++ b/app/common/service/easywechat/combinePay/Client.php
@@ -0,0 +1,265 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+
+namespace crmeb\services\easywechat\combinePay;
+
+
+use app\common\model\store\order\StoreRefundOrder;
+use crmeb\services\easywechat\BaseClient;
+use think\exception\ValidateException;
+use think\facade\Route;
+use function EasyWeChat\Payment\generate_sign;
+
+class Client extends BaseClient
+{
+
+    public function handleNotify($callback)
+    {
+        $request = request();
+        $success = $request->post('event_type') === 'TRANSACTION.SUCCESS';
+        $data = $this->decrypt($request->post('resource', []));
+
+        $handleResult = call_user_func_array($callback, [json_decode($data, true), $success]);
+        if (is_bool($handleResult) && $handleResult) {
+            $response = [
+                'code' => 'SUCCESS',
+                'message' => 'OK',
+            ];
+        } else {
+            $response = [
+                'code' => 'FAIL',
+                'message' => $handleResult,
+            ];
+        }
+
+        return response($response, 200, [], 'json');
+    }
+
+    public function pay($type, array $order)
+    {
+        $params = [
+            'combine_out_trade_no' => $order['order_sn'],
+            'combine_mchid' => $this->app['config']['service_payment']['merchant_id'],
+            'combine_appid' => $this->app['config']['app_id'],
+            'scene_info' => [
+                'device_id' => 'shop system',
+                'payer_client_ip' => request()->ip(),
+            ],
+            'sub_orders' => [],
+            'notify_url' => rtrim(systemConfig('site_url'), '/') . Route::buildUrl($this->app['config']['service_payment']['type'] . 'CombinePayNotify', ['type' => $order['attach']])->build(),
+        ];
+
+        if ($type === 'h5') {
+            $params['scene_info']['h5_info'] = [
+                'type' => $order['h5_type'] ?? 'Wap'
+            ];
+        }
+
+        foreach ($order['sub_orders'] as $sub_order) {
+            $subOrder = [
+                'mchid' => $this->app['config']['service_payment']['merchant_id'],
+                'amount' => [
+                    'total_amount' => intval($sub_order['pay_price'] * 100),
+                    'currency' => 'CNY',
+                ],
+                'settle_info' => [
+                    'profit_sharing' => true
+                ],
+                'out_trade_no' => $sub_order['order_sn'],
+                'sub_mchid' => $sub_order['sub_mchid']
+            ];
+            $subOrder['attach'] = $sub_order['attach'] ?? $order['attach'] ?? '';
+            $subOrder['description'] = $sub_order['body'] ?? $order['body'] ?? '';
+            $params['sub_orders'][] = $subOrder;
+        }
+
+        if (isset($order['openid'])) {
+            $params['combine_payer_info'] = [
+                'openid' => $order['openid'],
+            ];
+        }
+        $content = json_encode($params, JSON_UNESCAPED_UNICODE);
+
+        $res = $this->request('/v3/combine-transactions/' . $type, 'POST', ['sign_body' => $content]);
+        if (isset($res['code'])) {
+            throw new ValidateException('微信接口报错:' . $res['message']);
+        }
+        return $res;
+    }
+
+    public function payApp(array $options)
+    {
+        $res = $this->pay('app', $options);
+        return $this->configForAppPayment($res['prepay_id']);
+    }
+
+    /**
+     * @param string $type 场景类型,枚举值: iOS:IOS移动应用; Android:安卓移动应用; Wap:WAP网站应用
+     */
+    public function payH5(array $options, $type = 'Wap')
+    {
+        $options['h5_type'] = $type;
+        return $this->pay('h5', $options);
+    }
+
+    public function payJs($openId, array $options)
+    {
+        $options['openid'] = $openId;
+        $res = $this->pay('jsapi', $options);
+        return $this->configForJSSDKPayment($res['prepay_id']);
+    }
+
+    public function payNative(array $options)
+    {
+        return $this->pay('native', $options);
+    }
+
+    public function profitsharingOrder(array $options, bool $finish = false)
+    {
+        $params = [
+            'appid' => $this->app['config']['app_id'],
+            'sub_mchid' => $options['sub_mchid'],
+            'transaction_id' => $options['transaction_id'],
+            'out_order_no' => $options['out_order_no'],
+            'receivers' => [],
+            'finish' => $finish
+        ];
+
+        foreach ($options['receivers'] as $receiver) {
+            $data = [
+                'amount' => intval($receiver['amount'] * 100),
+                'description' => $receiver['body'] ?? $options['body'] ?? '',
+            ];
+            $data['receiver_account'] = $receiver['receiver_account'];
+            if (isset($receiver['receiver_name'])) {
+                $data['receiver_name'] = $receiver['receiver_name'];
+                $data['type'] = 'PERSONAL_OPENID';
+            } else {
+                $data['type'] = 'MERCHANT_ID';
+            }
+            $params['receivers'][] = $data;
+        }
+        $content = json_encode($params);
+        $res = $this->request('/v3/ecommerce/profitsharing/orders', 'POST', ['sign_body' => $content]);
+        if (isset($res['code'])) {
+            throw new ValidateException('微信接口报错:' . $res['message']);
+        }
+        return $res;
+    }
+
+    public function profitsharingFinishOrder(array $params)
+    {
+        $content = json_encode($params);
+        $res = $this->request('/v3/ecommerce/profitsharing/finish-order', 'POST', ['sign_body' => $content]);
+        if (isset($res['code'])) {
+            throw new ValidateException('微信接口报错:' . $res['message']);
+        }
+        return $res;
+    }
+
+    public function payOrderRefund(string $order_sn, array $options)
+    {
+        $params = [
+            'sub_mchid' => $options['sub_mchid'],
+            'sp_appid' => $this->app['config']['app_id'],
+            'out_trade_no' => $options['order_sn'],
+            'out_refund_no' => $options['refund_order_sn'],
+            'amount' => [
+                'refund' => intval($options['refund_price'] * 100),
+                'total' => intval($options['pay_price'] * 100),
+                'currency' => 'CNY'
+            ]
+        ];
+        if (isset($options['reason'])) {
+            $params['reason'] = $options['reason'];
+        }
+        if (isset($options['refund_account'])) {
+            $params['refund_account'] = $options['refund_account'];
+        }
+        $content = json_encode($params);
+        $res = $this->request('/v3/ecommerce/refunds/apply', 'POST', ['sign_body' => $content], true);
+        if (isset($res['code'])) {
+            throw new ValidateException('微信接口报错:' . $res['message']);
+        }
+        return $res;
+    }
+
+    public function returnAdvance($refund_id, $sub_mchid)
+    {
+        $res = $this->request('/v3/ecommerce/refunds/' . $refund_id . '/return-advance', 'POST', ['sign_body' => json_encode(compact('sub_mchid'))], true);
+        if (isset($res['code'])) {
+            throw new ValidateException('微信接口报错:' . $res['message']);
+        }
+        return $res;
+    }
+
+    public function configForPayment($prepayId, $json = true)
+    {
+        $params = [
+            'appId' => $this->app['config']['app_id'],
+            'timeStamp' => strval(time()),
+            'nonceStr' => uniqid(),
+            'package' => "prepay_id=$prepayId",
+            'signType' => 'RSA',
+        ];
+        $message = $params['appId'] . "\n" .
+            $params['timeStamp'] . "\n" .
+            $params['nonceStr'] . "\n" .
+            $params['package'] . "\n";
+        openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
+        $sign = base64_encode($raw_sign);
+
+        $params['paySign'] = $sign;
+
+        return $json ? json_encode($params) : $params;
+    }
+
+    /**
+     * Generate app payment parameters.
+     *
+     * @param string $prepayId
+     *
+     * @return array
+     */
+    public function configForAppPayment($prepayId)
+    {
+        $params = [
+            'appid' => $this->app['config']['app_id'],
+            'partnerid' => $this->app['config']['service_payment']['merchant_id'],
+            'prepayid' => $prepayId,
+            'noncestr' => uniqid(),
+            'timestamp' => time(),
+            'package' => 'Sign=WXPay',
+        ];
+        $message = $params['appid'] . "\n" .
+            $params['timestamp'] . "\n" .
+            $params['noncestr'] . "\n" .
+            $params['prepayid'] . "\n";
+        openssl_sign($message, $raw_sign, $this->getPrivateKey(), 'sha256WithRSAEncryption');
+        $sign = base64_encode($raw_sign);
+
+        $params['sign'] = $sign;
+
+        return $params;
+    }
+
+    public function configForJSSDKPayment($prepayId)
+    {
+        $config = $this->configForPayment($prepayId, false);
+
+        $config['timestamp'] = $config['timeStamp'];
+        unset($config['timeStamp']);
+
+        return $config;
+    }
+
+}
diff --git a/app/common/service/easywechat/combinePay/ServiceProvider.php b/app/common/service/easywechat/combinePay/ServiceProvider.php
new file mode 100644
index 0000000..5fd34e1
--- /dev/null
+++ b/app/common/service/easywechat/combinePay/ServiceProvider.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of the overtrue/wechat.
+ *
+ * (c) overtrue <i@overtrue.me>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace crmeb\services\easywechat\combinePay;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Class ServiceProvider.
+ *
+ * @author ClouderSky <clouder.flow@gmail.com>
+ */
+class ServiceProvider implements ServiceProviderInterface
+{
+    /**
+     * {@inheritdoc}.
+     */
+    public function register(Container $pimple)
+    {
+        $pimple['combinePay'] = function ($pimple) {
+            return new Client($pimple['access_token'], $pimple);
+        };
+    }
+}
diff --git a/app/common/service/easywechat/merchant/Client.php b/app/common/service/easywechat/merchant/Client.php
new file mode 100644
index 0000000..2a557ce
--- /dev/null
+++ b/app/common/service/easywechat/merchant/Client.php
@@ -0,0 +1,217 @@
+<?php
+
+/*
+ * This file is part of the overtrue/wechat.
+ *
+ * (c) overtrue <i@overtrue.me>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace crmeb\services\easywechat\merchant;
+
+use crmeb\services\easywechat\BaseClient;
+use EasyWeChat\Core\AbstractAPI;
+use EasyWeChat\Core\AccessToken;
+use EasyWeChat\Core\Exceptions\HttpException;
+use EasyWeChat\Core\Http;
+use EasyWeChat\Payment\API;
+use EasyWeChat\Payment\Merchant;
+use GuzzleHttp\HandlerStack;
+use think\Exception;
+use EasyWeChat\Support\XML;
+use EasyWeChat\Support\Collection;
+use Psr\Http\Message\ResponseInterface;
+use think\exception\ValidateException;
+
+/**
+ * Class Client.
+ *
+ * @author ClouderSky <clouder.flow@gmail.com>
+ */
+class Client extends BaseClient
+{
+
+    /**
+     * TODO 二级商户进件成为微信支付商户
+     * @param $params
+     * @return mixed
+     * @author Qinii
+     * @day 6/24/21
+     */
+    public function submitApplication($params)
+    {
+        $params = $this->processParams($params);
+        $res = $this->request('/v3/ecommerce/applyments/', 'POST', ['sign_body' => json_encode($params, JSON_UNESCAPED_UNICODE)], true);
+        if(isset($res['code'])) throw new ValidateException('[微信接口返回]:' . $res['message']);
+
+        return $res;
+    }
+
+    /**
+     * TODO 申请单ID查询申请状态
+     * @param $id
+     * @return mixed
+     * @author Qinii
+     * @day 6/24/21
+     */
+    public function getApplicationById($id)
+    {
+        $url = '/v3/ecommerce/applyments/'.$id;
+        $res = $this->request($url, 'GET');
+
+        if(isset($res['code'])) throw new ValidateException('[微信接口返回]:' . $res['message']);
+
+        return $res;
+    }
+
+    /**
+     * TODO 业务申请编号查询申请状
+     * @param $no
+     * @return mixed
+     * @author Qinii
+     * @day 6/24/21
+     */
+    public function getApplicationByNo($no)
+    {
+        $url = '/v3/ecommerce/applyments/out-request-no/'.$no;
+        $res = $this->request($url, 'GET');
+
+        if(isset($res['code'])) throw new ValidateException('[微信接口返回]:' . $res['message']);
+
+        return $res;
+    }
+
+    /**
+     * TODO 修改结算账号
+     * @param $mchid
+     * @param $params
+     * @return mixed
+     * @author Qinii
+     * @day 6/24/21
+     */
+    public function updateSubMerchat($mchid,$params)
+    {
+        $url = "/v3/apply4sub/sub_merchants/{$mchid}/modify-settlement";
+        $res = $this->request($url, 'POST',['sign_body' => json_encode($params, JSON_UNESCAPED_UNICODE)], true);
+        if(isset($res['code'])) throw new ValidateException('[微信接口返回]:' . $res['message']);
+
+        return $res;
+    }
+
+    /**
+     * TODO 查询结算账户
+     * @param $mchid
+     * @return mixed
+     * @author Qinii
+     * @day 6/24/21
+     */
+    public function getSubMerchant($mchid)
+    {
+        $url = "/v3/apply4sub/sub_merchants/{$mchid}/settlement";
+        $res = $this->request($url, 'GET');
+        if(isset($res['code'])) throw new ValidateException('[微信接口返回]:' . $res['message']);
+
+        return $res;
+    }
+
+    /**
+     * TODO 添加分账接收方
+     * @param array $params
+     * @return mixed
+     * @author Qinii
+     * @day 6/24/21
+     */
+    public function profitsharingAdd(array $params)
+    {
+        $url = '/v3/ecommerce/profitsharing/receivers/add';
+
+        $app_id = !empty($this->app->config->app_id) ? $this->app->config->app_id : $this->app->config->routine_appId;
+
+        $params['appid'] = $app_id;
+
+        $options['sign_body'] = json_encode($params,JSON_UNESCAPED_UNICODE);
+
+        $res = $this->request($url, 'POST',$options,true);
+
+        if(isset($res['code'])) throw new ValidateException('[微信接口返回]:' . $res['message']);
+
+        return $res;
+    }
+
+    /**
+     * TODO 删除分账接收方
+     * @param array $params
+     * @return mixed
+     * @author Qinii
+     * @day 6/24/21
+     */
+    public function profitsharingDel(array $params)
+    {
+        $url = '/v3/ecommerce/profitsharing/receivers/delete';
+
+        $app_id = !empty($this->app->config->app_id) ? $this->app->config->app_id : $this->app->config->routine_appId;
+
+        $params['appid'] = $app_id;
+
+        $options['sign_body'] = json_encode($params,JSON_UNESCAPED_UNICODE);
+
+        $res = $this->request($url, 'POST',$options,true);
+
+        if(isset($res['code'])) throw new ValidateException('[微信接口返回]:' . $res['message']);
+
+        return $res;
+    }
+
+    /**
+     * TODO 上传图片
+     * @param $filepath
+     * @param $filename
+     * @author Qinii
+     * @day 6/21/21
+     */
+    public function upload($filepath,$filename)
+    {
+
+        $boundary = uniqid();
+        try{
+           // $file = file_get_contents($filepath);
+            $file = fread(fopen($filepath,'r'),filesize($filepath));
+        }catch (\Exception $exception){
+            throw new ValidateException($exception->getMessage());
+        }
+
+
+        $options['headers'] = ['Content-Type' => 'multipart/form-data;boundary='.$boundary];
+
+        $options['sign_body'] = json_encode(['filename' => $filename,'sha256' => hash_file("sha256",$filepath)]);
+
+        $boundaryStr = "--{$boundary}\r\n";
+
+        $body = $boundaryStr;
+        $body .= 'Content-Disposition: form-data; name="meta"'."\r\n";
+        $body .= 'Content-Type: application/json'."\r\n";
+        $body .= "\r\n";
+        $body .= $options['sign_body']."\r\n";
+        $body .= $boundaryStr;
+        $body .= 'Content-Disposition: form-data; name="file"; filename="'.$filename.'"'."\r\n";
+        $body .= 'Content-Type: image/jpeg'.';'."\r\n";
+        $body .= "\r\n";
+        $body .= $file."\r\n";
+        $body .= "--{$boundary}--";
+
+        $options['data'] = (($body));
+
+        try {
+            $res = $this->request('/v3/merchant/media/upload', 'POST', $options, true);
+        }catch(\Exception $exception){
+            throw new ValidateException($exception->getMessage());
+        }
+
+        if(isset($res['code'])) throw new ValidateException('[微信接口返回]:' . $res['message']);
+
+        return $res;
+
+    }
+}
diff --git a/app/common/service/easywechat/merchant/ServiceProvider.php b/app/common/service/easywechat/merchant/ServiceProvider.php
new file mode 100644
index 0000000..fd32fcd
--- /dev/null
+++ b/app/common/service/easywechat/merchant/ServiceProvider.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of the overtrue/wechat.
+ *
+ * (c) overtrue <i@overtrue.me>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace crmeb\services\easywechat\merchant;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Class ServiceProvider.
+ *
+ * @author ClouderSky <clouder.flow@gmail.com>
+ */
+class ServiceProvider implements ServiceProviderInterface
+{
+    /**
+     * {@inheritdoc}.
+     */
+    public function register(Container $pimple)
+    {
+        $pimple['sub_merchant'] = function ($pimple) {
+            return new Client($pimple['access_token'], $pimple);
+        };
+    }
+}
diff --git a/app/common/service/easywechat/storePay/Client.php b/app/common/service/easywechat/storePay/Client.php
new file mode 100644
index 0000000..be99b02
--- /dev/null
+++ b/app/common/service/easywechat/storePay/Client.php
@@ -0,0 +1,59 @@
+<?php
+
+/*
+ * This file is part of the overtrue/wechat.
+ *
+ * (c) overtrue <i@overtrue.me>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace crmeb\services\easywechat\storePay;
+
+use crmeb\services\easywechat\BaseClient;
+use EasyWeChat\Core\AbstractAPI;
+use EasyWeChat\Core\AccessToken;
+use EasyWeChat\Core\Exceptions\HttpException;
+use EasyWeChat\Core\Http;
+use EasyWeChat\Payment\API;
+use EasyWeChat\Payment\Merchant;
+use GuzzleHttp\HandlerStack;
+use think\Exception;
+use EasyWeChat\Support\XML;
+use EasyWeChat\Support\Collection;
+use Psr\Http\Message\ResponseInterface;
+use think\exception\ValidateException;
+
+/**
+ * Class Client.
+ *
+ * @author ClouderSky <clouder.flow@gmail.com>
+ */
+class Client extends BaseClient
+{
+    const API = 'https://api.mch.weixin.qq.com';
+
+    public function transferBatches(array $data)
+    {
+        $api = '/v3/transfer/batches';
+        $params = [
+            "appid" => $this->app['config']['app_id'],
+            "out_batch_no" => "plfk2020042013",
+            "batch_name" => "分销明细",
+            "batch_remark" => "分销明细",
+            "total_amount" => 100,
+            "total_num" => 1,
+            "transfer_detail_list" => [
+                [
+                    "openid" => "oOdvCvjvCG0FnCwcMdDD_xIODRO0",
+                    "out_detail_no" => "x23zy545Bd5436",
+                    "transfer_amount" => 100,
+                    "transfer_remark" => "分销明细",
+              ]
+            ],
+        ];
+        $res = $this->request($api, 'POST', ['sign_body' => json_encode($params, JSON_UNESCAPED_UNICODE)], true);
+    }
+
+}
diff --git a/app/common/service/easywechat/storePay/ServiceProvider.php b/app/common/service/easywechat/storePay/ServiceProvider.php
new file mode 100644
index 0000000..938d8fd
--- /dev/null
+++ b/app/common/service/easywechat/storePay/ServiceProvider.php
@@ -0,0 +1,33 @@
+<?php
+
+/*
+ * This file is part of the overtrue/wechat.
+ *
+ * (c) overtrue <i@overtrue.me>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace crmeb\services\easywechat\storePay;
+
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * Class ServiceProvider.
+ *
+ * @author ClouderSky <clouder.flow@gmail.com>
+ */
+class ServiceProvider implements ServiceProviderInterface
+{
+    /**
+     * {@inheritdoc}.
+     */
+    public function register(Container $pimple)
+    {
+        $pimple['storePay'] = function ($pimple) {
+            return new Client($pimple['access_token'], $pimple);
+        };
+    }
+}
diff --git a/app/common/service/easywechat/subscribe/ProgramProvider.php b/app/common/service/easywechat/subscribe/ProgramProvider.php
new file mode 100644
index 0000000..57d0273
--- /dev/null
+++ b/app/common/service/easywechat/subscribe/ProgramProvider.php
@@ -0,0 +1,40 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+
+namespace crmeb\services\easywechat\subscribe;
+
+use EasyWeChat\MiniProgram\AccessToken;
+use Pimple\Container;
+use Pimple\ServiceProviderInterface;
+
+/**
+ * 注册订阅消息
+ * Class ProgramProvider
+ * @package crmeb\utils
+ */
+class ProgramProvider implements ServiceProviderInterface
+{
+    public function register(Container $pimple)
+    {
+        $pimple['mini_program.access_token'] = function ($pimple) {
+            return new AccessToken(
+                $pimple['config']['mini_program']['app_id'],
+                $pimple['config']['mini_program']['secret'],
+                $pimple['cache']
+            );
+        };
+
+        $pimple['mini_program.now_notice'] = function ($pimple) {
+            return new ProgramSubscribe($pimple['mini_program.access_token']);
+        };
+    }
+}
diff --git a/app/common/service/easywechat/subscribe/ProgramSubscribe.php b/app/common/service/easywechat/subscribe/ProgramSubscribe.php
new file mode 100644
index 0000000..993c66a
--- /dev/null
+++ b/app/common/service/easywechat/subscribe/ProgramSubscribe.php
@@ -0,0 +1,285 @@
+<?php
+// +----------------------------------------------------------------------
+// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2016~2022 https://www.crmeb.com All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
+// +----------------------------------------------------------------------
+// | Author: CRMEB Team <admin@crmeb.com>
+// +----------------------------------------------------------------------
+
+
+namespace crmeb\services\easywechat\subscribe;
+
+use EasyWeChat\Core\AbstractAPI;
+use EasyWeChat\Core\AccessToken;
+use EasyWeChat\Core\Exceptions\InvalidArgumentException;
+
+/**
+ * 小程序订阅消息
+ * Class ProgramSubscribe
+ * @package crmeb\utils
+ * @method $this
+ * @method $this template(string $template_id) 设置模板id
+ * @method $this withTemplateId(string $template_id) 设置模板id
+ * @method $this andTemplateId(string $template_id) 设置模板id
+ * @method $this andTemplate(string $template_id) 设置模板id
+ * @method $this andUses(string $template_id) 设置模板id
+ * @method $this to(string $touser) 设置opendid
+ * @method $this andReceiver(string $touser) 设置opendid
+ * @method $this withReceiver(string $touser) 设置opendid
+ * @method $this with(array $data) 设置发送内容
+ * @method $this andData(array $data) 设置发送内容
+ * @method $this withData(array $data) 设置发送内容
+ * @method $this data(array $data) 设置发送内容
+ * @method $this withUrl(string $page) 设置跳转路径
+ */
+class ProgramSubscribe extends AbstractAPI
+{
+
+    /**
+     * 添加模板接口
+     */
+    const API_SET_TEMPLATE_ADD = 'https://api.weixin.qq.com/wxaapi/newtmpl/addtemplate';
+
+    /**
+     * 删除模板消息接口
+     */
+    const API_SET_TEMPLATE_DEL = 'https://api.weixin.qq.com/wxaapi/newtmpl/deltemplate';
+
+    /**
+     * 获取模板消息列表
+     */
+    const API_GET_TEMPLATE_LIST = 'https://api.weixin.qq.com/wxaapi/newtmpl/gettemplate';
+
+    /**
+     * 获取模板消息分类
+     */
+    const API_GET_TEMPLATE_CATE = 'https://api.weixin.qq.com/wxaapi/newtmpl/getcategory';
+
+    /**
+     * 获取模板消息关键字
+     */
+    const API_GET_TEMPLATE_KEYWORKS = 'https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatekeywords';
+
+    /**
+     * 获取公共模板
+     */
+    const API_GET_PUBLIC_TEMPLATE = 'https://api.weixin.qq.com/wxaapi/newtmpl/getpubtemplatetitles';
+
+    /**
+     * 发送模板消息
+     */
+    const API_SUBSCRIBE_SEND = 'https://api.weixin.qq.com/cgi-bin/message/subscribe/send';
+
+    /**
+     * Attributes
+     * @var array
+     */
+    protected $message = [
+        'touser' => '',
+        'template_id' => '',
+        'page' => '',
+        'data' => [],
+    ];
+
+    /**
+     * Message backup.
+     *
+     * @var array
+     */
+    protected $messageBackup;
+
+    protected $required = ['template_id', 'touser'];
+
+    /**
+     * ProgramSubscribeService constructor.
+     * @param AccessToken $accessToken
+     */
+    public function __construct(AccessToken $accessToken)
+    {
+        parent::__construct($accessToken);
+
+        $this->messageBackup = $this->message;
+
+    }
+
+    /**
+     * 获取当前拥有的模板列表
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function getTemplateList()
+    {
+        return $this->parseJSON('get', [self::API_GET_TEMPLATE_LIST]);
+    }
+
+    /**
+     * 获取公众模板列表
+     * @param string $ids
+     * @param int $start
+     * @param int $limit
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function getPublicTemplateList(string $ids, int $start = 0, int $limit = 10)
+    {
+        $params = [
+            'ids' => $ids,
+            'start' => $start,
+            'limit' => $limit
+        ];
+        return $this->parseJSON('get', [self::API_GET_PUBLIC_TEMPLATE, $params]);
+    }
+
+    /**
+     * 获取模板分类
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function getTemplateCate()
+    {
+        return $this->parseJSON('get', [self::API_GET_TEMPLATE_CATE]);
+    }
+
+    /**
+     * 获取模板标题下的关键词列表
+     * @param string $tid 模板标题 id,可通过接口获取
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function getPublicTemplateKeywords(string $tid)
+    {
+        $params = [
+            'tid' => $tid
+        ];
+        return $this->parseJSON('get', [self::API_GET_TEMPLATE_KEYWORKS, $params]);
+    }
+
+    /**
+     * 添加订阅模板消息
+     * @param string $tid 模板标题 id,可通过接口获取,也可登录小程序后台查看获取
+     * @param array $kidList 模板序列号 关键词顺序可以自由搭配(例如 [3,5,4] 或 [4,5,3]),最多支持5个,最少2个关键词组合
+     * @param string $sceneDesc 服务场景描述,15个字以内
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function addTemplate(string $tid, array $kidList, string $sceneDesc = '')
+    {
+        $params = [
+            'tid' => $tid,
+            'kidList' => $kidList,
+            'sceneDesc' => $sceneDesc,
+        ];
+        return $this->parseJSON('json', [self::API_SET_TEMPLATE_ADD, $params]);
+    }
+
+    /**
+     * 删除模板消息
+     * @param string $priTmplId
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function delTemplate(string $priTmplId)
+    {
+        $params = [
+            'priTmplId' => $priTmplId
+        ];
+        return $this->parseJSON('json', [self::API_SET_TEMPLATE_DEL, $params]);
+    }
+
+    /**
+     * 发送订阅消息
+     * @param array $data
+     * @return \EasyWeChat\Support\Collection|null
+     * @throws InvalidArgumentException
+     * @throws \EasyWeChat\Core\Exceptions\HttpException
+     */
+    public function send(array $data = [])
+    {
+        $params = array_merge($this->message, $data);
+
+        foreach ($params as $key => $value) {
+            if (in_array($key, $this->required, true) && empty($value) && empty($this->message[$key])) {
+                throw new InvalidArgumentException("Attribute '$key' can not be empty!");
+            }
+
+            $params[$key] = empty($value) ? $this->message[$key] : $value;
+        }
+
+        $params['data'] = $this->formatData($params['data']);
+
+        $this->message = $this->messageBackup;
+
+        return $this->parseJSON('json', [self::API_SUBSCRIBE_SEND, $params]);
+    }
+
+    /**
+     * 设置订阅消息发送data
+     * @param array $data
+     * @return array
+     */
+    protected function formatData(array $data)
+    {
+        $return = [];
+
+        foreach ($data as $key => $item) {
+            if (is_scalar($item)) {
+                $value = $item;
+            } elseif (is_array($item) && !empty($item)) {
+                if (isset($item['value'])) {
+                    $value = strval($item['value']);
+                } elseif (count($item) < 2) {
+                    $value = array_shift($item);
+                } else {
+                    [$value] = $item;
+                }
+            } else {
+                $value = 'error data item.';
+            }
+
+            $return[$key] = ['value' => $value];
+        }
+
+        return $return;
+    }
+
+
+    /**
+     * Magic access..
+     *
+     * @param $method
+     * @param $args
+     * @return $this
+     */
+    public function __call($method, $args)
+    {
+        $map = [
+            'template' => 'template_id',
+            'templateId' => 'template_id',
+            'uses' => 'template_id',
+            'to' => 'touser',
+            'receiver' => 'touser',
+            'url' => 'page',
+            'link' => 'page',
+            'data' => 'data',
+            'with' => 'data',
+        ];
+
+        if (0 === stripos($method, 'with') && strlen($method) > 4) {
+            $method = lcfirst(substr($method, 4));
+        }
+
+        if (0 === stripos($method, 'and')) {
+            $method = lcfirst(substr($method, 3));
+        }
+
+        if (isset($map[$method])) {
+            $this->message[$map[$method]] = array_shift($args);
+        }
+
+        return $this;
+    }
+
+}
diff --git a/app/common/service/merchant/WechatService.php b/app/common/service/merchant/WechatService.php
new file mode 100644
index 0000000..9da872c
--- /dev/null
+++ b/app/common/service/merchant/WechatService.php
@@ -0,0 +1,167 @@
+<?php
+/**
+ * 微信服务接口
+ *  说明:移植自 shop 商城
+ * TODO: 未对接 EasyWechat
+ * 
+ * @author:刘孝全
+ * @email:q8197264@126.com
+ * @date :2023年03月17日
+ */
+namespace app\common\service\merchant;
+
+use Exception;
+use think\facade\Route;
+use think\exception\ValidateException;
+
+use app\common\model\merchant\system\config\SystemConfigValue;
+
+use EasyWeChat\Core\Exceptions\FaultException;
+use EasyWeChat\Core\Exceptions\InvalidArgumentException;
+use EasyWeChat\Core\Exceptions\RuntimeException;
+use EasyWeChat\Foundation\Application;
+use EasyWeChat\Message\Article;
+use EasyWeChat\Message\Image;
+use EasyWeChat\Message\Material;
+use EasyWeChat\Message\News;
+use EasyWeChat\Message\Text;
+use EasyWeChat\Message\Video;
+use EasyWeChat\Message\Voice;
+use EasyWeChat\Payment\Order;
+use EasyWeChat\Server\BadRequestException;
+use EasyWeChat\Support\Collection;
+
+
+class WechatService 
+{
+    protected $config;
+    protected $application;
+
+    public function __construct(array $config)
+    {
+        $this->config = $config;
+        $this->application = new Application($config);
+        $this->application->register(new \crmeb\services\easywechat\certficates\ServiceProvider());
+        $this->application->register(new \crmeb\services\easywechat\merchant\ServiceProvider);
+        $this->application->register(new \crmeb\services\easywechat\combinePay\ServiceProvider);
+        $this->application->register(new \crmeb\services\easywechat\storePay\ServiceProvider);
+    }
+
+    
+        /**
+     * @return self
+     * @author xaboy
+     * @day 2020-04-24
+     */
+    public static function create($isApp = null)
+    {
+        return new self(self::getConfig($isApp));
+    }
+
+        /**
+     * @return array
+     * @author xaboy
+     * @day 2020-04-24
+     */
+    public static function getConfig($isApp)
+    {
+        /** @var SystemConfigValue $make */
+        $make = app()->make(SystemConfigValue::class);
+        $wechat = $make->more([
+            'wechat_appid', 'wechat_appsecret', 'wechat_token', 'wechat_encodingaeskey', 'wechat_encode', 'wecaht_app_appid', 'wechat_app_appsecret'
+        ], 0);
+
+        if ($isApp ?? request()->isApp()) {
+            $wechat['wechat_appid'] = trim($wechat['wecaht_app_appid']);
+            $wechat['wechat_appsecret'] = trim($wechat['wechat_app_appsecret']);
+        }
+        $payment = $make->more(['site_url', 'pay_weixin_mchid', 'pay_weixin_client_cert', 'pay_weixin_client_key', 'pay_weixin_key', 'pay_weixin_open',
+            'wechat_service_merid', 'wechat_service_key', 'wechat_service_v3key', 'wechat_service_client_cert', 'wechat_service_client_key', 'wechat_service_serial_no'], 0);
+        $config = [
+            'app_id' => trim($wechat['wechat_appid']),
+            'secret' => trim($wechat['wechat_appsecret']),
+            'token' => trim($wechat['wechat_token']),
+            'routine_appId' => systemConfig('routine_appId'),
+            'guzzle' => [
+                'timeout' => 10.0, // 超时时间(秒)
+                'verify' => false
+            ],
+            'debug' => false,
+        ];
+        if ($wechat['wechat_encode'] > 0 && $wechat['wechat_encodingaeskey'])
+            $config['aes_key'] = trim($wechat['wechat_encodingaeskey']);
+        if (isset($payment['pay_weixin_open']) && $payment['pay_weixin_open'] == 1) {
+            $config['payment'] = [
+                'merchant_id' => trim($payment['pay_weixin_mchid']),
+                'key' => trim($payment['pay_weixin_key']),
+                'cert_path' => (app()->getRootPath() . 'public' . $payment['pay_weixin_client_cert']),
+                'key_path' => (app()->getRootPath() . 'public' . $payment['pay_weixin_client_key']),
+                'notify_url' => $payment['site_url'] . Route::buildUrl('wechatNotify')->build(),
+                'pay_weixin_client_cert' => $payment['pay_weixin_client_cert'],
+                'pay_weixin_client_key' => $payment['pay_weixin_client_key'],
+            ];
+        }
+        $config['service_payment'] = [
+            'merchant_id' => trim($payment['wechat_service_merid']),
+            'type' => 'wechat',
+            'key' => trim($payment['wechat_service_key']),
+            'cert_path' => (app()->getRootPath() . 'public' . $payment['wechat_service_client_cert']),
+            'key_path' => (app()->getRootPath() . 'public' . $payment['wechat_service_client_key']),
+            'pay_weixin_client_cert' => $payment['wechat_service_client_cert'],
+            'pay_weixin_client_key' => $payment['wechat_service_client_key'],
+            'serial_no' => trim($payment['wechat_service_serial_no']),
+            'apiv3_key' => trim($payment['wechat_service_v3key']),
+        ];
+        return $config;
+    }
+
+    /**
+     * @param $orderNo
+     * @param array $opt
+     * @author xaboy
+     * @day 2020-04-20
+     */
+    public function payOrderRefund($orderNo, array $opt)
+    {
+        if (!isset($opt['pay_price'])) throw new ValidateException('缺少pay_price');
+        $totalFee = floatval(bcmul($opt['pay_price'], 100, 0));
+        $refundFee = isset($opt['refund_price']) ? floatval(bcmul($opt['refund_price'], 100, 0)) : null;
+        $refundReason = isset($opt['desc']) ? $opt['desc'] : '';
+        $refundNo = isset($opt['refund_id']) ? $opt['refund_id'] : $orderNo;
+        $opUserId = isset($opt['op_user_id']) ? $opt['op_user_id'] : null;
+        $type = isset($opt['type']) ? $opt['type'] : 'out_trade_no';
+        /*仅针对老资金流商户使用
+        REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款)
+        REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款*/
+        $refundAccount = isset($opt['refund_account']) ? $opt['refund_account'] : 'REFUND_SOURCE_UNSETTLED_FUNDS';
+        try {
+            $res = ($this->refund($orderNo, $refundNo, $totalFee, $refundFee, $opUserId, $refundReason, $type, $refundAccount));
+            if ($res->return_code == 'FAIL') throw new ValidateException('退款失败:' . $res->return_msg);
+            if (isset($res->err_code)) throw new ValidateException('退款失败:' . $res->err_code_des);
+        } catch (Exception $e) {
+            throw new ValidateException($e->getMessage());
+        }
+    }
+
+        /**
+     * @param $orderNo
+     * @param $refundNo
+     * @param $totalFee
+     * @param null $refundFee
+     * @param null $opUserId
+     * @param string $refundReason
+     * @param string $type
+     * @param string $refundAccount
+     * @return Collection
+     */
+    public function refund($orderNo, $refundNo, $totalFee, $refundFee = null, $opUserId = null, $refundReason = '', $type = 'out_trade_no', $refundAccount = 'REFUND_SOURCE_UNSETTLED_FUNDS')
+    {
+        if (empty($this->config['payment']['pay_weixin_client_cert']) || empty($this->config['payment']['pay_weixin_client_key'])) {
+            throw new \Exception('请配置微信支付证书');
+        }
+        $totalFee = floatval($totalFee);
+        $refundFee = floatval($refundFee);
+        return $this->application->payment->refund($orderNo, $refundNo, $totalFee, $refundFee, $opUserId, $type, $refundAccount, $refundReason);
+    }
+
+}
\ No newline at end of file
diff --git a/composer.json b/composer.json
index e6e261c..47b861b 100644
--- a/composer.json
+++ b/composer.json
@@ -30,7 +30,9 @@
         "phpmailer/phpmailer": "^6.6",
 		"firebase/php-jwt": "6.1.2",
 		"symfony/var-exporter": "5.4.10",
-        "aliyuncs/oss-sdk-php": "^2.6"
+        "aliyuncs/oss-sdk-php": "^2.6",
+        "overtrue/wechat": "~5.0",
+        "topthink/think-queue": "^3.0"
     },
     "require-dev": {
         "symfony/var-dumper": "^4.2",
@@ -45,7 +47,10 @@
         }
     },
     "config": {
-        "preferred-install": "dist"
+        "preferred-install": "dist",
+        "allow-plugins": {
+            "easywechat-composer/easywechat-composer": false
+        }
     },
     "scripts": {
         "post-autoload-dump": [
diff --git a/config/queue.php b/config/queue.php
new file mode 100644
index 0000000..ac886b3
--- /dev/null
+++ b/config/queue.php
@@ -0,0 +1,38 @@
+<?php
+// +----------------------------------------------------------------------
+// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
+// +----------------------------------------------------------------------
+// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
+// +----------------------------------------------------------------------
+// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
+// +----------------------------------------------------------------------
+// | Author: yunwuxin <448901948@qq.com>
+// +----------------------------------------------------------------------
+
+return [
+    'default'     => 'redis',
+    'connections' => [
+        'sync'     => [
+            'type' => 'sync',
+        ],
+        'database' => [
+            'type'  => 'database',
+            'queue' => env('queue_name', 'default'),
+            'table' => 'jobs',
+        ],
+        'redis'    => [
+            'type'       => 'redis',
+            'queue'      => env('queue_name', 'default'),
+            'host'       => env('redis.redis_hostname','127.0.0.1'),
+            'port'       => env('redis.port', '6379'),
+            'password'   => env('redis.redis_password', ''),
+            'select'     => (int)env('redis.select', 0),
+            'timeout'    => 0,
+            'persistent' => false,
+        ],
+    ],
+    'failed'      => [
+        'type'  => 'none',
+        'table' => 'failed_jobs',
+    ],
+];