店铺保证金退回修复bug,未对接微信

This commit is contained in:
liuxiaoquan 2023-03-17 16:43:27 +08:00
parent 3f9fb63437
commit ef6cd16d1b
30 changed files with 3316 additions and 111 deletions

View File

@ -1,6 +1,6 @@
<?php
/**
* 保证金退款处理
* 财务提现处理
* 说明: 店铺类型 相关(不同类型需要不同的保证金)
* @author刘孝全
* @emailq8197264@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, '备注成功');
}

View File

@ -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);

View File

@ -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([

View File

@ -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;
});

View File

@ -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}
<!-- /脚本 -->

View File

@ -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}
<!-- /脚本 -->

View File

@ -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>

View File

@ -0,0 +1,17 @@
<?php
/**
* 清息队列任务 异步触发 接口
*
* @author刘孝全
* @emailq8197264@126.com
* @date 2023年03月17日
*/
namespace app\common\jobs\interfaces;
interface JobInterface
{
public function fire($job, $data);
public function failed($data);
}

View File

@ -0,0 +1,40 @@
<?php
/**
* 商户状态修改
* 说明:来自商户保证金退还-->退返保证金审核通过的消息
*
* @author刘孝全
* @emailq8197264@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.
}
}

View File

@ -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);
}
}

View File

@ -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');
}
}

View File

@ -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());
}
}
}

View File

@ -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

View File

@ -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'] !== '',

View File

@ -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');
}
}

View File

@ -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);
}
}

View File

@ -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]);
}
}

View File

@ -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;
}
}

View File

@ -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);
};
}
}

View File

@ -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 场景类型,枚举值: iOSIOS移动应用 Android安卓移动应用 WapWAP网站应用
*/
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;
}
}

View File

@ -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);
};
}
}

View File

@ -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;
}
}

View File

@ -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);
};
}
}

View File

@ -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);
}
}

View File

@ -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);
};
}
}

View File

@ -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']);
};
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,167 @@
<?php
/**
* 微信服务接口
* 说明:移植自 shop 商城
* TODO 未对接 EasyWechat
*
* @author刘孝全
* @emailq8197264@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);
}
}

View File

@ -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": [

38
config/queue.php Normal file
View File

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