Merge branch 'develop'
This commit is contained in:
@@ -6,3 +6,4 @@
|
||||
.env
|
||||
/tests/tmp
|
||||
/tests/.phpunit.result.cache
|
||||
/public/uploads
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\admin\service\CorpService;
|
||||
|
||||
class CorpController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new CorpService();
|
||||
}
|
||||
|
||||
protected function indexFormat($data)
|
||||
{
|
||||
if (isset($data['created_at']))
|
||||
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
if (isset($data['updated_at']))
|
||||
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果想对某些接口做额外校验或改写,
|
||||
* 也可以在这里覆盖 trait 提供的方法:
|
||||
*
|
||||
* public function store(Request $request)
|
||||
* {
|
||||
* // 自定义验证
|
||||
* $this->validate(...);
|
||||
* // 然后再调用标准逻辑
|
||||
* return parent::store($request);
|
||||
* }
|
||||
*/
|
||||
}
|
||||
@@ -2,97 +2,116 @@
|
||||
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\validate\RoleValidate;
|
||||
use app\common\service\RoleService;
|
||||
use support\Request;
|
||||
use app\common\controller\BaseController;
|
||||
use app\admin\service\RoleService;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
|
||||
class RoleController extends \app\common\controller\BaseController
|
||||
class RoleController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 获取角色列表
|
||||
*/
|
||||
public function list(Request $request)
|
||||
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
try {
|
||||
$params = $request->all();
|
||||
$result = (new RoleService())->getRoleList($params);
|
||||
// 定义后处理器
|
||||
$postProcessor = function($formattedData) {
|
||||
if (isset($formattedData['created_at']))
|
||||
$formattedData['created_at'] = date('Y-m-d H:i:s', $formattedData['created_at']);
|
||||
if (isset($formattedData['updated_at']))
|
||||
$formattedData['updated_at'] = date('Y-m-d H:i:s', $formattedData['updated_at']);
|
||||
return $formattedData;
|
||||
};
|
||||
return $this->success($this->transformData($result, $postProcessor), '角色列表获取成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
$this->service = new RoleService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色详情
|
||||
*/
|
||||
public function detail(Request $request)
|
||||
protected function indexFormat($data)
|
||||
{
|
||||
try {
|
||||
$roleId = $request->get('role_id');
|
||||
if (!$roleId) {
|
||||
return $this->error('缺少角色ID', 422);
|
||||
}
|
||||
$result = (new RoleService())->getRoleDetail($roleId);
|
||||
return $this->success($result, '角色详情获取成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
if (isset($data['created_at']))
|
||||
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
if (isset($data['updated_at']))
|
||||
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加角色
|
||||
*/
|
||||
public function create(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->validate(new RoleValidate(), $request->post(), 'role_create');
|
||||
$result = (new RoleService())->createRole($data);
|
||||
return $this->success($result, '角色创建成功');
|
||||
} catch (\think\exception\ValidateException $e) {
|
||||
return $this->error($e->getError(), 422);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * 获取角色列表
|
||||
// */
|
||||
// public function list(Request $request)
|
||||
// {
|
||||
// try {
|
||||
// $params = $request->all();
|
||||
// $result = (new RoleService())->getRoleList($params);
|
||||
// // 定义后处理器
|
||||
// $postProcessor = function($formattedData) {
|
||||
// if (isset($formattedData['created_at']))
|
||||
// $formattedData['created_at'] = date('Y-m-d H:i:s', $formattedData['created_at']);
|
||||
// if (isset($formattedData['updated_at']))
|
||||
// $formattedData['updated_at'] = date('Y-m-d H:i:s', $formattedData['updated_at']);
|
||||
// return $formattedData;
|
||||
// };
|
||||
// return $this->success($this->transformData($result, $postProcessor), '角色列表获取成功');
|
||||
// } catch (\Throwable $e) {
|
||||
// return $this->error($e->getMessage());
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 修改角色
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->validate(new RoleValidate(), $request->post(), 'role_update');
|
||||
$result = (new RoleService())->updateRole($data);
|
||||
return $this->success($result, '角色更新成功');
|
||||
} catch (\think\exception\ValidateException $e) {
|
||||
return $this->error($e->getError(), 422);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * 获取角色详情
|
||||
// */
|
||||
// public function detail(Request $request)
|
||||
// {
|
||||
// try {
|
||||
// $roleId = $request->get('role_id');
|
||||
// if (!$roleId) {
|
||||
// return $this->error('缺少角色ID', 422);
|
||||
// }
|
||||
// $result = (new RoleService())->getRoleDetail($roleId);
|
||||
// return $this->success($result, '角色详情获取成功');
|
||||
// } catch (\Throwable $e) {
|
||||
// return $this->error($e->getMessage());
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
public function delete(Request $request)
|
||||
{
|
||||
try {
|
||||
$roleId = $request->get('role_id');
|
||||
if (!$roleId) {
|
||||
return $this->error('缺少角色ID', 422);
|
||||
}
|
||||
(new RoleService())->deleteRole($roleId);
|
||||
return $this->success([], '角色删除成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
// /**
|
||||
// * 增加角色
|
||||
// */
|
||||
// public function create(Request $request)
|
||||
// {
|
||||
// try {
|
||||
// $data = $this->validate(new RoleValidate(), $request->post(), 'role_create');
|
||||
// $result = (new RoleService())->createRole($data);
|
||||
// return $this->success($result, '角色创建成功');
|
||||
// } catch (\think\exception\ValidateException $e) {
|
||||
// return $this->error($e->getError(), 422);
|
||||
// } catch (\Throwable $e) {
|
||||
// return $this->error($e->getMessage());
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 修改角色
|
||||
// */
|
||||
// public function update(Request $request)
|
||||
// {
|
||||
// try {
|
||||
// $data = $this->validate(new RoleValidate(), $request->post(), 'role_update');
|
||||
// $result = (new RoleService())->updateRole($data);
|
||||
// return $this->success($result, '角色更新成功');
|
||||
// } catch (\think\exception\ValidateException $e) {
|
||||
// return $this->error($e->getError(), 422);
|
||||
// } catch (\Throwable $e) {
|
||||
// return $this->error($e->getMessage());
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 删除角色
|
||||
// */
|
||||
// public function delete(Request $request)
|
||||
// {
|
||||
// try {
|
||||
// $roleId = $request->get('role_id');
|
||||
// if (!$roleId) {
|
||||
// return $this->error('缺少角色ID', 422);
|
||||
// }
|
||||
// (new RoleService())->deleteRole($roleId);
|
||||
// return $this->success([], '角色删除成功');
|
||||
// } catch (\Throwable $e) {
|
||||
// return $this->error($e->getMessage());
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\admin\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\admin\service\SystemsService;
|
||||
|
||||
class SystemsController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new SystemsService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
// protected function indexFormat($data)
|
||||
// {
|
||||
// if (isset($data['created_at']))
|
||||
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
// if (isset($data['updated_at']))
|
||||
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
|
||||
// return $data;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\service;
|
||||
|
||||
use app\common\validate\CorpValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\Corp;
|
||||
|
||||
class CorpService
|
||||
{
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Corp::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = CorpValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 如果需要自定义,比如强制加一个 where['status']=1,
|
||||
* 只要在这里重写:
|
||||
*/
|
||||
// protected function buildQueryConfig(Request $request): array
|
||||
// {
|
||||
// $config = [];
|
||||
// $config['where']['id'] = 2;
|
||||
// return $config;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 复杂查询示例:
|
||||
// * 只要在 URL 里添加 ?complex_query=1,
|
||||
// * 并传入你需要的任何参数,
|
||||
// * `serviceList()` 就会走到这里来:
|
||||
// */
|
||||
// protected function complexQuery(Request $request): array
|
||||
// {
|
||||
// // 假设前端传了 date_from、date_to
|
||||
// $from = $request->get('date_from');
|
||||
// $to = $request->get('date_to');
|
||||
|
||||
// // 交给 CrudUtil 执行一个自定义 query
|
||||
// $pager = $this->logicComplex(function($q) use ($from, $to) {
|
||||
// $q->where('status', 1);
|
||||
|
||||
// if ($from && $to) {
|
||||
// $q->whereBetween('created_at', [$from, $to]);
|
||||
// }
|
||||
|
||||
// // 再加个排序
|
||||
// $q->orderBy('created_at','desc');
|
||||
|
||||
// // 用 paginate 而不是 listAll
|
||||
// return $q->paginate(
|
||||
// /* perPage */ (int)request()->get('limit',15),
|
||||
// ['*'],
|
||||
// 'page',
|
||||
// (int)request()->get('page',1)
|
||||
// );
|
||||
// });
|
||||
|
||||
// // 把 Paginator 转成数组格式
|
||||
// return [
|
||||
// 'total' => $pager->total(),
|
||||
// 'data' => $pager->items(),
|
||||
// ];
|
||||
// }
|
||||
|
||||
|
||||
// protected function afterServiceList(array $res): array
|
||||
// {
|
||||
// // 对列表的 data 做处理
|
||||
// $res['data'] = array_map(fn($i)=>[...], $res['data']);
|
||||
// return $res;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\service;
|
||||
|
||||
use app\common\validate\RoleValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\Role;
|
||||
|
||||
class RoleService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Role::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = RoleValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\service;
|
||||
|
||||
use app\common\validate\SystemsValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\Systems;
|
||||
|
||||
class SystemsService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Systems::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = SystemsValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\model\InquiryLog;
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserSn;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
|
||||
class DomainCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'domain';
|
||||
protected static $defaultDescription = '';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$log = new InquiryLog();
|
||||
$list = $log->orderBy('id', 'desc')->get();
|
||||
|
||||
foreach ($list as $item) {
|
||||
if (!$item->content) {
|
||||
continue;
|
||||
}
|
||||
$item->content = processRichTextImagesEnhanced($item->content);
|
||||
$item->save();
|
||||
$output->writeln("处理了{$item->id} \n");
|
||||
}
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强版 - 处理富文本内容中的图片URL
|
||||
*
|
||||
* @param string $content 富文本HTML内容
|
||||
* @param string $newDomain 新域名(如: https://fdesk.hnxinyouhe.com)
|
||||
* @param string $oldDomain 旧域名(如: http://219.157.255.213:25326)
|
||||
* @return string 处理后的富文本内容
|
||||
*/
|
||||
function processRichTextImagesEnhanced($content, $newDomain = 'https://fdesk.hnxinyouhe.com', $oldDomain = 'http://219.157.255.213:25326') {
|
||||
// 标准化域名格式(去除末尾斜杠)
|
||||
$newDomain = rtrim($newDomain, '/');
|
||||
$oldDomain = rtrim($oldDomain, '/');
|
||||
|
||||
// 要处理的属性列表
|
||||
$attributes = ['src', 'data-href', 'href', 'data-src', 'data-original'];
|
||||
|
||||
foreach ($attributes as $attr) {
|
||||
// 情况1:替换旧域名为新域名
|
||||
$content = preg_replace(
|
||||
'/('.$attr.'=["\'])'.preg_quote($oldDomain, '/').'(\/uploads\/)/i',
|
||||
'$1' . $newDomain . '$2',
|
||||
$content
|
||||
);
|
||||
|
||||
// 情况2:给无域名的路径添加新域名
|
||||
$content = preg_replace(
|
||||
'/('.$attr.'=["\'])(\/uploads\/)/i',
|
||||
'$1' . $newDomain . '$2',
|
||||
$content
|
||||
);
|
||||
|
||||
// 情况3:处理可能存在的其他域名(可选)
|
||||
$content = preg_replace(
|
||||
'/('.$attr.'=["\'])(https?:\/\/[^\/]+)(\/uploads\/)/i',
|
||||
'$1' . $newDomain . '$3',
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace app\command;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserSn;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use support\Db;
|
||||
|
||||
class TbCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'tb-db';
|
||||
protected static $defaultDescription = '';
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$user = new User();
|
||||
$list = $user->all();
|
||||
|
||||
foreach ($list as $item) {
|
||||
// UserSn::where('user_sn', $item->sn)->update(['user_id' => $item->id]);
|
||||
}
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class AuthController extends BaseController
|
||||
/**
|
||||
* 不需要头信息的方法
|
||||
*/
|
||||
protected $noNeedHeader = ['feishuAuth'];
|
||||
protected $noNeedHeaderData = ['feishuAuth'];
|
||||
|
||||
protected $noNeedLogin = ['feishuScope', 'login', 'register', 'feishuAuth'];
|
||||
|
||||
|
||||
@@ -10,10 +10,54 @@ use support\Request;
|
||||
|
||||
class BaseController
|
||||
{
|
||||
/**
|
||||
* 响应处理、数据格式化处理
|
||||
*/
|
||||
use ResponseTrait, DataTransformer;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
* @var Validate
|
||||
*/
|
||||
protected mixed $validator = null;
|
||||
|
||||
/**
|
||||
* @var Model
|
||||
*/
|
||||
protected $model = null;
|
||||
|
||||
/**
|
||||
* 不登录不鉴权
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = [];
|
||||
|
||||
/**
|
||||
* 登录不鉴权
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedAuth = [];
|
||||
|
||||
/**
|
||||
* 不用头部数据
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedHeaderData = [];
|
||||
|
||||
/**
|
||||
* 数据限制
|
||||
* null 不做限制,任何管理员都可以查看该表的所有数据
|
||||
* auth 管理员能看到自己以及自己的子管理员插入的数据
|
||||
* personal 管理员只能看到自己插入的数据
|
||||
* @var string
|
||||
*/
|
||||
protected $dataLimit = null;
|
||||
|
||||
/**
|
||||
* 数据限制字段
|
||||
*/
|
||||
protected $dataLimitField = 'admin_id';
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
*
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\controller;
|
||||
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use app\common\utils\FeishuMessage;
|
||||
use app\inquiry\service\InquiryLogService;
|
||||
use app\common\model\CrmCustomer;
|
||||
use app\common\model\InquiryLog;
|
||||
use app\common\model\User;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class NotifyController
|
||||
{
|
||||
|
||||
/**
|
||||
* 不用头部数据
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedHeaderData = ['feishuCallback', 'feishuEvent'];
|
||||
|
||||
/**
|
||||
* 不登录不鉴权
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['feishuCallback', 'feishuEvent'];
|
||||
|
||||
/**
|
||||
* 飞书回调
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function feishuCallback(Request $request)
|
||||
{
|
||||
try {
|
||||
$param = $request->post();
|
||||
|
||||
// 解析数据
|
||||
// $param = json_decode($param['event'], true);
|
||||
$orderSn = $param['event']['action']['value'];
|
||||
if (strpos($orderSn, '"') !== false) {
|
||||
$orderSn = str_replace('"', '', $orderSn);
|
||||
}
|
||||
|
||||
// 处理询单&客户线索信息
|
||||
$openId = $param['event']['operator']['open_id'];
|
||||
|
||||
// 获取询单
|
||||
$order = InquiryLog::with(['channelInfo', 'countryInfo', 'enter'])->where('order_no', $orderSn)->first();
|
||||
if (!$order) {
|
||||
return $this->error('未找到询单记录');
|
||||
}
|
||||
|
||||
// 获取客户xians
|
||||
$customer = CrmCustomer::where('sn', $orderSn)->first();
|
||||
if (!$customer) {
|
||||
return $this->error('未找到客户线索');
|
||||
}
|
||||
|
||||
// 获取用户
|
||||
$uinfo = User::where('open_id', $openId)->first();
|
||||
|
||||
// 更新客户线索
|
||||
$customer->user_id = $uinfo->id;
|
||||
|
||||
// 更新询单数据
|
||||
$order->recipient = $uinfo->id;
|
||||
$order->status = 2;
|
||||
$order->t_time = time();
|
||||
|
||||
// 保存
|
||||
if (!$order->save() || !$customer->save()) {
|
||||
return $this->error('跟踪失败');
|
||||
}
|
||||
|
||||
// 获取飞书消息模板
|
||||
$feishuMessage = new FeishuMessage();
|
||||
$content = $feishuMessage->groupUpdateTemplates(
|
||||
(new InquiryLogService())->qualityName($order->quality),
|
||||
$order->order_no,
|
||||
$order->channelInfo ? $order->channelInfo->name : '',
|
||||
$order->countryInfo ? $order->countryInfo->cn_addr : '',
|
||||
$order->enter ? $order->enter->name : '',
|
||||
$order->r_time ? date('Y-m-d H:i:s', $order->r_time) : '',
|
||||
$order->t_time ? date('Y-m-d H:i:s', $order->t_time) : ''
|
||||
);
|
||||
|
||||
// 组装飞书消息模板
|
||||
$data = '{
|
||||
"toast": {
|
||||
"type": "success",
|
||||
"content": "操作成功"
|
||||
},
|
||||
"card": {
|
||||
"type": "raw",
|
||||
"data": '.$content.'
|
||||
}
|
||||
}';
|
||||
|
||||
// 响应消息
|
||||
return new Response(200, ['Content-Type' => 'application/json'], $data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function error($msg = '')
|
||||
{
|
||||
$data = '{
|
||||
"toast": {
|
||||
"type": "error",
|
||||
"content": "'.$msg.'"
|
||||
}
|
||||
}';
|
||||
return new Response(200, ['Content-Type' => 'application/json'], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书事件
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function feishuEvent(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $request->post();
|
||||
|
||||
var_dump('飞书事件回调:'.$data);
|
||||
|
||||
return json($data);
|
||||
} catch (\Throwable $e) {
|
||||
return json($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,15 @@ namespace app\common\controller;
|
||||
use support\Request;
|
||||
use app\common\service\CorpService;
|
||||
use app\common\service\CorpFeishuAppService;
|
||||
use app\common\service\SystemsService;
|
||||
use app\common\service\UserService;
|
||||
|
||||
class PresetController extends BaseController
|
||||
{
|
||||
|
||||
protected $noNeedLogin = ['corps', 'feishuApp'];
|
||||
protected $noNeedLogin = ['corps', 'feishuApp', 'systems'];
|
||||
|
||||
protected $noNeedHeaderData = ['corps', 'feishuApp', 'systems'];
|
||||
|
||||
public function corps()
|
||||
{
|
||||
@@ -27,10 +31,34 @@ class PresetController extends BaseController
|
||||
{
|
||||
try {
|
||||
$corpId = $request->get('corp_id');
|
||||
$result = (new CorpFeishuAppService())->getFeishuApp($corpId);
|
||||
$systemId = $request->get('system_id');
|
||||
$result = (new CorpFeishuAppService())->getFeishuApp($corpId, $systemId);
|
||||
return $this->success($result, '获取飞书应用信息');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function systems()
|
||||
{
|
||||
try {
|
||||
$result = (new SystemsService())->getList();
|
||||
return $this->success($result, '获取系统列表');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function users()
|
||||
{
|
||||
try {
|
||||
$result = (new UserService())->getList();
|
||||
return $this->success($result, '获取用户列表');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,10 @@ class UploadController
|
||||
|
||||
use ResponseTrait;
|
||||
|
||||
protected $noNeedLogin = ['proxy'];
|
||||
|
||||
protected $noNeedHeaderData = ['proxy'];
|
||||
|
||||
public function upload(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -15,11 +15,65 @@ class UserController extends BaseController
|
||||
{
|
||||
try {
|
||||
$uid = $request->user->uid;
|
||||
$result = (new UserService())->getUserInfo($uid);
|
||||
$systemCode = $request->system_code;
|
||||
$result = (new UserService())->getUserInfo($uid, $systemCode);
|
||||
return $this->success($result, '获取用户信息');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
public function getUser(Request $request)
|
||||
{
|
||||
try {
|
||||
$uid = $request->user->uid;
|
||||
$corp_abbr = $request->basic_data['corp_abbr'];
|
||||
$result = (new UserService())->getUser($uid,$corp_abbr);
|
||||
return $this->success($result, '获取用户信息');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑用户
|
||||
*/
|
||||
public function edit(Request $request)
|
||||
{
|
||||
try {
|
||||
$uid = $request->user->uid;
|
||||
$data = $request->all();
|
||||
$result = (new UserService())->edit($uid,$data);
|
||||
unset($result['password']);
|
||||
unset($result['union_id']);
|
||||
unset($result['open_id']);
|
||||
unset($result['tenant_key']);
|
||||
return $this->success($result, '编辑成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
* @throws \Random\RandomException
|
||||
*/
|
||||
public function savePassword(Request $request)
|
||||
{
|
||||
try {
|
||||
$uid = $request->user->uid;
|
||||
$data = $request->all();
|
||||
$result = (new UserService())->savePassword($uid,$data);;
|
||||
return $this->success($result, '编辑成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace app\common\logic;
|
||||
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\CorpFeishuApp;
|
||||
use app\common\model\Resource;
|
||||
use app\common\model\UserTokenLog;
|
||||
use app\common\model\Systems;
|
||||
use app\common\model\User;
|
||||
@@ -85,16 +86,23 @@ class AuthLogic
|
||||
{
|
||||
try {
|
||||
$phone = $this->sanitizePhone($feishuUserInfo['mobile'] ?? '');
|
||||
$user = User::where('phone', $phone)->where('corp_id', $feishuUserInfo['corp_id'])->first();
|
||||
$email = $feishuUserInfo['enterprise_email'];
|
||||
$user = User::where('corp_id', $feishuUserInfo['corp_id'])
|
||||
->where(function($query) use ($phone, $email) {
|
||||
$query->where('phone', $phone)
|
||||
->orWhere('email', $email);
|
||||
})
|
||||
->first();
|
||||
|
||||
if (!$user) {
|
||||
// 获取编号
|
||||
$userSn = $this->getAvailableUserSn();
|
||||
// 创建用户
|
||||
$user = User::create([
|
||||
'name' => $feishuUserInfo['en_name'],
|
||||
'name' => $feishuUserInfo['name'],
|
||||
'avatar' => $feishuUserInfo['avatar_middle'],
|
||||
'phone' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''),
|
||||
'email' => $feishuUserInfo['enterprise_email'],
|
||||
'union_id' => $feishuUserInfo['union_id'],
|
||||
'user_id' => $feishuUserInfo['user_id'],
|
||||
'open_id' => $feishuUserInfo['open_id'],
|
||||
@@ -111,12 +119,20 @@ class AuthLogic
|
||||
// 是否登陆
|
||||
$user->isLogin = false;
|
||||
} else {
|
||||
$user->update([
|
||||
$updated = [
|
||||
'union_id' => $feishuUserInfo['union_id'],
|
||||
'user_id' => $feishuUserInfo['user_id'],
|
||||
'open_id' => $feishuUserInfo['open_id'],
|
||||
'tenant_key' => $feishuUserInfo['tenant_key']
|
||||
]);
|
||||
];
|
||||
if ($phone && $phone == $user->phone) {
|
||||
$updated['phone'] = $phone;
|
||||
}
|
||||
if ($phone && $phone == $user->phone) {
|
||||
$updated['email'] = $email;
|
||||
}
|
||||
// 更新飞书信息
|
||||
$user->update($updated);
|
||||
}
|
||||
return $user;
|
||||
} catch (\Throwable $e) {
|
||||
@@ -140,6 +156,7 @@ class AuthLogic
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
$homeUrl = $this->getHome($system->id);
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
@@ -150,7 +167,8 @@ class AuthLogic
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'system_code' => $data['system_code'],
|
||||
'corp_abbr' => $data['corp_abbr'],
|
||||
'login_type' => $data['login_type']
|
||||
'login_type' => $data['login_type'],
|
||||
'home_url' => $homeUrl,
|
||||
// 'payload' => $payload,
|
||||
];
|
||||
}
|
||||
@@ -500,4 +518,42 @@ class AuthLogic
|
||||
Redis::del("user_token_meta:{$token}");
|
||||
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
|
||||
}
|
||||
|
||||
|
||||
public function getHome($systemId)
|
||||
{
|
||||
$info = Resource::where('system_id', $systemId)
|
||||
->where('is_home', 1)
|
||||
->first();
|
||||
|
||||
if (!$info) {
|
||||
return '/dashboard/console';
|
||||
}
|
||||
|
||||
// 递归拼接 URL
|
||||
$url = $this->buildParentUrl($info);
|
||||
|
||||
return $url ?: '/dashboard/console';
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建父级 URL
|
||||
* @param Resource $resource
|
||||
* @return string
|
||||
*/
|
||||
protected function buildParentUrl(Resource $resource)
|
||||
{
|
||||
$url = $resource->url ?? '';
|
||||
|
||||
// 如果存在父级,则递归拼接
|
||||
if ($resource->parent_id != 0) {
|
||||
$parent = Resource::find($resource->parent_id);
|
||||
if ($parent) {
|
||||
$parentUrl = $this->buildParentUrl($parent);
|
||||
$url = $parentUrl . ($url ? '/' . ltrim($url, '/') : '');
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
@@ -1,593 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\CorpFeishuApp;
|
||||
use app\common\model\UserTokenLog;
|
||||
use app\common\model\Systems;
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserFeishuApp;
|
||||
use app\common\model\UserSn;
|
||||
use app\common\utils\Feishu;
|
||||
use app\common\utils\Jwt;
|
||||
use support\Redis;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
|
||||
class AuthLogic_bak
|
||||
{
|
||||
/**
|
||||
* 账号密码登录
|
||||
*/
|
||||
public function uPwdLogin(array $data): array
|
||||
{
|
||||
$user = $this->validateUser($data['account'], $data['password']);
|
||||
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
$this->validateSystemPermission($user, $system);
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书登录
|
||||
* 如果用户不存在则新增用户信息
|
||||
*/
|
||||
public function feishuLogin(array $data, bool $isLogin = true): array
|
||||
{
|
||||
$feishuUserInfo = $this->getFeishuUserInfo($data);
|
||||
$user = $this->getOrCreateUser($feishuUserInfo);
|
||||
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
if ($user->isLogin === true) {
|
||||
$this->validateSystemPermission($user, $system);
|
||||
}
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码登录
|
||||
*/
|
||||
public function qrCodeLogin(array $data): array
|
||||
{
|
||||
$userId = $this->validateQrCodeToken($data['qr_code_token']);
|
||||
$user = User::find($userId);
|
||||
if (!$user || $user->status != 1) {
|
||||
throw new BusinessException('用户无效', 403);
|
||||
}
|
||||
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
$this->validateSystemPermission($user, $system);
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
|
||||
// 记录 token 到 Redis 和 MySQL
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*/
|
||||
public function register(array $data)
|
||||
{
|
||||
// 获取可用用户编号
|
||||
$sn = $this->getAvailableUserSn();
|
||||
$data['sn'] = $sn->user_sn;
|
||||
|
||||
// 对密码进行哈希处理
|
||||
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
|
||||
// 获取企业ID
|
||||
$corpInfo = $this->validateCorp($data['corp_abbr']);
|
||||
$data['corp_id'] = $corpInfo->id;
|
||||
|
||||
// 创建用户记录
|
||||
$user = $this->createUser($data);
|
||||
|
||||
// 更新用户编号的用户 ID
|
||||
$this->updateUserId($user->id, $sn->user_sn);
|
||||
|
||||
// 注册后如果直接登录(返回token)
|
||||
$system = $this->validateSystem($data['system_code']);
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$refreshToken = $this->generateRefreshToken($user->id, $data['system_code']);
|
||||
$this->recordUserToken($user->id, $token, $data['system_code']);
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'access_token' => $token,
|
||||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateUserId($userId, $userSn)
|
||||
{
|
||||
$updated = UserSn::where('user_sn', $userSn)
|
||||
->update(['user_id' => $userId]);
|
||||
|
||||
if ($updated === 0) {
|
||||
throw new BusinessException('用户编号不存在或更新失败');
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的用户编号
|
||||
*
|
||||
* @return \app\common\model\UserSn 用户编号模型实例
|
||||
* @throws BusinessException 当未找到适用编号时抛出
|
||||
*/
|
||||
private function getAvailableUserSn(): \app\common\model\UserSn
|
||||
{
|
||||
$userSnModel = new UserSn();
|
||||
$sn = $userSnModel->where('user_id', '=', 0)->inRandomOrder()->first();
|
||||
if (!$sn) {
|
||||
Log::error('用户注册时未找到适用编号');
|
||||
throw new BusinessException('未找到适用编号');
|
||||
}
|
||||
return $sn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户记录
|
||||
*
|
||||
* @param array $data 包含用户信息的数组
|
||||
* @return \app\common\model\User 用户模型实例
|
||||
*/
|
||||
private function createUser(array $data): \app\common\model\User
|
||||
{
|
||||
try {
|
||||
return User::create($data);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('用户注册时创建用户记录失败', [
|
||||
'data' => $data,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
throw new BusinessException('用户注册失败,请稍后重试', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
* @param string $refreshToken
|
||||
* @param string|null $systemCode 可选,指定系统
|
||||
* @return array
|
||||
*/
|
||||
public function refreshToken(string $refreshToken, ?string $systemCode = null): array
|
||||
{
|
||||
if (!$systemCode) throw new BusinessException('缺少system_code', 422);
|
||||
|
||||
// 校验refresh_token
|
||||
$userId = Redis::get("refresh_token_map:{$refreshToken}:{$systemCode}");
|
||||
if (!$userId) {
|
||||
throw new BusinessException('refresh_token无效或已过期', 401);
|
||||
}
|
||||
$validToken = Redis::get("refresh_token:{$userId}:{$systemCode}");
|
||||
if ($validToken !== $refreshToken) {
|
||||
throw new BusinessException('refresh_token无效或已过期', 401);
|
||||
}
|
||||
|
||||
$user = User::find($userId);
|
||||
if (!$user || $user->status != 1) {
|
||||
throw new BusinessException('用户无效', 403);
|
||||
}
|
||||
|
||||
$system = $this->validateSystem($systemCode);
|
||||
$this->validateSystemPermission($user, $system);
|
||||
|
||||
$payload = $this->buildJwtPayload($user, $system);
|
||||
$token = $this->generateAccessToken($payload);
|
||||
$this->recordUserToken($user->id, $token, $systemCode);
|
||||
|
||||
return [
|
||||
'access_token' => $token,
|
||||
'expires_in' => config('jwt.jwt_expiration'),
|
||||
'payload' => $payload,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录用户token到 Redis 和 MySQL
|
||||
*/
|
||||
protected function recordUserToken(int $userId, string $token, string $systemCode)
|
||||
{
|
||||
$request = request();
|
||||
$ip = $request ? $request->getRealIp() : '';
|
||||
$ua = $request ? $request->header('user-agent', '') : '';
|
||||
$now = time();
|
||||
$expireAt = $now + config('jwt.jwt_expiration');
|
||||
|
||||
// Redis Set 用户token集合
|
||||
Redis::sAdd("user_tokens:{$userId}:{$systemCode}", $token);
|
||||
Redis::expire("user_tokens:{$userId}:{$systemCode}", config('jwt.jwt_expiration'));
|
||||
|
||||
// Redis Hash token元信息
|
||||
$meta = [
|
||||
'system_code' => $systemCode,
|
||||
'login_ip' => $ip,
|
||||
'user_agent' => $ua,
|
||||
'login_time' => $now,
|
||||
'expired_at' => $expireAt,
|
||||
];
|
||||
Redis::hMSet("user_token_meta:{$token}", $meta);
|
||||
Redis::expire("user_token_meta:{$token}", config('jwt.jwt_expiration'));
|
||||
|
||||
// 可选:MySQL 持久化
|
||||
try {
|
||||
UserTokenLog::create([
|
||||
'user_id' => $userId,
|
||||
'token' => $token,
|
||||
'system_code'=> $systemCode,
|
||||
'login_ip' => $ip,
|
||||
'user_agent' => $ua,
|
||||
'login_time' => $now,
|
||||
'expired_at' => $expireAt,
|
||||
'is_valid' => 1,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('Token log write failed: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验token有效性
|
||||
*/
|
||||
public function verifyToken(string $token, string $systemCode): array
|
||||
{
|
||||
if (!$systemCode) throw new BusinessException('缺少system_code', 422);
|
||||
|
||||
$payload = (new Jwt())->verifyToken($token);
|
||||
|
||||
// 校验黑名单
|
||||
if (Redis::has("token_blacklist:{$token}")) {
|
||||
throw new BusinessException('token已失效', 403);
|
||||
}
|
||||
|
||||
// 强制校验aud(系统隔离)
|
||||
if ($systemCode && $payload['aud'] != $systemCode && $payload['aud'] != ($payload['system_code'] ?? '')) {
|
||||
throw new BusinessException('token不属于当前系统', 403);
|
||||
}
|
||||
|
||||
return (array)$payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出/失效token
|
||||
*/
|
||||
public function logout(?string $token, ?string $systemCode = null)
|
||||
{
|
||||
if ($token) {
|
||||
Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
|
||||
$payload = null;
|
||||
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
|
||||
$userId = $payload['uid'] ?? null;
|
||||
$aud = $payload['aud'] ?? null;
|
||||
$sc = $systemCode ?? $aud;
|
||||
if ($userId && $sc) {
|
||||
Redis::sRem("user_tokens:{$userId}:{$sc}", $token);
|
||||
}
|
||||
Redis::del("user_token_meta:{$token}");
|
||||
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制拉黑token
|
||||
*/
|
||||
public function blockToken(string $token, ?string $systemCode = null)
|
||||
{
|
||||
if (!$token) {
|
||||
throw new BusinessException('token不能为空', 422);
|
||||
}
|
||||
Redis::set("token_blacklist:{$token}", 1, config('jwt.jwt_expiration'));
|
||||
$payload = null;
|
||||
try { $payload = (new Jwt())->verifyToken($token); } catch (\Throwable $e) {}
|
||||
$userId = $payload['uid'] ?? null;
|
||||
$aud = $payload['aud'] ?? null;
|
||||
$sc = $systemCode ?? $aud;
|
||||
if ($userId && $sc) {
|
||||
Redis::sRem("user_tokens:{$userId}:{$sc}", $token);
|
||||
}
|
||||
Redis::del("user_token_meta:{$token}");
|
||||
UserTokenLog::where('token', $token)->update(['is_valid'=>0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有活跃token及元信息(按system_code)
|
||||
*/
|
||||
public function getUserTokens(int $userId, string $systemCode): array
|
||||
{
|
||||
$tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: [];
|
||||
$result = [];
|
||||
foreach ($tokens as $token) {
|
||||
$meta = Redis::hGetAll("user_token_meta:{$token}");
|
||||
$result[] = [
|
||||
'token' => $token,
|
||||
'meta' => $meta,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制某用户所有token全部下线
|
||||
*/
|
||||
public function blockAllTokensOfUser(int $userId, string $systemCode)
|
||||
{
|
||||
$tokens = Redis::sMembers("user_tokens:{$userId}:{$systemCode}") ?: [];
|
||||
foreach ($tokens as $token) {
|
||||
$this->blockToken($token, $systemCode);
|
||||
}
|
||||
}
|
||||
|
||||
/** ----------- 内部方法 ------------- **/
|
||||
|
||||
/**
|
||||
* 用户验证
|
||||
*/
|
||||
private function validateUser(string $account, string $password)
|
||||
{
|
||||
$user = User::byAccount($account)->first();
|
||||
if (!$user || !password_verify($password, $user->password)) {
|
||||
throw new BusinessException('账号或密码错误', 401);
|
||||
}
|
||||
if ($user->status != 1) {
|
||||
throw new BusinessException('账号已禁用', 403);
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统验证
|
||||
*/
|
||||
private function validateSystem(string $systemCode)
|
||||
{
|
||||
$system = Systems::where('code', $systemCode)->first();
|
||||
if (!$system) {
|
||||
throw new BusinessException('系统不存在', 403);
|
||||
}
|
||||
return $system;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公司验证
|
||||
*/
|
||||
private function validateCorp(string $corpAbbr)
|
||||
{
|
||||
$corp = Corp::where('abbr', $corpAbbr)->first();
|
||||
if (!$corp) {
|
||||
throw new BusinessException('公司不存在', 403);
|
||||
}
|
||||
return $corp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书应用信息
|
||||
*/
|
||||
private function validateFeishuApp(int $corpId, string $systemCode)
|
||||
{
|
||||
$feishuAppInfo = CorpFeishuApp::where('corp_id', $corpId)->where('system_code', $systemCode)->first();
|
||||
if (!$feishuAppInfo) {
|
||||
throw new BusinessException('飞书应用不存在', 403);
|
||||
}
|
||||
return $feishuAppInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户系统权限校验
|
||||
*/
|
||||
private function validateSystemPermission(User $user, Systems $system)
|
||||
{
|
||||
if ($system->is_role) {
|
||||
$systemIds = $user->systems->pluck('id')->toArray();
|
||||
if (!in_array($system->id, $systemIds)) {
|
||||
throw new BusinessException('无该系统权限', 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT payload 构建
|
||||
* 可扩展 payload 字段
|
||||
*/
|
||||
private function buildJwtPayload(User $user, ?Systems $system = null): array
|
||||
{
|
||||
$now = time();
|
||||
$exp = $now + config('jwt.jwt_expiration');
|
||||
$system_ids = $user->systems ? $user->systems->pluck('id')->toArray() : [];
|
||||
$role_ids = $user->roles ? $user->roles->pluck('id')->toArray() : [];
|
||||
|
||||
return [
|
||||
'sub' => 'user_' . $user->id,
|
||||
'aud' => $system->code,
|
||||
'iat' => $now,
|
||||
'nbf' => $now,
|
||||
'exp' => $exp,
|
||||
'typ' => 'access',
|
||||
'uid' => $user->id,
|
||||
'role_ids' => $role_ids,
|
||||
'system_ids' => $system_ids
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT签发
|
||||
*/
|
||||
private function generateAccessToken(array $payload)
|
||||
{
|
||||
return (new Jwt())->generateToken($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌生成
|
||||
*/
|
||||
private function generateRefreshToken(int $userId, string $systemCode)
|
||||
{
|
||||
$refreshToken = bin2hex(random_bytes(32));
|
||||
// user_id+system_code只允许一个refresh_token生效
|
||||
Redis::set("refresh_token:{$userId}:{$systemCode}", $refreshToken, config('jwt.jwt_refresh_token_expiration'));
|
||||
// 建立refresh_token到user_id+system_code映射,便于校验
|
||||
Redis::set("refresh_token_map:{$refreshToken}:{$systemCode}", $userId, config('jwt.jwt_refresh_token_expiration'));
|
||||
return $refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证扫码令牌
|
||||
*/
|
||||
private function validateQrCodeToken(string $qrCodeToken)
|
||||
{
|
||||
$userId = Redis::get("qr_code_token:{$qrCodeToken}");
|
||||
if (!$userId) {
|
||||
throw new BusinessException('扫码令牌无效或已过期', 401);
|
||||
}
|
||||
return $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书用户信息
|
||||
*/
|
||||
private function getFeishuUserInfo(array $data)
|
||||
{
|
||||
// 获取凭证
|
||||
$certificate = $this->feishuAppInfo($data['corp_abbr'], $data['system_code']);
|
||||
// 获取token
|
||||
$feishu = new Feishu($certificate['app_id'], $certificate['app_secret'], $certificate['abbr']);
|
||||
$userAccessToken = $feishu->userAccessToken($data['feishu_code'], $certificate['r_url']);
|
||||
// 获取用户信息
|
||||
$userInfo = $feishu->userInfo($userAccessToken['access_token']);
|
||||
if ($userInfo['code'] != 0) {
|
||||
throw new BusinessException('授权失败,请重试');
|
||||
}
|
||||
return $userInfo['data'] + $data + $certificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找或创建飞书用户
|
||||
*/
|
||||
private function getOrCreateUser(array $feishuUserInfo)
|
||||
{
|
||||
$userFeishuApp = UserFeishuApp::firstOrCreate([
|
||||
'name' => $feishuUserInfo['en_name'],
|
||||
'mobile' => $this->sanitizePhone($feishuUserInfo['mobile'] ?? ''),
|
||||
'union_id' => $feishuUserInfo['union_id'],
|
||||
'user_id' => $feishuUserInfo['user_id'],
|
||||
'open_id' => $feishuUserInfo['open_id'],
|
||||
'tenant_key' => $feishuUserInfo['tenant_key']
|
||||
]);
|
||||
|
||||
if ($userFeishuApp->uid) {
|
||||
$user = User::find($userFeishuApp->uid);
|
||||
$user->isLogin = true;
|
||||
return $user;
|
||||
} else {
|
||||
$user = User::where('phone', $userFeishuApp->mobile)->first();
|
||||
$user->isLogin = true;
|
||||
if (!$user) {
|
||||
$user = $this->createUserByFeishu($feishuUserInfo, $userFeishuApp);
|
||||
$user->isLogin = false;
|
||||
}
|
||||
$userFeishuApp->uid = $user->id;
|
||||
$userFeishuApp->corp_id = $feishuUserInfo['corp_id'];
|
||||
if (!$userFeishuApp->save()) {
|
||||
throw new BusinessException('绑定用户飞书记录失败');
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤和格式化手机号
|
||||
*/
|
||||
private function sanitizePhone($mobile)
|
||||
{
|
||||
return isset($mobile) ? str_replace('+86', '', $mobile) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建飞书用户
|
||||
*/
|
||||
private function createUserByFeishu(array $feishuUserInfo, UserFeishuApp $userFeishuApp)
|
||||
{
|
||||
// 1. 获取适用的用户编号
|
||||
$userSn = UserSn::where('user_id', '=', 0)->inRandomOrder()->first();
|
||||
if (!$userSn) {
|
||||
throw new BusinessException('未找到适用编号');
|
||||
}
|
||||
|
||||
// 2. 创建用户
|
||||
$user = new User();
|
||||
$user->sn = $userSn->user_sn;
|
||||
$user->name = $feishuUserInfo['en_name'];
|
||||
$user->avatar = $feishuUserInfo['avatar_middle'] ?? '';
|
||||
$user->password = password_hash($userSn->user_sn, PASSWORD_DEFAULT);
|
||||
$user->phone = $userFeishuApp->mobile;
|
||||
$user->email = '';
|
||||
$user->system_code = $feishuUserInfo['system_code'];
|
||||
$user->corp_id = $feishuUserInfo['corp_id'];
|
||||
|
||||
if (!$user->save()) {
|
||||
throw new BusinessException('用户创建失败');
|
||||
}
|
||||
|
||||
// 更新使用状态
|
||||
$this->updateUserId($user->id, $userSn->user_sn);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 飞书应用信息
|
||||
*/
|
||||
public function feishuAppInfo(string $corpAbbr, string $systemCode)
|
||||
{
|
||||
$corpInfo = $this->validateCorp($corpAbbr);
|
||||
$fsAppInfo = $this->validateFeishuApp($corpInfo->id, $systemCode);
|
||||
return [
|
||||
'app_id' => $fsAppInfo->app_id,
|
||||
'app_secret' => $fsAppInfo->app_secret,
|
||||
'r_url' => $fsAppInfo->r_url,
|
||||
'scope' => $fsAppInfo->scopes,
|
||||
'state' => $fsAppInfo->state,
|
||||
'abbr' => $corpAbbr,
|
||||
'corp_id' => $corpInfo->id
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class BaseLogic
|
||||
{
|
||||
/**
|
||||
* 获取列表
|
||||
*/
|
||||
public function getList(array $params, string $modelClass)
|
||||
{
|
||||
$query = $modelClass::query();
|
||||
|
||||
// 你可以在此处添加分页、排序等功能
|
||||
if (isset($params['page']) && isset($params['per_page'])) {
|
||||
$query->paginate($params['per_page'], ['*'], 'page', $params['page']);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个记录
|
||||
*/
|
||||
public function getDetail(int $id, string $modelClass)
|
||||
{
|
||||
$record = $modelClass::find($id);
|
||||
if (!$record) {
|
||||
throw new BusinessException("记录不存在", 404);
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建记录
|
||||
*/
|
||||
public function create(array $data, string $modelClass)
|
||||
{
|
||||
$model = new $modelClass;
|
||||
$model->fill($data); // 根据字段填充数据
|
||||
try {
|
||||
$model->save();
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("创建记录失败: " . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新记录
|
||||
*/
|
||||
public function update(array $data, string $modelClass)
|
||||
{
|
||||
$record = $modelClass::find($data['id']);
|
||||
if (!$record) {
|
||||
throw new BusinessException("记录不存在", 404);
|
||||
}
|
||||
|
||||
$record->update($data);
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除记录
|
||||
*/
|
||||
public function delete(int $id, string $modelClass)
|
||||
{
|
||||
$record = $modelClass::find($id);
|
||||
if (!$record) {
|
||||
throw new BusinessException("记录不存在", 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$record->delete();
|
||||
} catch (\Throwable $e) {
|
||||
throw new BusinessException("删除记录失败: " . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ class CorpFeishuAppLogic
|
||||
{
|
||||
|
||||
|
||||
public function getFeishuApp($corpId)
|
||||
public function getFeishuApp($corpId, $systemId)
|
||||
{
|
||||
return CorpFeishuApp::where('corp_id', $corpId)->first(['id', 'corp_id', 'name', 'app_id', 'r_url', 'state']);
|
||||
return CorpFeishuApp::where('corp_id', $corpId)->where('system_id', $systemId)->first(['id', 'corp_id', 'name', 'app_id', 'r_url', 'state']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,10 +2,17 @@
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\Corp;
|
||||
|
||||
class CorpLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Corp::class;
|
||||
|
||||
public function getList()
|
||||
{
|
||||
|
||||
+162
-14
@@ -4,14 +4,17 @@ namespace app\common\logic;
|
||||
|
||||
use app\common\model\User;
|
||||
use app\common\model\Resource;
|
||||
use app\common\model\Systems;
|
||||
|
||||
class PermLogic
|
||||
{
|
||||
/**
|
||||
* 获取用户所有资源(合并角色+直接分配),并加入公共菜单,排序按sort字段
|
||||
*/
|
||||
public function getUserResources(int $userId)
|
||||
public function getUserResources(int $userId, string $systemCode)
|
||||
{
|
||||
$system = Systems::where('code', $systemCode)->first();
|
||||
|
||||
// 获取用户及其关联角色资源和直接分配的资源
|
||||
$user = User::with(['roles.resources', 'resources'])->find($userId);
|
||||
|
||||
@@ -24,9 +27,10 @@ class PermLogic
|
||||
// 合并角色资源,排除不需要权限认证的资源
|
||||
foreach ($user->roles as $role) {
|
||||
$all = $all->merge(
|
||||
$role->resources->filter(function ($resource) {
|
||||
$role->resources->filter(function ($resource) use($system) {
|
||||
// var_dump($resource->toArray());
|
||||
// 只保留需要权限认证的资源(或公共资源)
|
||||
return !$resource->no_auth;
|
||||
return !$resource->no_auth && $system->id == $resource->system_id;
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -52,14 +56,159 @@ class PermLogic
|
||||
return $all->unique('id')->values()->sortBy('sort');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建目录/菜单树,节点包含权限与打开方式,拼接完整路径,并对子级菜单component处理
|
||||
* @param \Illuminate\Support\Collection $resources
|
||||
* @param int $parentId
|
||||
* @param string $parentPath
|
||||
* 构建目录/菜单树
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $resources 全部资源集合
|
||||
* @param int $parentId 当前父级资源 ID
|
||||
* @param string $parentPath 父级完整路径(用于递归)
|
||||
* @return array
|
||||
*/
|
||||
public function buildTree_bak($resources, int $parentId = 0, string $parentPath = ''): array
|
||||
{
|
||||
$tree = [];
|
||||
|
||||
foreach ($resources as $node) {
|
||||
// 只处理目录和菜单,不把 permission 当作菜单节点
|
||||
if ($node->parent_id !== $parentId || $node->type === 'permission') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// —— 拼接当前节点的完整路径 ——
|
||||
$currentPath = rtrim($parentPath, '/');
|
||||
if (!empty($node->url)) {
|
||||
$urlSegment = ltrim($node->url, '/');
|
||||
$currentPath = $currentPath === ''
|
||||
? $urlSegment
|
||||
: "{$currentPath}/{$urlSegment}";
|
||||
}
|
||||
|
||||
// —— 递归构建子节点 ——
|
||||
// 这里只会继续收目录和菜单,permission 永远不会进入此方法
|
||||
$children = $this->buildTree($resources, $node->id, $currentPath);
|
||||
|
||||
// —— 本级权限列表 ——
|
||||
$authList = $resources
|
||||
->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id)
|
||||
->map(fn($r) => [
|
||||
'id' => $r->id,
|
||||
'title' => $r->name,
|
||||
'auth_mark' => $r->code,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
// —— 判断是否有子目录或子菜单 ——
|
||||
$hasChildrenMenu = ! empty($children);
|
||||
|
||||
// —— 计算 component 字段 ——
|
||||
if ($hasChildrenMenu) {
|
||||
if ($node->type === 'menu' && !empty($node->url)) {
|
||||
// menu 类型且自身有 url,用自身 url(加前导 slash)
|
||||
$componentValue = '/' . ltrim($node->url, '/');
|
||||
} else {
|
||||
// 其它含子项的节点,固定 Layout 组件
|
||||
$componentValue = '/index/index';
|
||||
}
|
||||
} else {
|
||||
// 叶子节点:拼接路径,确保以 slash 开头
|
||||
$componentValue = $currentPath !== ''
|
||||
? '/' . ltrim($currentPath, '/')
|
||||
: '';
|
||||
}
|
||||
|
||||
$tree[] = [
|
||||
'id' => $node->id,
|
||||
'name' => $node->name,
|
||||
'path' => $node->url,
|
||||
'component' => $componentValue,
|
||||
'meta' => [
|
||||
'title' => $node->name,
|
||||
'icon' => $node->icon,
|
||||
'keepAlive' => true,
|
||||
'authList' => $authList,
|
||||
'openType' => $node->open_type,
|
||||
'isHide' => $node->is_hide == 0 ? false : true,
|
||||
'isHideTab' => $node->is_hide_tab == 0 ? false : true
|
||||
],
|
||||
'children' => $children
|
||||
];
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
public function buildTree_bak2($resources, int $parentId = 0, string $parentPath = ''): array
|
||||
{
|
||||
$tree = [];
|
||||
|
||||
foreach ($resources as $node) {
|
||||
if ($node->parent_id !== $parentId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// —— 拼接当前节点的完整路径 ——
|
||||
$currentPath = $parentPath;
|
||||
// 去掉末尾多余 slash
|
||||
$currentPath = rtrim($currentPath, '/');
|
||||
|
||||
if (!empty($node->url)) {
|
||||
// 如果当前节点有 url,则拼接
|
||||
$urlSegment = ltrim($node->url, '/');
|
||||
$currentPath = $currentPath === ''
|
||||
? $urlSegment
|
||||
: "{$currentPath}/{$urlSegment}";
|
||||
}
|
||||
|
||||
// —— 递归构建子节点 ——
|
||||
$children = [];
|
||||
if ($node->type === 'directory') {
|
||||
$children = $this->buildTree($resources, $node->id, $currentPath);
|
||||
}
|
||||
|
||||
// —— 收集本级的 permission 权限列表 ——
|
||||
$authList = $resources
|
||||
->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id)
|
||||
->map(fn($r) => [
|
||||
'id' => $r->id,
|
||||
'title' => $r->name,
|
||||
'auth_mark' => $r->code,
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
// —— 判断是否有子菜单/目录 ——
|
||||
$hasChildrenMenu = $resources
|
||||
->where('parent_id', $node->id)
|
||||
->whereIn('type', ['directory', 'menu'])
|
||||
->isNotEmpty();
|
||||
|
||||
// —— 计算 component 字段 ——
|
||||
if ($hasChildrenMenu) {
|
||||
// 只要有子项,就一律显示顶层 Layout 组件
|
||||
$componentValue = '/index/index';
|
||||
} else {
|
||||
// 叶子节点:如果定义了 url,就使用拼接后的路径;否则用占位符(这里用空串,你可替换)
|
||||
$componentValue = $currentPath !== '' ? $currentPath : '';
|
||||
}
|
||||
|
||||
$tree[] = [
|
||||
'id' => $node->id,
|
||||
'name' => $node->name,
|
||||
'path' => $node->url,
|
||||
'component' => $componentValue,
|
||||
'meta' => [
|
||||
'title' => $node->name,
|
||||
'icon' => $node->icon,
|
||||
'keepAlive' => true,
|
||||
'authList' => $authList,
|
||||
'openType' => $node->open_type,
|
||||
],
|
||||
'children' => $children,
|
||||
];
|
||||
}
|
||||
|
||||
return $tree;
|
||||
}
|
||||
public function buildTree($resources, int $parentId = 0, string $parentPath = ''): array
|
||||
{
|
||||
$tree = [];
|
||||
@@ -99,10 +248,7 @@ class PermLogic
|
||||
$hasChildrenMenu = collect($resources)->where('parent_id', $node->id)
|
||||
->whereIn('type', ['directory', 'menu'])->isNotEmpty();
|
||||
|
||||
// component 字段规则:
|
||||
// 1. 存在子菜单,固定 '/index/index'
|
||||
// 2. 否则为当前完整路径(或其它你想要的)
|
||||
$componentValue = ($hasChildrenMenu && !$node->url && $node->type !== 'directory') ? '/index/index' : $currentPath;
|
||||
$componentValue = $hasChildrenMenu ? '/index/index' : $currentPath;
|
||||
|
||||
$tree[] = [
|
||||
'id' => $node->id,
|
||||
@@ -115,6 +261,8 @@ class PermLogic
|
||||
'keepAlive' => true,
|
||||
'authList' => $authList,
|
||||
'openType' => $node->open_type,
|
||||
'isHide' => $node->is_hide == 0 ? false : true,
|
||||
'isHideTab' => $node->is_hide_tab == 0 ? false : true
|
||||
],
|
||||
'children' => $children,
|
||||
];
|
||||
@@ -125,9 +273,9 @@ class PermLogic
|
||||
/**
|
||||
* 获取用户所有权限code
|
||||
*/
|
||||
public function getPermissionCodes(int $userId): array
|
||||
public function getPermissionCodes(int $userId, string $systemCode): array
|
||||
{
|
||||
$resources = $this->getUserResources($userId);
|
||||
$resources = $this->getUserResources($userId, $systemCode);
|
||||
return $resources
|
||||
->filter(fn($res) => $res->type === 'permission')
|
||||
->pluck('code')
|
||||
|
||||
@@ -8,86 +8,7 @@ use support\Log;
|
||||
|
||||
class RoleLogic
|
||||
{
|
||||
/**
|
||||
* 获取角色列表
|
||||
*/
|
||||
public function getRoleList(array $params)
|
||||
{
|
||||
// 假设我们需要根据system_id进行过滤,其他过滤条件也可以添加
|
||||
$query = Role::query();
|
||||
|
||||
|
||||
if (isset($params['system_id'])) {
|
||||
$query->where('system_id', $params['system_id']);
|
||||
}
|
||||
if (isset($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
// 添加分页
|
||||
$roles = $query->paginate($params['per_page'] ?? 15);
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色详情
|
||||
*/
|
||||
public function getRoleDetail(int $roleId)
|
||||
{
|
||||
$role = Role::find($roleId);
|
||||
if (!$role) {
|
||||
throw new BusinessException('角色不存在', 404);
|
||||
}
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
*/
|
||||
public function createRole(array $data)
|
||||
{
|
||||
$role = new Role();
|
||||
$role->fill($data); // 根据字段填充数据
|
||||
try {
|
||||
$role->save();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('创建角色失败:'.$e->getMessage());
|
||||
throw new BusinessException('创建角色失败', 500);
|
||||
}
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
public function updateRole(array $data)
|
||||
{
|
||||
$role = Role::find($data['id']);
|
||||
if (!$role) {
|
||||
throw new BusinessException('角色不存在', 404);
|
||||
}
|
||||
|
||||
$role->update($data); // 更新字段
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
public function deleteRole(int $roleId)
|
||||
{
|
||||
$role = Role::find($roleId);
|
||||
if (!$role) {
|
||||
throw new BusinessException('角色不存在', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$role->delete();
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('删除角色失败:'.$e->getMessage());
|
||||
throw new BusinessException('删除角色失败', 500);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\Systems;
|
||||
|
||||
class SystemsLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Systems::class;
|
||||
|
||||
public function getList()
|
||||
{
|
||||
return Systems::where('status', 1)->orderBy('sort', 'asc')->get(['id', 'name', 'code', 'type', 'type']);
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,48 @@ class UserLogic
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户信息
|
||||
*/
|
||||
public function edit(int $uid, array $data)
|
||||
{
|
||||
$user = User::find($uid);
|
||||
|
||||
if (!$user) {
|
||||
throw new BusinessException('账户不存在', 404);
|
||||
}
|
||||
|
||||
$user->fill($data);
|
||||
$user->save();
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
public function savePassword(int $uid, array $data)
|
||||
{
|
||||
$password = password_hash($data['password_new'], PASSWORD_DEFAULT);
|
||||
$res = User::where('id',$uid)->update(['password' => $password]);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
$list = User::where('status', 1)->orderBy('id', 'desc')->get();
|
||||
$arr = [];
|
||||
foreach ($list->toArray() as $item) {
|
||||
$arr[] = [
|
||||
'name' => $item['name'],
|
||||
'id' => $item['id'],
|
||||
];
|
||||
}
|
||||
return $arr;
|
||||
}
|
||||
}
|
||||
@@ -46,4 +46,14 @@ class Corp extends Model
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
}
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'abbr',
|
||||
'tenant_key',
|
||||
'sort',
|
||||
'status'
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class Country extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'country';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'cn_addr', 'en_addr', 'en_full_name', 'two_letter', 'three_letter', 'num_code', 'steering', 'c_time', 'u_time'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class CrmCustomer extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'crm_customer';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'sn', 'corp_abbr', 'user_id', 'user_sn', 'type', 'grade', 'sex', 'channel_id', 'country_id', 'car_type_id', 'magnitude', 'email', 'phone', 'WhatsApp', 'contacts', 'describe', 'desc_collect', 'address', 'lat', 'lng', 'follow_last', 'follow_next', 'warn', 'follow_remark', 'enter_user_id', 'created_at', 'updated_at', 'deleted_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class InquiryChannel extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inquiry_channel';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'name', 'sort', 'status', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
use app\common\model\User;
|
||||
use app\common\model\Country;
|
||||
|
||||
class InquiryLog extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inquiry_log';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = [
|
||||
'id', 'order_no', 'enter_user_id', 'recipient',
|
||||
'quality', 'channel', 'email', 'tel', 'whatsapp',
|
||||
'country', 'contacts', 'content', 'status',
|
||||
'r_time', 't_time', 'created_at', 'updated_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
|
||||
|
||||
public function enter()
|
||||
{
|
||||
return $this->hasOne(User::class, 'id', 'enter_user_id');
|
||||
}
|
||||
|
||||
public function recipient()
|
||||
{
|
||||
return $this->hasOne(User::class, 'id', 'recipient');
|
||||
}
|
||||
|
||||
public function channelInfo()
|
||||
{
|
||||
return $this->hasOne(InquiryChannel::class, 'id', 'channel');
|
||||
}
|
||||
|
||||
public function countryInfo()
|
||||
{
|
||||
return $this->hasOne(Country::class, 'id', 'country');
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,14 @@ class Role extends Model
|
||||
public $timestamps = true;
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
protected $fillable = [
|
||||
'system_id',
|
||||
'name',
|
||||
'desc',
|
||||
'sort',
|
||||
'status'
|
||||
];
|
||||
|
||||
// 角色下所有用户
|
||||
public function users()
|
||||
@@ -53,6 +61,6 @@ class Role extends Model
|
||||
// 角色拥有的资源(权限点、菜单)
|
||||
public function resources()
|
||||
{
|
||||
return $this->belongsToMany(Resource::class, 'role_resources', 'role_id', 'resource_id')->orderBy('sort', 'asc');
|
||||
return $this->belongsToMany(Resource::class, 'role_resources', 'role_id', 'resource_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,6 @@ namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* systems 系统/应用表
|
||||
* @property integer $id 系统ID(主键)
|
||||
* @property string $code 系统编码(唯一英文标识)
|
||||
* @property string $name 系统名称
|
||||
* @property string $type 系统类型
|
||||
* @property string $desc 系统描述
|
||||
* @property string $icon 系统图标
|
||||
* @property string $domain_url 系统URL
|
||||
* @property string $is_role 是否区分角色权限
|
||||
* @property integer $sort 排序
|
||||
* @property integer $status 状态
|
||||
* @property integer $created_at 创建时间
|
||||
* @property integer $updated_at 更新时间
|
||||
*/
|
||||
class Systems extends Model
|
||||
{
|
||||
/**
|
||||
@@ -50,15 +35,15 @@ class Systems extends Model
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
// 系统下的资源
|
||||
public function resources()
|
||||
{
|
||||
return $this->hasMany(Resource::class, 'system_id', 'id');
|
||||
}
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
// 拥有该系统访问权限的用户
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'user_systems', 'system_id', 'user_id');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = ['id', 'code', 'name', 'type', 'desc', 'icon', 'domain_url', 'is_role', 'sort', 'status', 'created_at', 'updated_at'];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -54,6 +54,7 @@ class User extends Model
|
||||
'sn',
|
||||
'name',
|
||||
'avatar',
|
||||
'sex',
|
||||
'password',
|
||||
'phone',
|
||||
'email',
|
||||
@@ -63,7 +64,11 @@ class User extends Model
|
||||
'union_id',
|
||||
'user_id',
|
||||
'open_id',
|
||||
'tenant_key'
|
||||
'tenant_key',
|
||||
'country_id',
|
||||
'position',
|
||||
'socialize',
|
||||
'socialize_pic',
|
||||
];
|
||||
|
||||
// 用户所有角色
|
||||
@@ -91,4 +96,4 @@ class User extends Model
|
||||
->orWhere('phone', $account)
|
||||
->orWhere('email', $account);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
/**
|
||||
* user_feishu_app 用户-飞书应用
|
||||
* @property integer $id ID(主键)
|
||||
* @property integer $uid 用户ID
|
||||
* @property integer $corp_id 公司ID
|
||||
* @property string $name 名称
|
||||
* @property string $mobile 手机
|
||||
* @property string $union_id ISV的唯一标识
|
||||
* @property string $user_id 用户 user_id
|
||||
* @property string $open_id 应用内的唯一标识
|
||||
* @property string $tenant_key 当前企业标识
|
||||
* @property integer $sort 排序
|
||||
* @property integer $status 状态
|
||||
* @property integer $created_at 创建时间
|
||||
* @property integer $updated_at 更新时间
|
||||
*/
|
||||
class UserFeishuApp_bak extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'user_feishu_app';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
|
||||
protected $fillable = [
|
||||
'uid',
|
||||
'corp_id',
|
||||
'name',
|
||||
'mobile',
|
||||
'union_id',
|
||||
'user_id',
|
||||
'open_id',
|
||||
'tenant_key',
|
||||
'sort',
|
||||
'status'
|
||||
];
|
||||
}
|
||||
@@ -86,13 +86,4 @@ class AuthService
|
||||
{
|
||||
(new AuthLogic())->blockToken($token, $systemCode);
|
||||
}
|
||||
|
||||
|
||||
// public function perms()
|
||||
// {
|
||||
// $permService = new PermService();
|
||||
// $menuTree = $permService->getMenuTree($uid);
|
||||
// $permissionCodes = $permService->getPermissionCodes($uid);
|
||||
// return (new PermService())->getMenuTree($this->userId)
|
||||
// }
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\AuthLogic;
|
||||
use app\common\model\User;
|
||||
use support\Cache;
|
||||
|
||||
class AuthService_bak
|
||||
{
|
||||
// 缓存 Key
|
||||
protected static function cacheKey(int $userId): string
|
||||
{
|
||||
return "user_resources_{$userId}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有资源(目录/菜单/权限),合并角色+直接分配,并缓存
|
||||
*/
|
||||
public static function getUserResources(int $userId)
|
||||
{
|
||||
$cacheKey = self::cacheKey($userId);
|
||||
$cached = Cache::get($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$user = User::with(['roles.resources', 'resources'])->find($userId);
|
||||
if (!$user) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// 合并:角色资源 + 用户直接资源
|
||||
$all = collect();
|
||||
foreach ($user->roles as $role) {
|
||||
$all = $all->merge($role->resources);
|
||||
}
|
||||
$all = $all->merge($user->resources);
|
||||
$unique = $all->unique('id')->values();
|
||||
|
||||
Cache::set($cacheKey, $unique, 3600);
|
||||
return $unique;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建目录/菜单树,并给每个菜单节点挂载权限列表和打开方式
|
||||
*/
|
||||
public static function getMenuTree(int $userId): array
|
||||
{
|
||||
$resources = self::getUserResources($userId);
|
||||
$menus = $resources
|
||||
->filter(fn($r) => in_array($r->type, ['directory','menu']))
|
||||
->values();
|
||||
return self::buildTree($menus, $resources);
|
||||
}
|
||||
|
||||
protected static function buildTree($nodes, $allResources, int $parentId = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($nodes as $node) {
|
||||
if ($node->parent_id !== $parentId) continue;
|
||||
|
||||
// 子节点
|
||||
$children = self::buildTree($nodes, $allResources, $node->id);
|
||||
|
||||
// 本级权限列表
|
||||
$authList = $allResources
|
||||
->filter(fn($r) => $r->type === 'permission' && $r->parent_id === $node->id)
|
||||
->map(fn($r) => [
|
||||
'id' => $r->id,
|
||||
'title' => $r->name,
|
||||
'auth_mark' => $r->code,
|
||||
])->values()->all();
|
||||
|
||||
$tree[] = [
|
||||
'id' => $node->id,
|
||||
'name' => $node->name,
|
||||
'path' => $node->url,
|
||||
'meta' => [
|
||||
'title' => $node->name,
|
||||
'icon' => $node->icon,
|
||||
'keepAlive' => true,
|
||||
'authList' => $authList,
|
||||
// 新增打开方式字段
|
||||
'openType' => $node->open_type, // 'internal' | 'external' | 'iframe'
|
||||
],
|
||||
'children' => $children,
|
||||
];
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有权限 code(仅 type=permission)
|
||||
*/
|
||||
public static function getPermissionCodes(int $userId): array
|
||||
{
|
||||
return self::getUserResources($userId)
|
||||
->filter(fn($res) => $res->type === 'permission')
|
||||
->pluck('code')
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/** 清除缓存(角色/资源变更时调用) */
|
||||
public static function clearCache(int $userId): void
|
||||
{
|
||||
Cache::delete(self::cacheKey($userId));
|
||||
}
|
||||
|
||||
public function login(array $data): array
|
||||
{
|
||||
$loginType = $data['login_type'] ?? 'upwd';
|
||||
$logic = new AuthLogic();
|
||||
|
||||
return match ($loginType) {
|
||||
'feishu' => $logic->feishuLogin($data),
|
||||
'qrcode' => $logic->qrCodeLogin($data),
|
||||
default => $logic->uPwdLogin($data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param array $data
|
||||
* @return User
|
||||
*/
|
||||
public function register(array $data): User
|
||||
{
|
||||
return (new AuthLogic())->register($data);
|
||||
}
|
||||
|
||||
|
||||
public function verifyToken(string $token): array
|
||||
{
|
||||
return (new AuthLogic())->verifyToken($token);
|
||||
}
|
||||
|
||||
public function refreshToken(string $refreshToken): array
|
||||
{
|
||||
return (new AuthLogic())->refreshToken($refreshToken);
|
||||
}
|
||||
|
||||
public function logout(?string $token)
|
||||
{
|
||||
(new AuthLogic())->logout($token);
|
||||
}
|
||||
|
||||
public function blockToken(string $token)
|
||||
{
|
||||
(new AuthLogic())->blockToken($token);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class BasicDataService
|
||||
public function checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr)
|
||||
{
|
||||
if (!$loginType && !$systemCode && !$corpAbbr) {
|
||||
throw new BusinessException(htmlpage());
|
||||
throw new BusinessException('header缺少必要参数');
|
||||
}
|
||||
|
||||
// 检查并缓存登录类型
|
||||
|
||||
@@ -14,8 +14,8 @@ class CorpFeishuAppService
|
||||
$this->logic = (new CorpFeishuAppLogic());
|
||||
}
|
||||
|
||||
public function getFeishuApp(int $corpId)
|
||||
public function getFeishuApp(int $corpId, int $systemId)
|
||||
{
|
||||
return $this->logic->getFeishuApp($corpId);
|
||||
return $this->logic->getFeishuApp($corpId, $systemId);
|
||||
}
|
||||
}
|
||||
@@ -15,23 +15,23 @@ class PermService
|
||||
}
|
||||
|
||||
// 用户资源缓存key
|
||||
protected static function cacheKey(int $userId): string
|
||||
protected static function cacheKey(int $userId, string $systemCode): string
|
||||
{
|
||||
return "user_resources_{$userId}";
|
||||
return "user_resources_{$userId}_{$systemCode}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有资源(合并角色+直接分配),结果缓存
|
||||
*/
|
||||
public function getUserResources(int $userId)
|
||||
public function getUserResources(int $userId, string $systemCode)
|
||||
{
|
||||
$cacheKey = self::cacheKey($userId);
|
||||
$cacheKey = self::cacheKey($userId, $systemCode);
|
||||
$cached = Cache::get($cacheKey);
|
||||
if ($cached !== null) {
|
||||
return $cached;
|
||||
}
|
||||
// 调用 Logic 层获取资源
|
||||
$resources = $this->logic->getUserResources($userId);
|
||||
$resources = $this->logic->getUserResources($userId, $systemCode);
|
||||
// 缓存结果
|
||||
Cache::set($cacheKey, $resources, 3600);
|
||||
return $resources;
|
||||
@@ -40,26 +40,26 @@ class PermService
|
||||
/**
|
||||
* 构建目录/菜单树,节点包含权限与打开方式
|
||||
*/
|
||||
public function getMenuTree(int $userId): array
|
||||
public function getMenuTree(int $userId, string $systemCode): array
|
||||
{
|
||||
// 调用 Logic 层处理菜单构建
|
||||
$resources = $this->getUserResources($userId);
|
||||
$resources = $this->getUserResources($userId, $systemCode);
|
||||
return $this->logic->buildTree($resources);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户所有权限code
|
||||
*/
|
||||
public function getPermissionCodes(int $userId): array
|
||||
public function getPermissionCodes(int $userId, string $systemCode): array
|
||||
{
|
||||
return $this->logic->getPermissionCodes($userId);
|
||||
return $this->logic->getPermissionCodes($userId, $systemCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除资源相关缓存
|
||||
*/
|
||||
public function clearCache(int $userId): void
|
||||
public function clearCache(int $userId, string $systemCode): void
|
||||
{
|
||||
Cache::delete(self::cacheKey($userId));
|
||||
Cache::delete(self::cacheKey($userId, $systemCode));
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\RoleLogic;
|
||||
|
||||
class RoleService
|
||||
{
|
||||
protected $roleLogic;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->roleLogic = (new RoleLogic());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色列表
|
||||
*/
|
||||
public function getRoleList(array $params)
|
||||
{
|
||||
return $this->roleLogic->getRoleList($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色详情
|
||||
*/
|
||||
public function getRoleDetail(int $roleId)
|
||||
{
|
||||
return $this->roleLogic->getRoleDetail($roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
*/
|
||||
public function createRole(array $data)
|
||||
{
|
||||
return $this->roleLogic->createRole($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
public function updateRole(array $data)
|
||||
{
|
||||
return $this->roleLogic->updateRole($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
public function deleteRole(int $roleId)
|
||||
{
|
||||
return $this->roleLogic->deleteRole($roleId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\SystemsLogic;
|
||||
|
||||
class SystemsService
|
||||
{
|
||||
|
||||
protected $logic;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logic = (new SystemsLogic());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统list
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
return $this->logic->getList();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,11 @@
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\logic\UserLogic;
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\User;
|
||||
use app\common\utils\CrudUtil;
|
||||
use app\crm\model\Country;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class UserService
|
||||
{
|
||||
@@ -17,12 +22,12 @@ class UserService
|
||||
/**
|
||||
* 获取用户信息(包括菜单和权限)
|
||||
*/
|
||||
public function getUserInfo(int $uid)
|
||||
public function getUserInfo(int $uid, string $systemCode)
|
||||
{
|
||||
$user = $this->logic->userInfo($uid);
|
||||
$permService = new PermService();
|
||||
$menuTree = $permService->getMenuTree($uid);
|
||||
$permissionCodes = $permService->getPermissionCodes($uid);
|
||||
$menuTree = $permService->getMenuTree($uid, $systemCode);
|
||||
$permissionCodes = $permService->getPermissionCodes($uid, $systemCode);
|
||||
return [
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
@@ -37,4 +42,70 @@ class UserService
|
||||
'permissions' => $permissionCodes
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户信息(包括菜单和权限)
|
||||
*/
|
||||
public function getUser(int $uid, $corp_abbr)
|
||||
{
|
||||
$user = $this->logic->userInfo($uid);
|
||||
$corp_name = Corp::where('abbr', $corp_abbr)->value('name');
|
||||
$ossservice = new OssService();
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'position' => $user->position,
|
||||
'socialize' => $user->socialize,
|
||||
'socialize_pic' => $user->socialize_pic,
|
||||
'socialize_pic_full' => (!empty($user->socialize_pic))?$ossservice->generatePrivateUrl($user->socialize_pic):'',
|
||||
'country_id' => $user->country_id,
|
||||
'country_txt' => Country::where('id',$user->country_id)->value('cn_addr'),
|
||||
'avatar' => $user->avatar,
|
||||
'avatar_full' => (!empty($user->avatar))?$ossservice->generatePrivateUrl($user->avatar):'',
|
||||
'phone' => $user->phone,
|
||||
'corp_name' => $corp_name,
|
||||
'email' => $user->email,
|
||||
'status' => $user->status,
|
||||
'roles' => $user->roles->pluck('name')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑用户信息
|
||||
*/
|
||||
public function edit(int $uid, array $data)
|
||||
{
|
||||
unset($data['password']);
|
||||
unset($data['phone']);
|
||||
unset($data['email']);
|
||||
unset($data['sn']);
|
||||
|
||||
$user = $this->logic->edit($uid,$data);
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改用户密码
|
||||
*/
|
||||
public function savePassword(int $uid, array $data)
|
||||
{
|
||||
// 对密码进行哈希处理
|
||||
$password = $data['password'];
|
||||
$user = User::find($uid);
|
||||
if (!password_verify($password, $user->password)) {
|
||||
throw new BusinessException('密码错误');
|
||||
}
|
||||
|
||||
$user = $this->logic->savePassword($uid,$data);
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
public function getList()
|
||||
{
|
||||
return $this->logic->getList();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\traits;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* 数据转换器 Trait
|
||||
* 用于将原始数据(如模型、集合、分页等)统一转换为数组格式,
|
||||
* 并支持字段级与结果级的灵活后置加工。
|
||||
*/
|
||||
trait DataTransformer
|
||||
{
|
||||
/**
|
||||
* 核心方法:对任意数据进行转换并返回数组
|
||||
* 核心转换方法
|
||||
*
|
||||
* @param mixed $rawData 原始数据,可为 Paginator / Model / Collection / array 等
|
||||
* @param callable|null $postProcessor 可选的后置处理回调,接受处理完的数组再返回新的数组
|
||||
* @param callable|null $fieldFormatter 可选的字段命名转换回调,如 snake_case
|
||||
* @param mixed $rawData 原始数据:Model、Collection、Paginator、数组等
|
||||
* @param array $opts 可选配置:
|
||||
* - 'fieldFormatter' => callable 字段名格式化回调,如 fn($f)=>snake_case($f)
|
||||
* - 'postFormatter' => callable 结果级加工回调,接收整个数组并返回
|
||||
*
|
||||
* @return array
|
||||
* @return array 转换并加工后的纯 PHP 数组
|
||||
*/
|
||||
protected function transformData(
|
||||
mixed $rawData,
|
||||
?callable $postProcessor = null,
|
||||
?callable $postFormatter = null,
|
||||
?callable $fieldFormatter = null
|
||||
): array {
|
||||
try {
|
||||
$fieldFormatter = $fieldFormatter ?: fn($field) => $field;
|
||||
|
||||
if ($rawData instanceof LengthAwarePaginator) {
|
||||
$formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter);
|
||||
$arr = $this->formatPaginated($rawData, $postFormatter, $fieldFormatter);
|
||||
} elseif ($rawData instanceof Model) {
|
||||
$formatted = $this->formatModel($rawData, $fieldFormatter);
|
||||
$arr = $this->formatModel($rawData, $fieldFormatter);
|
||||
} elseif ($rawData instanceof Collection) {
|
||||
$formatted = $this->formatCollection($rawData, $fieldFormatter);
|
||||
$arr = $this->formatCollection($rawData, $fieldFormatter);
|
||||
} elseif (is_array($rawData)) {
|
||||
$formatted = $this->formatArray($rawData, $fieldFormatter);
|
||||
$arr = $this->formatArray($rawData, $fieldFormatter);
|
||||
} else {
|
||||
$formatted = (array) $rawData;
|
||||
$arr = (array)$rawData;
|
||||
}
|
||||
|
||||
if (is_callable($postProcessor)) {
|
||||
$formatted = $postProcessor($formatted);
|
||||
if (is_callable($postFormatter)) {
|
||||
$arr = $postFormatter($arr);
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
return $arr;
|
||||
} catch (\Throwable $e) {
|
||||
return [
|
||||
'error' => true,
|
||||
@@ -52,84 +56,96 @@ trait DataTransformer
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化分页数据:将每条记录也进行 transformData 递归处理
|
||||
* 格式化分页数据:转换每条记录
|
||||
*
|
||||
* @param LengthAwarePaginator $paginator
|
||||
* @param callable|null $postProcessor
|
||||
* @param callable $fieldFormatter
|
||||
* @return array
|
||||
* @param LengthAwarePaginator $p
|
||||
* @param callable $fmt
|
||||
* @return array{records: array, total: int, current: int, size: int}
|
||||
*/
|
||||
protected function formatPaginated(
|
||||
LengthAwarePaginator $paginator,
|
||||
?callable $postProcessor,
|
||||
callable $fieldFormatter
|
||||
LengthAwarePaginator $p,
|
||||
?callable $postFormatter,
|
||||
callable $fmt
|
||||
): array {
|
||||
$list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) {
|
||||
return $this->transformData($item, $postProcessor, $fieldFormatter);
|
||||
})->toArray();
|
||||
$records = $p->getCollection()
|
||||
->map(fn($item) => $this->transformData($item, $postFormatter, $fmt))
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'records' => $list,
|
||||
'total' => $paginator->total(),
|
||||
'current' => $paginator->currentPage(),
|
||||
'size' => $paginator->perPage()
|
||||
'records' => $records,
|
||||
'total' => $p->total(),
|
||||
'current' => $p->currentPage(),
|
||||
'size' => $p->perPage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 Eloquent 模型:递归嵌套关联
|
||||
* 格式化 Eloquent 模型:主表属性 + 关联递归
|
||||
*
|
||||
* @param Model $model
|
||||
* @param callable $fieldFormatter
|
||||
* @param callable $fmt
|
||||
* @return array
|
||||
*/
|
||||
protected function formatModel(Model $model, callable $fieldFormatter): array
|
||||
{
|
||||
$formatted = [];
|
||||
|
||||
// 主表字段
|
||||
protected function formatModel(
|
||||
Model $model,
|
||||
callable $fmt
|
||||
): array {
|
||||
$out = [];
|
||||
// 1) 主表字段
|
||||
foreach ($model->getAttributes() as $key => $value) {
|
||||
$formatted[$fieldFormatter($key)] = $value;
|
||||
$out[$fmt($key)] = $value;
|
||||
}
|
||||
|
||||
// 关联数据,递归嵌套
|
||||
foreach ($model->getRelations() as $relation => $relationData) {
|
||||
if ($relationData instanceof Model) {
|
||||
// 单个关联模型
|
||||
$formatted[$relation] = $this->formatModel($relationData, $fieldFormatter);
|
||||
} elseif ($relationData instanceof Collection) {
|
||||
// 关联集合
|
||||
$formatted[$relation] = $relationData
|
||||
->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
|
||||
// 2) 关联递归
|
||||
foreach ($model->getRelations() as $rel => $relData) {
|
||||
if ($relData instanceof Model) {
|
||||
$out[$rel] = $this->formatModel($relData, $fmt);
|
||||
} elseif ($relData instanceof Collection) {
|
||||
$out[$rel] = $relData
|
||||
->map(fn($item) => $this->transformData($item, null, $fmt))
|
||||
->toArray();
|
||||
} else {
|
||||
$out[$rel] = $relData;
|
||||
}
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化集合
|
||||
* 格式化集合数据:对每个元素递归调用 transformData
|
||||
*
|
||||
* @param Collection $collection
|
||||
* @param callable $fieldFormatter
|
||||
* @param Collection $c
|
||||
* @param callable $fmt
|
||||
* @return array
|
||||
*/
|
||||
protected function formatCollection(Collection $collection, callable $fieldFormatter): array
|
||||
{
|
||||
return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter))
|
||||
->toArray();
|
||||
protected function formatCollection(
|
||||
Collection $c,
|
||||
callable $fmt
|
||||
): array {
|
||||
return $c
|
||||
->map(fn($item) => $this->transformData($item, null, $fmt))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化普通数组
|
||||
* 格式化普通数组:递归处理可能出现的模型或集合
|
||||
*
|
||||
* @param array $array
|
||||
* @param callable $fieldFormatter
|
||||
* @param array $arr
|
||||
* @param callable $fmt
|
||||
* @return array
|
||||
*/
|
||||
protected function formatArray(array $array, callable $fieldFormatter): array
|
||||
{
|
||||
return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array);
|
||||
protected function formatArray(
|
||||
array $arr,
|
||||
callable $fmt
|
||||
): array {
|
||||
return array_map(function($item) use ($fmt) {
|
||||
if (is_array($item)
|
||||
|| $item instanceof Model
|
||||
|| $item instanceof Collection
|
||||
|| $item instanceof LengthAwarePaginator
|
||||
) {
|
||||
return $this->transformData($item, null, $fmt);
|
||||
}
|
||||
return $item;
|
||||
}, $arr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
namespace app\common\traits;
|
||||
|
||||
use support\Request;
|
||||
|
||||
trait FormatKvTrait
|
||||
{
|
||||
/**
|
||||
* 主入口:一行把前端扁平化参数变成完整的查询配置
|
||||
*
|
||||
* @param Request $request
|
||||
* @param string $modelClass Eloquent 模型类名
|
||||
* @return array{
|
||||
* model: string,
|
||||
* where: array,
|
||||
* order: array<string,string>,
|
||||
* fields: string[],
|
||||
* include: string[]
|
||||
* }
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $modelClass): array
|
||||
{
|
||||
$all = $request->all();
|
||||
|
||||
return [
|
||||
'model' => $modelClass,
|
||||
'where' => $this->parseFilters($all),
|
||||
'order' => $this->parseSort($all['sort'] ?? ''),
|
||||
'fields' => $this->parseCsv($all['fields'] ?? ''),
|
||||
'include' => $this->parseCsv($all['include'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 把除 sort/fields/include/page/limit 以外的所有参数
|
||||
* 智能转换为 where 条件
|
||||
*
|
||||
* 支持:
|
||||
* - 等值:foo=bar
|
||||
* - NOT:foo=!bar 或 foo=!bar1,bar2
|
||||
* - 范围:foo=2025-07-01,2025-07-15 → between
|
||||
* - 列表:foo=a,b,c → in
|
||||
* - NULL:foo=null
|
||||
* - NOT NULL:foo=!null
|
||||
* - 通配符:foo=xia* → like 'xia%'
|
||||
*
|
||||
* 关联字段直接写 relation.field=xxx
|
||||
*/
|
||||
protected function parseFilters(array $params): array
|
||||
{
|
||||
$where = [];
|
||||
$reserved = ['sort','fields','include','page','limit'];
|
||||
|
||||
foreach ($params as $key => $val) {
|
||||
if (in_array($key, $reserved, true)) {
|
||||
continue;
|
||||
}
|
||||
// NULL / NOT NULL
|
||||
if ($val === 'null') {
|
||||
$where[$key] = ['is null'];
|
||||
continue;
|
||||
}
|
||||
if ($val === '!null') {
|
||||
$where[$key] = ['is not null'];
|
||||
continue;
|
||||
}
|
||||
// NOT equal or NOT IN
|
||||
if (str_starts_with($val, '!')) {
|
||||
$v = substr($val, 1);
|
||||
if (str_contains($v, ',')) {
|
||||
$list = array_map('trim', explode(',', $v));
|
||||
$where[$key] = ['not in', $list];
|
||||
} else {
|
||||
$where[$key] = ['<>', $this->cast($v)];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// BETWEEN or IN
|
||||
if (str_contains($val, ',')) {
|
||||
$parts = array_map('trim', explode(',', $val));
|
||||
if (count($parts) === 2 && $this->isRangeValue($parts[0], $parts[1])) {
|
||||
$where[$key] = ['between', $parts];
|
||||
} else {
|
||||
$where[$key] = ['in', $this->castArray($parts)];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// LIKE
|
||||
if (str_contains($val, '*') || str_contains($val, '%')) {
|
||||
$pattern = str_replace('*', '%', $val);
|
||||
$where[$key] = ['like', $pattern];
|
||||
continue;
|
||||
}
|
||||
// 默认 EQ
|
||||
$where[$key] = $this->cast($val);
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 sort=id,-created_at → ['id'=>'asc','created_at'=>'desc']
|
||||
*/
|
||||
protected function parseSort(string $sort): array
|
||||
{
|
||||
$order = [];
|
||||
foreach (explode(',', $sort) as $piece) {
|
||||
$f = trim($piece);
|
||||
if ($f === '') {
|
||||
continue;
|
||||
}
|
||||
if ($f[0] === '-') {
|
||||
$order[substr($f, 1)] = 'desc';
|
||||
} else {
|
||||
$order[$f] = 'asc';
|
||||
}
|
||||
}
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* 逗号分割成数组
|
||||
*/
|
||||
protected function parseCsv(string $csv): array
|
||||
{
|
||||
if ($csv === '') {
|
||||
return [];
|
||||
}
|
||||
return array_filter(array_map('trim', explode(',', $csv)));
|
||||
}
|
||||
|
||||
/** 简单类型转换:全数字→int/float,其余保留原字符串 */
|
||||
protected function cast(string $v): mixed
|
||||
{
|
||||
if (is_numeric($v)) {
|
||||
return strpos($v, '.') !== false ? (float)$v : (int)$v;
|
||||
}
|
||||
return $v;
|
||||
}
|
||||
|
||||
/** 批量类型转换 */
|
||||
protected function castArray(array $arr): array
|
||||
{
|
||||
return array_map(fn($v) => $this->cast($v), $arr);
|
||||
}
|
||||
|
||||
/** 判断两端是否可做 BETWEEN(数字或 YYYY-MM-DD 日期) */
|
||||
protected function isRangeValue(string $a, string $b): bool
|
||||
{
|
||||
return (is_numeric($a) && is_numeric($b))
|
||||
|| (preg_match('/^\d{4}-\d{2}-\d{2}$/', $a)
|
||||
&& preg_match('/^\d{4}-\d{2}-\d{2}$/', $b));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\traits;
|
||||
|
||||
use think\Validate;
|
||||
use support\Request;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
trait RequestTrait
|
||||
{
|
||||
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
*
|
||||
* @param Validate $validator 验证器实例
|
||||
* @param array $data 待验证的数据
|
||||
* @param string $scene 验证场景
|
||||
* @return array 验证后的数据
|
||||
* @throws BusinessException 验证失败时抛出业务异常
|
||||
*/
|
||||
public function validate(Validate $validator, array $data, string $scene = ''): array
|
||||
{
|
||||
// 如果指定了验证场景,并且验证器支持场景方法,则设置验证场景
|
||||
if ($scene && method_exists($validator, 'scene')) {
|
||||
$validator->scene($scene);
|
||||
}
|
||||
// 执行数据验证
|
||||
if (!$validator->check($data)) {
|
||||
// 验证失败,抛出业务异常,异常信息为验证器返回的错误信息
|
||||
throw new BusinessException($validator->getError());
|
||||
}
|
||||
// 验证成功,返回原始数据
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\traits\crud;
|
||||
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* CRUD 操作控制器 Trait
|
||||
* 提供通用的增删改查控制器方法,通过调用 service 层实现业务逻辑解耦。
|
||||
*/
|
||||
trait CrudControllerTrait
|
||||
{
|
||||
/**
|
||||
* 获取列表
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回分页数据及总数
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
try {
|
||||
$raw = $this->service->serviceList($request);
|
||||
$data = $this->transformData($raw, method_exists($this, 'indexFormat')
|
||||
? [$this, 'indexFormat']
|
||||
: null);
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @param int|string $id 资源 ID
|
||||
* @return mixed 返回资源详情数据
|
||||
*/
|
||||
public function show(Request $request)
|
||||
{
|
||||
try {
|
||||
$raw = $this->service->serviceShow($request);
|
||||
$data = $this->transformData($raw, method_exists($this, 'showFormat')
|
||||
? [$this, 'showFormat']
|
||||
: null);
|
||||
return $this->success($data);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回创建成功的数据或信息
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'store');
|
||||
$res = $this->service->serviceStore($request, $data);
|
||||
return $this->success($res, '创建成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回更新成功的数据或信息
|
||||
*/
|
||||
public function update(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'update');
|
||||
$res = $this->service->serviceUpdate($request, $data);
|
||||
return $this->success([], '更新成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param Request $request 请求对象
|
||||
* @return mixed 返回删除操作结果
|
||||
*/
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
try {
|
||||
$count = $this->service->serviceDestroy($request);
|
||||
return $this->success([], "成功删除了 {$count} 条");
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// 以下三个钩子方法,**在使用此 Trait 的 Controller** 中实现
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 字段名格式化:例如全部下划线
|
||||
*
|
||||
* @param string $field
|
||||
* @return string
|
||||
*/
|
||||
// protected function fieldFormatter(string $field): string
|
||||
// {
|
||||
// return snake_case($field);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 字段级后置加工规则
|
||||
*
|
||||
* @return array<string,callable>
|
||||
* 键为 “点号嵌套”或 “通配符” 字段路径,
|
||||
* 值为 fn($value, $row)=>mixed
|
||||
*/
|
||||
// protected function getFieldTransforms(): array
|
||||
// {
|
||||
// return [
|
||||
// 'name' => fn($v,$row)=>strtoupper($v),
|
||||
// 'orders.*.total' => fn($v,$row)=>number_format($v,2),
|
||||
// ];
|
||||
// }
|
||||
|
||||
/**
|
||||
* 结果级后置加工:接收整个数组,可任意增删改
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
// protected function postFormat(array $data): array
|
||||
// {
|
||||
// // 例如重命名 records 为 items
|
||||
// if (isset($data['records'])) {
|
||||
// $data['items'] = $data['records'];
|
||||
// unset($data['records']);
|
||||
// }
|
||||
// return $data;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace app\common\traits\crud;
|
||||
|
||||
use app\common\utils\CrudUtil;
|
||||
|
||||
/**
|
||||
* Trait CrudLogicTrait
|
||||
*
|
||||
* 提供最基础的 CRUD 和复杂查询方法,基于 CrudUtil。
|
||||
* 使用此 Trait 的类,须定义:
|
||||
* protected string $modelClass = \App\common\model\YourModel::class;
|
||||
*/
|
||||
trait CrudLogicTrait
|
||||
{
|
||||
/**
|
||||
* 分页列表
|
||||
*/
|
||||
public function logicList(array $config, int $limit, int $page)
|
||||
{
|
||||
$config['model'] = $this->modelClass;
|
||||
return CrudUtil::paginate(CrudUtil::buildQuery($config), $limit, $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全量列表
|
||||
*/
|
||||
public function logicListAll(array $config)
|
||||
{
|
||||
$config['model'] = $this->modelClass;
|
||||
return CrudUtil::getAll(CrudUtil::buildQuery($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单条
|
||||
*/
|
||||
public function logicShow(array $config)
|
||||
{
|
||||
return CrudUtil::findOneBy(CrudUtil::buildQuery($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建(可带 dataLimit)
|
||||
*/
|
||||
public function logicCreate(array $data, array $dataLimit = [])
|
||||
{
|
||||
return CrudUtil::create($this->modelClass, $data, $dataLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意条件更新(可带 dataLimit)
|
||||
*/
|
||||
public function logicUpdateBy(array $config, array $data)
|
||||
{
|
||||
return CrudUtil::updateBy(CrudUtil::buildQuery($config), $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意条件删除(可带 dataLimit)
|
||||
*/
|
||||
public function logicDeleteBy(array $config)
|
||||
{
|
||||
return CrudUtil::deleteBy(CrudUtil::buildQuery($config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 复杂查询回调
|
||||
*/
|
||||
public function logicComplex(callable $callback)
|
||||
{
|
||||
return CrudUtil::executeComplex($this->modelClass, $callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据主键查询
|
||||
*/
|
||||
public function findByPk(int $id, array $with = [])
|
||||
{
|
||||
return CrudUtil::findByPk($this->modelClass, $id, $with);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
namespace app\common\traits\crud;
|
||||
|
||||
use support\Request;
|
||||
|
||||
/**
|
||||
* Trait CrudServiceTrait
|
||||
*
|
||||
* 提供标准的增删改查 Service 接口,依赖 CrudLogicTrait。
|
||||
* 使用此 Trait 的 Service 类,须:
|
||||
* use CrudLogicTrait, CrudServiceTrait;
|
||||
* protected string $modelClass = \App\common\model\YourModel::class;
|
||||
*/
|
||||
trait CrudServiceTrait
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/** 具体 Service 里定义:protected string $modelClass = YourModel::class; */
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
/** 分页列表 */
|
||||
public function serviceList(Request $request)
|
||||
{
|
||||
if (method_exists($this, 'complexQuery')) {
|
||||
$result = $this->complexQuery($request->get());
|
||||
} else {
|
||||
$config = $this->buildQueryConfig($request, 'index');
|
||||
$limit = (int)$request->get('size', 15);
|
||||
$page = (int)$request->get('current', 1);
|
||||
$result = $this->logicList($config, $limit, $page);
|
||||
}
|
||||
return $this->postProcess('List', $result);
|
||||
}
|
||||
|
||||
/** 全量列表 */
|
||||
public function serviceListAll(Request $request)
|
||||
{
|
||||
if (method_exists($this, 'complexQuery')) {
|
||||
$result = $this->complexQuery($request->get());
|
||||
} else {
|
||||
$config = $this->buildQueryConfig($request, 'default');
|
||||
$result = $this->logicListAll($config);
|
||||
}
|
||||
return $this->postProcess('List', $result);
|
||||
}
|
||||
|
||||
/** 查看单条 */
|
||||
public function serviceShow(Request $request)
|
||||
{
|
||||
$config = $this->buildQueryConfig($request, 'show');
|
||||
$item = $this->logicShow($config);
|
||||
return $this->postProcess('Show', $item);
|
||||
}
|
||||
|
||||
/** 创建 */
|
||||
public function serviceStore(Request $request, array $data = [])
|
||||
{
|
||||
$data = $this->preProcess('Store', $data, $request);
|
||||
$dl = $this->getDataLimitFromRequest($request);
|
||||
$created = $this->logicCreate($data, $dl);
|
||||
return $this->postProcess('Store', $created);
|
||||
}
|
||||
|
||||
/** 更新 */
|
||||
public function serviceUpdate(Request $request, array $data = [])
|
||||
{
|
||||
$data = $this->preProcess('Update', $data, $request);
|
||||
$config = $this->buildQueryConfig($request, 'update');
|
||||
$updated = $this->logicUpdateBy($config, $data);
|
||||
return $this->postProcess('Update', $updated);
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
public function serviceDestroy(Request $request)
|
||||
{
|
||||
$config = $this->buildQueryConfig($request, 'destroy');
|
||||
$deleted = $this->logicDeleteBy($config);
|
||||
return $this->postProcess('Destroy', $deleted);
|
||||
}
|
||||
|
||||
/** ----------------- 公共辅助方法 ----------------- */
|
||||
|
||||
/**
|
||||
* 构造 CrudUtil 查询配置
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
|
||||
{
|
||||
$p = $request->all();
|
||||
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
switch ($scene) {
|
||||
case 'default':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'index':
|
||||
$config['where']['status'] = 1;
|
||||
$config['order']['sort'] = 'desc';
|
||||
$config['order']['id'] = 'desc';
|
||||
break;
|
||||
case 'show':
|
||||
$config['where']['id'] = $p['id'];
|
||||
$config['where']['status'] = 1;
|
||||
break;
|
||||
case 'store':
|
||||
break;
|
||||
case 'update':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
case 'destroy':
|
||||
$config['where']['id'] = $p['id'];
|
||||
break;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/** 解析 withFields 字符串 "rel:field,field;..." */
|
||||
protected function parseWithFields(string $str): array
|
||||
{
|
||||
if (!$str) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach (explode(';', $str) as $pair) {
|
||||
[$rel, $flds] = explode(':', $pair);
|
||||
$out[$rel] = explode(',', $flds);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/** 从请求读取 dataLimitType, dataLimitField */
|
||||
protected function getDataLimitFromRequest(Request $req): array
|
||||
{
|
||||
$type = $req->get('dataLimitType');
|
||||
if (!$type) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'type' => $type,
|
||||
'field' => $req->get('dataLimitField', 'admin_id'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* $data 前置处理
|
||||
*
|
||||
* @param array $data
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
protected function preProcess(string $op, mixed $data, Request $request): mixed
|
||||
{
|
||||
$hook = "beforeService{$op}";
|
||||
return method_exists($this, $hook)
|
||||
? $this->{$hook}($data, $request)
|
||||
: $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据操作类型,调用对应的后置钩子
|
||||
*
|
||||
* @param string $op 操作:List, Show, Store, Update, Destroy
|
||||
* @param mixed $data 原始返回值
|
||||
* @return mixed 处理后的返回值
|
||||
*/
|
||||
protected function postProcess(string $op, mixed $data): mixed
|
||||
{
|
||||
$hook = "afterService{$op}";
|
||||
return method_exists($this, $hook)
|
||||
? $this->{$hook}($data)
|
||||
: $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理复杂查询
|
||||
* 覆盖此方法,实现复杂查询逻辑
|
||||
*/
|
||||
// protected function complexQuery(array $params): array
|
||||
// {
|
||||
// throw new \LogicException('请在子类实现 complexQuery');
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use plugin\admin\app\common\Auth;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
/**
|
||||
* 构建查询工具类
|
||||
* | 方法 | 说明 |
|
||||
* | ------------------------------- | --------------------- |
|
||||
* | `buildQuery($config)` | 构建动态查询(关联加载/字段/过滤/排序) |
|
||||
* | `paginate($q,$per,$page)` | 分页返回 Paginator |
|
||||
* | `getAll($q)` | 不分页直接 `get()` |
|
||||
* | `findByPk($model,$id)` | 根据主键查询单条,找不到抛 404 |
|
||||
* | `findOneBy($model,$where)` | 根据任意条件查询单条,找不到抛 404 |
|
||||
* | `deleteByPk($model,$ids)` | 根据主键删除(可批量),返回删除条数 |
|
||||
* | `deleteBy($model,$where)` | 根据任意条件删除,返回删除条数 |
|
||||
* | `updateByPk($model,$id,$data)` | 根据主键更新,找不到抛 404 |
|
||||
* | `updateBy($model,$where,$data)` | 根据任意条件更新,返回影响行数 |
|
||||
*/
|
||||
|
||||
class CrudUtil
|
||||
{
|
||||
|
||||
/**
|
||||
* 构建查询,支持:
|
||||
* - 本表字段过滤 where
|
||||
* - 关联加载 with
|
||||
* - 关联表字段选择 withFields
|
||||
* - 关联表字段过滤 where with dot-notation
|
||||
* - 排序 order
|
||||
* - 本表字段选择 fields
|
||||
*
|
||||
* $config = [
|
||||
* 'model' => User::class,
|
||||
* 'where' => [
|
||||
* 'status' => 1,
|
||||
* 'name' => ['like','%张%'],
|
||||
* 'roles.name' => '管理员', // 关联表字段过滤
|
||||
* 'posts.created_at'=> ['>','2025-01-01'],
|
||||
* ],
|
||||
* 'order' => ['id'=>'desc'],
|
||||
* 'fields' => ['id','name','email'],
|
||||
* 'with' => ['roles','posts'],
|
||||
* 'withFields' => [
|
||||
* 'roles' => ['id','name'], // 关联表字段选择
|
||||
* 'posts' => ['id','title','user_id'],
|
||||
* ],
|
||||
* ];
|
||||
*/
|
||||
public static function buildQuery(array $config): Builder
|
||||
{
|
||||
/** @var Model $model */
|
||||
$model = new $config['model'];
|
||||
$q = $model->newQuery();
|
||||
|
||||
// eager-load:支持 ['rel','rel2'] 和 ['rel'=>fields[], ...]
|
||||
if (! empty($config['with'])) {
|
||||
foreach ($config['with'] as $rel => $fields) {
|
||||
if (is_int($rel)) {
|
||||
// 普通关联
|
||||
$q->with($fields);
|
||||
} else {
|
||||
// 带字段筛选的关联
|
||||
$q->with([$rel => function ($qr) use ($fields) {
|
||||
$key = $qr->getModel()->getKeyName();
|
||||
if (! in_array($key, $fields, true)) {
|
||||
$fields[] = $key;
|
||||
}
|
||||
$qr->select($fields);
|
||||
}]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表字段
|
||||
if (!empty($config['fields'])) {
|
||||
$q->select($config['fields']);
|
||||
}
|
||||
|
||||
// where
|
||||
if (!empty($config['where'])) {
|
||||
foreach ($config['where'] as $col => $val) {
|
||||
if (strpos($col, '.') !== false) {
|
||||
// 多层支持
|
||||
$segments = explode('.', $col);
|
||||
$leaf = array_pop($segments);
|
||||
self::applyNestedWhere($q, $segments, $leaf, $val);
|
||||
} else {
|
||||
self::applyWhere($q, $col, $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// or where
|
||||
if (!empty($config['orWhere'])) {
|
||||
$q->where(function($q) use($config) {
|
||||
foreach ($config['orWhere'] as $cond) {
|
||||
[$col,$op,$val] = $cond;
|
||||
$q->orWhere($col,$op,$val);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (!empty($config['order'])) {
|
||||
foreach ($config['order'] as $col => $dir) {
|
||||
$q->orderBy($col, strtolower($dir) === 'asc' ? 'asc' : 'desc');
|
||||
}
|
||||
}
|
||||
|
||||
// 数据权限过滤
|
||||
if (!empty($config['dataLimit'])) {
|
||||
self::applyDataLimit($q, $config['dataLimit']);
|
||||
}
|
||||
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param int $perPage
|
||||
* @param int $page
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public static function paginate(Builder $q, int $perPage, int $page)
|
||||
{
|
||||
return $q->paginate($perPage, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 非分页查询
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return \Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public static function getAll(Builder $q)
|
||||
{
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行复杂查询——仅在显式调用时生效
|
||||
*
|
||||
* @param string $modelClass 模型类
|
||||
* @param callable $callback function(Builder $q) { ... }
|
||||
* @return mixed 由 callback 返回的结果
|
||||
*/
|
||||
public static function executeComplex(string $modelClass, callable $cb)
|
||||
{
|
||||
/** @var Model $model */
|
||||
$model = new $modelClass;
|
||||
return $cb($model->newQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键单条查询
|
||||
*
|
||||
* @param string $modelClass
|
||||
* @param mixed $id
|
||||
* @param array $with
|
||||
* @return Model
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function findByPk(string $modelClass, $id, array $with = []): Model
|
||||
{
|
||||
$m = $modelClass::with($with)->find($id);
|
||||
if (!$m) {
|
||||
throw new BusinessException("记录 ID={$id} 不存在", 404);
|
||||
}
|
||||
return $m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 任意条件单条查询,支持 dataLimit
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return Model
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function findOneBy(Builder $q): Model
|
||||
{
|
||||
return $q->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建,支持 dataLimit
|
||||
*
|
||||
* @param string $modelClass
|
||||
* @param array $data
|
||||
* @param array $dataLimit
|
||||
* @return Model
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function create(
|
||||
string $modelClass,
|
||||
array $data,
|
||||
array $dataLimit = []
|
||||
): Model {
|
||||
// dataLimit 检查/注入
|
||||
if (!empty($dataLimit)) {
|
||||
self::applyDataLimitOnMutate($data, $dataLimit);
|
||||
}
|
||||
return $modelClass::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键更新,支持 dataLimit
|
||||
*
|
||||
* @param string $modelClass
|
||||
* @param mixed $id
|
||||
* @param array $data
|
||||
* @param array $dataLimit
|
||||
* @return Model
|
||||
* @throws BusinessException
|
||||
*/
|
||||
public static function updateByPk(
|
||||
string $modelClass,
|
||||
$id,
|
||||
array $data,
|
||||
array $dataLimit = []
|
||||
): Model {
|
||||
/** @var Model $m */
|
||||
$m = $modelClass::find($id);
|
||||
if (!$m) {
|
||||
throw new BusinessException("记录 ID={$id} 不存在", 404);
|
||||
}
|
||||
// dataLimit 校验
|
||||
if (!empty($dataLimit)) {
|
||||
self::applyDataLimitOnMutate($m, $dataLimit, $data);
|
||||
}
|
||||
$m->fill($data)->save();
|
||||
return $m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 非主键条件更新(批量),支持 dataLimit
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param array $data
|
||||
* @return int 受影响行数
|
||||
*/
|
||||
public static function updateBy(Builder $q, array $data): int
|
||||
{
|
||||
return $q->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主键删除(支持批量)
|
||||
*
|
||||
* @param string $modelClass
|
||||
* @param array $ids
|
||||
* @return int 删除条数
|
||||
*/
|
||||
public static function deleteByPk(string $modelClass, array $ids): int
|
||||
{
|
||||
return $modelClass::destroy($ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* 非主键条件删除,支持 dataLimit
|
||||
*
|
||||
* @param Builder $query
|
||||
* @return int 删除条数
|
||||
*/
|
||||
public static function deleteBy(Builder $q): int
|
||||
{
|
||||
return $q->delete();
|
||||
}
|
||||
|
||||
|
||||
//=================== Internal Methods ===================//
|
||||
|
||||
|
||||
/**
|
||||
* 支持多层的 whereHas:$relations=['roles','permissions'],$leaf='name'
|
||||
*/
|
||||
protected static function applyNestedWhere(Builder $q, array $relations, string $leaf, $val): void
|
||||
{
|
||||
$rel = array_shift($relations);
|
||||
$q->whereHas($rel, function (Builder $qr) use ($relations, $leaf, $val) {
|
||||
if ($relations) {
|
||||
// 还有更深层,继续递归
|
||||
self::applyNestedWhere($qr, $relations, $leaf, $val);
|
||||
} else {
|
||||
// 最后一层,使用普通 applyWhere
|
||||
self::applyWhere($qr, $leaf, $val);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 单字段 where 支持:
|
||||
* - 多条件数组 [ [op1,val1], [op2,val2], … ]
|
||||
* - 单条件 ['op', val]
|
||||
* - 直接值
|
||||
*
|
||||
* @param Builder $q
|
||||
* @param string $col
|
||||
* @param mixed $val
|
||||
* @return void
|
||||
*/
|
||||
protected static function applyWhere(Builder $q, string $col, $val): void
|
||||
{
|
||||
// 多条件数组
|
||||
if (is_array($val) && isset($val[0]) && is_array($val[0])) {
|
||||
foreach ($val as $pair) {
|
||||
if (is_array($pair) && count($pair) === 2) {
|
||||
self::applySingleWhere($q, $col, $pair[0], $pair[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 单条件
|
||||
elseif (is_array($val) && count($val) === 2) {
|
||||
self::applySingleWhere($q, $col, $val[0], $val[1]);
|
||||
}
|
||||
// 直接等值
|
||||
else {
|
||||
$q->where($col, $val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行条件 where
|
||||
*/
|
||||
protected static function applySingleWhere(Builder $q, string $col, string $op, $v): void
|
||||
{
|
||||
switch (strtolower($op)) {
|
||||
case 'like':
|
||||
$q->where($col, 'like', $v);
|
||||
break;
|
||||
case 'in':
|
||||
$q->whereIn($col, (array)$v);
|
||||
break;
|
||||
case 'not in':
|
||||
$q->whereNotIn($col, (array)$v);
|
||||
break;
|
||||
case 'null':
|
||||
$q->whereNull($col);
|
||||
break;
|
||||
case 'not null':
|
||||
$q->whereNotNull($col);
|
||||
break;
|
||||
case 'between':
|
||||
$q->whereBetween($col, (array)$v);
|
||||
break;
|
||||
default:
|
||||
$q->where($col, $op, $v);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用数据权限过滤到查询(buildQuery 场景)
|
||||
*/
|
||||
protected static function applyDataLimit(Builder $q, array $dataLimit): void
|
||||
{
|
||||
if (Auth::isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
$type = $dataLimit['type'] ?? null;
|
||||
$field = $dataLimit['field'] ?? 'admin_id';
|
||||
|
||||
if ($type === 'personal') {
|
||||
$q->where($field, admin_id());
|
||||
} elseif ($type === 'auth') {
|
||||
$q->whereIn($field, Auth::getScopeAdminIds(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用数据权限到“创建/更新”操作
|
||||
*
|
||||
* - $target 可能是 array|Model 对象
|
||||
* - $newData 仅在 updateByPk 场景时传入,用以校验变更归属
|
||||
*/
|
||||
protected static function applyDataLimitOnMutate(&$target, array $dataLimit, array $newData = []): void
|
||||
{
|
||||
if (Auth::isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
$type = $dataLimit['type'] ?? null;
|
||||
$field = $dataLimit['field'] ?? 'admin_id';
|
||||
|
||||
// 读取当前归属:create 时 $target 是 array,update 时是 Model
|
||||
$current = is_array($target) ? null : $target->{$field};
|
||||
|
||||
// 1. personal → 只能自己
|
||||
if ($type === 'personal') {
|
||||
// create: 强制设置
|
||||
if (is_array($target)) {
|
||||
$target[$field] = admin_id();
|
||||
}
|
||||
// update: 校验当前归属
|
||||
elseif ($current !== admin_id()) {
|
||||
throw new BusinessException("无权操作该记录", 403);
|
||||
}
|
||||
// 若要修改归属,也必须和自己一致
|
||||
if (isset($newData[$field]) && $newData[$field] !== admin_id()) {
|
||||
throw new BusinessException("无权变更归属字段", 403);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. auth → 自己及下属
|
||||
if ($type === 'auth') {
|
||||
$scope = Auth::getScopeAdminIds(true);
|
||||
// create: 若前端指定归属,则校验范围;否则默认自己
|
||||
if (is_array($target)) {
|
||||
if (isset($target[$field])) {
|
||||
if (!in_array($target[$field], $scope)) {
|
||||
throw new BusinessException("无权为指定用户操作", 403);
|
||||
}
|
||||
} else {
|
||||
$target[$field] = admin_id();
|
||||
}
|
||||
}
|
||||
// update: 校验当前归属
|
||||
elseif (!in_array($current, $scope)) {
|
||||
throw new BusinessException("无权操作该记录", 403);
|
||||
}
|
||||
// 校验更新中指定的新归属
|
||||
if (isset($newData[$field]) && !in_array($newData[$field], $scope)) {
|
||||
throw new BusinessException("无权变更归属字段", 403);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
use Webman\Exception\BusinessException;
|
||||
|
||||
class FeishuMessage
|
||||
{
|
||||
|
||||
protected $app_id = 'cli_a72f13d6c022500d';
|
||||
protected $app_secret = 'Kxtbq70OQWrEhpltrufRZc76z7kjDqPT';
|
||||
protected $class = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->class = new Feishu($this->app_id, $this->app_secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* 飞书消息模板
|
||||
* @param string $scene
|
||||
* @param array $options
|
||||
*/
|
||||
public function messageTemplates(string $scene = 'group-send', array $options = [])
|
||||
{
|
||||
try {
|
||||
$template = match ($scene) {
|
||||
'inquiry-group-send' => $this->groupNewTemplates(
|
||||
$options['quality'],
|
||||
$options['order_no'],
|
||||
$options['channel'],
|
||||
$options['country'],
|
||||
$options['enter_user'],
|
||||
$options['r_time']
|
||||
),
|
||||
'inquiry-group-update' => $this->groupUpdateTemplates(
|
||||
$options['quality'],
|
||||
$options['order_no'],
|
||||
$options['channel'],
|
||||
$options['country'],
|
||||
$options['enter_user'],
|
||||
$options['r_time'],
|
||||
$options['t_time']
|
||||
),
|
||||
'inquiry-person-allocation' => $this->personAllocationTemplates(
|
||||
$options['quality'],
|
||||
$options['order_no'],
|
||||
$options['channel'],
|
||||
$options['country'],
|
||||
$options['enter_user'],
|
||||
$options['r_time'],
|
||||
$options['t_time']
|
||||
),
|
||||
'crm-person-follow-up' => $this->personFollowUpTemplates(
|
||||
$options['contacts'],
|
||||
$options['follow_next'],
|
||||
$options['follow_last']
|
||||
)
|
||||
};
|
||||
} catch (\UnhandledMatchError $e) {
|
||||
throw new BusinessException($e->getMessage());
|
||||
}
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送飞书消息
|
||||
* @param array $options
|
||||
*/
|
||||
public function messageSend(string $scene = 'group-send', array $options = [])
|
||||
{
|
||||
$template = $this->messageTemplates($scene, $options);
|
||||
$result = $this->class->sendMessages($options['receive_id'], $template, $options['msg_type'], $options['receive_id_type']);
|
||||
if (!isset($result['code']) || $result['code'] !== 0) {
|
||||
throw new BusinessException('发送飞书消息失败');
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线索消息发送
|
||||
*/
|
||||
public function groupNewTemplates($quality = '💛 银牌商机', $order_no = 'SN2025072365640', $channel = '谷歌', $country = '白俄罗斯', $enter_user = '木子李', $r_time = '2025-07-24 09:41:23')
|
||||
{
|
||||
$content = '{
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"update_multi": true,
|
||||
"style": {
|
||||
"text_size": {
|
||||
"normal_v2": {
|
||||
"default": "normal",
|
||||
"pc": "normal",
|
||||
"mobile": "heading"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "16px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 20px 20px 20px",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "weighted",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": " **'.$quality.'**\n- **编号:** '.$order_no.'\n- **渠道:** '.$channel.'\n- **国家:** '.$country.'\n- **发布人:** '.$enter_user.'\n- **发布时间:** '.$r_time.'",
|
||||
"text_align": "left",
|
||||
"text_size": "normal_v2",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "form",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"flex_mode": "stretch",
|
||||
"horizontal_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "auto",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "button",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "立即跟踪"
|
||||
},
|
||||
"type": "primary_filled",
|
||||
"width": "fill",
|
||||
"size": "medium",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "list-check-bold_outlined"
|
||||
},
|
||||
"confirm": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "确认跟踪?"
|
||||
},
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "客户线索:'.$order_no.'"
|
||||
}
|
||||
},
|
||||
"behaviors": [
|
||||
{
|
||||
"type": "callback",
|
||||
"value": "'.$order_no.'"
|
||||
}
|
||||
],
|
||||
"form_action_type": "submit",
|
||||
"name": "Button_m7d0zqa2",
|
||||
"element_id": "track"
|
||||
}
|
||||
],
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top"
|
||||
},
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "auto",
|
||||
"elements": [],
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top"
|
||||
},
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "auto",
|
||||
"elements": [],
|
||||
"padding": "0px 0px 0px 0px",
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"margin": "4px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "12px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 0px 0px 0px",
|
||||
"margin": "0px 0px 0px 0px",
|
||||
"name": "Form_m7d0zqa1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "新的【公海】客户线索"
|
||||
},
|
||||
"subtitle": {
|
||||
"tag": "plain_text",
|
||||
"content": ""
|
||||
},
|
||||
"template": "default",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "ai-common_colorful"
|
||||
},
|
||||
"padding": "12px 12px 12px 20px"
|
||||
}
|
||||
}';
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线索消息更新
|
||||
*/
|
||||
public function groupUpdateTemplates($quality = '💛 银牌商机', $order_no = 'SN2025072365640', $channel = '谷歌', $country = '白俄罗斯', $enter_user = '木子李', $r_time = '2025-07-24 09:41:23', $t_time = '2025-07-24 14:41:23')
|
||||
{
|
||||
$content = '{
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"update_multi": true,
|
||||
"style": {
|
||||
"text_size": {
|
||||
"normal_v2": {
|
||||
"default": "normal",
|
||||
"pc": "normal",
|
||||
"mobile": "heading"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "16px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 20px 20px 20px",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "weighted",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": " **'.$quality.'**\n- **编号:** '.$order_no.'\n- **渠道:** '.$channel.'\n- **国家:** '.$country.'\n- **发布人:** '.$enter_user.'\n- **发布时间:** '.$r_time.'",
|
||||
"text_align": "left",
|
||||
"text_size": "normal_v2",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "跟踪时间:'.$t_time.'",
|
||||
"text_size": "notation",
|
||||
"text_align": "left",
|
||||
"text_color": "green"
|
||||
},
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "chat-done_outlined",
|
||||
"color": "green"
|
||||
},
|
||||
"element_id": "track_time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "【公海】客户线索"
|
||||
},
|
||||
"subtitle": {
|
||||
"tag": "plain_text",
|
||||
"content": ""
|
||||
},
|
||||
"text_tag_list": [
|
||||
{
|
||||
"tag": "text_tag",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "已跟踪"
|
||||
},
|
||||
"color": "orange"
|
||||
}
|
||||
],
|
||||
"template": "green",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "ai-common_colorful"
|
||||
},
|
||||
"padding": "12px 12px 12px 20px"
|
||||
}
|
||||
}';
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线索分配消息
|
||||
*/
|
||||
public function personAllocationTemplates($quality = '💛 银牌商机', $order_no = 'SN2025072365640', $channel = '谷歌', $country = '白俄罗斯', $enter_user = '木子李', $r_time = '2025-07-24 09:41:23', $t_time = '2025-07-24 14:41:23')
|
||||
{
|
||||
$content = '{
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"update_multi": true,
|
||||
"style": {
|
||||
"text_size": {
|
||||
"normal_v2": {
|
||||
"default": "normal",
|
||||
"pc": "normal",
|
||||
"mobile": "heading"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "16px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 20px 20px 20px",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "weighted",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": " **'.$quality.'**\n- **编号:** '.$order_no.'\n- **渠道:** '.$channel.'\n- **国家:** '.$country.'\n- **录单人:** '.$enter_user.'\n- **录入时间:** '.$r_time.'",
|
||||
"text_align": "left",
|
||||
"text_size": "normal_v2",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "分配时间:'.$t_time.'",
|
||||
"text_size": "notation",
|
||||
"text_align": "left",
|
||||
"text_color": "green"
|
||||
},
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "chat-done_outlined",
|
||||
"color": "green"
|
||||
},
|
||||
"element_id": "allocation_time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "新的客户线索"
|
||||
},
|
||||
"subtitle": {
|
||||
"tag": "plain_text",
|
||||
"content": ""
|
||||
},
|
||||
"text_tag_list": [
|
||||
{
|
||||
"tag": "text_tag",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "指定"
|
||||
},
|
||||
"color": "yellow"
|
||||
}
|
||||
],
|
||||
"template": "blue",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "todo_colorful"
|
||||
},
|
||||
"padding": "12px 12px 12px 20px"
|
||||
}
|
||||
}';
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟进提醒消息
|
||||
*/
|
||||
public function personFollowUpTemplates($contacts = '木子李', $follow_next = '2025-07-24 14:41:23', $follow_last = '2025-07-24 14:41:23')
|
||||
{
|
||||
$content = '{
|
||||
"schema": "2.0",
|
||||
"config": {
|
||||
"update_multi": true,
|
||||
"style": {
|
||||
"text_size": {
|
||||
"normal_v2": {
|
||||
"default": "normal",
|
||||
"pc": "normal",
|
||||
"mobile": "heading"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"body": {
|
||||
"direction": "vertical",
|
||||
"horizontal_spacing": "8px",
|
||||
"vertical_spacing": "16px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"padding": "0px 20px 20px 20px",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "column_set",
|
||||
"horizontal_align": "left",
|
||||
"columns": [
|
||||
{
|
||||
"tag": "column",
|
||||
"width": "weighted",
|
||||
"elements": [
|
||||
{
|
||||
"tag": "markdown",
|
||||
"content": "- **客户名称:** '.$contacts.'\n- **跟进时间:** '.$follow_next.'",
|
||||
"text_align": "left",
|
||||
"text_size": "normal_v2",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
}
|
||||
],
|
||||
"vertical_spacing": "8px",
|
||||
"horizontal_align": "left",
|
||||
"vertical_align": "top",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "hr",
|
||||
"margin": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"text": {
|
||||
"tag": "plain_text",
|
||||
"content": "最近跟进:'.$follow_last.'",
|
||||
"text_size": "notation",
|
||||
"text_align": "left",
|
||||
"text_color": "grey"
|
||||
},
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "chat_outlined",
|
||||
"color": "grey"
|
||||
},
|
||||
"element_id": "track_time"
|
||||
}
|
||||
]
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"tag": "plain_text",
|
||||
"content": "跟进提醒"
|
||||
},
|
||||
"subtitle": {
|
||||
"tag": "plain_text",
|
||||
"content": ""
|
||||
},
|
||||
"template": "green",
|
||||
"icon": {
|
||||
"tag": "standard_icon",
|
||||
"token": "calendar_colorful"
|
||||
},
|
||||
"padding": "12px 12px 12px 20px"
|
||||
}
|
||||
}';
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils;
|
||||
|
||||
use support\Db;
|
||||
|
||||
class TableInspector
|
||||
{
|
||||
/**
|
||||
* 获取某个表的所有字段信息
|
||||
*/
|
||||
public static function getColumns(string $table, ?string $database = null): array
|
||||
{
|
||||
$database = $database ?: Db::connection()->getDatabaseName();
|
||||
|
||||
$columns = Db::select("
|
||||
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE,
|
||||
COLUMN_DEFAULT, COLUMN_COMMENT, EXTRA
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?
|
||||
ORDER BY ORDINAL_POSITION
|
||||
", [$database, $table]);
|
||||
|
||||
return array_map(function ($col) {
|
||||
return [
|
||||
'name' => $col->COLUMN_NAME,
|
||||
'type' => $col->DATA_TYPE,
|
||||
'column_type' => $col->COLUMN_TYPE,
|
||||
'nullable' => $col->IS_NULLABLE === 'YES',
|
||||
'default' => $col->COLUMN_DEFAULT,
|
||||
'comment' => $col->COLUMN_COMMENT,
|
||||
'extra' => $col->EXTRA,
|
||||
];
|
||||
}, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段名列表
|
||||
*/
|
||||
public static function getColumnNames(string $table, ?string $database = null): array
|
||||
{
|
||||
return array_column(self::getColumns($table, $database), 'name');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段注释 map:字段名 => 注释
|
||||
*/
|
||||
public static function getColumnComments(string $table, ?string $database = null): array
|
||||
{
|
||||
$columns = self::getColumns($table, $database);
|
||||
$map = [];
|
||||
foreach ($columns as $col) {
|
||||
$map[$col['name']] = $col['comment'];
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\utils\scaffold;
|
||||
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use app\common\utils\TableInspector;
|
||||
|
||||
class Generate
|
||||
{
|
||||
|
||||
// 配置项
|
||||
protected $options = [];
|
||||
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
if (empty($options)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function doGenerate()
|
||||
{
|
||||
$fs = new Filesystem();
|
||||
|
||||
$templateDir = __DIR__ . '/templates/';
|
||||
|
||||
$resultStr = '';
|
||||
|
||||
if (!empty($this->options['controller'])) {
|
||||
$controllerPath = __DIR__ . "/../../../{$this->options['controller']['module']}/controller/".$this->rName($this->options['controller']['name'])."Controller.php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['controller']['module']}/controller");
|
||||
$this->deleteFileIfExists($controllerPath);
|
||||
$ControllerStub = $this->generateFile($templateDir, 'ControllerStub.stub', $controllerPath, 'controller');
|
||||
$resultStr .= $ControllerStub;
|
||||
}
|
||||
|
||||
if (!empty($this->options['service'])) {
|
||||
$servicePath = __DIR__ . "/../../../{$this->options['service']['module']}/service/".$this->rName($this->options['service']['name'])."Service.php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['service']['module']}/service");
|
||||
$this->deleteFileIfExists($servicePath);
|
||||
$ServiceStub = $this->generateFile($templateDir, 'ServiceStub.stub', $servicePath, 'service');
|
||||
$resultStr .= $ServiceStub;
|
||||
}
|
||||
|
||||
if (!empty($this->options['logic'])) {
|
||||
$logicPath = __DIR__ . "/../../../{$this->options['logic']['module']}/logic/".$this->rName($this->options['logic']['name'])."Logic.php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['logic']['module']}/logic");
|
||||
$this->deleteFileIfExists($logicPath);
|
||||
$LogicStub = $this->generateFile($templateDir, 'LogicStub.stub', $logicPath, 'logic');
|
||||
$resultStr .= $LogicStub;
|
||||
}
|
||||
|
||||
if (!empty($this->options['model'])) {
|
||||
$modelPath = __DIR__ . "/../../../{$this->options['model']['module']}/model/".$this->rName($this->options['model']['name']).".php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['model']['module']}/model");
|
||||
$this->deleteFileIfExists($modelPath);
|
||||
$ModelStub = $this->generateFile($templateDir, 'ModelStub.stub', $modelPath, 'model');
|
||||
$resultStr .= $ModelStub;
|
||||
}
|
||||
|
||||
if (!empty($this->options['validate'])) {
|
||||
$validatePath = __DIR__ . "/../../../{$this->options['validate']['module']}/validate/".$this->rName($this->options['validate']['name'])."Validate.php";
|
||||
$fs->mkdir(__DIR__ . "/../../../{$this->options['validate']['module']}/validate");
|
||||
$this->deleteFileIfExists($validatePath);
|
||||
$ValidatorStub = $this->generateFile($templateDir, 'ValidateStub.stub', $validatePath, 'validate');
|
||||
$resultStr .= $ValidatorStub;
|
||||
}
|
||||
|
||||
return $resultStr ? $resultStr . "<info>文件生成成功!</info> <br/>" : '未生成任何文件';
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模板生成文件
|
||||
*
|
||||
* @param string $templateDir 模板目录
|
||||
* @param string $templateFile 模板文件名
|
||||
* @param string $outputPath 输出文件路径
|
||||
*/
|
||||
protected function generateFile($templateDir, $templateFile, $outputPath, $fileType)
|
||||
{
|
||||
$templatePath = $templateDir . $templateFile;
|
||||
|
||||
if (!file_exists($templatePath)) {
|
||||
echo "<error>模板文件 {$templateFile} 不存在!</error> \n";
|
||||
return;
|
||||
}
|
||||
|
||||
$content = file_get_contents($templatePath);
|
||||
|
||||
switch ($fileType) {
|
||||
case 'controller':
|
||||
$content = str_replace('{{controllerModule}}', $this->options['controller']['module'], $content);
|
||||
$content = str_replace('{{serviceModule}}', $this->options['service']['module'], $content);
|
||||
$content = str_replace('{{serviceName}}', $this->rName($this->options['service']['name']), $content);
|
||||
$content = str_replace('{{controllerName}}', $this->rName($this->options['controller']['name']), $content);
|
||||
break;
|
||||
case 'service':
|
||||
$content = str_replace('{{serviceModule}}', $this->options['service']['module'], $content);
|
||||
$content = str_replace('{{validateModule}}', $this->options['validate']['module'], $content);
|
||||
$content = str_replace('{{validateName}}', $this->rName($this->options['validate']['name']), $content);
|
||||
$content = str_replace('{{modelModule}}', $this->options['model']['module'], $content);
|
||||
$content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content);
|
||||
$content = str_replace('{{serviceName}}', $this->rName($this->options['service']['name']), $content);
|
||||
break;
|
||||
case 'logic':
|
||||
$content = str_replace('{{logicModule}}', $this->options['logic']['module'], $content);
|
||||
$content = str_replace('{{modelModule}}', $this->options['model']['module'], $content);
|
||||
$content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content);
|
||||
$content = str_replace('{{logicName}}', $this->rName($this->options['logic']['name']), $content);
|
||||
break;
|
||||
case 'model':
|
||||
$content = str_replace('{{modelModule}}', $this->options['model']['module'], $content);
|
||||
$content = str_replace('{{modelName}}', $this->rName($this->options['model']['name']), $content);
|
||||
$content = str_replace('{{table}}', $this->options['table'], $content);
|
||||
$content = str_replace('{{fillable}}', $this->fillableFields(), $content);
|
||||
break;
|
||||
case 'validate':
|
||||
$content = str_replace('{{validateModule}}', $this->options['validate']['module'], $content);
|
||||
$content = str_replace('{{validateName}}', $this->rName($this->options['validate']['name']), $content);
|
||||
break;
|
||||
}
|
||||
|
||||
file_put_contents($outputPath, $content);
|
||||
|
||||
return "生成文件: <info>{$outputPath}</info> <br/>";
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置字段
|
||||
*
|
||||
* @param array $fields 字段数组
|
||||
*/
|
||||
public function fillableFields()
|
||||
{
|
||||
$tableName = $this->options['table'];
|
||||
$fields = TableInspector::getColumnNames($tableName);
|
||||
$str = '';
|
||||
foreach ($fields as $key => $val) {
|
||||
if ($key < (count($fields)-1)) {
|
||||
$str .= "'{$val}', ";
|
||||
} else {
|
||||
$str .= "'{$val}'";
|
||||
}
|
||||
}
|
||||
$fields = trim($str, '"');
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果文件存在,删除它
|
||||
*
|
||||
* @param string $path 文件路径
|
||||
*/
|
||||
protected function deleteFileIfExists($path)
|
||||
{
|
||||
if (file_exists($path)) {
|
||||
unlink($path);
|
||||
echo "删除已存在文件: <comment>{$path}</comment> \n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置名称
|
||||
*/
|
||||
public function rName($name = '')
|
||||
{
|
||||
return ucfirst(camelize($name));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下划线转驼峰
|
||||
* 思路:
|
||||
* step1.原字符串转小写,原字符串中的分隔符用空格替换,在字符串开头加上分隔符
|
||||
* step2.将字符串中每个单词的首字母转换为大写,再去空格,去字符串首部附加的分隔符.
|
||||
*/
|
||||
function camelize($uncamelized_words, $separator='_') {
|
||||
$uncamelized_words = $separator . str_replace($separator, " ", strtolower($uncamelized_words));
|
||||
return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator );
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰命名转下划线命名
|
||||
* 思路:
|
||||
* 小写和大写紧挨一起的地方,加上分隔符,然后全部转小写
|
||||
*/
|
||||
function uncamelize($camelCaps, $separator='_') {
|
||||
return strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . $separator . "$2", $camelCaps));
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\{{controllerModule}}\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\{{serviceModule}}\service\{{serviceName}}Service;
|
||||
|
||||
class {{controllerName}}Controller extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new {{serviceName}}Service();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
// protected function indexFormat($data)
|
||||
// {
|
||||
// if (isset($data['created_at']))
|
||||
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
// if (isset($data['updated_at']))
|
||||
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
|
||||
// return $data;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\{{logicModule}}\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\{{modelModule}}\model\{{modelName}};
|
||||
|
||||
class {{logicName}}Logic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = {{modelName}}::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace app\{{modelModule}}\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class {{modelName}} extends Model
|
||||
{
|
||||
/**
|
||||
* The connection name for the model.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $connection = 'mysql';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = '{{table}}';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* 允许自动填充模型字段
|
||||
*/
|
||||
protected $fillable = [{{fillable}}];
|
||||
|
||||
/**
|
||||
* 不允许自动填充模型字段
|
||||
*/
|
||||
protected $guarded = [];
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\{{serviceModule}}\service;
|
||||
|
||||
use app\{{validateModule}}\validate\{{validateName}}Validate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\{{modelModule}}\model\{{modelName}};
|
||||
|
||||
class {{serviceName}}Service
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = {{modelName}}::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = {{validateName}}Validate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\{{validateModule}}\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class {{validateName}}Validate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class CorpValidate extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'id' => 'require|number',
|
||||
'name' => 'require|max:255',
|
||||
'abbr' => 'require|in:hesu,xinyouhe,paika,simate,mbaf',
|
||||
'tenant_key' => 'string',
|
||||
'sort' => 'number',
|
||||
'status' => 'require|in:0,1'
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'id.require' => 'ID不能为空',
|
||||
'id.number' => 'ID必须是数字',
|
||||
'name.require' => '企业名称不能为空',
|
||||
'name.max' => '企业名称长度不能超过255个字符',
|
||||
'abbr.require' => '企业缩写不能为空',
|
||||
'abbr.in' => '企业缩写必须是 hesu,xinyouhe,paika,simate,mbaf 中的一个',
|
||||
'tenant_key.string' => '飞书企业标识必须是字符串',
|
||||
'sort.number' => '排序值必须是数字',
|
||||
'status.require' => '状态不能为空',
|
||||
'status.in' => '状态必须是0或1'
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'store' => ['name', 'abbr', 'tenant_key', 'sort', 'status'],
|
||||
'update' => ['id', 'name', 'abbr', 'tenant_key', 'sort', 'status']
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
use app\common\model\InquiryLog;
|
||||
|
||||
class InquiryLogValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
'id' => 'require|integer|gt:0',
|
||||
'order_no' => 'require|alphaDash|length:36',
|
||||
'enter_user_id'=> 'integer|egt:0',
|
||||
'recipient' => 'integer|egt:0',
|
||||
'quality' => 'require|in:1,2,3',
|
||||
'channel' => 'integer|between:0,99',
|
||||
'email' => 'email|max:255|checkEmailContactUnique',
|
||||
'tel' => 'max:255|checkTelContactUnique',
|
||||
'whatsapp' => 'max:255|checkWhatsappContactUnique',
|
||||
'country' => 'integer|egt:0|requireWith:contacts',
|
||||
'contacts' => 'require|max:255|requireWithout:email,tel,whatsapp|checkContactUnique',
|
||||
'content' => 'string',
|
||||
'status' => 'in:0,1,2',
|
||||
'r_time' => 'integer|egt:0',
|
||||
't_time' => 'integer|egt:0'
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
'id.require' => 'ID不能为空',
|
||||
'id.integer' => 'ID必须是整数',
|
||||
'id.gt' => 'ID必须大于0',
|
||||
'order_no.require' => '订单号不能为空',
|
||||
'order_no.alphaDash'=> '订单号只能包含字母、数字和下划线',
|
||||
'order_no.length' => '订单号长度必须为36位',
|
||||
'enter_user_id.integer' => '录入人ID必须是整数',
|
||||
'enter_user_id.egt' => '录入人ID不能为负数',
|
||||
'recipient.integer' => '接单人ID必须是整数',
|
||||
'recipient.egt' => '接单人ID不能为负数',
|
||||
'quality.require' => '询单等级不能为空',
|
||||
'quality.in' => '询单等级只能是1、2或3',
|
||||
'channel.integer' => '渠道编号必须是整数',
|
||||
'channel.between' => '渠道编号范围0-99',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'email.max' => '邮箱长度不能超过255个字符',
|
||||
'tel.max' => '电话长度不能超过255个字符',
|
||||
'whatsapp.max' => 'WhatsApp长度不能超过255个字符',
|
||||
'country.integer' => '国家代码必须是整数',
|
||||
'country.egt' => '国家代码不能为负数',
|
||||
'contacts.require' => '联系人不能为空',
|
||||
'contacts.max' => '联系人长度不能超过255个字符',
|
||||
'content.string' => '询单内容必须是字符串',
|
||||
'status.in' => '状态值只能是0、1或2',
|
||||
'r_time.integer' => '发布时间必须是整数',
|
||||
'r_time.egt' => '发布时间不能为负数',
|
||||
't_time.integer' => '跟踪时间必须是整数',
|
||||
't_time.egt' => '跟踪时间不能为负数',
|
||||
'email.checkEmailContactUnique' => '邮箱联系方式已存在',
|
||||
'tel.checkTelContactUnique' => '电话联系方式已存在',
|
||||
'whatsapp.checkWhatsappContactUnique' => 'WhatsApp联系方式已存在',
|
||||
'contacts.requireWithout' => '至少需要提供一种联系方式',
|
||||
'contacts.checkContactUnique' => '联系人已存在',
|
||||
'country.requireWith' => '国家必须与联系人一起提供'
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
'enter_user_id', 'quality', 'channel', 'email', 'tel', 'whatsapp', 'country', 'contacts', 'content'
|
||||
],
|
||||
'update' => [
|
||||
'quality', 'channel', 'email', 'tel', 'whatsapp', 'country', 'contacts', 'content'
|
||||
],
|
||||
'release' => [
|
||||
'status', 'r_time'
|
||||
],
|
||||
'allocation' => [
|
||||
'status', 't_time'
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* 验证联系方式唯一性
|
||||
*/
|
||||
protected function checkContactUnique($value, $rule, $data, $field, $title)
|
||||
{
|
||||
// 如果是更新操作且联系人未修改,跳过检查
|
||||
if ($this->currentScene == 'update') {
|
||||
$original = InquiryLog::find($data['id']);
|
||||
if ($original && $original->contacts == $value && $original->country == ($data['country'] ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($data['email']) && empty($data['tel']) && empty($data['whatsapp'])) {
|
||||
$query = InquiryLog::where('contacts', trim($value))
|
||||
->where('country', $data['country']);
|
||||
|
||||
if (!empty($data['id'])) {
|
||||
$query->where('id', '<>', $data['id']);
|
||||
}
|
||||
|
||||
return !$query->exists();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱联系方式唯一性
|
||||
*/
|
||||
protected function checkEmailContactUnique($value, $rule, $data, $field, $title)
|
||||
{
|
||||
if (empty($value)) return true;
|
||||
|
||||
// 如果是更新操作且邮箱未修改,跳过检查
|
||||
if ($this->currentScene == 'update' && !empty($data['id'])) {
|
||||
$original = InquiryLog::find($data['id']);
|
||||
if ($original && $original->email == $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$query = InquiryLog::where('email', trim($value));
|
||||
if (!empty($data['id'])) {
|
||||
$query->where('id', '<>', $data['id']);
|
||||
}
|
||||
return !$query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证电话联系方式唯一性
|
||||
*/
|
||||
protected function checkTelContactUnique($value, $rule, $data, $field, $title)
|
||||
{
|
||||
if (empty($value)) return true;
|
||||
|
||||
// 如果是更新操作且电话未修改,跳过检查
|
||||
if ($this->currentScene == 'update' && !empty($data['id'])) {
|
||||
$original = InquiryLog::find($data['id']);
|
||||
if ($original && $original->tel == $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$query = InquiryLog::where('tel', trim($value));
|
||||
if (!empty($data['id'])) {
|
||||
$query->where('id', '<>', $data['id']);
|
||||
}
|
||||
return !$query->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证WhatsApp联系方式唯一性
|
||||
*/
|
||||
protected function checkWhatsappContactUnique($value, $rule, $data, $field, $title)
|
||||
{
|
||||
if (empty($value)) return true;
|
||||
|
||||
// 如果是更新操作且WhatsApp未修改,跳过检查
|
||||
if ($this->currentScene == 'update' && !empty($data['id'])) {
|
||||
$original = InquiryLog::find($data['id']);
|
||||
if ($original && $original->whatsapp == $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$query = InquiryLog::where('whatsapp', trim($value));
|
||||
if (!empty($data['id'])) {
|
||||
$query->where('id', '<>', $data['id']);
|
||||
}
|
||||
return !$query->exists();
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class RoleValidate extends Validate
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'role_create' => ['system_id', 'name', 'desc', 'sort'],
|
||||
'role_update' => ['id', 'system_id', 'name', 'desc', 'sort', 'status'],
|
||||
'store' => ['system_id', 'name', 'desc', 'sort'],
|
||||
'update' => ['id', 'system_id', 'name', 'desc', 'sort', 'status'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class SystemsValidate extends Validate
|
||||
{
|
||||
// 规则
|
||||
protected $rule = [
|
||||
|
||||
];
|
||||
|
||||
// 提示
|
||||
protected $message = [
|
||||
|
||||
];
|
||||
|
||||
// 场景
|
||||
protected $scene = [
|
||||
'store' => [
|
||||
|
||||
],
|
||||
|
||||
'update' => [
|
||||
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -5,32 +5,35 @@ namespace app\controller;
|
||||
use app\common\model\User;
|
||||
use app\common\model\UserRoles;
|
||||
use app\common\model\UserSystems;
|
||||
use app\common\utils\scaffold\Generate;
|
||||
use support\Request;
|
||||
use support\Db;
|
||||
use support\Cache;
|
||||
use app\inquiry\service\InquiryLogService;
|
||||
|
||||
class IndexController
|
||||
{
|
||||
|
||||
protected $noNeedLogin = ['index', 'view', 'perm'];
|
||||
protected $noNeedLogin = ['index', 'view', 'perm', 'createResource', 'test'];
|
||||
|
||||
protected $noNeedHeaderData = ['index', 'view', 'perm', 'createResource', 'test'];
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
return htmlpage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试飞书
|
||||
*/
|
||||
public function view(Request $request)
|
||||
{
|
||||
return view('index/qrcode', ['name' => 'webman']);
|
||||
}
|
||||
|
||||
public function json(Request $request)
|
||||
{
|
||||
return json(['code' => 0, 'msg' => 'ok']);
|
||||
}
|
||||
|
||||
|
||||
// 临时增加用户权限
|
||||
/**
|
||||
* 临时增加用户权限
|
||||
*/
|
||||
public function perm(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -62,13 +65,59 @@ class IndexController
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// test function
|
||||
public function test(Request $request)
|
||||
/**
|
||||
* 生成资源
|
||||
*/
|
||||
public function createResource()
|
||||
{
|
||||
$user = User::find(1);
|
||||
$user->roles()->sync([1]);
|
||||
$user->systems()->sync([1]);
|
||||
return json(['code' => 0, 'msg' => 'success']);
|
||||
$options = [
|
||||
// 'controller' => [
|
||||
// 'module' => 'inquiry',
|
||||
// 'name' => 'country'
|
||||
// ],
|
||||
'model' => [
|
||||
'module' => 'common',
|
||||
'name' => 'crm_customer'
|
||||
],
|
||||
// 'service' => [
|
||||
// 'module' => 'inquiry',
|
||||
// 'name' => 'country'
|
||||
// ],
|
||||
// 'logic' => [
|
||||
// 'module' => 'inquiry',
|
||||
// 'name' => 'country'
|
||||
// ],
|
||||
// 'validate' => [
|
||||
// 'module' => 'inquiry',
|
||||
// 'name' => 'country'
|
||||
// ],
|
||||
'table' => 'crm_customer'
|
||||
];
|
||||
|
||||
$result = (new Generate($options))->doGenerate();
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function test()
|
||||
{
|
||||
// $res = ['id'=>25714];
|
||||
// \Webman\RedisQueue\Redis::send('inquiry-to-crm', $res);
|
||||
|
||||
// $res = [
|
||||
// 'id'=>25717,
|
||||
// 'category'=>'inquiry',
|
||||
// 'scene'=>'inquiry-person-allocation',
|
||||
// 'receive_type'=>'person'
|
||||
// ];
|
||||
|
||||
// $res = [
|
||||
// 'id'=>1,
|
||||
// 'category'=>'crm',
|
||||
// 'scene'=>'crm-person-follow-up',
|
||||
// 'receive_type'=>'person'
|
||||
// ];
|
||||
// \Webman\RedisQueue\Redis::send('feishu-message', $res);
|
||||
return 'success';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\controller;
|
||||
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
use support\exception\BusinessException;
|
||||
use app\common\traits\ResponseTrait;
|
||||
use think\Validate;
|
||||
|
||||
abstract class BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
protected mixed $validator = null;
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
*
|
||||
* @param Validate $validator 验证器实例
|
||||
* @param array $data 待验证的数据
|
||||
* @param string $scene 验证场景
|
||||
* @return array 验证后的数据
|
||||
* @throws BusinessException 验证失败时抛出业务异常
|
||||
*/
|
||||
public function validate(Validate $validator, array $data, string $scene = ''): array
|
||||
{
|
||||
// 如果指定了验证场景,并且验证器支持场景方法,则设置验证场景
|
||||
if ($scene && method_exists($validator, 'scene')) {
|
||||
$validator->scene($scene);
|
||||
}
|
||||
// 执行数据验证
|
||||
if (!$validator->check($data)) {
|
||||
// 验证失败,抛出业务异常,异常信息为验证器返回的错误信息
|
||||
throw new BusinessException($validator->getError());
|
||||
}
|
||||
// 验证成功,返回原始数据
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function page($result){
|
||||
$current = $result->currentPage();
|
||||
$total = $result->total();
|
||||
$size = $result->perPage();
|
||||
$result = $result->toArray();
|
||||
$result['current'] = $current;
|
||||
$result['total'] = $total;
|
||||
$result['size'] = $size;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use app\crm\controller\BaseController;
|
||||
use app\crm\model\CarType;
|
||||
use app\crm\model\Customer;
|
||||
use app\crm\utils\Util;
|
||||
use PDO;
|
||||
use support\Db;
|
||||
use support\Redis;
|
||||
use support\Request;
|
||||
|
||||
class CarTypeController extends BaseController
|
||||
{
|
||||
|
||||
public function getAll(Request $request)
|
||||
{
|
||||
$list = CarType::getAll();
|
||||
|
||||
if($request->user['language'] != 'zh_CN'){
|
||||
foreach ($list as &$value) {
|
||||
$value['name'] = $value['name_en'];
|
||||
}
|
||||
}elseif ($request->user['language'] == 'ru'){
|
||||
foreach ($list as &$value) {
|
||||
$value['name'] = $value['name_ru'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success($list);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\controller;
|
||||
|
||||
use app\common\model\Corp;
|
||||
use app\common\model\User;
|
||||
use app\crm\model\Channel;
|
||||
use app\crm\model\Country;
|
||||
use app\crm\model\Customer;
|
||||
use app\crm\model\CustomerHis;
|
||||
use app\crm\model\Follow;
|
||||
use app\crm\service\CustomerService;
|
||||
use app\crm\utils\Util;
|
||||
use config\constants\Constants;
|
||||
use support\Request;
|
||||
|
||||
class CustomerController extends BaseController
|
||||
{
|
||||
|
||||
/**
|
||||
* 不需要头信息的方法
|
||||
*/
|
||||
protected $noNeedHeader = [];
|
||||
|
||||
protected $noNeedLogin = ['getChannel', 'getCountry'];
|
||||
|
||||
/**
|
||||
* 客户分级
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function getGrade(Request $request)
|
||||
{
|
||||
$grade = [];
|
||||
$gradeConstants = Constants::GRADE_EN;
|
||||
if('zh_CN' == 'zh_CN'){
|
||||
$gradeConstants = Constants::GRADE_ZH;
|
||||
}elseif ('zh_CN' == 'ru'){
|
||||
$gradeConstants = Constants::GRADE_RU;
|
||||
}
|
||||
foreach ($gradeConstants as $key => $value) {
|
||||
$grade[] = [
|
||||
'id' => $key,
|
||||
'name' => $value,
|
||||
];
|
||||
}
|
||||
return $this->success($grade);
|
||||
}
|
||||
|
||||
/**
|
||||
* 商机
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function getMagnitude(Request $request)
|
||||
{
|
||||
$grade = [];
|
||||
$magnitudeConstants = Constants::MAGNITUDE_EN;
|
||||
if('zh_CN' == 'zh_CN'){
|
||||
$magnitudeConstants = Constants::MAGNITUDE;
|
||||
}elseif ('zh_CN' == 'ru'){
|
||||
$magnitudeConstants = Constants::MAGNITUDE_RU;
|
||||
}
|
||||
foreach ($magnitudeConstants as $key => $value) {
|
||||
$grade[] = [
|
||||
'id' => $key,
|
||||
'name' => $value,
|
||||
];
|
||||
}
|
||||
return $this->success($grade);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户渠道
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function getChannel(Request $request)
|
||||
{
|
||||
$list = Channel::getAll();
|
||||
|
||||
if('zh_CN' != 'zh_CN'){
|
||||
foreach ($list as &$value) {
|
||||
$value['name'] = $value['name_en'];
|
||||
}
|
||||
}elseif ('zh_CN' == 'ru'){
|
||||
foreach ($list as &$value) {
|
||||
$value['name'] = $value['name_ru'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success($list);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 国家
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function getCountry(Request $request)
|
||||
{
|
||||
$list = Country::getAll();
|
||||
|
||||
if('zh_CN' != 'zh_CN'){
|
||||
foreach ($list as &$value) {
|
||||
$value['name'] = $value['name_en'];
|
||||
}
|
||||
}elseif ('zh_CN' == 'ru'){
|
||||
foreach ($list as &$value) {
|
||||
$value['name'] = $value['name_ru'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success($list);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 待跟进客户列表
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function waitFollow(Request $request)
|
||||
{
|
||||
$userId = $request->user->uid;
|
||||
$filter = Util::getParam($request, 'filter');
|
||||
$page = $request->input('current', 1);
|
||||
$perPage = $request->input('size', 10);
|
||||
$where = [
|
||||
['user_id', '=', $userId],
|
||||
['corp_abbr', '=', $request->basic_data['corp_abbr']],
|
||||
['follow_next', '<>', 0],
|
||||
];
|
||||
|
||||
if($filter == 0){
|
||||
$where[] = ['follow_next', '<=', time() + 3600 * 24];
|
||||
} elseif ($filter == 1) {
|
||||
$where[] = ['follow_next', '<=', time() + 3600 * 24 * 3];
|
||||
} elseif ($filter == 2) {
|
||||
$where[] = ['follow_next', '<=', time() + 3600 * 24 * 7];
|
||||
}
|
||||
$customer = Customer::where($where)
|
||||
->whereNotNull('follow_next')
|
||||
->orderBy('follow_next')
|
||||
->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
foreach ($customer as &$item) {
|
||||
$item['follow_remaining'] = $item->follow_next-time();
|
||||
$item['follow_next'] = date('Y-m-d H:i:s',$item->follow_next);
|
||||
}
|
||||
return $this->success($this->page($customer));
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户列表
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
try {
|
||||
$param = $request->all();
|
||||
$param['user_id'] = $request->user->uid;
|
||||
$param['corp_abbr'] = $request->basic_data['corp_abbr'];
|
||||
$result = (new CustomerService())->list($param);
|
||||
|
||||
return $this->success($this->page($result));
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage().$e->getFile().$e->getLine());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户恢复
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function restore(Request $request)
|
||||
{
|
||||
$id = Util::getParam($request,'id', '');
|
||||
if (Util::isEmpty($id)) {
|
||||
return $this->error(trans('id_empty'));
|
||||
}
|
||||
$userId = $request->user->uid;
|
||||
$where = [
|
||||
'id' => $id,
|
||||
'user_id' => $userId,
|
||||
];
|
||||
$res = Customer::where($where)->restore();
|
||||
if($res){
|
||||
return $this->success($res, trans('success'));
|
||||
}else{
|
||||
return $this->error(trans('fail'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 客户详情
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function detail(Request $request)
|
||||
{
|
||||
$id = Util::getParam($request, 'id');
|
||||
if (Util::isEmpty($id)) {
|
||||
return $this->error('id' . trans('not_null'));
|
||||
}
|
||||
|
||||
// $res = Customer::with('tags')->select('id', 'user_id', 'grade', 'channel_id', 'car_type_id', 'country_id', 'magnitude', 'email', 'phone', 'WhatsApp', 'contacts', 'describe', 'address', 'lat', 'lng', 'follow_next', 'follow_remark', 'sex')
|
||||
// ->where('id', $id)
|
||||
// ->first();
|
||||
$res = (new CustomerService)->detail($request->all());
|
||||
if ($res) {
|
||||
$res->toArray();
|
||||
} else {
|
||||
return $this->error('记录不存在');
|
||||
}
|
||||
|
||||
$res = CustomerService::getCustomer($res, 'zh_CN', true);
|
||||
|
||||
return $this->success($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户录入
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function save(Request $request)
|
||||
{
|
||||
$id = Util::getParam($request, 'id');
|
||||
$channel_id = Util::getParam($request, 'channel_id');
|
||||
if (Util::isEmpty($channel_id)) {
|
||||
return $this->error(trans('channel_not_null'));
|
||||
}
|
||||
$country_id = Util::getParam($request, 'country_id', 0);
|
||||
if (Util::isEmpty($country_id)) {
|
||||
// return $this->error('country_id'.trans('not_null'));
|
||||
}
|
||||
// $car_type_id = Util::getParam($request, 'car_type_id');
|
||||
// if (Util::isEmpty($car_type_id)) {
|
||||
// return $this->error(trans('car_type_not_null'));
|
||||
// }
|
||||
$contacts = Util::getParam($request, 'contacts');
|
||||
$email = Util::getParam($request, 'email');
|
||||
$phone = Util::getParam($request, 'phone');
|
||||
if (Util::isEmpty($contacts) && Util::isEmpty($email) && Util::isEmpty($phone)) {
|
||||
return $this->error(trans('contacts_not_null'));
|
||||
}
|
||||
|
||||
$corpInfo = Corp::where('abbr', $request->basic_data['corp_abbr'])->first();
|
||||
$corpId = $corpInfo->id;
|
||||
$data = [
|
||||
'user_id' => $request->user->uid,
|
||||
'corp_abbr' => $corpId,
|
||||
'grade' => Util::getParam($request, 'grade', 0),
|
||||
'channel_id' => $channel_id,
|
||||
'country_id' => $country_id,
|
||||
// 'car_type_id' => $car_type_id,
|
||||
'magnitude' => Util::getParam($request, 'magnitude', 0),
|
||||
'email' => $email,
|
||||
'phone' => $phone,
|
||||
'WhatsApp' => Util::getParam($request, 'WhatsApp', ''),
|
||||
'contacts' => $contacts,
|
||||
'describe' => Util::getParam($request, 'describe'),
|
||||
'lat' => Util::getParam($request, 'lat', 0),
|
||||
'lng' => Util::getParam($request, 'lng', 0),
|
||||
'address' => Util::getParam($request, 'address', ''),
|
||||
'sex' => Util::getParam($request, 'sex', 0),
|
||||
];
|
||||
|
||||
if (Util::isEmpty($id)) {
|
||||
$res = Customer::create($data);
|
||||
} else {
|
||||
unset($data['grade']);
|
||||
$res = Customer::where('id', $id)
|
||||
->update($data);
|
||||
// Log::info('update', ['res' => $res]);
|
||||
(!$res) ?: $res = Customer::find($id)->toArray();
|
||||
}
|
||||
|
||||
if ($res) {
|
||||
return $this->success($res, trans('save_success'));
|
||||
} else {
|
||||
return $this->error(trans('save_fail'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新下次跟进时间
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function followNext(Request $request)
|
||||
{
|
||||
$id = Util::getParam($request, 'id');
|
||||
if (Util::isEmpty($id)) {
|
||||
return $this->error('id' . trans('not_null'));
|
||||
}
|
||||
$data = [
|
||||
'follow_next' => Util::getParam($request, 'follow_next', 0),
|
||||
'follow_remark' => Util::getParam($request, 'follow_remark', 0),
|
||||
'warn' => 0,
|
||||
];
|
||||
|
||||
$res = Customer::where('id', $id)
|
||||
->update($data);
|
||||
|
||||
if ($res) {
|
||||
return $this->success($res, trans('save_success'));
|
||||
} else {
|
||||
return $this->error(trans('save_fail'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除客户
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$id = Util::getParam($request, 'id');
|
||||
$force = Util::getParam($request, 'force','');
|
||||
|
||||
$userId = $request->user->uid;
|
||||
$where = [
|
||||
'id' => $id,
|
||||
'user_id' => $userId,
|
||||
];
|
||||
$customer = Customer::where($where)->withTrashed()->first();
|
||||
if(empty($customer)){
|
||||
return $this->error( trans('fail'));
|
||||
}
|
||||
if($force == 'force'){
|
||||
$res = $customer->forceDelete();
|
||||
}else{
|
||||
$res = $customer->delete();
|
||||
}
|
||||
if ($res) {
|
||||
return $this->success($res, trans('success'));
|
||||
} else {
|
||||
return $this->error(trans('fail'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计客户
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function statistics(Request $request)
|
||||
{
|
||||
$created = Util::getParam($request, 'created');
|
||||
if (Util::isEmpty($created)) {
|
||||
return $this->error('created' . trans('not_null'));
|
||||
}
|
||||
$userId = $request->user->uid;
|
||||
//$created_at = $request->user['created_at'];
|
||||
$created_at = User::find($userId)->value('created_at');
|
||||
$created_at = strtotime($created_at);
|
||||
$day = ceil((time() - $created_at + 1) / 60 / 60 / 24);
|
||||
$where[] = ['user_id', '=', $userId];
|
||||
//$where[] = ['corp_abbr', '=', $request->basic_data['corp_abbr']];
|
||||
$start = strtotime($created . '-01');
|
||||
$year = date('Y', $start);
|
||||
$month = date('n', $start); //获取月份
|
||||
$end = mktime(00, 00, 00, $month + 1, 1, $year);
|
||||
$where[] = ['created_at', '>=', $start];
|
||||
$where[] = ['created_at', '<', $end];
|
||||
$customer = Customer::where($where)->count();
|
||||
|
||||
//初步接触
|
||||
$whereGrade = $where;
|
||||
$whereGrade[] = ['grade', '<=', 2];
|
||||
$customerInit = Customer::where($whereGrade)->count();
|
||||
|
||||
//跟进中
|
||||
$customeroFllow = Customer::where($where)->whereIn('grade', [3, 4])->count();
|
||||
|
||||
//已成交
|
||||
$whereGrade = $where;
|
||||
$whereGrade[] = ['grade', '=', 5];
|
||||
$customerDeal = Customer::where($whereGrade)->count();
|
||||
|
||||
//拜访数
|
||||
$whereVisit = $where;
|
||||
$whereVisit[] = ['type', '=', 0];
|
||||
$visit = Follow::where($whereVisit)->count();
|
||||
|
||||
//跟进数
|
||||
$whereVisit = $where;
|
||||
$whereVisit[] = ['type', '=', 1];
|
||||
$follow = Follow::where($whereVisit)->count();
|
||||
|
||||
$res = [
|
||||
'customer' => $customer, //客户数
|
||||
'visit' => $visit, //拜访
|
||||
'follow' => $follow, //跟进
|
||||
'day' => $day, //注册天数
|
||||
'customer_init' => $customerInit, //初步接触
|
||||
'customero_fllow' => $customeroFllow, //跟进中
|
||||
'customer_deal' => $customerDeal, //已成交
|
||||
];
|
||||
if ($res) {
|
||||
return $this->success($res, trans('success'));
|
||||
} else {
|
||||
return $this->error(trans('fail'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 领取公海客户
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function claim(Request $request)
|
||||
{
|
||||
$customerId = Util::getParam($request, 'customer_id');
|
||||
$userId = $request->user->uid;
|
||||
if (Util::isEmpty($customerId)) {
|
||||
return $this->error(trans('customer_id').trans('not_null'));
|
||||
}
|
||||
$data = [
|
||||
'user_id' => $request->user->uid
|
||||
];
|
||||
$customer= Customer::withTrashed()->find($customerId);
|
||||
|
||||
if(empty($customer) || $customer->user_id != 0){
|
||||
return $this->error( trans('claim_fail'));
|
||||
}
|
||||
|
||||
$customer->restore();
|
||||
$res = $customer->update($data);
|
||||
|
||||
//记录客户关系变化
|
||||
$his = CustomerHis::create([
|
||||
'customer_id' => $customerId,
|
||||
'user_his' => 0,
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
|
||||
if ($res) {
|
||||
return $this->success($res, trans('success'));
|
||||
} else {
|
||||
return $this->error(trans('fail'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 批量转让
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function cede(Request $request)
|
||||
{
|
||||
$customerId = Util::getParam($request, 'customer_id');
|
||||
$user_sn = Util::getParam($request, 'user_sn');
|
||||
$userId = $request->user->uid;
|
||||
if (Util::isEmpty($user_sn)) {
|
||||
return $this->error('ID'.trans('not_null'));
|
||||
}
|
||||
|
||||
$user = User::where(['sn' => $user_sn])->first();
|
||||
if (Util::isEmpty($user)) {
|
||||
return $this->error( trans('user_none'));
|
||||
}
|
||||
|
||||
if(Util::isEmpty($customerId)){
|
||||
$data = [
|
||||
'user_id' => $userId
|
||||
];
|
||||
$query= Customer::where($data);
|
||||
}else{
|
||||
$query= Customer::whereIn('id',explode(',',$customerId));
|
||||
}
|
||||
$customer = $query->get();
|
||||
|
||||
foreach ($customer as $item) {
|
||||
//记录客户关系变化
|
||||
$his = CustomerHis::create([
|
||||
'customer_id' => $item->id,
|
||||
'user_his' => $item->user_id,
|
||||
'user_id' => $user['id'],
|
||||
]);
|
||||
}
|
||||
$res = $query->update(['user_id' => $user->id]);
|
||||
|
||||
|
||||
if ($res) {
|
||||
return $this->success($res, trans('success'));
|
||||
} else {
|
||||
return $this->error(trans('fail'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟进状态
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function grade(Request $request)
|
||||
{
|
||||
$id = Util::getParam($request, 'id');
|
||||
$grade = Util::getParam($request, 'grade');
|
||||
if (Util::isEmpty($id)) {
|
||||
return $this->error( 'id' . trans('not_null'));
|
||||
}
|
||||
if (Util::isEmpty($id)) {
|
||||
return $this->error( 'grade' . trans('not_null'));
|
||||
}
|
||||
$data = [
|
||||
'grade' => $grade,
|
||||
];
|
||||
$res = Customer::where('id', $id)
|
||||
->update($data);
|
||||
|
||||
if ($res) {
|
||||
return $this->success($res, trans('save_success'));
|
||||
} else {
|
||||
return $this->error(trans('save_fail'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\controller;
|
||||
|
||||
use app\common\model\Corp;
|
||||
use app\crm\model\Customer;
|
||||
use app\crm\model\Follow;
|
||||
use app\crm\service\CustomerService;
|
||||
use app\crm\service\FollowService;
|
||||
use app\crm\utils\Util;
|
||||
use app\crm\validate\FollowValidate;
|
||||
use config\constants\Constants;
|
||||
use support\Request;
|
||||
|
||||
class FollowController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 客户分级
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function getFollowType(Request $request)
|
||||
{
|
||||
$grade = [];
|
||||
foreach (Constants::FOLLOW as $key => $value) {
|
||||
$grade[] = [
|
||||
'status' => $key,
|
||||
'text' => $value,
|
||||
];
|
||||
}
|
||||
return $this->success($grade);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 跟进列表
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function list(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->validate(new FollowValidate(), $request->get(), 'list');
|
||||
$request->setGet('corp_abbr', $request->basic_data['corp_abbr']);
|
||||
$result = (new FollowService())->list($request->all(), $request->user->uid);
|
||||
return $this->success($result);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟进详情
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function detail(Request $request)
|
||||
{
|
||||
//跟进id
|
||||
$id = Util::getParam($request, 'id');
|
||||
if (Util::isEmpty($id)) {
|
||||
return $this->error('id' . trans('not_null'));
|
||||
}
|
||||
|
||||
$res = Follow::select('id', 'user_id', 'customer_id', 'type', 'picture', 'remark', 'audio', 'audio_text', 'lat', 'lng', 'address')
|
||||
->where('id', $id)
|
||||
->first();
|
||||
if ($res) {
|
||||
$res->toArray();
|
||||
} else {
|
||||
return $this->error('记录不存在');
|
||||
}
|
||||
|
||||
$res = FollowService::getFollow($res);
|
||||
|
||||
return $this->success($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户录入
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function save(Request $request)
|
||||
{
|
||||
$customer_id = Util::getParam($request, 'customer_id');
|
||||
|
||||
if (Util::isEmpty($customer_id)) {
|
||||
return $this->error('customer_id' . trans('not_null'));
|
||||
}
|
||||
$type = Util::getParam($request, 'type');
|
||||
if (Util::isEmpty($type)) {
|
||||
return $this->error('type' . trans('not_null'));
|
||||
}
|
||||
$audio_text = Util::getParam($request, 'audio_text', '');
|
||||
$picture = Util::getParam($request, 'picture');
|
||||
$remark = Util::getParam($request,'remark');
|
||||
if(Util::isEmpty($picture) && Util::isEmpty($audio_text) && Util::isEmpty($remark)){
|
||||
return $this->error(trans('follow_not_null'));
|
||||
}
|
||||
|
||||
$corpInfo = Corp::where('abbr', $request->basic_data['corp_abbr'])->first();
|
||||
$corpId = $corpInfo->id;
|
||||
$data = [
|
||||
'user_id' => $request->user->uid,
|
||||
'corp_abbr' => $corpId,
|
||||
'customer_id' => $customer_id,
|
||||
'type' => $type,
|
||||
'picture' => $picture,
|
||||
'remark' => Util::getParam($request, 'remark', ''),
|
||||
'audio' => Util::getParam($request, 'audio', ''),
|
||||
'audio_text' => $audio_text,
|
||||
'lat' => Util::getParam($request, 'lat', 0),
|
||||
'lng' => Util::getParam($request, 'lng', 0),
|
||||
'address' => Util::getParam($request, 'address'),
|
||||
'follow_time' => strtotime(Util::getParam($request, 'firstFollowtime', 0)),
|
||||
];
|
||||
|
||||
$res = Follow::create($data);
|
||||
|
||||
$customer = Customer::find($customer_id);
|
||||
$customer->follow_last = strtotime($res->created_at);
|
||||
$customer->follow_next = null;
|
||||
$customer->warn = 0;
|
||||
$customer->save();
|
||||
|
||||
$follow_next = Util::getParam($request, 'follow_next', 0);
|
||||
$follow_remark = Util::getParam($request, 'follow_remark', 0);
|
||||
if ($res && (!empty($follow_next))) {
|
||||
$data = [
|
||||
'follow_next' => $follow_next,
|
||||
'follow_remark' => $follow_remark,
|
||||
'warn' => 0,
|
||||
];
|
||||
$res = Customer::where('id', $customer_id)
|
||||
->update($data);
|
||||
}
|
||||
|
||||
if ($res) {
|
||||
return $this->success($res, trans('save_success'));
|
||||
} else {
|
||||
return $this->error(trans('save_fail'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\controller;
|
||||
|
||||
use app\crm\controller\BaseController;
|
||||
use app\crm\model\Customer;
|
||||
use app\crm\model\CustomerTag;
|
||||
use app\crm\model\TagGroup;
|
||||
use app\crm\model\Tag;
|
||||
use app\crm\utils\Util;
|
||||
use config\constants\Constants;
|
||||
use support\Request;
|
||||
|
||||
class TagGroupController extends BaseController
|
||||
{
|
||||
|
||||
// 列表
|
||||
public function list(Request $request)
|
||||
{
|
||||
$uid = $request->user->uid;
|
||||
$scene = $request->input("scene");
|
||||
$list = TagGroup::with('tags')->get();
|
||||
$languageArr = Constants::LANGUAGE;
|
||||
$language = 'zh_CN';
|
||||
foreach ($list as &$item) {
|
||||
$item['names'] = unserialize($item['names']);
|
||||
if (in_array($language, $languageArr)) {
|
||||
$item['name'] = $item['names'][$language];
|
||||
}
|
||||
foreach ($item['tags'] as &$tag) {
|
||||
$tag['names'] = unserialize($tag['names']);
|
||||
if (in_array($language, $languageArr)) {
|
||||
$tag['name'] = $tag['names'][$language];
|
||||
}
|
||||
if($scene == 'customers'){
|
||||
// 统计每个标签关联的客户数量,增加user_id条件
|
||||
$tag['count'] = $tag->customers()->where('user_id', $uid)->count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success($list);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新客户标签关系
|
||||
* @param Request $request
|
||||
* @return \support\Response
|
||||
*/
|
||||
public function updatePivot(Request $request)
|
||||
{
|
||||
$customerId = Util::getParam($request, 'customer_id');
|
||||
$groupId = Util::getParam($request, 'group_id');
|
||||
$tagId = Util::getParam($request, 'tag_id');
|
||||
if (Util::isEmpty($customerId)) {
|
||||
return $this->error( trans('customer_id') . trans('not_null'));
|
||||
}
|
||||
if (Util::isEmpty($groupId)) {
|
||||
return $this->error( trans('tag_group_id') . trans('not_null'));
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
if(empty($tagId)){
|
||||
$res = CustomerTag::where(['customer_id' => $customerId, 'group_id' => $groupId])->delete();
|
||||
}else{
|
||||
$res = CustomerTag::updateOrCreate(['customer_id' => $customerId, 'group_id' => $groupId], ['tag_id' => $tagId]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
if($res){
|
||||
return $this->success($res, trans('success'));
|
||||
}else{
|
||||
return $this->error( trans('fail'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\logic;
|
||||
|
||||
use app\crm\model\Customer;
|
||||
|
||||
class CustomerLogic
|
||||
{
|
||||
|
||||
public function find(int $id)
|
||||
{
|
||||
$customer = Customer::find($id);
|
||||
return $customer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\logic;
|
||||
|
||||
class FollowLogic
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
|
||||
|
||||
use support\Model;
|
||||
class CarType extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'car_type';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
public static function getAll()
|
||||
{
|
||||
$list = self::get()->select('id','name','name_en','status')->where('status',0)->toArray();
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class Channel extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'inquiry_channel';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
//设置黑名单
|
||||
protected $guarded = ['id','created_at','updated_at'];
|
||||
|
||||
// 指定日期字段存储格式为时间戳
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
// 'updated_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
|
||||
// 'created_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
|
||||
'updated_at' => 'timestamp', // 指定序列化格式
|
||||
'created_at' => 'timestamp', // 指定序列化格式
|
||||
];
|
||||
|
||||
public static function getAll()
|
||||
{
|
||||
$list = self::get()->select('id','name','name_en','status')->where('status',1)->toArray();
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
use support\Model;
|
||||
|
||||
class Country extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'country';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
//设置黑名单
|
||||
protected $guarded = ['id'];
|
||||
|
||||
// 指定日期字段存储格式为时间戳
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
public static function getAll()
|
||||
{
|
||||
$list = self::select('id','cn_addr','en_addr')->get();
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
|
||||
use app\crm\utils\Util;
|
||||
use config\constants\Constants;
|
||||
use DateTimeInterface;
|
||||
use Illuminate\Support\Carbon;
|
||||
use support\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use app\common\model\User;
|
||||
|
||||
class Customer extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'crm_customer';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
//设置黑名单
|
||||
protected $guarded = ['id','created_at','updated_at'];
|
||||
|
||||
// 指定日期字段存储格式为时间戳
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
use SoftDeletes;
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
protected $casts = [
|
||||
'updated_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
|
||||
'created_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
|
||||
];
|
||||
|
||||
/**
|
||||
* 重新格式化时间
|
||||
* @param DateTimeInterface $date
|
||||
* @return string
|
||||
*/
|
||||
protected function serializeDate(DateTimeInterface $date): string
|
||||
{
|
||||
return $date->format(Carbon::parse($date)->toDateTimeString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户的标签
|
||||
*/
|
||||
public function tags()
|
||||
{
|
||||
return $this->belongsToMany(Tag::class, 'crm_customer_tag', 'customer_id', 'tag_id')
|
||||
->withTimestamps(); // 如果需要记录关联时间
|
||||
}
|
||||
|
||||
|
||||
// 定义与 变更历史 的一对多关系
|
||||
public function his()
|
||||
{
|
||||
return $this->hasMany(CustomerHis::class, 'customer_id', 'id');
|
||||
}
|
||||
|
||||
// 定义反向多对一关联
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
|
||||
use app\crm\utils\Util;
|
||||
use config\constants\Constants;
|
||||
use DateTimeInterface;
|
||||
use Illuminate\Support\Carbon;
|
||||
use support\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CustomerHis extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'crm_customer_his';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
//设置黑名单
|
||||
protected $guarded = ['id','created_at'];
|
||||
|
||||
// 指定日期字段存储格式为时间戳
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
|
||||
// 'created_at' => 'timestamp', // 指定序列化格式
|
||||
];
|
||||
|
||||
/**
|
||||
* 重新格式化时间
|
||||
* @param DateTimeInterface $date
|
||||
* @return string
|
||||
*/
|
||||
protected function serializeDate(DateTimeInterface $date): string
|
||||
{
|
||||
return $date->format(Carbon::parse($date)->toDateTimeString());
|
||||
}
|
||||
|
||||
// 定义反向多对一关联
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(Customer::class, 'customer_id', 'id');
|
||||
}
|
||||
|
||||
// 禁用 updated_at 的自动更新
|
||||
public function setUpdatedAt($value)
|
||||
{
|
||||
return $this; // 不做任何操作
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
|
||||
use app\crm\utils\Util;
|
||||
use support\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CustomerTag extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'crm_customer_tag';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
//设置黑名单
|
||||
protected $guarded = ['id', 'created_at'];
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = false;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
|
||||
use app\service\OssService;
|
||||
use app\crm\utils\Util;
|
||||
use config\constants\Constants;
|
||||
use support\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Follow extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'crm_follow';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
//设置黑名单
|
||||
protected $guarded = ['id','created_at','updated_at'];
|
||||
|
||||
// 指定日期字段存储格式为时间戳
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
protected $dates = ['created_at', 'updated_at', 'follow_time'];
|
||||
|
||||
protected $casts = [
|
||||
'updated_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
|
||||
'created_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
|
||||
'follow_time' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
|
||||
// 'updated_at' => 'timestamp', // 指定序列化格式
|
||||
// 'created_at' => 'timestamp', // 指定序列化格式
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
|
||||
use app\util\Util;
|
||||
use config\constants\Constants;
|
||||
use DateTimeInterface;
|
||||
use Illuminate\Support\Carbon;
|
||||
use support\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Tag extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'crm_tag';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
//设置黑名单
|
||||
protected $guarded = ['id','created_at','updated_at'];
|
||||
|
||||
// 指定日期字段存储格式为时间戳
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
use SoftDeletes;
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
// 定义反向多对一关联
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(TagGroup::class, 'group_id', 'id');
|
||||
}
|
||||
|
||||
public function customers()
|
||||
{
|
||||
return $this->belongsToMany(Customer::class, 'crm_customer_tag', 'tag_id', 'customer_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\model;
|
||||
|
||||
|
||||
use app\crm\utils\Util;
|
||||
use config\constants\Constants;
|
||||
use DateTimeInterface;
|
||||
use Illuminate\Support\Carbon;
|
||||
use support\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class TagGroup extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'crm_tag_group';
|
||||
|
||||
/**
|
||||
* The primary key associated with the table.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
//设置黑名单
|
||||
protected $guarded = ['id','created_at','updated_at'];
|
||||
|
||||
// 指定日期字段存储格式为时间戳
|
||||
protected $dateFormat = 'U';
|
||||
|
||||
/**
|
||||
* Indicates if the model should be timestamped.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $timestamps = true;
|
||||
|
||||
use SoftDeletes;
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
// 定义与 Tag 的一对多关系
|
||||
public function tags()
|
||||
{
|
||||
return $this->hasMany(Tag::class, 'group_id', 'id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\service;
|
||||
|
||||
use app\crm\logic\CustomerLogic;
|
||||
use app\crm\model\CarType;
|
||||
use app\crm\model\Channel;
|
||||
use app\crm\model\Country;
|
||||
use app\crm\model\Customer;
|
||||
use app\crm\model\Follow;
|
||||
use app\crm\utils\Util;
|
||||
use config\constants\Constants;
|
||||
|
||||
class CustomerService
|
||||
{
|
||||
public function list($params)
|
||||
{
|
||||
// $page = $request->input('page', 1);
|
||||
// $perPage = $request->input('per_page', 10);
|
||||
$perPage = $params['size'] ?? 10;
|
||||
$page = $params['current'] ?? 1;
|
||||
$keyword = $params['keyword'] ?? '';
|
||||
$recycle = $params['recycle'] ?? '';
|
||||
$follow = $params['follow'] ?? '';
|
||||
$follow_last = $params['follow_last'] ?? '';
|
||||
$tagId = $params['tag_id'] ?? [];
|
||||
$where = [];
|
||||
$user_id =$params['user_id'];
|
||||
|
||||
$seas = $params['seas'] ?? '';
|
||||
if($seas == 'seas'){
|
||||
$where[] = ['user_id', 0];
|
||||
// }elseif($recycle == 'recycle'){
|
||||
// $where[] = ['user_id', $user_id];
|
||||
}else{
|
||||
$where[] = ['user_id', $user_id];
|
||||
}
|
||||
|
||||
|
||||
$grade = [];
|
||||
if (!Util::isEmpty($follow) && $follow == 0) {
|
||||
$where[] = ['grade', '<=', 2];
|
||||
} elseif (!Util::isEmpty($follow) && $follow == 1) {
|
||||
$grade = [3, 4];
|
||||
} elseif (!Util::isEmpty($follow) && $follow == 2) {
|
||||
$where[] = ['grade', '=', 5];
|
||||
}
|
||||
|
||||
if (!Util::isEmpty($follow_last) && $follow_last == 0) {
|
||||
$where[] = ['follow_last', '>', strtotime('-1 day')];
|
||||
} elseif (!Util::isEmpty($follow_last) && $follow_last == 1) {
|
||||
$where[] = ['follow_last', '>', strtotime('-3 day')];
|
||||
} elseif (!Util::isEmpty($follow_last) && $follow_last == 2) {
|
||||
$where[] = ['follow_last', '>', strtotime('-7 day')];
|
||||
}
|
||||
|
||||
$query = Customer::where($where);
|
||||
|
||||
if(!empty($tagId)){
|
||||
//查询有$tagId 里任意一个标签的客户
|
||||
//$query->whereHas('tags', function($query) use ($tagId){
|
||||
// $query->whereIn ('tag.id', $tagId);
|
||||
//});
|
||||
//查询有$tagId 里所有标签的客户
|
||||
foreach ($tagId as $id) {
|
||||
$query->whereHas('tags', function($query) use ($id){
|
||||
$query->where('tag_id', $id);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!Util::isEmpty($keyword)) {
|
||||
$query->where(function ($query) use ($keyword) {
|
||||
$query->where('contacts', 'like', "%{$keyword}%")
|
||||
->orWhere('phone', 'like', "%{$keyword}%");
|
||||
});
|
||||
}
|
||||
if (!Util::isEmpty($grade)) {
|
||||
$query->whereIn('grade', $grade);
|
||||
}
|
||||
|
||||
if ($recycle == 'recycle') {
|
||||
$query->onlyTrashed();
|
||||
}
|
||||
if ($seas == 'seas') {
|
||||
$query->withTrashed();
|
||||
}
|
||||
|
||||
$customer = $query->paginate($perPage, ['*'], 'page', $page);
|
||||
foreach ($customer as &$item) {
|
||||
$item = $this->getCustomer($item);
|
||||
if($seas == 'seas'){
|
||||
$item['phone'] = Util::mask($item['phone']);
|
||||
$item['email'] = Util::mask($item['email']);
|
||||
$item['WhatsApp'] = Util::mask($item['WhatsApp']);
|
||||
$item['seas_time'] = time()-(empty($item['deleted_at'])?strtotime($item['updated_at']):strtotime($item['deleted_at']));
|
||||
}
|
||||
}
|
||||
unset($item);
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $customer
|
||||
* @param $request
|
||||
* @param $tag 是否返回客户标签
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getCustomer($customer, $language = 'zh_CN', $tag = false)
|
||||
{
|
||||
$customer['grade_txt'] = Util::language($language, Constants::GRADE_EN[$customer['grade']], Constants::GRADE_ZH[$customer['grade']], Constants::GRADE_RU[$customer['grade']]);
|
||||
$customer['magnitude_txt'] = Util::language($language, Constants::MAGNITUDE_EN[$customer['magnitude']], Constants::MAGNITUDE[$customer['magnitude']], Constants::MAGNITUDE_RU[$customer['magnitude']]);
|
||||
$customer['sex_txt'] = Util::language($language, Constants::SEX_EN[$customer['sex']], Constants::SEX[$customer['sex']], Constants::SEX_RU[$customer['sex']]);
|
||||
|
||||
if ($customer['grade']<=2) {
|
||||
$customer['follow'] = 0;
|
||||
$customer['follow_txt'] = Util::language($language,'Following','跟进中','Продолжение');
|
||||
}elseif ($customer['grade']<5) {
|
||||
$customer['follow'] = 1;
|
||||
$customer['follow_txt'] = Util::language($language,'transacted','已成交','Транзакция завершена');
|
||||
}else{
|
||||
$customer['follow'] = 2;
|
||||
$customer['follow_txt'] = Util::language($language,'lost','已流失','Потерянный');
|
||||
}
|
||||
|
||||
$where = [
|
||||
'customer_id' => $customer['id']
|
||||
];
|
||||
$begin_follow = Follow::select('id','created_at','updated_at')
|
||||
->where($where)
|
||||
->min('created_at');
|
||||
|
||||
|
||||
$customer['begin_follow'] = Util::timeTotDate($begin_follow);
|
||||
// $customer['created_at'] = Util::timeTotDate($customer['created_at']);
|
||||
// $customer['updated_at'] = Util::timeTotDate($customer['updated_at']);
|
||||
|
||||
$customer = Util::formatDate($customer,['follow_last']);
|
||||
$nameLang = $language=='en'?'name_en':($language=='ru'?'name_ru':'name');
|
||||
$countryLang = $language=='en'?'en_addr':'cn_addr';
|
||||
// $customer['car_type_name'] = empty($customer['car_type_id'])?'':CarType::where(['id'=>$customer['car_type_id']])->value($nameLang);
|
||||
$customer['country_name'] = empty($customer['country_id'])?'':Country::where(['id'=>$customer['country_id']])->value($countryLang);
|
||||
$customer['channel_name'] = empty($customer['channel_id'])?'':Channel::where(['id'=>$customer['channel_id']])->value($nameLang);
|
||||
// $customer['last_follow'] = $last_follow;
|
||||
|
||||
if ($tag) {
|
||||
foreach ($customer['tags'] as &$tag) {
|
||||
$tag['names'] = unserialize($tag['names']);
|
||||
$tag['name'] = $tag['names'][$language];
|
||||
}
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
|
||||
public function detail($params)
|
||||
{
|
||||
$id =$params['id'];
|
||||
$customer = Customer::with('tags')->select('id', 'user_id', 'grade', 'channel_id', 'car_type_id', 'country_id', 'magnitude', 'email', 'phone', 'WhatsApp', 'contacts', 'describe', 'address', 'lat', 'lng', 'follow_next', 'follow_remark', 'sex')
|
||||
->where('id', $id)
|
||||
->first();
|
||||
|
||||
return $customer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\service;
|
||||
|
||||
use app\common\service\OssService;
|
||||
use app\crm\model\Follow;
|
||||
use app\crm\utils\Util;
|
||||
use config\constants\Constants;
|
||||
|
||||
class FollowService
|
||||
{
|
||||
|
||||
public function list($param, $userId)
|
||||
{
|
||||
|
||||
|
||||
// $page = $request->input('page', 1);
|
||||
// $perPage = $request->input('per_page', 10);
|
||||
|
||||
//客户id
|
||||
$customer_id = $param['customer_id'];
|
||||
$corp_abbr = $param['corp_abbr'];
|
||||
|
||||
$where = [];
|
||||
$where['customer_id'] = $customer_id;
|
||||
// $where['corp_abbr'] = $corp_abbr;
|
||||
|
||||
//用户角色
|
||||
|
||||
//跟进id
|
||||
$id = $param['id'] ?? null;
|
||||
if (!empty($id)) {
|
||||
$where['id'] = $id;
|
||||
}
|
||||
|
||||
// 执行分页查询
|
||||
$data = Follow::where($where)
|
||||
->orderBy('id', 'desc')->get();
|
||||
// ->paginate($perPage, ['*'], 'page', $page);
|
||||
|
||||
|
||||
// $host = $request->host();
|
||||
// $url = 'http:'.$request->url();
|
||||
foreach ($data as &$value) {
|
||||
$value = self::getFollow($value);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
public static function getFollow($follow)
|
||||
{
|
||||
$follow['type_txt'] = Constants::FOLLOW[$follow['type']];
|
||||
$follow['firstFollowtime'] = $follow['follow_time'];
|
||||
$oss_picture = '';
|
||||
if (!empty($follow['picture'])) {
|
||||
$ossservice = new OssService();
|
||||
$oss_picture = $ossservice->generatePrivateUrl($follow['picture'], 86400);
|
||||
}
|
||||
$follow['picture_full'] = $oss_picture;
|
||||
// $follow = Util::formatDate($follow);
|
||||
return $follow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\utils;
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
use Firebase\JWT\ExpiredException;
|
||||
use Firebase\JWT\SignatureInvalidException;
|
||||
use support\exception\BusinessException;
|
||||
use support\Log;
|
||||
use support\Request;
|
||||
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* 参数是否为空
|
||||
*/
|
||||
public static function isEmpty($param)
|
||||
{
|
||||
if(!empty($param) || $param == 0){
|
||||
$result = false;
|
||||
}else{
|
||||
$result = true;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数是否为空
|
||||
*/
|
||||
public static function getParam(Request $request,$param,$default='')
|
||||
{
|
||||
$param = $request->input($param,$default);
|
||||
if(empty($param) && $default!==''){
|
||||
$param = $default;
|
||||
}
|
||||
return $param;
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败
|
||||
*/
|
||||
public static function fail($data = [],$message = '', $code = -1)
|
||||
{
|
||||
$result = [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
];
|
||||
return json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功
|
||||
*/
|
||||
public static function success($data = [],$message = '', $code = 0)
|
||||
{
|
||||
$result = [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => $data
|
||||
];
|
||||
return json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询返回
|
||||
*/
|
||||
public static function page($data,$message = '', $code = 0)
|
||||
{
|
||||
$result = [
|
||||
'code' => $code,
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
$result['data'] = [
|
||||
'list' => $data->items(), // 当前页数据
|
||||
'total' => $data->total(), // 总记录数
|
||||
'current_page' => $data->currentPage(),
|
||||
'per_page' => $data->perPage(),
|
||||
'last_page' => $data->lastPage()
|
||||
];
|
||||
return json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期格式转化
|
||||
*/
|
||||
public static function timeTotDate($time = 0, $format = 'Y-m-d H:i:s')
|
||||
{
|
||||
if (empty($time)) {
|
||||
return '';
|
||||
}
|
||||
return date($format, $time);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化创建时间
|
||||
*/
|
||||
public static function formatDate($value,$key = ['created_at','updated_at'])
|
||||
{
|
||||
foreach ($key as $k){
|
||||
$value[$k]= self::timeTotDate($value[$k]);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化创建时间
|
||||
*/
|
||||
public static function language($language,$en,$zh_CN,$ru)
|
||||
{
|
||||
if ($language == 'zh_CN'){
|
||||
return $zh_CN;
|
||||
}elseif ($language == 'ru'){
|
||||
return $ru;
|
||||
}
|
||||
return $en;
|
||||
}
|
||||
|
||||
public static function mask($str)
|
||||
{
|
||||
if(empty($str)){
|
||||
return '';
|
||||
}
|
||||
return substr($str, 0, 3) . '****' . substr($str, -4);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace app\crm\validate;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class FollowValidate extends Validate
|
||||
{
|
||||
|
||||
protected $rule = [
|
||||
'customer_id' => 'require',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'customer_id.require' => '用户id不能为空',
|
||||
];
|
||||
|
||||
protected $scene = [
|
||||
'list' => ['customer_id'],
|
||||
];
|
||||
}
|
||||
@@ -66,6 +66,15 @@ if (!function_exists('getConfig')) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 询单号生成
|
||||
*/
|
||||
if (!function_exists('orderNum')) {
|
||||
function orderNum() {
|
||||
return "SN".date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 调试用
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace app\inquiry\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\inquiry\service\CountryService;
|
||||
|
||||
class CountryController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new CountryService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
// protected function indexFormat($data)
|
||||
// {
|
||||
// if (isset($data['created_at']))
|
||||
// $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
// if (isset($data['updated_at']))
|
||||
// $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
|
||||
// return $data;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
namespace app\inquiry\controller;
|
||||
|
||||
use app\common\controller\BaseController;
|
||||
use app\common\traits\crud\CrudControllerTrait;
|
||||
use app\inquiry\service\InquiryLogService;
|
||||
use support\Request;
|
||||
|
||||
class InquiryLogController extends BaseController
|
||||
{
|
||||
use CrudControllerTrait;
|
||||
|
||||
protected $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->service = new InquiryLogService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表页数据格式化
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
protected function indexFormat($data)
|
||||
{
|
||||
$data['countdown'] = time()-1;
|
||||
if (isset($data['r_time'])) {
|
||||
$countdown = 60 * 60;
|
||||
$data['countdown'] = (time() - $data['r_time']) < $countdown ? $data['r_time'] + (60*60) : time()-1;
|
||||
}
|
||||
|
||||
if (isset($data['created_at'])) {
|
||||
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
|
||||
}
|
||||
|
||||
if (isset($data['updated_at'])) {
|
||||
$data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']);
|
||||
}
|
||||
|
||||
if (isset($data['r_time'])) {
|
||||
$data['r_time'] = $data['r_time'] > 0 ? date('Y-m-d H:i:s', $data['r_time']) : 0;
|
||||
}
|
||||
|
||||
if (isset($data['t_time'])) {
|
||||
$data['t_time'] = $data['t_time'] > 0 ? date('Y-m-d H:i:s', $data['t_time']) : 0;
|
||||
}
|
||||
|
||||
if (isset($data['quality'])) {
|
||||
$inquiryLogService = new InquiryLogService();
|
||||
$data['quality'] = $inquiryLogService->qualityName($data['quality']);
|
||||
}
|
||||
// var_dump($data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function release(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'release');
|
||||
$res = $this->service->serviceRelease($request, $data);
|
||||
return $this->success([], '发布成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配
|
||||
*
|
||||
* @param Request $request
|
||||
* @return mixed
|
||||
*/
|
||||
public function allocation(Request $request)
|
||||
{
|
||||
try {
|
||||
$data = $this->validate($this->service->getValidator(), $request->post(), 'allocation');
|
||||
$res = $this->service->serviceAllocation($request, $data);
|
||||
return $this->success([], '分配成功');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
// public function export(Request $request)
|
||||
// {
|
||||
// if ($request->method() == "POST") {
|
||||
// $data = $request->all();
|
||||
|
||||
// $where = [];
|
||||
// if (isset($data['log_lusers']) && $data['log_lusers']) {
|
||||
// $where[] = ['enter_user_id', '=', $data['log_lusers']];
|
||||
// }
|
||||
// if (isset($data['log_gusers']) && $data['log_gusers']) {
|
||||
// $where[] = ['recipient', '=', $data['log_gusers']];
|
||||
// }
|
||||
// if (isset($data['log_country']) && $data['log_country']) {
|
||||
// $where[] = ['log_country', '=', $data['log_country']];
|
||||
// }
|
||||
// if (isset($data['log_channel']) && $data['log_channel']) {
|
||||
// $where[] = ['log_type', '=', $data['log_channel']];
|
||||
// }
|
||||
// if (isset($data['log_times']) && $data['log_times']) {
|
||||
// $times = explode(' - ', $data["log_times"]);
|
||||
// $specificDayStart = trim($times[0]);
|
||||
// $specificDayEnd = trim($times[1]);
|
||||
// $where[] = [
|
||||
// "c_time", ">", strtotime($specificDayStart)
|
||||
// ];
|
||||
// $where[] = [
|
||||
// "c_time", "<=", strtotime($specificDayEnd)
|
||||
// ];
|
||||
// }
|
||||
|
||||
// // 查询
|
||||
// $results = InquiryLogModel::with(['enterUser', 'recipientUser', 'country'])
|
||||
// ->where($where)
|
||||
// ->get()
|
||||
// ->map(function ($log) use($data) {
|
||||
// $arr = [];
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['订单号'] = $log->order_no;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['录入人'] = $log->enterUser->name ?? null;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['接单人'] = $log->recipientUser->name ?? null;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['等级'] = $log->log_quality;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['邮箱'] = $log->log_contact_type;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['电话'] = $log->log_contact_tel;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['WhatsApp'] = $log->log_contact_whatsapp;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['联系人'] = $log->log_contacts;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['国家(中文)'] = $log->country->cn_addr ?? null;
|
||||
// }
|
||||
// if (isset($data['hsl_order_no']) && $data['hsl_order_no']) {
|
||||
// $arr['国家(英文)'] = $log->country->en_full_name ?? null;
|
||||
// }
|
||||
// if (isset($data['hsl_r_time']) && $data['hsl_r_time']) {
|
||||
// $arr['发单时间'] = $log->r_time ? date('Y-m-d H:i:s', $log->r_time) : null;
|
||||
// }
|
||||
// if (isset($data['hsl_t_time']) && $data['hsl_t_time']) {
|
||||
// $arr['跟单时间'] = $log->t_time ? date('Y-m-d H:i:s', $log->t_time) : null;
|
||||
// }
|
||||
// return $arr;
|
||||
// });
|
||||
// // 创建一个新的 Spreadsheet 对象
|
||||
// $spreadsheet = new Spreadsheet();
|
||||
// // 设置当前活动的工作表
|
||||
// $sheet = $spreadsheet->getActiveSheet();
|
||||
// // 设置工作表的标题
|
||||
// $sheet->setCellValue('A1', '订单号');
|
||||
// $sheet->setCellValue('B1', '录入人');
|
||||
// $sheet->setCellValue('C1', '接单人');
|
||||
// $sheet->setCellValue('D1', '等级');
|
||||
// $sheet->setCellValue('E1', '邮箱');
|
||||
// $sheet->setCellValue('F1', '电话');
|
||||
// $sheet->setCellValue('G1', 'WhatsApp');
|
||||
// $sheet->setCellValue('H1', '联系人');
|
||||
// $sheet->setCellValue('I1', '国家(中文)');
|
||||
// $sheet->setCellValue('J1', '国家(英文)');
|
||||
// $sheet->setCellValue('K1', '发单时间');
|
||||
// $sheet->setCellValue('L1', '跟单时间');
|
||||
// $data = $results->toArray();
|
||||
|
||||
// // 将数据填充到工作表中
|
||||
// $row = 2; // 从第2行开始填充
|
||||
// foreach ($data as $item) {
|
||||
// $sheet->setCellValue('A' . $row, $item['订单号']);
|
||||
// $sheet->setCellValue('B' . $row, $item['录入人']);
|
||||
// $sheet->setCellValue('C' . $row, $item['接单人']);
|
||||
|
||||
// $sheet->setCellValue('D' . $row, $item['等级']);
|
||||
// $sheet->setCellValue('E' . $row, $item['邮箱']);
|
||||
// $sheet->setCellValue('F' . $row, $item['电话']);
|
||||
|
||||
// $sheet->setCellValue('G' . $row, $item['WhatsApp']);
|
||||
// $sheet->setCellValue('H' . $row, $item['联系人']);
|
||||
// $sheet->setCellValue('I' . $row, $item['国家(中文)']);
|
||||
|
||||
// $sheet->setCellValue('J' . $row, $item['国家(英文)']);
|
||||
// $sheet->setCellValue('K' . $row, $item['发单时间']);
|
||||
// $sheet->setCellValue('L' . $row, $item['跟单时间']);
|
||||
// $row++;
|
||||
// }
|
||||
|
||||
// // 创建 Excel 文件的写入器
|
||||
// $writer = new Xlsx($spreadsheet);
|
||||
// // 名称
|
||||
// $fileName = $request->user->name.'_'.date('Ymd').'_'.uniqid().'.xlsx';
|
||||
// // 路径
|
||||
// $filePath = '/export/'.$fileName;
|
||||
// // 将文件保存到本地
|
||||
// $writer->save(public_path().$filePath);
|
||||
|
||||
// return $this->successResponse(['url'=>$filePath, 'name'=>$fileName]);
|
||||
// }
|
||||
// return view('inquiry-log/export');
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\inquiry\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\inquiry\model\Country;
|
||||
|
||||
class CountryLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Country::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace app\inquiry\logic;
|
||||
|
||||
use app\common\traits\crud\CrudLogicTrait;
|
||||
use app\common\model\InquiryLog;
|
||||
|
||||
class InquiryLogLogic
|
||||
{
|
||||
use CrudLogicTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InquiryLog::class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace app\inquiry\service;
|
||||
|
||||
use app\inquiry\validate\CountryValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\inquiry\model\Country;
|
||||
|
||||
class CountryService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = Country::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = CountryValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace app\inquiry\service;
|
||||
|
||||
use app\common\validate\InquiryLogValidate;
|
||||
use app\common\traits\crud\CrudServiceTrait;
|
||||
use app\common\model\InquiryLog;
|
||||
use support\exception\BusinessException;
|
||||
use support\Request;
|
||||
use Webman\RedisQueue\Redis as RedisQueueRedis;
|
||||
|
||||
class InquiryLogService
|
||||
{
|
||||
|
||||
use CrudServiceTrait;
|
||||
|
||||
/**
|
||||
* 指定当前要操作的模型
|
||||
*/
|
||||
protected string $modelClass = InquiryLog::class;
|
||||
|
||||
/**
|
||||
* 验证器
|
||||
*/
|
||||
protected $validator = InquiryLogValidate::class;
|
||||
|
||||
/**
|
||||
* 获取验证器
|
||||
*/
|
||||
public function getValidator()
|
||||
{
|
||||
return new $this->validator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布
|
||||
*/
|
||||
public function serviceRelease(Request $request, array $data = [])
|
||||
{
|
||||
$data = $this->preProcess('Release', $data, $request);
|
||||
$config = $this->buildQueryConfig($request, 'release');
|
||||
$updated = $this->logicUpdateBy($config, $data);
|
||||
return $this->postProcess('Release', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配
|
||||
*/
|
||||
public function serviceAllocation(Request $request, array $data = [])
|
||||
{
|
||||
$data = $this->preProcess('Allocation', $data, $request);
|
||||
$config = $this->buildQueryConfig($request, 'allocation');
|
||||
$updated = $this->logicUpdateBy($config, $data);
|
||||
return $this->postProcess('Allocation', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*/
|
||||
protected function buildQueryConfig(Request $request, string $scene = 'index'): array
|
||||
{
|
||||
$p = $request->all();
|
||||
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
switch ($scene) {
|
||||
case 'index':
|
||||
// ====== ====== 排序 ====== ======
|
||||
$config['order']['id'] = 'desc';
|
||||
|
||||
// ====== ====== 筛选 ====== ======
|
||||
// 渠道
|
||||
if (isset($p['channel']) && $p['channel']) {
|
||||
$config['where']['channel'] = $p['channel'];
|
||||
}
|
||||
// 国家
|
||||
if (isset($p['country']) && $p['country']) {
|
||||
$config['where']['country'] = $p['country'];
|
||||
}
|
||||
// 时间
|
||||
if (isset($p['time']) && $p['time']) {
|
||||
$time = explode(' - ', $p['time']);
|
||||
$config['where']['created_at'] = [
|
||||
'between', [strtotime($time[0]), strtotime($time[1])]
|
||||
];
|
||||
}
|
||||
// 状态
|
||||
if (isset($p['status']) && in_array($p['status'], [0,1,2])) {
|
||||
$config['where']['status'] = $p['status'];
|
||||
}
|
||||
// 关键字
|
||||
if (isset($p['keyword']) && $p['keyword']) {
|
||||
$k = '%' . $p['keyword'] . '%';
|
||||
foreach (['order_no','email','tel','whatsapp','contacts'] as $col) {
|
||||
$config['orWhere'][] = [$col, 'like', $k];
|
||||
}
|
||||
}
|
||||
|
||||
// ====== ====== 关联 ====== ======
|
||||
$config['with']['countryInfo'][] = 'cn_addr';
|
||||
$config['with']['countryInfo'][] = 'steering';
|
||||
$config['with']['enter'][] = 'name';
|
||||
$config['with']['recipient'][] = 'name';
|
||||
$config['with']['channelInfo'][] = 'name';
|
||||
break;
|
||||
case 'show':
|
||||
case 'update':
|
||||
case 'destroy':
|
||||
case 'release':
|
||||
case 'allocation':
|
||||
$config['where']['id'] = $p['id'];
|
||||
case 'store':
|
||||
break;
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加前置处理
|
||||
*/
|
||||
public function beforeServiceStore($data, Request $request)
|
||||
{
|
||||
$data['order_no'] = orderNum();
|
||||
$data['enter_user_id'] = $request->user->uid;
|
||||
$data['recipient'] = 0;
|
||||
$data['email'] = trim($data["email"]);
|
||||
$data['tel'] = trim($data['tel']);
|
||||
$data['whatsapp'] = trim($data['whatsapp']);
|
||||
|
||||
// 将OSS直连地址替换为代理地址
|
||||
$ossDomain = 'https://vehicle-album.oss-cn-beijing.aliyuncs.com/';
|
||||
$proxyPath = getenv('MAIN_DOMAIN').'/proxy/ossurl?file=';
|
||||
|
||||
$data['content'] = str_replace(
|
||||
'?Expires=',
|
||||
'&Expires=',
|
||||
$data['content']
|
||||
);
|
||||
$data['content'] = str_replace(
|
||||
$ossDomain,
|
||||
$proxyPath,
|
||||
$data['content']
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布前置处理
|
||||
*/
|
||||
public function beforeServiceRelease($data, Request $request)
|
||||
{
|
||||
$info = $this->findByPk($request->post('id'));
|
||||
if (!$info) {
|
||||
throw new BusinessException('数据不存在');
|
||||
}
|
||||
if ($info->status != 0) {
|
||||
throw new BusinessException('询单已被发布');
|
||||
}
|
||||
$data['r_time'] = time();
|
||||
$data['status'] = 1;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配前置处理
|
||||
*/
|
||||
public function beforeServiceAllocation($data, Request $request)
|
||||
{
|
||||
$info = $this->findByPk($request->post('id'));
|
||||
if (!$info) {
|
||||
throw new BusinessException('数据不存在');
|
||||
}
|
||||
if ($info->status == 2) {
|
||||
throw new BusinessException('询单已被分配');
|
||||
}
|
||||
if ($info->status == 0) {
|
||||
$data['r_time'] = time();
|
||||
}
|
||||
$data['t_time'] = time();
|
||||
$data['status'] = 2;
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布后置处理
|
||||
*/
|
||||
protected function afterServiceRelease($res)
|
||||
{
|
||||
RedisQueueRedis::send('inquiry-to-crm', $res);
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分配后置处理
|
||||
*/
|
||||
protected function afterServiceAllocation($res)
|
||||
{
|
||||
RedisQueueRedis::send('inquiry-to-crm', $res);
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取询单ById
|
||||
* @param int|string $id
|
||||
* @param string $orderSn
|
||||
* @return Model|null
|
||||
*/
|
||||
public function getInquiryLogBy(int|string $id)
|
||||
{
|
||||
$config = [];
|
||||
$config['model'] = $this->modelClass;
|
||||
$config['with']['countryInfo'][] = 'cn_addr';
|
||||
$config['with']['countryInfo'][] = 'steering';
|
||||
$config['with']['enter'][] = 'name';
|
||||
$config['with']['recipient'][] = 'name';
|
||||
$config['with']['channelInfo'][] = 'name';
|
||||
$config['orWhere'][] = ['id', '=', $id];
|
||||
$config['orWhere'][] = ['order_no', '=', $id];
|
||||
return $this->logicShow($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取等级名称
|
||||
* @param $id
|
||||
* @return string
|
||||
*/
|
||||
public function qualityName($id)
|
||||
{
|
||||
if (!$id) {
|
||||
return '🤍 银牌商机';
|
||||
}
|
||||
$quality = [
|
||||
1 => '💛金牌商机',
|
||||
2 => '🤍银牌商机',
|
||||
3 => '🧡铜牌商机'
|
||||
];
|
||||
return $quality[$id];
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,10 @@ class AuthCheck implements MiddlewareInterface
|
||||
public function process(Request $request, callable $handler) : Response
|
||||
{
|
||||
$controller = new ReflectionClass($request->controller);
|
||||
$noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? [];
|
||||
|
||||
$properties = $controller->getDefaultProperties();
|
||||
$noNeedLogin = $properties['noNeedLogin'] ?? [];
|
||||
$noNeedAuth = $properties['noNeedAuth'] ?? [];
|
||||
|
||||
if (in_array($request->action, $noNeedLogin)) {
|
||||
// 白名单
|
||||
return $handler($request);
|
||||
@@ -31,6 +33,7 @@ class AuthCheck implements MiddlewareInterface
|
||||
}
|
||||
$token = $matches[1];
|
||||
$request->user = (new Jwt())->verifyToken($token);
|
||||
return $handler($request);
|
||||
} catch (\Throwable $e) {
|
||||
$body = [
|
||||
'status' => 'fail',
|
||||
@@ -40,7 +43,8 @@ class AuthCheck implements MiddlewareInterface
|
||||
];
|
||||
return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
return $handler($request);
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ class BasicData implements MiddlewareInterface
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$controller = new ReflectionClass($request->controller);
|
||||
$noNeedHeader = $controller->getDefaultProperties()['noNeedHeader'] ?? [];
|
||||
$noNeedHeaderData = $controller->getDefaultProperties()['noNeedHeaderData'] ?? [];
|
||||
|
||||
if (in_array($request->action, $noNeedHeader) || in_array($request->path(), config('whitelist.basi_cdata'))) {
|
||||
if (in_array($request->action, $noNeedHeaderData)) {
|
||||
// 白名单
|
||||
return $handler($request);
|
||||
} else {
|
||||
@@ -35,7 +35,13 @@ class BasicData implements MiddlewareInterface
|
||||
}
|
||||
return $handler($request);
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
$body = [
|
||||
'status' => 'fail',
|
||||
'code' => 403,
|
||||
'message' => $th->getMessage(),
|
||||
'data' => [],
|
||||
];
|
||||
return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
namespace app\middleware;
|
||||
|
||||
use Webman\MiddlewareInterface;
|
||||
use Webman\Http\Response;
|
||||
use Webman\Http\Request;
|
||||
use app\service\AuthService;
|
||||
|
||||
class PermissionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
protected $permission_code;
|
||||
|
||||
// 支持传参(如 "user:view"),可以在路由闭包里传入
|
||||
public function __construct($permission_code = null)
|
||||
{
|
||||
$this->permission_code = $permission_code;
|
||||
}
|
||||
|
||||
public function process(Request $request, callable $next): Response
|
||||
{
|
||||
// 假设有 auth() helper 方法能获取当前登录用户ID
|
||||
$user_id = $request->session()->get('user_id');
|
||||
if (!$user_id) {
|
||||
return json(['code' => 401, 'msg' => '未登录']);
|
||||
}
|
||||
|
||||
if ($this->permission_code && !AuthService::check($user_id, $this->permission_code)) {
|
||||
return json(['code' => 403, 'msg' => '没有权限']);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace app\process;
|
||||
|
||||
use app\crm\model\Customer;
|
||||
use support\Log;
|
||||
use Workerman\Crontab\Crontab;
|
||||
|
||||
class Task
|
||||
{
|
||||
|
||||
public function onWorkerStart()
|
||||
{
|
||||
// 每半小时执行一次
|
||||
new Crontab('0 */30 * * * *', function(){
|
||||
echo date('Y-m-d H:i:s') . "\n";
|
||||
Log::info('send follow start',[time()]);
|
||||
|
||||
$where = [
|
||||
['follow_next', '<=', time() + 3600],
|
||||
['follow_next', '>', time()],
|
||||
['warn', '<', 1]
|
||||
];
|
||||
$customer = Customer::where($where)->get();
|
||||
foreach ($customer as $item) {
|
||||
$res =[
|
||||
'id'=>$item->id,
|
||||
'category'=>'crm',
|
||||
'scene'=>'crm-person-follow-up',
|
||||
'receive type'=>'person'
|
||||
];
|
||||
\Webman\RedisQueue\Redis::send("feishu-message",$res);
|
||||
$item->warn = 1;
|
||||
$item->save();
|
||||
}
|
||||
|
||||
Log::info('send follow start',[time()]);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use app\common\model\CorpFeishuApp;
|
||||
use app\common\model\CrmCustomer;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
use app\common\model\User;
|
||||
use app\common\utils\FeishuMessage as FeishuMsg;
|
||||
use support\exception\BusinessException;
|
||||
use app\inquiry\service\InquiryLogService;
|
||||
|
||||
class FeishuMessage implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'feishu-message';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
|
||||
/**
|
||||
* @param string $category 分类:crm | inquiry
|
||||
* @param string $scene 场景:group-send | group-update | person-send | person-update
|
||||
* @param string $id 数据ID:询单ID | 客户ID
|
||||
* @param string $receive_type 接收者类型:group | person
|
||||
*/
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
if ($data['category'] === 'crm') {
|
||||
// 客户线索
|
||||
$crmCustomer = new CrmCustomer();
|
||||
$info = $crmCustomer->where('id', $data['id'])->first();
|
||||
|
||||
// 跟单人
|
||||
$reInfo = User::where('id', $info->user_id)->first();
|
||||
|
||||
// 飞书应用
|
||||
$feishuApp = new CorpFeishuApp();
|
||||
$feishuAppInfo = $feishuApp->where('corp_id', $reInfo->corp_id)->where('system_id', 1)->first();
|
||||
|
||||
// 推送数据
|
||||
$options = [
|
||||
'contacts' => $info->contacts,
|
||||
'follow_last' => $info->follow_last ? date('Y-m-d H:i', $info->follow_last) : '',
|
||||
'follow_next' => $info->follow_next ? date('Y-m-d H:i', $info->follow_next) : '',
|
||||
|
||||
'app_id' => $feishuAppInfo->app_id,
|
||||
'app_secret' => $feishuAppInfo->app_secret,
|
||||
'msg_type' => 'interactive',
|
||||
'receive_id_type' => 'open_id',
|
||||
'receive_id' => $reInfo->open_id
|
||||
];
|
||||
}
|
||||
|
||||
if ($data['category'] === 'inquiry') {
|
||||
// 询单信息
|
||||
$inquiryLogService = new InquiryLogService();
|
||||
$info = $inquiryLogService->getInquiryLogBy($data['id']);
|
||||
|
||||
// 接受者处理
|
||||
if ($data['receive_type'] == 'group') {
|
||||
$appId = 'cli_a72f13d6c022500d';
|
||||
$appSecret = 'Kxtbq70OQWrEhpltrufRZc76z7kjDqPT';
|
||||
$receiveId = 'oc_4f574e6f8bc5b8f3e5175e67ebc5725c';
|
||||
$receiveIdType = 'chat_id';
|
||||
} else {
|
||||
// 跟单人
|
||||
$reInfo = User::where('id', $info->recipient)->first();
|
||||
// 飞书应用
|
||||
$feishuApp = new CorpFeishuApp();
|
||||
$feishuAppInfo = $feishuApp->where('corp_id', $reInfo->corp_id)->where('system_id', 1)->first();
|
||||
// 推送信息
|
||||
$appId = $feishuAppInfo->app_id;
|
||||
$appSecret = $feishuAppInfo->app_secret;
|
||||
$receiveId = $reInfo->open_id;
|
||||
$receiveIdType = 'open_id';
|
||||
}
|
||||
|
||||
// 推送数据
|
||||
$options = [
|
||||
'order_no' => $info->order_no,
|
||||
'quality' => $inquiryLogService->qualityName($info->quality),
|
||||
'channel' => $info->channelInfo ? $info->channelInfo->name : '',
|
||||
'country' => $info->countryInfo ? $info->countryInfo->cn_addr : '',
|
||||
'enter_user' => $info->enter ? $info->enter->name : '',
|
||||
'r_time' => $info->r_time ? date('Y-m-d H:i:s', $info->r_time) : '',
|
||||
't_time' => $info->t_time ? date('Y-m-d H:i:s', $info->t_time) : '',
|
||||
|
||||
'app_id' => $appId,
|
||||
'app_secret' => $appSecret,
|
||||
'msg_type' => 'interactive',
|
||||
'receive_id_type' => $receiveIdType,
|
||||
'receive_id' => $receiveId
|
||||
];
|
||||
}
|
||||
|
||||
$feishuMsg = new FeishuMsg();
|
||||
$feishuMsg->messageSend($data['scene'], $options);
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace app\queue\redis;
|
||||
|
||||
use app\inquiry\service\InquiryLogService;
|
||||
use app\common\model\CrmCustomer;
|
||||
use app\common\model\InquiryLog;
|
||||
use Webman\RedisQueue\Consumer;
|
||||
use app\common\model\User;
|
||||
use app\common\utils\FeishuMessage;
|
||||
use support\exception\BusinessException;
|
||||
|
||||
class InquiryToCrm implements Consumer
|
||||
{
|
||||
// 要消费的队列名
|
||||
public $queue = 'inquiry-to-crm';
|
||||
|
||||
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
|
||||
public $connection = 'default';
|
||||
|
||||
// 消费
|
||||
public function consume($data)
|
||||
{
|
||||
try {
|
||||
// 获取询盘信息
|
||||
$info = InquiryLog::with(['channelInfo', 'countryInfo', 'enter'])->where('id', $data['id'])->where('sync_crm', 0)->first();
|
||||
if ($info) {
|
||||
// 获取用户信息
|
||||
$recipient = 0;
|
||||
$corpId = 0;
|
||||
if ($info->recipient) {
|
||||
$recipient = $info->recipient;
|
||||
$uinfo = User::where('id', $info->recipient)->first();
|
||||
if ($uinfo) {
|
||||
$corpId = $uinfo->corp_id;
|
||||
}
|
||||
// 消息推送参数 分配
|
||||
$scene = 'inquiry-person-allocation';
|
||||
$receiveType = 'person';
|
||||
} else {
|
||||
// 消息推送参数 发布
|
||||
$scene = 'inquiry-group-send';
|
||||
$receiveType = 'group';
|
||||
}
|
||||
// 同步客户数据
|
||||
$crmCustomer = new CrmCustomer();
|
||||
$crmCustomer->sn = $info->order_no;
|
||||
$crmCustomer->corp_abbr = $corpId;
|
||||
$crmCustomer->user_id = $recipient;
|
||||
$crmCustomer->type = 1;
|
||||
$crmCustomer->channel_id = $info->channel;
|
||||
$crmCustomer->country_id = $info->country;
|
||||
$crmCustomer->magnitude = $info->quality - 1;
|
||||
$crmCustomer->email = $info->email;
|
||||
$crmCustomer->phone = $info->tel;
|
||||
$crmCustomer->WhatsApp = $info->whatsapp;
|
||||
$crmCustomer->contacts = $info->contacts;
|
||||
$crmCustomer->desc_collect = $info->content;
|
||||
$crmCustomer->enter_user_id = $info->enter_user_id;
|
||||
// 修改同步CRM状态
|
||||
$info->sync_crm = 1;
|
||||
// 保存
|
||||
if (!$crmCustomer->save() || !$info->save()) {
|
||||
throw new BusinessException('同步失败~');
|
||||
}
|
||||
|
||||
// 发送飞书消息
|
||||
$options = [
|
||||
'id' => $info->id,
|
||||
'category' => 'inquiry',
|
||||
'scene' => $scene,
|
||||
'receive_type' => $receiveType
|
||||
];
|
||||
\Webman\RedisQueue\Redis::send('feishu-message', $options);
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
throw $th;
|
||||
}
|
||||
}
|
||||
|
||||
// 消费失败回调
|
||||
/*
|
||||
$package = [
|
||||
'id' => 1357277951, // 消息ID
|
||||
'time' => 1709170510, // 消息时间
|
||||
'delay' => 0, // 延迟时间
|
||||
'attempts' => 2, // 消费次数
|
||||
'queue' => 'send-mail', // 队列名
|
||||
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
|
||||
'max_attempts' => 5, // 最大重试次数
|
||||
'error' => '错误信息' // 错误信息
|
||||
]
|
||||
*/
|
||||
public function onConsumeFailure(\Throwable $e, $package)
|
||||
{
|
||||
echo "consume failure\n";
|
||||
echo $e->getMessage() . "\n";
|
||||
// 无需反序列化
|
||||
var_export($package);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
// const REDIRECT_URI = encodeURIComponent('https://example.com/api/oauth/callback/feishu');
|
||||
// const STATE = '随机串_防CSRF';
|
||||
// // 用于跳转获取 code 的 URL
|
||||
const GOTO = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=cli_a72f13d6c022500d&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&response_type=code&state=MZL`;
|
||||
const GOTO = `https://passport.feishu.cn/suite/passport/oauth/authorize?client_id=cli_a72f13d6c022500d&redirect_uri=https://web-fdesk.hnxinyouhe.com/auth/feishu-callback&response_type=code&state=MZL`;
|
||||
// const GOTO = `https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=cli_a73e1e6406fa501c&redirect_uri=https://mzl.dev.hnxinyouhe.com/common/auth/feishu/auth&scope=contact:user.base:readonly contact:contact.base:readonly contact:user.employee_id:readonly im:message:send_as_bot&state=xinyouhe`;
|
||||
// 2. 创建二维码实例
|
||||
const QRLoginObj = QRLogin({
|
||||
|
||||
+3
-1
@@ -41,7 +41,9 @@
|
||||
"shopwwi/webman-filesystem": "1.1",
|
||||
"shopwwi/filesystem-oss": "^1.0",
|
||||
"webman/redis-queue": "^2.1",
|
||||
"symfony/translation": "^7.3"
|
||||
"symfony/translation": "^7.3",
|
||||
"symfony/filesystem": "^7.3",
|
||||
"workerman/crontab": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-event": "For better performance. "
|
||||
|
||||
Generated
+117
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "30cc3cbb162022972a21efed9b69d1e1",
|
||||
"content-hash": "8ef9d9a07b2be477345002a7623ea951",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aliyuncs/oss-sdk-php",
|
||||
@@ -4284,6 +4284,72 @@
|
||||
],
|
||||
"time": "2024-09-25T14:21:43+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/filesystem",
|
||||
"version": "v7.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/filesystem.git",
|
||||
"reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
|
||||
"reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/polyfill-ctype": "~1.8",
|
||||
"symfony/polyfill-mbstring": "~1.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/process": "^6.4|^7.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Filesystem\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides basic utilities for the filesystem",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/filesystem/tree/v7.3.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-10-25T15:15:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v7.3.0",
|
||||
@@ -6226,6 +6292,56 @@
|
||||
},
|
||||
"time": "2025-02-17T03:34:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "workerman/crontab",
|
||||
"version": "v1.0.7",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/walkor/crontab.git",
|
||||
"reference": "74f51ca8204e8eb628e57bc0e640561d570da2cb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/walkor/crontab/zipball/74f51ca8204e8eb628e57bc0e640561d570da2cb",
|
||||
"reference": "74f51ca8204e8eb628e57bc0e640561d570da2cb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"workerman/workerman": ">=4.0.20"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Workerman\\Crontab\\": "./src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "walkor",
|
||||
"email": "walkor@workerman.net",
|
||||
"homepage": "http://www.workerman.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A crontab written in PHP based on workerman",
|
||||
"homepage": "http://www.workerman.net",
|
||||
"keywords": [
|
||||
"crontab"
|
||||
],
|
||||
"support": {
|
||||
"email": "walkor@workerman.net",
|
||||
"forum": "http://wenda.workerman.net/",
|
||||
"issues": "https://github.com/walkor/workerman/issues",
|
||||
"source": "https://github.com/walkor/crontab",
|
||||
"wiki": "http://doc.workerman.net/"
|
||||
},
|
||||
"time": "2025-01-15T07:20:50+00:00"
|
||||
},
|
||||
{
|
||||
"name": "workerman/redis",
|
||||
"version": "v2.0.4",
|
||||
|
||||
@@ -34,4 +34,104 @@ class Constants
|
||||
self::TICKET_STATUS['REJECTED'] => 'Закрыть',
|
||||
];
|
||||
|
||||
//客户分级 USTOMER_GRADE
|
||||
|
||||
public const GRADE = [
|
||||
'INITIAL' => 0, // 初步接触
|
||||
'COMMUNICATE' => 1, // 需求沟通
|
||||
'PLAN' => 2, // 方案推进
|
||||
'DEAL' => 3, // 成交阶段
|
||||
'SALES' => 4, // 售后阶段
|
||||
'LOSS' => 5, // 停滞或流失
|
||||
];
|
||||
|
||||
public const GRADE_ZH = [
|
||||
self::GRADE['INITIAL'] => '初步接触',
|
||||
self::GRADE['COMMUNICATE'] => '需求沟通',
|
||||
self::GRADE['PLAN'] => '方案推进',
|
||||
self::GRADE['DEAL'] => '成交阶段',
|
||||
self::GRADE['SALES'] => '售后阶段',
|
||||
self::GRADE['LOSS'] => '停滞或流失',
|
||||
];
|
||||
public const GRADE_EN = [
|
||||
self::GRADE['INITIAL'] => 'Initial contact',
|
||||
self::GRADE['COMMUNICATE'] => 'Demand Chat',
|
||||
self::GRADE['PLAN'] => 'promotion of the plan',
|
||||
self::GRADE['DEAL'] => 'Transaction Stage',
|
||||
self::GRADE['SALES'] => 'After Sales',
|
||||
self::GRADE['LOSS'] => 'Stagnation or loss',
|
||||
];
|
||||
public const GRADE_RU = [
|
||||
self::GRADE['INITIAL'] => 'Первоначальные контакты',
|
||||
self::GRADE['COMMUNICATE'] => 'Потребности в общении',
|
||||
self::GRADE['PLAN'] => 'Продвижение программы',
|
||||
self::GRADE['DEAL'] => 'Этап сделки',
|
||||
self::GRADE['SALES'] => 'Послепродажный этап',
|
||||
self::GRADE['LOSS'] => 'Застой или потеря',
|
||||
];
|
||||
|
||||
//客户重要性
|
||||
public const CUSTOMER_MAGNITUDE_INITIAL = 0; // 金牌商机
|
||||
public const CUSTOMER_MAGNITUDE_COMMUNICATE = 1; // 银牌商机
|
||||
public const CUSTOMER_MAGNITUDE_PLAN = 2; // 铜牌商机
|
||||
|
||||
public const MAGNITUDE = [
|
||||
self::CUSTOMER_MAGNITUDE_INITIAL => '金牌商机',
|
||||
self::CUSTOMER_MAGNITUDE_COMMUNICATE => '银牌商机',
|
||||
self::CUSTOMER_MAGNITUDE_PLAN => '铜牌商机',
|
||||
];
|
||||
public const MAGNITUDE_EN = [
|
||||
self::CUSTOMER_MAGNITUDE_INITIAL => 'Gold Medal',
|
||||
self::CUSTOMER_MAGNITUDE_COMMUNICATE => 'Silver Medal',
|
||||
self::CUSTOMER_MAGNITUDE_PLAN => 'Bronze Medal',
|
||||
];
|
||||
|
||||
public const MAGNITUDE_RU = [
|
||||
self::CUSTOMER_MAGNITUDE_INITIAL => 'Золотой бизнес',
|
||||
self::CUSTOMER_MAGNITUDE_COMMUNICATE => 'Серебряный бизнес',
|
||||
self::CUSTOMER_MAGNITUDE_PLAN => 'Бронзовый бизнес.',
|
||||
];
|
||||
|
||||
|
||||
//跟进类型
|
||||
public const CUSTOMER_FOLLOW_VISIT = 0; // 线下拜访
|
||||
public const CUSTOMER_FOLLOW_UP = 1; // 线上跟进
|
||||
|
||||
public const FOLLOW = [
|
||||
self::CUSTOMER_FOLLOW_VISIT => '线下拜访',
|
||||
self::CUSTOMER_FOLLOW_UP => '线上跟进',
|
||||
];
|
||||
public const FOLLOW_EN = [
|
||||
self::CUSTOMER_FOLLOW_VISIT => 'Visit',
|
||||
self::CUSTOMER_FOLLOW_UP => 'On line',
|
||||
];
|
||||
|
||||
//跟进类型
|
||||
public const CUSTOMER_SEX_UNKNOWN = 0; // 未知
|
||||
public const CUSTOMER_SEX_MAN = 1; // 男
|
||||
public const CUSTOMER_SEX_WOMAN = 2; // 女
|
||||
|
||||
public const SEX = [
|
||||
self::CUSTOMER_SEX_UNKNOWN => '未知',
|
||||
self::CUSTOMER_SEX_MAN => '男',
|
||||
self::CUSTOMER_SEX_WOMAN => '女',
|
||||
];
|
||||
public const SEX_EN = [
|
||||
self::CUSTOMER_SEX_UNKNOWN => 'unknown',
|
||||
self::CUSTOMER_SEX_MAN => 'man',
|
||||
self::CUSTOMER_SEX_WOMAN => 'woman',
|
||||
];
|
||||
|
||||
public const SEX_RU = [
|
||||
self::CUSTOMER_SEX_UNKNOWN => 'неизвестный',
|
||||
self::CUSTOMER_SEX_MAN => 'мужской',
|
||||
self::CUSTOMER_SEX_WOMAN => 'женский',
|
||||
];
|
||||
|
||||
public const SIGN = [
|
||||
['id' => 'xinyouhe', 'name' => '新友合'],
|
||||
['id' => 'mbaf', 'name' => 'MAGIC BRIDGE AUTO FZE']
|
||||
];
|
||||
|
||||
public const LANGUAGE = ['en', 'zh_CN','ru'];
|
||||
}
|
||||
@@ -5,7 +5,7 @@ return [
|
||||
'count' => getenv('REDIS_QUEUE_PROCESS_COUNT'), // 可以设置多进程同时消费
|
||||
'constructor' => [
|
||||
// 消费者类目录
|
||||
// 'consumer_dir' => app_path() . '/queue/redis'
|
||||
'consumer_dir' => app_path() . '/queue/redis',
|
||||
'consumer_dir' => app_path() . getenv('REDIS_QUEUE_CONSUMER_DIR')
|
||||
]
|
||||
]
|
||||
|
||||
+4
-1
@@ -59,5 +59,8 @@ return [
|
||||
'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/',
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'task' => [
|
||||
'handler' => app\process\Task::class
|
||||
],
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user