diff --git a/.gitignore b/.gitignore index 516299c..496f0a0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .env /tests/tmp /tests/.phpunit.result.cache +/public/uploads \ No newline at end of file diff --git a/app/admin/controller/CorpController.php b/app/admin/controller/CorpController.php new file mode 100644 index 0000000..748f79f --- /dev/null +++ b/app/admin/controller/CorpController.php @@ -0,0 +1,40 @@ +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); + * } + */ +} \ No newline at end of file diff --git a/app/admin/controller/RoleController.php b/app/admin/controller/RoleController.php index 59ccb54..691e070 100644 --- a/app/admin/controller/RoleController.php +++ b/app/admin/controller/RoleController.php @@ -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()); + // } + // } } diff --git a/app/admin/controller/SystemsController.php b/app/admin/controller/SystemsController.php new file mode 100644 index 0000000..9c300ac --- /dev/null +++ b/app/admin/controller/SystemsController.php @@ -0,0 +1,33 @@ +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; + // } +} \ No newline at end of file diff --git a/app/admin/service/CorpService.php b/app/admin/service/CorpService.php new file mode 100644 index 0000000..5767ea6 --- /dev/null +++ b/app/admin/service/CorpService.php @@ -0,0 +1,89 @@ +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; + // } +} \ No newline at end of file diff --git a/app/admin/service/RoleService.php b/app/admin/service/RoleService.php new file mode 100644 index 0000000..248bc3d --- /dev/null +++ b/app/admin/service/RoleService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/admin/service/SystemsService.php b/app/admin/service/SystemsService.php new file mode 100644 index 0000000..8086245 --- /dev/null +++ b/app/admin/service/SystemsService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/command/DomainCommand.php b/app/command/DomainCommand.php new file mode 100644 index 0000000..d3c7496 --- /dev/null +++ b/app/command/DomainCommand.php @@ -0,0 +1,76 @@ +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; +} \ No newline at end of file diff --git a/app/command/TbCommand.php b/app/command/TbCommand.php new file mode 100644 index 0000000..d60482f --- /dev/null +++ b/app/command/TbCommand.php @@ -0,0 +1,28 @@ +all(); + + foreach ($list as $item) { + // UserSn::where('user_sn', $item->sn)->update(['user_id' => $item->id]); + } + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/app/common/controller/AuthController.php b/app/common/controller/AuthController.php index 02676e8..93c39fb 100644 --- a/app/common/controller/AuthController.php +++ b/app/common/controller/AuthController.php @@ -12,7 +12,7 @@ class AuthController extends BaseController /** * 不需要头信息的方法 */ - protected $noNeedHeader = ['feishuAuth']; + protected $noNeedHeaderData = ['feishuAuth']; protected $noNeedLogin = ['feishuScope', 'login', 'register', 'feishuAuth']; diff --git a/app/common/controller/BaseController.php b/app/common/controller/BaseController.php index 2dbf636..f617239 100644 --- a/app/common/controller/BaseController.php +++ b/app/common/controller/BaseController.php @@ -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'; + /** * 验证数据 * diff --git a/app/common/controller/NotifyController.php b/app/common/controller/NotifyController.php new file mode 100644 index 0000000..f6e728e --- /dev/null +++ b/app/common/controller/NotifyController.php @@ -0,0 +1,136 @@ +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()); + } + } +} \ No newline at end of file diff --git a/app/common/controller/PresetController.php b/app/common/controller/PresetController.php index 964b2a6..ad88a8f 100644 --- a/app/common/controller/PresetController.php +++ b/app/common/controller/PresetController.php @@ -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()); + } + } } \ No newline at end of file diff --git a/app/common/controller/UploadController.php b/app/common/controller/UploadController.php index 9d8959d..e08fa9b 100644 --- a/app/common/controller/UploadController.php +++ b/app/common/controller/UploadController.php @@ -12,6 +12,10 @@ class UploadController use ResponseTrait; + protected $noNeedLogin = ['proxy']; + + protected $noNeedHeaderData = ['proxy']; + public function upload(Request $request) { try { diff --git a/app/common/controller/UserController.php b/app/common/controller/UserController.php index efab4a1..d725166 100644 --- a/app/common/controller/UserController.php +++ b/app/common/controller/UserController.php @@ -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()); + } + } + + } \ No newline at end of file diff --git a/app/common/logic/AuthLogic.php b/app/common/logic/AuthLogic.php index ccdbf16..754504b 100644 --- a/app/common/logic/AuthLogic.php +++ b/app/common/logic/AuthLogic.php @@ -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; + } } \ No newline at end of file diff --git a/app/common/logic/AuthLogic_bak.php b/app/common/logic/AuthLogic_bak.php deleted file mode 100644 index 0e68093..0000000 --- a/app/common/logic/AuthLogic_bak.php +++ /dev/null @@ -1,593 +0,0 @@ -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 - ]; - } -} \ No newline at end of file diff --git a/app/common/logic/BaseLogic.php b/app/common/logic/BaseLogic.php deleted file mode 100644 index 94beff3..0000000 --- a/app/common/logic/BaseLogic.php +++ /dev/null @@ -1,86 +0,0 @@ -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; - } -} diff --git a/app/common/logic/CorpFeishuAppLogic.php b/app/common/logic/CorpFeishuAppLogic.php index 2073aac..e246316 100644 --- a/app/common/logic/CorpFeishuAppLogic.php +++ b/app/common/logic/CorpFeishuAppLogic.php @@ -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']); } } \ No newline at end of file diff --git a/app/common/logic/CorpLogic.php b/app/common/logic/CorpLogic.php index cac9672..60e46e6 100644 --- a/app/common/logic/CorpLogic.php +++ b/app/common/logic/CorpLogic.php @@ -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() { diff --git a/app/common/logic/PermLogic.php b/app/common/logic/PermLogic.php index 674fbc8..fbd5218 100644 --- a/app/common/logic/PermLogic.php +++ b/app/common/logic/PermLogic.php @@ -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') diff --git a/app/common/logic/RoleLogic.php b/app/common/logic/RoleLogic.php index 5cb3ae7..bd8f121 100644 --- a/app/common/logic/RoleLogic.php +++ b/app/common/logic/RoleLogic.php @@ -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); - } - } + } diff --git a/app/common/logic/SystemsLogic.php b/app/common/logic/SystemsLogic.php new file mode 100644 index 0000000..93e40d0 --- /dev/null +++ b/app/common/logic/SystemsLogic.php @@ -0,0 +1,21 @@ +orderBy('sort', 'asc')->get(['id', 'name', 'code', 'type', 'type']); + } +} \ No newline at end of file diff --git a/app/common/logic/UserLogic.php b/app/common/logic/UserLogic.php index 1d6dc93..9f5ae44 100644 --- a/app/common/logic/UserLogic.php +++ b/app/common/logic/UserLogic.php @@ -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; + } } \ No newline at end of file diff --git a/app/common/model/Corp.php b/app/common/model/Corp.php index 52c160e..8820c80 100644 --- a/app/common/model/Corp.php +++ b/app/common/model/Corp.php @@ -46,4 +46,14 @@ class Corp extends Model public $timestamps = true; -} + protected $dateFormat = 'U'; + + + protected $fillable = [ + 'name', + 'abbr', + 'tenant_key', + 'sort', + 'status' + ]; +} \ No newline at end of file diff --git a/app/common/model/Country.php b/app/common/model/Country.php new file mode 100644 index 0000000..80439a4 --- /dev/null +++ b/app/common/model/Country.php @@ -0,0 +1,49 @@ +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'); + } +} \ No newline at end of file diff --git a/app/common/model/Role.php b/app/common/model/Role.php index c9eaabd..c758321 100644 --- a/app/common/model/Role.php +++ b/app/common/model/Role.php @@ -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'); } } diff --git a/app/common/model/Systems.php b/app/common/model/Systems.php index f9181ec..ddb180e 100644 --- a/app/common/model/Systems.php +++ b/app/common/model/Systems.php @@ -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 = []; +} \ No newline at end of file diff --git a/app/common/model/User.php b/app/common/model/User.php index e9b4627..446ed46 100644 --- a/app/common/model/User.php +++ b/app/common/model/User.php @@ -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); } -} +} \ No newline at end of file diff --git a/app/common/model/UserFeishuApp_bak.php b/app/common/model/UserFeishuApp_bak.php deleted file mode 100644 index 9aa06c0..0000000 --- a/app/common/model/UserFeishuApp_bak.php +++ /dev/null @@ -1,69 +0,0 @@ -blockToken($token, $systemCode); } - - - // public function perms() - // { - // $permService = new PermService(); - // $menuTree = $permService->getMenuTree($uid); - // $permissionCodes = $permService->getPermissionCodes($uid); - // return (new PermService())->getMenuTree($this->userId) - // } } \ No newline at end of file diff --git a/app/common/service/AuthService_bak.php b/app/common/service/AuthService_bak.php deleted file mode 100644 index 917518f..0000000 --- a/app/common/service/AuthService_bak.php +++ /dev/null @@ -1,155 +0,0 @@ -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); - } -} \ No newline at end of file diff --git a/app/common/service/BasicDataService.php b/app/common/service/BasicDataService.php index e77a4e0..d5ff0ce 100644 --- a/app/common/service/BasicDataService.php +++ b/app/common/service/BasicDataService.php @@ -20,7 +20,7 @@ class BasicDataService public function checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr) { if (!$loginType && !$systemCode && !$corpAbbr) { - throw new BusinessException(htmlpage()); + throw new BusinessException('header缺少必要参数'); } // 检查并缓存登录类型 diff --git a/app/common/service/CorpFeishuAppService.php b/app/common/service/CorpFeishuAppService.php index 0c10be8..ad84582 100644 --- a/app/common/service/CorpFeishuAppService.php +++ b/app/common/service/CorpFeishuAppService.php @@ -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); } } \ No newline at end of file diff --git a/app/common/service/PermService.php b/app/common/service/PermService.php index 8f8770c..16030b1 100644 --- a/app/common/service/PermService.php +++ b/app/common/service/PermService.php @@ -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)); } } \ No newline at end of file diff --git a/app/common/service/RoleService.php b/app/common/service/RoleService.php deleted file mode 100644 index 24c14cf..0000000 --- a/app/common/service/RoleService.php +++ /dev/null @@ -1,55 +0,0 @@ -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); - } -} diff --git a/app/common/service/SystemsService.php b/app/common/service/SystemsService.php new file mode 100644 index 0000000..10b80a3 --- /dev/null +++ b/app/common/service/SystemsService.php @@ -0,0 +1,24 @@ +logic = (new SystemsLogic()); + } + + /** + * 获取系统list + */ + public function getList() + { + return $this->logic->getList(); + } +} \ No newline at end of file diff --git a/app/common/service/UserService.php b/app/common/service/UserService.php index 98375bc..69fb2f4 100644 --- a/app/common/service/UserService.php +++ b/app/common/service/UserService.php @@ -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(); + } } \ No newline at end of file diff --git a/app/common/traits/DataTransformer.php b/app/common/traits/DataTransformer.php index 42faf81..4f3abac 100644 --- a/app/common/traits/DataTransformer.php +++ b/app/common/traits/DataTransformer.php @@ -1,47 +1,51 @@ 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); } } \ No newline at end of file diff --git a/app/common/traits/FormatKvTrait.php b/app/common/traits/FormatKvTrait.php new file mode 100644 index 0000000..c3045a7 --- /dev/null +++ b/app/common/traits/FormatKvTrait.php @@ -0,0 +1,154 @@ +, + * 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)); + } +} diff --git a/app/common/traits/RequestTrait.php b/app/common/traits/RequestTrait.php new file mode 100644 index 0000000..8fefcc4 --- /dev/null +++ b/app/common/traits/RequestTrait.php @@ -0,0 +1,38 @@ +scene($scene); + } + // 执行数据验证 + if (!$validator->check($data)) { + // 验证失败,抛出业务异常,异常信息为验证器返回的错误信息 + throw new BusinessException($validator->getError()); + } + // 验证成功,返回原始数据 + return $data; + } + + +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudControllerTrait.php b/app/common/traits/crud/CrudControllerTrait.php new file mode 100644 index 0000000..ca30ba3 --- /dev/null +++ b/app/common/traits/crud/CrudControllerTrait.php @@ -0,0 +1,148 @@ +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 + * 键为 “点号嵌套”或 “通配符” 字段路径, + * 值为 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; + // } +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudLogicTrait.php b/app/common/traits/crud/CrudLogicTrait.php new file mode 100644 index 0000000..9e629f9 --- /dev/null +++ b/app/common/traits/crud/CrudLogicTrait.php @@ -0,0 +1,80 @@ +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); + } +} diff --git a/app/common/traits/crud/CrudServiceTrait.php b/app/common/traits/crud/CrudServiceTrait.php new file mode 100644 index 0000000..5ad6dbc --- /dev/null +++ b/app/common/traits/crud/CrudServiceTrait.php @@ -0,0 +1,193 @@ +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'); + // } +} \ No newline at end of file diff --git a/app/common/utils/CrudUtil.php b/app/common/utils/CrudUtil.php new file mode 100644 index 0000000..dec779b --- /dev/null +++ b/app/common/utils/CrudUtil.php @@ -0,0 +1,431 @@ + 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); + } + } + } +} \ No newline at end of file diff --git a/app/common/utils/FeishuMessage.php b/app/common/utils/FeishuMessage.php new file mode 100644 index 0000000..311ffd9 --- /dev/null +++ b/app/common/utils/FeishuMessage.php @@ -0,0 +1,555 @@ +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; + } +} \ No newline at end of file diff --git a/app/common/utils/TableInspector.php b/app/common/utils/TableInspector.php new file mode 100644 index 0000000..6b00995 --- /dev/null +++ b/app/common/utils/TableInspector.php @@ -0,0 +1,57 @@ +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; + } +} \ No newline at end of file diff --git a/app/common/utils/scaffold/Generate.php b/app/common/utils/scaffold/Generate.php new file mode 100644 index 0000000..753a9cf --- /dev/null +++ b/app/common/utils/scaffold/Generate.php @@ -0,0 +1,191 @@ +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 . "文件生成成功!
" : '未生成任何文件'; + } + + /** + * 根据模板生成文件 + * + * @param string $templateDir 模板目录 + * @param string $templateFile 模板文件名 + * @param string $outputPath 输出文件路径 + */ + protected function generateFile($templateDir, $templateFile, $outputPath, $fileType) + { + $templatePath = $templateDir . $templateFile; + + if (!file_exists($templatePath)) { + echo "模板文件 {$templateFile} 不存在! \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 "生成文件: {$outputPath}
"; + } + + /** + * 设置字段 + * + * @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 "删除已存在文件: {$path} \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)); +} \ No newline at end of file diff --git a/app/common/utils/scaffold/templates/ControllerStub.stub b/app/common/utils/scaffold/templates/ControllerStub.stub new file mode 100644 index 0000000..a74d97c --- /dev/null +++ b/app/common/utils/scaffold/templates/ControllerStub.stub @@ -0,0 +1,33 @@ +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; + // } +} \ No newline at end of file diff --git a/app/common/utils/scaffold/templates/LogicStub.stub b/app/common/utils/scaffold/templates/LogicStub.stub new file mode 100644 index 0000000..da1afdc --- /dev/null +++ b/app/common/utils/scaffold/templates/LogicStub.stub @@ -0,0 +1,17 @@ +validator; + } +} \ No newline at end of file diff --git a/app/common/utils/scaffold/templates/ValidateStub.stub b/app/common/utils/scaffold/templates/ValidateStub.stub new file mode 100644 index 0000000..862b6f1 --- /dev/null +++ b/app/common/utils/scaffold/templates/ValidateStub.stub @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/common/validate/CorpValidate.php b/app/common/validate/CorpValidate.php new file mode 100644 index 0000000..f1710d6 --- /dev/null +++ b/app/common/validate/CorpValidate.php @@ -0,0 +1,35 @@ + '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'] + ]; +} \ No newline at end of file diff --git a/app/common/validate/InquiryLogValidate.php b/app/common/validate/InquiryLogValidate.php new file mode 100644 index 0000000..2a869bb --- /dev/null +++ b/app/common/validate/InquiryLogValidate.php @@ -0,0 +1,176 @@ + '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(); + } +} \ No newline at end of file diff --git a/app/common/validate/RoleValidate.php b/app/common/validate/RoleValidate.php index 330fae6..372159f 100644 --- a/app/common/validate/RoleValidate.php +++ b/app/common/validate/RoleValidate.php @@ -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'], ]; } diff --git a/app/common/validate/SystemsValidate.php b/app/common/validate/SystemsValidate.php new file mode 100644 index 0000000..d0783f5 --- /dev/null +++ b/app/common/validate/SystemsValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 0d7b7f7..b942887 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -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'; } } \ No newline at end of file diff --git a/app/crm/controller/BaseController.php b/app/crm/controller/BaseController.php new file mode 100644 index 0000000..1b824e0 --- /dev/null +++ b/app/crm/controller/BaseController.php @@ -0,0 +1,51 @@ +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; + } +} \ No newline at end of file diff --git a/app/crm/controller/CarTypeController.php b/app/crm/controller/CarTypeController.php new file mode 100644 index 0000000..6156876 --- /dev/null +++ b/app/crm/controller/CarTypeController.php @@ -0,0 +1,34 @@ +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); + } + +} \ No newline at end of file diff --git a/app/crm/controller/CustomerController.php b/app/crm/controller/CustomerController.php new file mode 100644 index 0000000..42003c0 --- /dev/null +++ b/app/crm/controller/CustomerController.php @@ -0,0 +1,530 @@ + $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')); + } + } +} \ No newline at end of file diff --git a/app/crm/controller/FollowController.php b/app/crm/controller/FollowController.php new file mode 100644 index 0000000..42d6a01 --- /dev/null +++ b/app/crm/controller/FollowController.php @@ -0,0 +1,146 @@ + $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')); + } + } + +} \ No newline at end of file diff --git a/app/crm/controller/TagGroupController.php b/app/crm/controller/TagGroupController.php new file mode 100644 index 0000000..2ac30e8 --- /dev/null +++ b/app/crm/controller/TagGroupController.php @@ -0,0 +1,80 @@ +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')); + } + } +} \ No newline at end of file diff --git a/app/crm/logic/CustomerLogic.php b/app/crm/logic/CustomerLogic.php new file mode 100644 index 0000000..4e0619d --- /dev/null +++ b/app/crm/logic/CustomerLogic.php @@ -0,0 +1,15 @@ +select('id','name','name_en','status')->where('status',0)->toArray(); + return $list; + } +} \ No newline at end of file diff --git a/app/crm/model/Channel.php b/app/crm/model/Channel.php new file mode 100644 index 0000000..ac02c71 --- /dev/null +++ b/app/crm/model/Channel.php @@ -0,0 +1,48 @@ + '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; + } +} \ No newline at end of file diff --git a/app/crm/model/Country.php b/app/crm/model/Country.php new file mode 100644 index 0000000..f1fffd1 --- /dev/null +++ b/app/crm/model/Country.php @@ -0,0 +1,41 @@ +get(); + return $list; + } +} \ No newline at end of file diff --git a/app/crm/model/Customer.php b/app/crm/model/Customer.php new file mode 100644 index 0000000..af5c571 --- /dev/null +++ b/app/crm/model/Customer.php @@ -0,0 +1,82 @@ + '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'); + } +} \ No newline at end of file diff --git a/app/crm/model/CustomerHis.php b/app/crm/model/CustomerHis.php new file mode 100644 index 0000000..65d7de6 --- /dev/null +++ b/app/crm/model/CustomerHis.php @@ -0,0 +1,68 @@ + '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; // 不做任何操作 + } +} \ No newline at end of file diff --git a/app/crm/model/CustomerTag.php b/app/crm/model/CustomerTag.php new file mode 100644 index 0000000..3a0c567 --- /dev/null +++ b/app/crm/model/CustomerTag.php @@ -0,0 +1,36 @@ + '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', // 指定序列化格式 + ]; +} \ No newline at end of file diff --git a/app/crm/model/Tag.php b/app/crm/model/Tag.php new file mode 100644 index 0000000..e58bb87 --- /dev/null +++ b/app/crm/model/Tag.php @@ -0,0 +1,55 @@ +belongsTo(TagGroup::class, 'group_id', 'id'); + } + + public function customers() + { + return $this->belongsToMany(Customer::class, 'crm_customer_tag', 'tag_id', 'customer_id'); + } +} \ No newline at end of file diff --git a/app/crm/model/TagGroup.php b/app/crm/model/TagGroup.php new file mode 100644 index 0000000..5b5e28e --- /dev/null +++ b/app/crm/model/TagGroup.php @@ -0,0 +1,50 @@ +hasMany(Tag::class, 'group_id', 'id'); + } +} \ No newline at end of file diff --git a/app/crm/service/CustomerService.php b/app/crm/service/CustomerService.php new file mode 100644 index 0000000..0e5ca8f --- /dev/null +++ b/app/crm/service/CustomerService.php @@ -0,0 +1,167 @@ +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; + } +} \ No newline at end of file diff --git a/app/crm/service/FollowService.php b/app/crm/service/FollowService.php new file mode 100644 index 0000000..e8ce0f7 --- /dev/null +++ b/app/crm/service/FollowService.php @@ -0,0 +1,65 @@ +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; + } +} \ No newline at end of file diff --git a/app/crm/utils/Util.php b/app/crm/utils/Util.php new file mode 100644 index 0000000..e6ac4d0 --- /dev/null +++ b/app/crm/utils/Util.php @@ -0,0 +1,129 @@ +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); + } + +} \ No newline at end of file diff --git a/app/crm/validate/FollowValidate.php b/app/crm/validate/FollowValidate.php new file mode 100644 index 0000000..a92c4a0 --- /dev/null +++ b/app/crm/validate/FollowValidate.php @@ -0,0 +1,21 @@ + 'require', + ]; + + protected $message = [ + 'customer_id.require' => '用户id不能为空', + ]; + + protected $scene = [ + 'list' => ['customer_id'], + ]; +} \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index 2a05e3f..3fed6a2 100644 --- a/app/functions.php +++ b/app/functions.php @@ -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); + } +} + /* * 调试用 */ diff --git a/app/inquiry/controller/CountryController.php b/app/inquiry/controller/CountryController.php new file mode 100644 index 0000000..bb709d4 --- /dev/null +++ b/app/inquiry/controller/CountryController.php @@ -0,0 +1,33 @@ +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; + // } +} \ No newline at end of file diff --git a/app/inquiry/controller/InquiryLogController.php b/app/inquiry/controller/InquiryLogController.php new file mode 100644 index 0000000..dec1ca4 --- /dev/null +++ b/app/inquiry/controller/InquiryLogController.php @@ -0,0 +1,220 @@ +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'); + // } +} \ No newline at end of file diff --git a/app/inquiry/logic/CountryLogic.php b/app/inquiry/logic/CountryLogic.php new file mode 100644 index 0000000..34508b2 --- /dev/null +++ b/app/inquiry/logic/CountryLogic.php @@ -0,0 +1,17 @@ +validator; + } +} \ No newline at end of file diff --git a/app/inquiry/service/InquiryLogService.php b/app/inquiry/service/InquiryLogService.php new file mode 100644 index 0000000..c736ceb --- /dev/null +++ b/app/inquiry/service/InquiryLogService.php @@ -0,0 +1,242 @@ +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]; + } +} \ No newline at end of file diff --git a/app/middleware/AuthCheck.php b/app/middleware/AuthCheck.php index c085901..a03e632 100644 --- a/app/middleware/AuthCheck.php +++ b/app/middleware/AuthCheck.php @@ -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); } } \ No newline at end of file diff --git a/app/middleware/BasicData.php b/app/middleware/BasicData.php index 472456e..9df57e7 100644 --- a/app/middleware/BasicData.php +++ b/app/middleware/BasicData.php @@ -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']); } } } diff --git a/app/middleware/PermissionMiddleware.php b/app/middleware/PermissionMiddleware.php deleted file mode 100644 index 9642b52..0000000 --- a/app/middleware/PermissionMiddleware.php +++ /dev/null @@ -1,33 +0,0 @@ -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); - } -} diff --git a/app/process/Task.php b/app/process/Task.php new file mode 100644 index 0000000..910b5ad --- /dev/null +++ b/app/process/Task.php @@ -0,0 +1,41 @@ +', 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()]); + }); + } + +} \ No newline at end of file diff --git a/app/queue/redis/FeishuMessage.php b/app/queue/redis/FeishuMessage.php new file mode 100644 index 0000000..c2663c6 --- /dev/null +++ b/app/queue/redis/FeishuMessage.php @@ -0,0 +1,127 @@ +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); + } +} \ No newline at end of file diff --git a/app/queue/redis/InquiryToCrm.php b/app/queue/redis/InquiryToCrm.php new file mode 100644 index 0000000..ecac73e --- /dev/null +++ b/app/queue/redis/InquiryToCrm.php @@ -0,0 +1,101 @@ +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); + } +} \ No newline at end of file diff --git a/app/view/index/qrcode.html b/app/view/index/qrcode.html index fb6f11c..97c2a7c 100644 --- a/app/view/index/qrcode.html +++ b/app/view/index/qrcode.html @@ -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({ diff --git a/composer.json b/composer.json index e4e63bb..64b3e58 100644 --- a/composer.json +++ b/composer.json @@ -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. " diff --git a/composer.lock b/composer.lock index b9b58c7..7f84b75 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/constants/Constants.php b/config/constants/Constants.php index 1e77a2b..6797078 100644 --- a/config/constants/Constants.php +++ b/config/constants/Constants.php @@ -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']; } \ No newline at end of file diff --git a/config/plugin/webman/redis-queue/process.php b/config/plugin/webman/redis-queue/process.php index 9fac733..8474aee 100644 --- a/config/plugin/webman/redis-queue/process.php +++ b/config/plugin/webman/redis-queue/process.php @@ -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') ] ] diff --git a/config/process.php b/config/process.php index 224a111..05b250f 100644 --- a/config/process.php +++ b/config/process.php @@ -59,5 +59,8 @@ return [ 'enable_memory_monitor' => DIRECTORY_SEPARATOR === '/', ] ] - ] + ], + 'task' => [ + 'handler' => app\process\Task::class + ], ]; diff --git a/config/route.php b/config/route.php index d970528..303c4aa 100644 --- a/config/route.php +++ b/config/route.php @@ -25,9 +25,18 @@ use Webman\Route; // POST /admin/user/{user_id}/kickout // body: {"tokens": ["..."]} // 踢下线 // POST /admin/user/{user_id}/kickout_all // 一键踢全部 -// 调试 + +Route::any('/c', [app\controller\IndexController::class, 'createResource']); Route::any('/perm', [app\controller\IndexController::class, 'perm']); +// 飞书 +Route::any('/feishu/event', [app\common\controller\NotifyController::class, 'feishuEvent']); +// 飞书回调 +Route::any('/feishu/callback', [app\common\controller\NotifyController::class, 'feishuCallback']); +// 代理oss文件地址 +Route::any('/proxy/ossurl', [app\common\controller\UploadController::class, 'proxy']); + +// 公共 Route::group('/common', function () { Route::group('/auth', function () { Route::any('/login', [app\common\controller\AuthController::class, 'login']); @@ -42,21 +51,44 @@ Route::group('/common', function () { Route::group('/user', function () { Route::any('/info', [app\common\controller\UserController::class, 'info']); + Route::any('/get', [app\common\controller\UserController::class, 'getUser']); }); Route::group('/preset', function () { Route::any('/corps', [app\common\controller\PresetController::class, 'corps']); Route::any('/feishu/app', [app\common\controller\PresetController::class, 'feishuApp']); + Route::any('/systems', [app\common\controller\PresetController::class, 'systems']); }); }); +// 后台管理 Route::group('/admin', function () { + // 角色 Route::group('/role', function () { - Route::any('/list', [app\admin\controller\RoleController::class, 'list']); - Route::any('/detail', [app\admin\controller\RoleController::class, 'detail']); - Route::any('/create', [app\admin\controller\RoleController::class, 'create']); + Route::any('/list', [app\admin\controller\RoleController::class, 'index']); + Route::any('/show', [app\admin\controller\RoleController::class, 'show']); + Route::any('/store', [app\admin\controller\RoleController::class, 'store']); Route::any('/update', [app\admin\controller\RoleController::class, 'update']); - Route::any('/delete', [app\admin\controller\RoleController::class, 'delete']); + Route::any('/destroy', [app\admin\controller\RoleController::class, 'destroy']); + }); + // 公司 + Route::group('/corp', function () { + Route::any('/list', [app\admin\controller\CorpController::class, 'index']); + Route::any('/show', [app\admin\controller\CorpController::class, 'show']); + Route::any('/store', [app\admin\controller\CorpController::class, 'store']); + Route::any('/update', [app\admin\controller\CorpController::class, 'update']); + Route::any('/destroy', [app\admin\controller\CorpController::class, 'destroy']); + }); + + // 询单 + Route::group('/inquiry', function () { + Route::any('/list', [app\inquiry\controller\InquiryLogController::class, 'index']); + Route::any('/show', [app\inquiry\controller\InquiryLogController::class, 'show']); + Route::any('/store', [app\inquiry\controller\InquiryLogController::class, 'store']); + Route::any('/update', [app\inquiry\controller\InquiryLogController::class, 'update']); + Route::any('/destroy', [app\inquiry\controller\InquiryLogController::class, 'destroy']); + Route::any('/release', [app\inquiry\controller\InquiryLogController::class, 'release']); + Route::any('/allocation', [app\inquiry\controller\InquiryLogController::class, 'allocation']); }); }); @@ -87,3 +119,34 @@ Route::group('/ticket', function () { }); +//crm +Route::group('/crm', function () { + //跟进 + Route::any('/follow/list', [app\crm\controller\FollowController::class, 'list']); + Route::any('/follow/detail', [app\crm\controller\FollowController::class, 'detail']); + Route::any('/follow/add', [app\crm\controller\FollowController::class, 'save']); + Route::any('/follow/getFollowType', [app\crm\controller\FollowController::class, 'getFollowType']); + + //客户 + Route::any('/customer/list', [app\crm\controller\CustomerController::class, 'list']); + Route::any('/customer/detail', [app\crm\controller\CustomerController::class, 'detail']); + Route::any('/customer/add', [app\crm\controller\CustomerController::class, 'save']); + Route::any('/customer/claim', [app\crm\controller\CustomerController::class, 'claim']); + Route::any('/customer/restore', [app\crm\controller\CustomerController::class, 'restore']); + Route::any('/customer/delete', [app\crm\controller\CustomerController::class, 'delete']); + Route::any('/customer/waitFollow', [app\crm\controller\CustomerController::class, 'waitFollow']); + Route::any('/customer/statistics', [app\crm\controller\CustomerController::class, 'statistics']); + Route::any('/customer/getChannel', [app\crm\controller\CustomerController::class, 'getChannel']); + Route::any('/customer/getCountry', [app\crm\controller\CustomerController::class, 'getCountry']); + Route::any('/customer/followNext', [app\crm\controller\CustomerController::class, 'followNext']); + + Route::any('/tagGroup/list', [app\crm\controller\TagGroupController::class, 'list']); + Route::any('/tagGroup/updatePivot', [app\crm\controller\TagGroupController::class, 'updatePivot']); + + Route::any('/upload/image', [app\common\controller\UploadController::class, 'uploadImage']); + + + Route::any('/user/edit', [app\common\controller\UserController::class, 'edit']); + Route::any('/user/savePassword', [app\common\controller\UserController::class, 'savePassword']); +}); + diff --git a/config/whitelist.php b/config/whitelist.php deleted file mode 100644 index 1736876..0000000 --- a/config/whitelist.php +++ /dev/null @@ -1,13 +0,0 @@ - [ - '/', // 首页 - '/index/index', // 首页 - '/index/view', // 飞书调试 - '/perm', // 调试-添加权限 - - '/common/preset/corps', // 公司列表 - '/common/preset/feishu/app', // 飞书应用信息 - ] -]; \ No newline at end of file diff --git a/resource/translations/ar/messages.php b/resource/translations/ar/messages.php index a701f87..ed99240 100644 --- a/resource/translations/ar/messages.php +++ b/resource/translations/ar/messages.php @@ -2,4 +2,36 @@ return [ 'status_not_modified' => 'الدولة لا يمكن تعديلها عكسيا', + 'success' => ' النجاح', + 'fail' => ' الفشل', + 'login_success' => ' تم تسجيل الدخول بنجاح', + 'password_error' => ' كلمة المرور غير صحيحة', + 'user_none' => ' المستخدم غير موجود', + 'user_id' => ' معرف المستخدم', + 'user_login' => ' الرجاء تسجيل الدخول أولاً', + 'not_null' => ' لا يمكن أن يكون فارغًا', + 'code_error' => ' رمز التحقق غير صحيح', + 'format_error' => ' تنسيق غير صحيح', + 'email_repeat' => ' البريد الإلكتروني مكرر', + 'email_error' => ' تنسيق البريد الإلكتروني غير صحيح', + 'save_success' => ' تم الحفظ بنجاح', + 'save_fail' => ' فشل الحفظ', + 'register_success' => ' تم التسجيل بنجاح', + 'register_fail' => ' فشل التسجيل', + 'language_success' => ' تم تغيير اللغة بنجاح', + 'language_fail' => ' فشل تغيير اللغة', + 'param_error' => ' خطأ في المعلمات', + 'required_not_null' => ' الحقول المطلوبة لا يمكن أن تكون فارغة', + 'picture_not_null' => ' لا يمكن أن تكون الصورة فارغة', + 'follow_not_null' => ' الصورة و متابعة المحتوى لا يمكن أن تكون فارغة في نفس الوقت', + 'contacts_not_null' => ' لا يمكن أن يكون جهة الاتصال أو البريد الإلكتروني أو الهاتف فارغين جميعًا في نفس الوقت', + 'car_type_not_null' => ' لا يمكن أن تكون التفضيلات فارغة', + 'channel_not_null' => ' لا يمكن أن تكون القناة فارغة', + 'email_consistent' => ' البريد الإلكتروني غير متطابق', + 'sign_not_null' => ' لم يتم تعيين الشركة التابعة', + 'password_short' => ' لا يمكن أن تكون كلمة المرور أقل من 6 أحرف', + 'id_empty' => ' لا يمكن أن يكون المعرف فارغًا', + 'customer_id' => ' معرف العميل', + 'tag_group_id' => ' معرف مجموعة العلامات', + 'claim_fail' => ' لا يمكن استلام العملاء الذين لديهم مالك بالفعل', ]; \ No newline at end of file diff --git a/resource/translations/en/messages.php b/resource/translations/en/messages.php index c676eb2..4c73bcd 100644 --- a/resource/translations/en/messages.php +++ b/resource/translations/en/messages.php @@ -2,4 +2,36 @@ return [ 'status_not_modified' => 'The status cannot be reversed for modification', + 'success' => ' success', + 'fail' => ' fail', + 'login_success' => ' login success', + 'password_error' => ' password error', + 'user_none' => ' user none', + 'user_id' => ' user ID', + 'user_login' => ' Please log in first', + 'not_null' => ' not null', + 'code_error' => ' code error', + 'format_error' => ' format error', + 'email_repeat' => ' email repeat', + 'email_error' => ' Email format error', + 'save_success' => ' save success', + 'save_fail' => ' save fail', + 'register_success' => ' register success', + 'register_fail' => ' register fail', + 'language_success' => ' Successfully switched languages', + 'language_fail' => ' Language switching failed', + 'param_error' => ' param error', + 'required_not_null' => ' required not_ ull', + 'picture_not_null' => ' picture not null', + 'follow_not_null' => ' The image and follow-up content cannot be empty at the same time', + 'contacts_not_null' => ' Contact person, email, and phone cannot be empty at the same time', + 'car_type_not_null' => ' preference_not_null', + 'channel_not_null' => ' channel not null', + 'email_consistent' => ' Inconsistent email addresses', + 'sign_not_null' => ' No affiliated company has been set', + 'password_short' => ' Password cannot be less than 6 characters', + 'id_empty' => ' ID cannot be empty', + 'customer_id' => ' customer ID', + 'tag_group_id' => ' tag group ID', + 'claim_fail' => ' Cannot claim ownership of customers', ]; \ No newline at end of file diff --git a/resource/translations/ru/messages.php b/resource/translations/ru/messages.php index 96ab3a4..b275ac6 100644 --- a/resource/translations/ru/messages.php +++ b/resource/translations/ru/messages.php @@ -2,4 +2,36 @@ return [ 'status_not_modified' => 'Статус не может быть изменен в обратном направлении', + 'success' => ' Успех', + 'fail' => ' Неудача', + 'login_success' => ' Регистрация завершена', + 'password_error' => ' Ошибка пароля', + 'user_none' => ' Пользователь не существует', + 'user_id' => ' Идентификатор пользователя', + 'user_login' => ' Пожалуйста, войдите.', + 'not_null' => ' Нельзя быть пустым', + 'code_error' => ' Ошибка кода проверки', + 'format_error' => ' Ошибка формата', + 'email_repeat' => ' Почтовый ящик повторяется', + 'email_error' => ' Ошибка формата почтового ящика', + 'save_success' => ' Сохранить успешно', + 'save_fail' => ' Ошибка сохранения', + 'register_success' => ' Регистрация успешно', + 'register_fail' => ' Ошибка регистрации', + 'language_success' => ' Переключить язык успешно', + 'language_fail' => ' Ошибка переключения языка', + 'param_error' => ' Ошибка параметра', + 'required_not_null' => ' Обязательное заполнение не может быть пустым.', + 'picture_not_null' => ' Фотографии не могут быть пустыми', + 'follow_not_null' => ' Изображение и последующий контент не могут быть пустыми одновременно.X', + 'contacts_not_null' => ' Контакты, почтовый ящик, телефон не может быть одновременно пустым', + 'car_type_not_null' => ' Предпочтения не могут быть пустыми', + 'channel_not_null' => ' Каналы не могут быть пустыми', + 'email_consistent' => ' Почтовый ящик не совпадает', + 'sign_not_null' => ' Компания не создана', + 'password_short' => ' Пароль не может быть меньше 6.', + 'id_empty' => ' ID не может быть пустым', + 'customer_id' => ' Идентификатор клиента', + 'tag_group_id' => ' Идентификатор группы меток', + 'claim_fail' => ' Невозможность получения принадлежащего клиенту', ]; \ No newline at end of file diff --git a/resource/translations/zh/messages.php b/resource/translations/zh/messages.php index 229d7ee..b8ca32d 100644 --- a/resource/translations/zh/messages.php +++ b/resource/translations/zh/messages.php @@ -2,4 +2,36 @@ return [ 'status_not_modified' => '状态不能逆向修改', + 'success' => ' 成功', + 'fail' => ' 失败', + 'login_success' => ' 登录成功', + 'password_error' => ' 密码错误', + 'user_none' => ' 用户不存在', + 'user_id' => ' 用户ID', + 'user_login' => ' 请先登录', + 'not_null' => ' 不能为空', + 'code_error' => ' 验证码错误', + 'format_error' => ' 格式错误', + 'email_repeat' => ' 邮箱重复', + 'email_error' => ' 邮箱格式错误', + 'save_success' => ' 保存成功', + 'save_fail' => ' 保存失败', + 'register_success' => ' 注册成功', + 'register_fail' => ' 注册失败', + 'language_success' => ' 切换语言成功', + 'language_fail' => ' 切换语言失败', + 'param_error' => ' 参数错误', + 'required_not_null' => ' 必填项不能为空', + 'picture_not_null' => ' 图片不能为空', + 'follow_not_null' => ' 图片与跟进内容不能同时为空', + 'contacts_not_null' => ' 联系人,邮箱,电话不能同时为空', + 'car_type_not_null' => ' 偏好不能为空', + 'channel_not_null' => ' 渠道不能为空', + 'email_consistent' => ' 邮箱不一致', + 'sign_not_null' => ' 未设置所属公司', + 'password_short' => ' 密码不能少于6位', + 'id_empty' => ' id不能为空', + 'customer_id' => ' 客户ID', + 'tag_group_id' => ' 标签组ID', + 'claim_fail' => ' 不能领取有归属客户', ]; \ No newline at end of file