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', + ], +];