From 63e532f19972abc2f07d40898c782918c119e7ac Mon Sep 17 00:00:00 2001 From: mzl Date: Thu, 7 Aug 2025 10:16:22 +0800 Subject: [PATCH] Merge branch 'develop' --- app/admin/controller/UserController.php | 53 ++ app/admin/logic/UserLogic.php | 17 + app/admin/service/UserService.php | 78 ++ app/admin/validate/UserValidate.php | 29 + app/command/CustomerCodeCommand.php | 61 ++ app/common/traits/DataPermission.php | 425 +++++++++ app/common/traits/crud/CrudServiceTrait.php | 2 +- app/common/utils/CrudUtil.php | 475 ++++++++-- app/common/utils/CrudUtil_bak.php | 843 ++++++++++++++++++ app/common/validate/InquiryLogValidate.php | 4 +- app/controller/IndexController.php | 6 +- app/crm/controller/CustomerController.php | 119 ++- app/crm/service/CustomerService.php | 32 +- app/crmAdmin/controller/BaseController.php | 51 ++ .../controller/CustomerController.php | 157 ++++ app/crmAdmin/logic/CustomerLogic.php | 26 + app/crmAdmin/service/CustomerService.php | 397 +++++++++ app/functions.php | 48 + .../controller/InquiryLogController.php | 29 +- app/inquiry/service/InquiryLogService.php | 146 ++- app/process/Emails.php | 42 +- app/queue/redis/InquiryToCrmUpdate.php | 110 +++ config/route.php | 21 + 23 files changed, 3038 insertions(+), 133 deletions(-) create mode 100644 app/admin/controller/UserController.php create mode 100644 app/admin/logic/UserLogic.php create mode 100644 app/admin/service/UserService.php create mode 100644 app/admin/validate/UserValidate.php create mode 100644 app/command/CustomerCodeCommand.php create mode 100644 app/common/traits/DataPermission.php create mode 100644 app/common/utils/CrudUtil_bak.php create mode 100644 app/crmAdmin/controller/BaseController.php create mode 100644 app/crmAdmin/controller/CustomerController.php create mode 100644 app/crmAdmin/logic/CustomerLogic.php create mode 100644 app/crmAdmin/service/CustomerService.php create mode 100644 app/queue/redis/InquiryToCrmUpdate.php diff --git a/app/admin/controller/UserController.php b/app/admin/controller/UserController.php new file mode 100644 index 0000000..33baa25 --- /dev/null +++ b/app/admin/controller/UserController.php @@ -0,0 +1,53 @@ +service = new UserService(); + } + + /** + * 列表页数据格式化 + * + * @param array $data + * @return array + */ + protected function indexFormat($data) + { + if(isset($data['records'])){ + return $data; + } + $data['sex_txt'] = ''; + if (isset($data['sex']) && $data['sex'] == 1){ + $data['sex_txt'] = '男'; + }elseif(isset($data['sex']) && $data['sex'] == 2){ + $data['sex_txt'] = '女'; + } + + switch ($data['status']){ + case 1: + $data['status_txt'] = '正常'; + break; + case 2: + $data['status_txt'] = '离职'; + break; + default: + $data['status_txt'] = ''; + break; + } + + $data['customer_count'] = Customer::where('user_id', $data['id'])->count(); + return $data; + } +} \ No newline at end of file diff --git a/app/admin/logic/UserLogic.php b/app/admin/logic/UserLogic.php new file mode 100644 index 0000000..de0d15d --- /dev/null +++ b/app/admin/logic/UserLogic.php @@ -0,0 +1,17 @@ +validator; + } + + + /** + * 构造 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']['id'] = 'desc'; + break; + case 'index': +// $config['where']['status'] = 1; + $config['order']['id'] = 'desc'; + + // 关键字 + if (isset($p['keyword']) && $p['keyword']) { + $k = '%' . $p['keyword'] . '%'; + $config['where']['name'] = ['like', $k]; + } + if (isset($p['id']) && $p['id']) { + $config['where']['id'] = $p['id']; + } + + $config['fields'] = ['id','sn', 'name', 'email', 'sex', 'phone', 'avatar', 'corp_id', 'country_id', 'position', 'status', 'created_at']; + break; + case 'show': + $config['where']['id'] = $p['id']; + break; + case 'store': + break; + case 'update': + $config['where']['id'] = $p['id']; + break; + case 'destroy': + $config['where']['id'] = $p['id']; + break; + } + + return $config; + } +} \ No newline at end of file diff --git a/app/admin/validate/UserValidate.php b/app/admin/validate/UserValidate.php new file mode 100644 index 0000000..e478c5b --- /dev/null +++ b/app/admin/validate/UserValidate.php @@ -0,0 +1,29 @@ + [ + + ], + + 'update' => [ + + ] + ]; +} \ No newline at end of file diff --git a/app/command/CustomerCodeCommand.php b/app/command/CustomerCodeCommand.php new file mode 100644 index 0000000..2d3e447 --- /dev/null +++ b/app/command/CustomerCodeCommand.php @@ -0,0 +1,61 @@ +setDescription('为crm_customer表所有记录生成唯一code码,分批处理'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $batchSize = 1000; + $total = Customer::withTrashed()->where('code','')->count(); + $output->writeln("总计需处理:$total 条记录"); + + $processed = 0; + do { + // 查询一批未生成code的记录 + $customers = Customer::withTrashed()->where('code','')->limit($batchSize)->get(); + if ($customers->isEmpty()) { + break; + } + + // Eloquent 批量处理 + foreach ($customers as $customer) { + // 生成唯一16位code,CM+14位 + $unique = strtoupper(substr(md5($customer->id . microtime(true) . mt_rand(1000, 9999)), 0, 14)); + $code = 'CM' . $unique; + + // 防止极小概率重复 + while (Customer::where('code', $code)->exists()) { + $unique = strtoupper(substr(md5($customer->id . microtime(true) . mt_rand(1000, 9999)), 0, 14)); + $code = 'CM' . $unique; + } + + $obtain = $customer->created_at>strtotime('2025-07-29')?$customer->created_at:strtotime('2025-07-29'); + + $customer->code = $code; + $customer->obtain = $obtain; + $customer->save(); + $processed++; + } + + $output->writeln("已处理:$processed / $total"); + } while (true); + + $output->writeln('全部处理完成!'); + return self::SUCCESS; + } +} \ No newline at end of file diff --git a/app/common/traits/DataPermission.php b/app/common/traits/DataPermission.php new file mode 100644 index 0000000..fa5e917 --- /dev/null +++ b/app/common/traits/DataPermission.php @@ -0,0 +1,425 @@ +where($userField, $currentUserId); + break; + + case 'same_role': + // 同角色权限:访问同角色的数据 + $userIds = self::getSameRoleUserIds($currentRoleId); + $query->whereIn($userField, $userIds); + break; + + case 'role_tree': + // 角色树权限:访问自己角色及子角色的数据 + $userIds = self::getRoleTreeUserIds($currentRoleId); + $query->whereIn($userField, $userIds); + break; + + case 'relation': + // 从属关系权限:访问自己及下属的数据 + $userIds = self::getRelationUserIds($currentUserId); + $query->whereIn($userField, $userIds); + break; + + default: + // 默认个人权限 + $query->where($userField, $currentUserId); + } + } + + /** + * 应用数据权限到创建/更新操作 + * + * @param mixed $target 创建时为数组,更新时为Model对象 + * @param string $type 权限类型 + * @param int $currentUserId 当前用户ID + * @param int $currentRoleId 当前角色ID + * @param array $newData 更新时的数据 + * @param string $userField 用户ID字段名 + * @param string $roleField 角色ID字段名 + */ + public static function applyMutationPermission( + &$target, + string $type, + int $currentUserId, + int $currentRoleId, + array $newData = [], + string $userField = 'admin_id', + string $roleField = 'role_id' + ): void { + // 超管不需要检查 + if (self::isSuperAdmin($currentUserId)) { + return; + } + + switch ($type) { + case 'personal': + self::handlePersonalPermission($target, $newData, $userField, $currentUserId); + break; + + case 'same_role': + self::handleSameRolePermission($target, $newData, $userField, $roleField, $currentRoleId); + break; + + case 'role_tree': + self::handleRoleTreePermission($target, $newData, $userField, $roleField, $currentRoleId); + break; + + case 'relation': + self::handleRelationPermission($target, $newData, $userField, $currentUserId); + break; + + default: + throw new BusinessException("未定义的数据权限类型: $type", 403); + } + } + + /** + * 获取同角色的用户ID列表 + * + * @param int $roleId 角色ID + * @return array 用户ID列表 + */ + public static function getSameRoleUserIds(int $roleId): array + { + return Db::table('user_roles') + ->where('role_id', $roleId) + ->pluck('user_id') + ->toArray(); + } + + /** + * 获取角色树下的所有用户ID列表 + * + * @param int $roleId 当前角色ID + * @return array 用户ID列表 + */ + public static function getRoleTreeUserIds(int $roleId): array + { + // 获取角色树中的所有角色ID + $roleIds = self::getRoleTreeIds($roleId); + + // 获取这些角色下的所有用户ID + return Db::table('user_roles') + ->whereIn('role_id', $roleIds) + ->pluck('user_id') + ->toArray(); + } + + /** + * 获取从属关系下的用户ID列表(包括自己和下属) + * + * @param int $userId 当前用户ID + * @return array 用户ID列表 + */ + public static function getRelationUserIds(int $userId): array + { + // 获取所有下属用户ID(递归) + $subordinateIds = self::getSubordinateIds($userId); + + // 返回自己和所有下属的ID + return array_merge([$userId], $subordinateIds); + } + + /** + * 获取直接下属的用户ID列表 + * + * @param int $userId 用户ID + * @return array 直接下属用户ID列表 + */ + public static function getDirectSubordinateIds(int $userId): array + { + return Db::table('user') + ->where('report_to', $userId) + ->pluck('id') + ->toArray(); + } + + /** + * 递归获取所有下属用户ID列表 + * + * @param int $userId 用户ID + * @return array 所有下属用户ID列表 + */ + private static function getSubordinateIds(int $userId): array + { + $subordinates = self::getDirectSubordinateIds($userId); + $allSubordinates = $subordinates; + + foreach ($subordinates as $subordinateId) { + $allSubordinates = array_merge($allSubordinates, self::getSubordinateIds($subordinateId)); + } + + return $allSubordinates; + } + + /** + * 处理个人权限 + */ + private static function handlePersonalPermission( + &$target, + array $newData, + string $userField, + int $currentUserId + ): void { + if (is_array($target)) { + // 创建时强制设置用户ID + $target[$userField] = $currentUserId; + } else { + // 更新时校验原始记录 + if ($target->{$userField} !== $currentUserId) { + throw new BusinessException("无权操作该记录", 403); + } + + // 禁止变更归属字段 + if (isset($newData[$userField]) && $newData[$userField] !== $currentUserId) { + throw new BusinessException("无权变更归属字段", 403); + } + } + } + + /** + * 处理同角色权限 + */ + private static function handleSameRolePermission( + &$target, + array $newData, + string $userField, + string $roleField, + int $currentRoleId + ): void { + // 获取同角色用户ID列表 + $sameRoleUserIds = self::getSameRoleUserIds($currentRoleId); + + if (is_array($target)) { + // 创建时设置归属用户并校验 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $sameRoleUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentRoleId; + } + } else { + // 更新时校验原始记录 + if (!in_array($target->{$userField}, $sameRoleUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 禁止变更归属字段到无权限的用户 + if (isset($newData[$userField]) && !in_array($newData[$userField], $sameRoleUserIds)) { + throw new BusinessException("无权变更归属字段", 403); + } + } + } + + /** + * 处理角色树权限 + */ + private static function handleRoleTreePermission( + &$target, + array $newData, + string $userField, + string $roleField, + int $currentRoleId + ): void { + // 获取角色树用户ID列表 + $roleTreeUserIds = self::getRoleTreeUserIds($currentRoleId); + + if (is_array($target)) { + // 创建时校验用户是否在允许范围内 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $roleTreeUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentRoleId; + } + } else { + // 更新时校验原始记录用户 + if (!in_array($target->{$userField}, $roleTreeUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 校验新用户是否在允许范围内 + if (isset($newData[$userField])) { + if (!in_array($newData[$userField], $roleTreeUserIds)) { + throw new BusinessException("无权变更用户字段", 403); + } + } + } + } + + /** + * 处理从属关系权限 + */ + private static function handleRelationPermission( + &$target, + array $newData, + string $userField, + int $currentUserId + ): void { + // 获取从属关系用户ID列表 + $relationUserIds = self::getRelationUserIds($currentUserId); + + if (is_array($target)) { + // 创建时校验用户是否在允许范围内 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $relationUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentUserId; + } + } else { + // 更新时校验原始记录用户 + if (!in_array($target->{$userField}, $relationUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 校验新用户是否在允许范围内 + if (isset($newData[$userField])) { + if (!in_array($newData[$userField], $relationUserIds)) { + throw new BusinessException("无权变更用户字段", 403); + } + } + } + } + + /** + * 获取角色树ID(当前角色及其所有子角色) + * 从数据库中动态获取角色树关系 + */ + private static function getRoleTreeIds(int $roleId): array + { + static $roleTreeCache = []; + + if (isset($roleTreeCache[$roleId])) { + return $roleTreeCache[$roleId]; + } + + // 从数据库中递归获取角色树 + $ids = [$roleId]; + $queue = [$roleId]; + + while (!empty($queue)) { + $current = array_shift($queue); + // 查找当前角色的所有子角色 + $childRoles = Db::table('roles') + ->where('parent_id', $current) + ->where('status', 1) // 只获取启用的角色 + ->pluck('id') + ->toArray(); + + foreach ($childRoles as $childId) { + if (!in_array($childId, $ids)) { + $ids[] = $childId; + $queue[] = $childId; + } + } + } + + $roleTreeCache[$roleId] = $ids; + return $ids; + } + + /** + * 检查是否为超管 + * + * @param int $userId 用户ID + * @return bool 是否为超管 + */ + public static function isSuperAdmin(int $userId): bool + { + // 检查用户是否关联了超级管理员角色 + // 假设角色ID为1的是超级管理员角色 + $superAdminRoleId = 1; + + return Db::table('user_roles') + ->where('user_id', $userId) + ->where('role_id', $superAdminRoleId) + ->exists(); + } + + /** + * 获取用户的角色ID + * + * @param int $userId 用户ID + * @return int 角色ID + */ + public static function getUserIdRoleId(int $userId): int + { + // 从 user_roles 表中获取当前用户的角色ID + $roleId = Db::table('user_roles') + ->where('user_id', $userId) + ->value('role_id'); + + return $roleId ?: 0; + } + + /** + * 获取用户的所有角色ID(包括继承的角色) + * + * @param int $userId 用户ID + * @return array 角色ID列表 + */ + public static function getUserAllRoleIds(int $userId): array + { + // 获取用户直接关联的角色 + $directRoleIds = Db::table('user_roles') + ->where('user_id', $userId) + ->pluck('role_id') + ->toArray(); + + // 获取所有继承的角色 + $allRoleIds = []; + foreach ($directRoleIds as $roleId) { + $allRoleIds = array_merge($allRoleIds, self::getRoleTreeIds($roleId)); + } + + // 去重并返回 + return array_unique($allRoleIds); + } +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudServiceTrait.php b/app/common/traits/crud/CrudServiceTrait.php index 5ad6dbc..344756d 100644 --- a/app/common/traits/crud/CrudServiceTrait.php +++ b/app/common/traits/crud/CrudServiceTrait.php @@ -74,7 +74,7 @@ trait CrudServiceTrait $data = $this->preProcess('Update', $data, $request); $config = $this->buildQueryConfig($request, 'update'); $updated = $this->logicUpdateBy($config, $data); - return $this->postProcess('Update', $updated); + return $this->postProcess('Update', $data); } /** 删除 */ diff --git a/app/common/utils/CrudUtil.php b/app/common/utils/CrudUtil.php index dec779b..836e39e 100644 --- a/app/common/utils/CrudUtil.php +++ b/app/common/utils/CrudUtil.php @@ -4,8 +4,8 @@ namespace app\common\utils; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use plugin\admin\app\common\Auth; use support\exception\BusinessException; +use support\Db; /** * 构建查询工具类 @@ -49,6 +49,13 @@ class CrudUtil * 'roles' => ['id','name'], // 关联表字段选择 * 'posts' => ['id','title','user_id'], * ], + * 'dataLimit' => [ + * 'type' => 'personal|same_role|role_tree|relation', // personal:个人数据,same_role:同角色,role_tree:角色树,relation:从属关联数据 + * 'userId' => 1, + * 'roleId' => 2, + * 'userField' => 'user_id', + * 'roleField' => 'role_id' + * ] * ]; */ public static function buildQuery(array $config): Builder @@ -354,78 +361,444 @@ class CrudUtil } } + + // ================== Data Limit ================== + /** * 应用数据权限过滤到查询(buildQuery 场景) */ protected static function applyDataLimit(Builder $q, array $dataLimit): void { - if (Auth::isSuperAdmin()) { - return; - } - $type = $dataLimit['type'] ?? null; - $field = $dataLimit['field'] ?? 'admin_id'; + $type = $dataLimit['type'] ?? 'personal'; + $userId = $dataLimit['userId'] ?? 0; + $roleId = $dataLimit['roleId'] ?? 0; + $userField = $dataLimit['userField'] ?? 'user_id'; + $roleField = $dataLimit['roleField'] ?? 'role_id'; - if ($type === 'personal') { - $q->where($field, admin_id()); - } elseif ($type === 'auth') { - $q->whereIn($field, Auth::getScopeAdminIds(true)); - } + self::applyQueryPermission($q, $type, $userId, $roleId, $userField, $roleField); } /** - * 应用数据权限到“创建/更新”操作 + * 应用数据权限到"创建/更新"操作 * * - $target 可能是 array|Model 对象 * - $newData 仅在 updateByPk 场景时传入,用以校验变更归属 */ protected static function applyDataLimitOnMutate(&$target, array $dataLimit, array $newData = []): void { - if (Auth::isSuperAdmin()) { + $type = $dataLimit['type'] ?? 'personal'; + $userId = $dataLimit['userId'] ?? 0; + $roleId = $dataLimit['roleId'] ?? 0; + $userField = $dataLimit['userField'] ?? 'user_id'; + $roleField = $dataLimit['roleField'] ?? 'role_id'; + + self::applyMutationPermission($target, $type, $userId, $roleId, $newData, $userField, $roleField); + } + + /** + * 应用数据权限过滤到查询 + * + * @param Builder $query 查询构造器 + * @param string $type 权限类型:superadmin, personal, same_role, role_tree, relation + * @param int $currentUserId 当前用户ID + * @param int $currentRoleId 当前角色ID + * @param string $userField 用户ID字段名 + * @param string $roleField 角色ID字段名 + */ + public static function applyQueryPermission( + Builder $query, + string $type, + int $currentUserId = 0, + int $currentRoleId = 0, + string $userField = 'user_id', + string $roleField = 'role_id' + ): void { + // 超管不需要过滤 + if (self::isSuperAdmin($currentUserId)) { 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); - } + + switch ($type) { + case 'personal': + // 个人权限:只能访问自己的数据 + $query->where($userField, $currentUserId); + break; + + case 'same_role': + // 同角色权限:访问同角色的数据 + $userIds = self::getSameRoleUserIds($currentRoleId); + $query->whereIn($userField, $userIds); + break; + + case 'role_tree': + // 角色树权限:访问自己角色及子角色的数据 + $userIds = self::getRoleTreeUserIds($currentRoleId); + $query->whereIn($userField, $userIds); + break; + + case 'relation': + // 从属关系权限:访问自己及下属的数据 + $userIds = self::getRelationUserIds($currentUserId); + $query->whereIn($userField, $userIds); + break; + + default: + // 默认个人权限 + $query->where($userField, $currentUserId); } - - // 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)) { + } + + /** + * 应用数据权限到创建/更新操作 + * + * @param mixed $target 创建时为数组,更新时为Model对象 + * @param string $type 权限类型 + * @param int $currentUserId 当前用户ID + * @param int $currentRoleId 当前角色ID + * @param array $newData 更新时的数据 + * @param string $userField 用户ID字段名 + * @param string $roleField 角色ID字段名 + */ + public static function applyMutationPermission( + &$target, + string $type, + int $currentUserId, + int $currentRoleId, + array $newData = [], + string $userField = 'user_id', + string $roleField = 'role_id' + ): void { + // 超管不需要检查 + if (self::isSuperAdmin($currentUserId)) { + return; + } + + switch ($type) { + case 'personal': + self::handlePersonalPermission($target, $newData, $userField, $currentUserId); + break; + + case 'same_role': + self::handleSameRolePermission($target, $newData, $userField, $roleField, $currentRoleId); + break; + + case 'role_tree': + self::handleRoleTreePermission($target, $newData, $userField, $roleField, $currentRoleId); + break; + + case 'relation': + self::handleRelationPermission($target, $newData, $userField, $currentUserId); + break; + + default: + throw new BusinessException("未定义的数据权限类型: $type", 403); + } + } + + /** + * 获取同角色的用户ID列表 + * + * @param int $roleId 角色ID + * @return array 用户ID列表 + */ + public static function getSameRoleUserIds(int $roleId): array + { + return Db::table('user_roles') + ->where('role_id', $roleId) + ->pluck('user_id') + ->toArray(); + } + + /** + * 获取角色树下的所有用户ID列表 + * + * @param int $roleId 当前角色ID + * @return array 用户ID列表 + */ + public static function getRoleTreeUserIds(int $roleId): array + { + // 获取角色树中的所有角色ID + $roleIds = self::getRoleTreeIds($roleId); + + // 获取这些角色下的所有用户ID + return Db::table('user_roles') + ->whereIn('role_id', $roleIds) + ->pluck('user_id') + ->toArray(); + } + + /** + * 获取从属关系下的用户ID列表(包括自己和下属) + * + * @param int $userId 当前用户ID + * @return array 用户ID列表 + */ + public static function getRelationUserIds(int $userId): array + { + // 获取所有下属用户ID(递归) + $subordinateIds = self::getSubordinateIds($userId); + + // 返回自己和所有下属的ID + return array_merge([$userId], $subordinateIds); + } + + /** + * 获取直接下属的用户ID列表 + * + * @param int $userId 用户ID + * @return array 直接下属用户ID列表 + */ + public static function getDirectSubordinateIds(int $userId): array + { + return Db::table('user') + ->where('report_to', $userId) + ->pluck('id') + ->toArray(); + } + + /** + * 递归获取所有下属用户ID列表 + * + * @param int $userId 用户ID + * @return array 所有下属用户ID列表 + */ + private static function getSubordinateIds(int $userId): array + { + $subordinates = self::getDirectSubordinateIds($userId); + $allSubordinates = $subordinates; + + foreach ($subordinates as $subordinateId) { + $allSubordinates = array_merge($allSubordinates, self::getSubordinateIds($subordinateId)); + } + + return $allSubordinates; + } + + /** + * 处理个人权限 + */ + private static function handlePersonalPermission( + &$target, + array $newData, + string $userField, + int $currentUserId + ): void { + if (is_array($target)) { + // 创建时强制设置用户ID + $target[$userField] = $currentUserId; + } else { + // 更新时校验原始记录 + if ($target->{$userField} !== $currentUserId) { throw new BusinessException("无权操作该记录", 403); } - // 校验更新中指定的新归属 - if (isset($newData[$field]) && !in_array($newData[$field], $scope)) { + + // 禁止变更归属字段 + if (isset($newData[$userField]) && $newData[$userField] !== $currentUserId) { throw new BusinessException("无权变更归属字段", 403); } } } + + /** + * 处理同角色权限 + */ + private static function handleSameRolePermission( + &$target, + array $newData, + string $userField, + string $roleField, + int $currentRoleId + ): void { + // 获取同角色用户ID列表 + $sameRoleUserIds = self::getSameRoleUserIds($currentRoleId); + + if (is_array($target)) { + // 创建时设置归属用户并校验 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $sameRoleUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentRoleId; + } + } else { + // 更新时校验原始记录 + if (!in_array($target->{$userField}, $sameRoleUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 禁止变更归属字段到无权限的用户 + if (isset($newData[$userField]) && !in_array($newData[$userField], $sameRoleUserIds)) { + throw new BusinessException("无权变更归属字段", 403); + } + } + } + + /** + * 处理角色树权限 + */ + private static function handleRoleTreePermission( + &$target, + array $newData, + string $userField, + string $roleField, + int $currentRoleId + ): void { + // 获取角色树用户ID列表 + $roleTreeUserIds = self::getRoleTreeUserIds($currentRoleId); + + if (is_array($target)) { + // 创建时校验用户是否在允许范围内 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $roleTreeUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentRoleId; + } + } else { + // 更新时校验原始记录用户 + if (!in_array($target->{$userField}, $roleTreeUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 校验新用户是否在允许范围内 + if (isset($newData[$userField])) { + if (!in_array($newData[$userField], $roleTreeUserIds)) { + throw new BusinessException("无权变更用户字段", 403); + } + } + } + } + + /** + * 处理从属关系权限 + */ + private static function handleRelationPermission( + &$target, + array $newData, + string $userField, + int $currentUserId + ): void { + // 获取从属关系用户ID列表 + $relationUserIds = self::getRelationUserIds($currentUserId); + + if (is_array($target)) { + // 创建时校验用户是否在允许范围内 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $relationUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentUserId; + } + } else { + // 更新时校验原始记录用户 + if (!in_array($target->{$userField}, $relationUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 校验新用户是否在允许范围内 + if (isset($newData[$userField])) { + if (!in_array($newData[$userField], $relationUserIds)) { + throw new BusinessException("无权变更用户字段", 403); + } + } + } + } + + /** + * 获取角色树ID(当前角色及其所有子角色) + * 从数据库中动态获取角色树关系 + */ + private static function getRoleTreeIds(int $roleId): array + { + static $roleTreeCache = []; + + if (isset($roleTreeCache[$roleId])) { + return $roleTreeCache[$roleId]; + } + + // 从数据库中递归获取角色树 + $ids = [$roleId]; + $queue = [$roleId]; + + while (!empty($queue)) { + $current = array_shift($queue); + // 查找当前角色的所有子角色 + $childRoles = Db::table('roles') + ->where('parent_id', $current) + ->where('status', 1) // 只获取启用的角色 + ->pluck('id') + ->toArray(); + + foreach ($childRoles as $childId) { + if (!in_array($childId, $ids)) { + $ids[] = $childId; + $queue[] = $childId; + } + } + } + + $roleTreeCache[$roleId] = $ids; + return $ids; + } + + /** + * 检查是否为超管 + * + * @param int $userId 用户ID + * @return bool 是否为超管 + */ + public static function isSuperAdmin(int $userId): bool + { + // 检查用户是否关联了超级管理员角色 + // 设定角色ID为1的是超级管理员角色 + $superAdminRoleId = 1; + + return Db::table('user_roles') + ->where('user_id', $userId) + ->where('role_id', $superAdminRoleId) + ->exists(); + } + + /** + * 获取用户的角色ID + * + * @param int $userId 用户ID + * @return int 角色ID + */ + public static function getUserIdRoleId(int $userId): int + { + // 从 user_roles 表中获取当前用户的角色ID + $roleId = Db::table('user_roles') + ->where('user_id', $userId) + ->value('role_id'); + + return $roleId ?: 0; + } + + /** + * 获取用户的所有角色ID(包括继承的角色) + * + * @param int $userId 用户ID + * @return array 角色ID列表 + */ + public static function getUserAllRoleIds(int $userId): array + { + // 获取用户直接关联的角色 + $directRoleIds = Db::table('user_roles') + ->where('user_id', $userId) + ->pluck('role_id') + ->toArray(); + + // 获取所有继承的角色 + $allRoleIds = []; + foreach ($directRoleIds as $roleId) { + $allRoleIds = array_merge($allRoleIds, self::getRoleTreeIds($roleId)); + } + + // 去重并返回 + return array_unique($allRoleIds); + } } \ No newline at end of file diff --git a/app/common/utils/CrudUtil_bak.php b/app/common/utils/CrudUtil_bak.php new file mode 100644 index 0000000..10617e2 --- /dev/null +++ b/app/common/utils/CrudUtil_bak.php @@ -0,0 +1,843 @@ + 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); + } + } + + + // ================== Data Limit ================== + + /** + * 应用数据权限过滤到查询 + * + * @param Builder $query 查询构造器 + * @param string $type 权限类型:superadmin, personal, same_role, role_tree, relation + * @param int $currentUserId 当前用户ID + * @param int $currentRoleId 当前角色ID + * @param string $userField 用户ID字段名 + * @param string $roleField 角色ID字段名 + */ + public static function applyQueryPermission( + Builder $query, + string $type, + int $currentUserId = 0, + int $currentRoleId = 0, + string $userField = 'user_id', + string $roleField = 'role_id' + ): void { + // 超管不需要过滤 + if (self::isSuperAdmin($currentUserId)) { + return; + } + + switch ($type) { + case 'personal': + // 个人权限:只能访问自己的数据 + $query->where($userField, $currentUserId); + break; + + case 'same_role': + // 同角色权限:访问同角色的数据 + $userIds = self::getSameRoleUserIds($currentRoleId); + $query->whereIn($userField, $userIds); + break; + + case 'role_tree': + // 角色树权限:访问自己角色及子角色的数据 + $userIds = self::getRoleTreeUserIds($currentRoleId); + $query->whereIn($userField, $userIds); + break; + + case 'relation': + // 从属关系权限:访问自己及下属的数据 + $userIds = self::getRelationUserIds($currentUserId); + $query->whereIn($userField, $userIds); + break; + + default: + // 默认个人权限 + $query->where($userField, $currentUserId); + } + } + + /** + * 应用数据权限到创建/更新操作 + * + * @param mixed $target 创建时为数组,更新时为Model对象 + * @param string $type 权限类型 + * @param int $currentUserId 当前用户ID + * @param int $currentRoleId 当前角色ID + * @param array $newData 更新时的数据 + * @param string $userField 用户ID字段名 + * @param string $roleField 角色ID字段名 + */ + public static function applyMutationPermission( + &$target, + string $type, + int $currentUserId, + int $currentRoleId, + array $newData = [], + string $userField = 'admin_id', + string $roleField = 'role_id' + ): void { + // 超管不需要检查 + if (self::isSuperAdmin($currentUserId)) { + return; + } + + switch ($type) { + case 'personal': + self::handlePersonalPermission($target, $newData, $userField, $currentUserId); + break; + + case 'same_role': + self::handleSameRolePermission($target, $newData, $userField, $roleField, $currentRoleId); + break; + + case 'role_tree': + self::handleRoleTreePermission($target, $newData, $userField, $roleField, $currentRoleId); + break; + + case 'relation': + self::handleRelationPermission($target, $newData, $userField, $currentUserId); + break; + + default: + throw new BusinessException("未定义的数据权限类型: $type", 403); + } + } + + /** + * 获取同角色的用户ID列表 + * + * @param int $roleId 角色ID + * @return array 用户ID列表 + */ + public static function getSameRoleUserIds(int $roleId): array + { + return Db::table('user_roles') + ->where('role_id', $roleId) + ->pluck('user_id') + ->toArray(); + } + + /** + * 获取角色树下的所有用户ID列表 + * + * @param int $roleId 当前角色ID + * @return array 用户ID列表 + */ + public static function getRoleTreeUserIds(int $roleId): array + { + // 获取角色树中的所有角色ID + $roleIds = self::getRoleTreeIds($roleId); + + // 获取这些角色下的所有用户ID + return Db::table('user_roles') + ->whereIn('role_id', $roleIds) + ->pluck('user_id') + ->toArray(); + } + + /** + * 获取从属关系下的用户ID列表(包括自己和下属) + * + * @param int $userId 当前用户ID + * @return array 用户ID列表 + */ + public static function getRelationUserIds(int $userId): array + { + // 获取所有下属用户ID(递归) + $subordinateIds = self::getSubordinateIds($userId); + + // 返回自己和所有下属的ID + return array_merge([$userId], $subordinateIds); + } + + /** + * 获取直接下属的用户ID列表 + * + * @param int $userId 用户ID + * @return array 直接下属用户ID列表 + */ + public static function getDirectSubordinateIds(int $userId): array + { + return Db::table('user') + ->where('report_to', $userId) + ->pluck('id') + ->toArray(); + } + + /** + * 递归获取所有下属用户ID列表 + * + * @param int $userId 用户ID + * @return array 所有下属用户ID列表 + */ + private static function getSubordinateIds(int $userId): array + { + $subordinates = self::getDirectSubordinateIds($userId); + $allSubordinates = $subordinates; + + foreach ($subordinates as $subordinateId) { + $allSubordinates = array_merge($allSubordinates, self::getSubordinateIds($subordinateId)); + } + + return $allSubordinates; + } + + /** + * 处理个人权限 + */ + private static function handlePersonalPermission( + &$target, + array $newData, + string $userField, + int $currentUserId + ): void { + if (is_array($target)) { + // 创建时强制设置用户ID + $target[$userField] = $currentUserId; + } else { + // 更新时校验原始记录 + if ($target->{$userField} !== $currentUserId) { + throw new BusinessException("无权操作该记录", 403); + } + + // 禁止变更归属字段 + if (isset($newData[$userField]) && $newData[$userField] !== $currentUserId) { + throw new BusinessException("无权变更归属字段", 403); + } + } + } + + /** + * 处理同角色权限 + */ + private static function handleSameRolePermission( + &$target, + array $newData, + string $userField, + string $roleField, + int $currentRoleId + ): void { + // 获取同角色用户ID列表 + $sameRoleUserIds = self::getSameRoleUserIds($currentRoleId); + + if (is_array($target)) { + // 创建时设置归属用户并校验 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $sameRoleUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentRoleId; + } + } else { + // 更新时校验原始记录 + if (!in_array($target->{$userField}, $sameRoleUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 禁止变更归属字段到无权限的用户 + if (isset($newData[$userField]) && !in_array($newData[$userField], $sameRoleUserIds)) { + throw new BusinessException("无权变更归属字段", 403); + } + } + } + + /** + * 处理角色树权限 + */ + private static function handleRoleTreePermission( + &$target, + array $newData, + string $userField, + string $roleField, + int $currentRoleId + ): void { + // 获取角色树用户ID列表 + $roleTreeUserIds = self::getRoleTreeUserIds($currentRoleId); + + if (is_array($target)) { + // 创建时校验用户是否在允许范围内 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $roleTreeUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentRoleId; + } + } else { + // 更新时校验原始记录用户 + if (!in_array($target->{$userField}, $roleTreeUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 校验新用户是否在允许范围内 + if (isset($newData[$userField])) { + if (!in_array($newData[$userField], $roleTreeUserIds)) { + throw new BusinessException("无权变更用户字段", 403); + } + } + } + } + + /** + * 处理从属关系权限 + */ + private static function handleRelationPermission( + &$target, + array $newData, + string $userField, + int $currentUserId + ): void { + // 获取从属关系用户ID列表 + $relationUserIds = self::getRelationUserIds($currentUserId); + + if (is_array($target)) { + // 创建时校验用户是否在允许范围内 + if (isset($target[$userField])) { + if (!in_array($target[$userField], $relationUserIds)) { + throw new BusinessException("无权为指定用户创建数据", 403); + } + } else { + $target[$userField] = $currentUserId; + } + } else { + // 更新时校验原始记录用户 + if (!in_array($target->{$userField}, $relationUserIds)) { + throw new BusinessException("无权操作该记录", 403); + } + + // 校验新用户是否在允许范围内 + if (isset($newData[$userField])) { + if (!in_array($newData[$userField], $relationUserIds)) { + throw new BusinessException("无权变更用户字段", 403); + } + } + } + } + + /** + * 获取角色树ID(当前角色及其所有子角色) + * 从数据库中动态获取角色树关系 + */ + private static function getRoleTreeIds(int $roleId): array + { + static $roleTreeCache = []; + + if (isset($roleTreeCache[$roleId])) { + return $roleTreeCache[$roleId]; + } + + // 从数据库中递归获取角色树 + $ids = [$roleId]; + $queue = [$roleId]; + + while (!empty($queue)) { + $current = array_shift($queue); + // 查找当前角色的所有子角色 + $childRoles = Db::table('roles') + ->where('parent_id', $current) + ->where('status', 1) // 只获取启用的角色 + ->pluck('id') + ->toArray(); + + foreach ($childRoles as $childId) { + if (!in_array($childId, $ids)) { + $ids[] = $childId; + $queue[] = $childId; + } + } + } + + $roleTreeCache[$roleId] = $ids; + return $ids; + } + + /** + * 检查是否为超管 + * + * @param int $userId 用户ID + * @return bool 是否为超管 + */ + public static function isSuperAdmin(int $userId): bool + { + // 检查用户是否关联了超级管理员角色 + // 假设角色ID为1的是超级管理员角色 + $superAdminRoleId = 1; + + return Db::table('user_roles') + ->where('user_id', $userId) + ->where('role_id', $superAdminRoleId) + ->exists(); + } + + /** + * 获取用户的角色ID + * + * @param int $userId 用户ID + * @return int 角色ID + */ + public static function getUserIdRoleId(int $userId): int + { + // 从 user_roles 表中获取当前用户的角色ID + $roleId = Db::table('user_roles') + ->where('user_id', $userId) + ->value('role_id'); + + return $roleId ?: 0; + } + + /** + * 获取用户的所有角色ID(包括继承的角色) + * + * @param int $userId 用户ID + * @return array 角色ID列表 + */ + public static function getUserAllRoleIds(int $userId): array + { + // 获取用户直接关联的角色 + $directRoleIds = Db::table('user_roles') + ->where('user_id', $userId) + ->pluck('role_id') + ->toArray(); + + // 获取所有继承的角色 + $allRoleIds = []; + foreach ($directRoleIds as $roleId) { + $allRoleIds = array_merge($allRoleIds, self::getRoleTreeIds($roleId)); + } + + // 去重并返回 + return array_unique($allRoleIds); + } + + // ================== Data Limit ================== + + /** + * 应用数据权限过滤到查询(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/validate/InquiryLogValidate.php b/app/common/validate/InquiryLogValidate.php index 2a869bb..e4cc026 100644 --- a/app/common/validate/InquiryLogValidate.php +++ b/app/common/validate/InquiryLogValidate.php @@ -67,10 +67,10 @@ class InquiryLogValidate extends Validate // 场景 protected $scene = [ 'store' => [ - 'enter_user_id', 'quality', 'channel', 'email', 'tel', 'whatsapp', 'country', 'contacts', 'content' + 'enter_user_id', 'quality', 'channel', 'email', 'tel', 'whatsapp', 'country', 'contacts', 'content', 'recipient' ], 'update' => [ - 'quality', 'channel', 'email', 'tel', 'whatsapp', 'country', 'contacts', 'content' + 'quality', 'channel', 'email', 'tel', 'whatsapp', 'country', 'contacts', 'content', 'recipient' ], 'release' => [ 'status', 'r_time' diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 6dc874b..77c5622 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -6,6 +6,7 @@ use app\common\model\InquiryLog; use app\common\model\User; use app\common\model\UserRoles; use app\common\model\UserSystems; +use app\common\traits\DataPermission; use app\common\utils\email\MailSend; use app\common\utils\scaffold\Generate; use support\Request; @@ -105,9 +106,8 @@ class IndexController public function test() { - $data = MailSend::getContent(22); - MailSend::send('mzl@hnxinyouhe.com', '【XYH】新的客户线索', replaceTpl($data)); - + $data = DataPermission::getRoleTreeUserIds(1); + var_dump($data); return 22; } } \ No newline at end of file diff --git a/app/crm/controller/CustomerController.php b/app/crm/controller/CustomerController.php index 4eb3a6b..2efbceb 100644 --- a/app/crm/controller/CustomerController.php +++ b/app/crm/controller/CustomerController.php @@ -3,6 +3,7 @@ namespace app\crm\controller; use app\common\model\Corp; +use app\common\model\InquiryLog; use app\common\model\User; use app\crm\model\Channel; use app\crm\model\Country; @@ -12,6 +13,7 @@ use app\crm\model\Follow; use app\crm\service\CustomerService; use app\crm\utils\Util; use config\constants\Constants; +use support\Db; use support\Request; class CustomerController extends BaseController @@ -162,15 +164,12 @@ class CustomerController extends BaseController try { $param = $request->all(); $param['user_id'] = $request->user->uid; - if($param['user_id']==78){ - $param['user_id'] = 101; - } $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()); + return $this->error($e->getMessage()); } } @@ -198,6 +197,29 @@ class CustomerController extends BaseController } } + /** + * 无用线索举报 + * @param Request $request + * @return \support\Response + */ + public function report(Request $request) + { + $param = $request->all(); + if (Util::isEmpty($param['id'])) { + return $this->error(trans('id_empty')); + } + + $param['user_id'] = $request->user->uid; + $param['corp_abbr'] = $request->basic_data['corp_abbr']; + $res = (new CustomerService())->report($param); + + if($res){ + return $this->success($res, trans('success')); + }else{ + return $this->error(trans('fail')); + } + } + /** * 客户详情 @@ -341,8 +363,10 @@ class CustomerController extends BaseController return $this->error( trans('fail')); } if($force == 'force'){ + //强制删除 $res = $customer->forceDelete(); }else{ + //软删除 $res = $customer->delete(); } if ($res) { @@ -432,22 +456,38 @@ class CustomerController extends BaseController $data = [ 'user_id' => $request->user->uid ]; - $customer= Customer::withTrashed()->find($customerId); - if(empty($customer) || $customer->user_id != 0){ - return $this->error( trans('claim_fail')); + + try { + Db::beginTransaction(); + $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 (!empty($customer->sn) && $customer->type==1){ + $inquiryLog = InquiryLog::where('order_no', $customer->sn)->first(); + if (!empty($inquiryLog)) { + $inquiryLog->recipient = $userId; + $inquiryLog->save(); + } + } + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + return $this->error($e->getMessage()); } - $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 { @@ -484,17 +524,44 @@ class CustomerController extends BaseController }else{ $query= Customer::whereIn('id',explode(',',$customerId)); } - $customer = $query->get(); +// $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]); - foreach ($customer as $item) { - //记录客户关系变化 - $his = CustomerHis::create([ - 'customer_id' => $item->id, - 'user_his' => $item->user_id, - 'user_id' => $user['id'], - ]); + try { + $customer = $query->get(); + + foreach ($customer as $item) { + //记录客户关系变化 + $his = CustomerHis::create([ + 'customer_id' => $item->id, + 'user_his' => $item->user_id, + 'user_id' => $user['id'], + ]); + if (!empty($customer->sn) && $customer->type==1){ + $inquiryLog = InquiryLog::where('order_no', $customer->sn)->first(); + if (!empty($inquiryLog)) { + $inquiryLog->recipient = $userId; + $inquiryLog->save(); + } + } + } + $res = $query->update(['user_id' => $user->id]); + + Db::commit(); + } catch (\Throwable $e) { + Db::rollback(); + return $this->error($e->getMessage()); } - $res = $query->update(['user_id' => $user->id]); + if ($res) { diff --git a/app/crm/service/CustomerService.php b/app/crm/service/CustomerService.php index 631da7e..608ec04 100644 --- a/app/crm/service/CustomerService.php +++ b/app/crm/service/CustomerService.php @@ -5,10 +5,10 @@ namespace app\crm\service; use app\common\model\Corp; use app\common\model\User; use app\crm\logic\CustomerLogic; -use app\crm\model\CarType; use app\crm\model\Channel; use app\crm\model\Country; use app\crm\model\Customer; +use app\crm\model\CustomerHis; use app\crm\model\Follow; use app\crm\utils\Util; use config\constants\Constants; @@ -37,6 +37,7 @@ class CustomerService }else{ $where[] = ['user_id', $user_id]; } + $where[] = ['status', 1]; $grade = []; @@ -111,6 +112,35 @@ class CustomerService return $customer; } + public function report($params) + { + $id = $params['id']; + $userId = $params['user_id']; + + $where = [ + 'id' => $id, + 'user_id' => $userId, + ]; + $customer = Customer::where($where)->withTrashed()->first(); + if(empty($customer)){ + return false; + } + + CustomerHis::create([ + 'customer_id' => $id, + 'user_his' => $userId, + 'user_id' => 0, + ]); + + $customer->user_id = 0; + $customer->status = 2; + $res = $customer->save(); + + //软删除 + $customer->delete(); + return $res; + } + /** * @param $customer diff --git a/app/crmAdmin/controller/BaseController.php b/app/crmAdmin/controller/BaseController.php new file mode 100644 index 0000000..1c066b5 --- /dev/null +++ b/app/crmAdmin/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/crmAdmin/controller/CustomerController.php b/app/crmAdmin/controller/CustomerController.php new file mode 100644 index 0000000..153941a --- /dev/null +++ b/app/crmAdmin/controller/CustomerController.php @@ -0,0 +1,157 @@ +all(); + $corp = Corp::where(['abbr' => $request->basic_data['corp_abbr']])->first(); + $param['corp_abbr'] = $corp->id; +// $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().$e->getTraceAsString()); + } + } + + /** + * 客户导出 + * @param Request $request + * @return \support\Response + */ + public function export(Request $request) + { + try { + $param = $request->all(); +// $corp = Corp::where(['abbr' => $request->basic_data['corp_abbr']])->first(); +// $param['corp_abbr'] = $corp->id; + + $result = (new CustomerService())->export($param); + $excelContent = file_get_contents($result['filepath']); + // 生成文件名 + $filename = '客户数据_' . date('Y-m-d_H-i-s') . '.xlsx'; + return response() + ->withHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + ->withHeader('Content-Disposition', 'attachment; filename="'.$filename.'"') + ->withBody($excelContent); + + /*$result = (new CustomerService())->export($param); + // 检查文件是否存在 + if (!file_exists($result['filepath'])) { + return $this->error('导出文件生成失败'); + } + + // 获取文件信息 + $fileSize = filesize($result['filepath']); + $filename = $result['filename']; + + // 设置响应头 + $response = response()->file($result['filepath']); + $response->header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + $response->header('Content-Disposition', 'attachment; filename="' . $filename . '"'); + $response->header('Content-Length', $fileSize); + $response->header('Cache-Control', 'no-cache, must-revalidate'); + $response->header('Pragma', 'no-cache'); + + return $response;*/ + + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 客户列表 + * @param Request $request + * @return \support\Response + */ + public function audit(Request $request) + { + try { + $param = $request->all(); + if (empty($param['id'])) { + return $this->error('ID'.trans('not_null')); + } + + if (!in_array($param['scene'],['delete','report'])) { + return $this->error('scene 不合法'); + } + $result = (new CustomerService())->audit($param); + + return $this->success($result); + + } catch (\Throwable $e) { + return $this->error($e->getMessage().$e->getFile().$e->getLine().$e->getTraceAsString()); + } + } + + + /** + * 批量转让 + * @param Request $request + * @return \support\Response + */ + public function transfer(Request $request) + { + try { + $param = $request->all(); + if (empty($param['customer_id'])) { + return $this->error('customer_id'.trans('not_null')); + } + if (empty($param['user_id'])) { + return $this->error('user_id'.trans('not_null')); + } + + $result = (new CustomerService())->transfer($param); + + return $this->success($result); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + + /** + * 客户详情 + * @param Request $request + * @return \support\Response + */ + public function detail(Request $request) + { + try { + $param = $request->all(); + if (empty($param['id'])) { + return $this->error('id'.trans('not_null')); + } + + $result = (new CustomerService())->detail($param); + + return $this->success($result); + } catch (\Throwable $th) { + return $this->error($th->getMessage()); + } + } + +} \ No newline at end of file diff --git a/app/crmAdmin/logic/CustomerLogic.php b/app/crmAdmin/logic/CustomerLogic.php new file mode 100644 index 0000000..869982f --- /dev/null +++ b/app/crmAdmin/logic/CustomerLogic.php @@ -0,0 +1,26 @@ +find($id); + $customer->status = $status; + $res = $customer->save(); + + return $res; + } + + public function forceDelete(int $id) + { + $customer = Customer::withTrashed()->find($id); + //强制删除 + $res = $customer->forceDelete(); + return $res; + } +} \ No newline at end of file diff --git a/app/crmAdmin/service/CustomerService.php b/app/crmAdmin/service/CustomerService.php new file mode 100644 index 0000000..79c9193 --- /dev/null +++ b/app/crmAdmin/service/CustomerService.php @@ -0,0 +1,397 @@ +search($params); + return $customer; + } + + /** + * 查询列表数据 + * @param $params + * @param $export + * @return Customer[]|\Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Collection[]|\Illuminate\Pagination\LengthAwarePaginator + */ + public function search($params, $export = false) + { + +// $page = $request->input('page', 1); +// $perPage = $request->input('per_page', 10); + $perPage = $params['size'] ?? 10; + $page = $params['current'] ?? 1; + $keyword = $params['keyword'] ?? ''; + $recycle = $params['recycle'] ?? ''; + $follow = $params['follow'] ?? ''; + $follow_last = $params['follow_last'] ?? ''; + $tagId = $params['tag_id'] ?? []; + $user_id = $params['user_id'] ?? ''; + $seas = $params['seas'] ?? ''; + $scene = $params['scene'] ?? ''; + $where = []; + + if ($scene == 'audit') { + $where[] = ['status', '=', 2]; + } else { + $where[] = ['status', '=', 1]; + } + if ($seas == 'seas' || $scene == 'seas') { + $where[] = ['user_id', 0]; + } +// if($recycle == 'recycle'){ +// $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($user_id)) { + $query->whereIn('user_id', explode(',', $user_id)); + } elseif ($user_id === 0 && $export) { + $query->where('user_id', 0); + } + + if (!empty($tagId)) { + //查询有$tagId 里任意一个标签的客户 + //$query->whereHas('crm_tag', 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' || $scene == 'recycle' || $seas == 'seas' || $scene == 'seas' || $scene == 'audit') { + $query->onlyTrashed(); + } + + if (!$export) { + $customer = $query->orderBy('id', 'desc')->paginate($perPage, ['*'], 'page', $page); + } else { + $customer = $query->orderBy('id', 'desc')->get(); + return $customer; + } + if (!$export) { + foreach ($customer as &$item) { + $item = $this->getCustomer($item); + if ($seas == 'seas' || $scene == 'seas') { + $item['seas_time'] = time() - strtotime($item['deleted_at']); + } + if ($scene == 'audit') { //审核时查询举报人姓名 + $customerHis = CustomerHis::where('customer_id', $item->id)->orderBy('id', 'desc')->first(['user_his']); + var_dump($customerHis['user_his']); + $item['user_name'] = empty($customerHis['user_his']) ? '' : User::where('id',$customerHis['user_his'])->value('name'); + } + } + } + unset($item); + + return $customer; + } + + public function export($params) + { + if(empty($param['user_id']) && empty($param['tag_id'])){ + $params['user_id'] = 0; + } + $customers = $this->search($params, true); + + // 创建新的Spreadsheet对象 + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // 设置表头 + $headers = [ + 'A1' => '客户ID', + 'B1' => '联系人', + 'C1' => '电话', + 'D1' => '邮箱', + 'E1' => 'WhatsApp', + 'F1' => '性别', + 'G1' => '国家/地区', + 'H1' => '渠道来源', + 'I1' => '描述', + 'J1' => '所属人', + 'K1' => '录入人', + ]; + + // 设置表头样式 + foreach ($headers as $cell => $value) { + $sheet->setCellValue($cell, $value); + } + + // 设置表头样式 + $headerStyle = [ + 'font' => [ + 'bold' => true, + 'color' => ['rgb' => 'FFFFFF'], + ], + 'fill' => [ + 'fillType' => Fill::FILL_SOLID, + 'startColor' => ['rgb' => '4472C4'], + ], + 'alignment' => [ + 'horizontal' => Alignment::HORIZONTAL_CENTER, + 'vertical' => Alignment::VERTICAL_CENTER, + ], + 'borders' => [ + 'allBorders' => [ + 'borderStyle' => Border::BORDER_THIN, + 'color' => ['rgb' => '000000'], + ], + ], + ]; + + $sheet->getStyle('A1:K1')->applyFromArray($headerStyle); + + // 填充数据 + $row = 2; + foreach ($customers as $customer) { + // 获取完整的客户信息 + $customerData = $this->getCustomer($customer); + + // 填充数据行 + $sheet->setCellValue('A' . $row, $customerData['id']); + $sheet->setCellValue('B' . $row, $customerData['contacts']); + $sheet->setCellValue('C' . $row, $customerData['phone']); + $sheet->setCellValue('D' . $row, $customerData['email']); + $sheet->setCellValue('E' . $row, $customerData['WhatsApp']); + $sheet->setCellValue('F' . $row, $customerData['sex_txt']); + $sheet->setCellValue('G' . $row, $customerData['country_name']); + $sheet->setCellValue('H' . $row, $customerData['channel_name']); + $sheet->setCellValue('I' . $row, $customerData['describe']); + $sheet->setCellValue('J' . $row, $customerData['user_name']); // 所属人姓名 + $sheet->setCellValue('K' . $row, $customerData['enter_user_name']); + + $row++; + } + + // 设置数据行样式 + $dataStyle = [ + 'borders' => [ + 'allBorders' => [ + 'borderStyle' => Border::BORDER_THIN, + 'color' => ['rgb' => '000000'], + ], + ], + 'alignment' => [ + 'vertical' => Alignment::VERTICAL_CENTER, + ], + ]; + + $lastRow = $row - 1; + if ($lastRow >= 2) { + $sheet->getStyle('A2:K' . $lastRow)->applyFromArray($dataStyle); + } + + // 自动调整列宽 + foreach (range('A', 'K') as $column) { + $sheet->getColumnDimension($column)->setAutoSize(true); + } + + // 设置行高 + $sheet->getRowDimension(1)->setRowHeight(25); + + // 生成文件名 + $filename = '客户数据_' . date('Y-m-d_H-i-s') . '.xlsx'; + + // 创建输出目录 + $exportDir = runtime_path() . '/export/'; + if (!is_dir($exportDir)) { + mkdir($exportDir, 0755, true); + } + + $filepath = $exportDir . $filename; + + // 创建Excel写入器 + $writer = new Xlsx($spreadsheet); + $writer->save($filepath); +// $writer->save('php://output'); +// $excelContent = ob_get_clean(); +// return $excelContent; + +// 返回文件信息 + return [ + 'filename' => $filename, + 'filepath' => $filepath, + 'size' => filesize($filepath), + 'count' => count($customers) + ]; + } + + /** + * @param $params + * @return bool + */ + public function audit($params) + { + + $id = $params['id']; + + $logic = new CustomerLogic(); + if ($params['scene'] == 'delete') { + return $logic->forceDelete($params['id']); + } else { + return $logic->status($params['id'], 1); + } + } + + /** + * @param $params + * @return bool + */ + public function transfer($params) + { + try { + Db::beginTransaction(); + $customerId = $params['customer_id']; + $userId = $params['user_id']; + + $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' => $userId, + ]); + if (!empty($item->sn) && $item->type==1){ + $inquiryLog = InquiryLog::where('order_no', $item->sn)->first(); + if (!empty($inquiryLog)) { + $inquiryLog->recipient = $userId; + $inquiryLog->save(); + } + } + } + $res = $query->update(['user_id' => $userId]); + + Db::commit(); + return $res; + } catch (\Throwable $e) { + Db::rollback(); + throw $e; + } + } + + /** + * @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'] = ($customer['magnitude']==0)?1:$customer['magnitude']; + $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]; + } + } + + + $customer['enter_user_name'] = empty($customer['enter_user_id']) ? '' : User::where('id',$customer['enter_user_id'])->value('name'); + $customer['user_name'] = empty($customer['user_id']) ? '公海' : User::where('id',$customer['user_id'])->value('name'); + + return $customer; + } + + + public function detail($params) + { + $id = $params['id']; + $customer = Customer::with('tags') + ->where('id', $id) + ->first(); + $customer = $this->getCustomer($customer); + + return $customer; + } +} \ No newline at end of file diff --git a/app/functions.php b/app/functions.php index f69f082..59fdd24 100644 --- a/app/functions.php +++ b/app/functions.php @@ -220,4 +220,52 @@ if (!function_exists('removeParentheses')) { return trim(preg_replace($pattern, '', $str, 1)); } +} + +/** + * 规范hmtl富文本 + */ +if (!function_exists('normalizeRichText')) { + function normalizeRichText(string $html): string + { + $dom = new DOMDocument(); + + // 处理UTF-8编码和HTML5标签 + @$dom->loadHTML('' . $html, + LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + + // 设置编码并规范化文档 + $dom->encoding = 'UTF-8'; + $dom->normalizeDocument(); + + // 统一处理空白字符 + $xpath = new DOMXPath($dom); + foreach ($xpath->query('//text()') as $node) { + $normalized = preg_replace('/\s+/u', ' ', trim($node->nodeValue)); + $node->nodeValue = $normalized; + } + + // 标准化属性顺序(可选) + foreach ($xpath->query('//*[@*]') as $element) { + $attributes = $element->attributes; + $sorted = []; + foreach ($attributes as $attr) { + $sorted[$attr->nodeName] = $attr->nodeValue; + } + ksort($sorted); + + // 移除所有属性后按排序后顺序重新添加 + while ($attributes->length) { + $element->removeAttributeNode($attributes->item(0)); + } + foreach ($sorted as $name => $value) { + $element->setAttribute($name, $value); + } + } + + // 返回规范化后的HTML + $result = trim($dom->saveHTML()); + // 替换所有空白实体为普通空格 + return preg_replace('/\s+/u', ' ', $result); + } } \ No newline at end of file diff --git a/app/inquiry/controller/InquiryLogController.php b/app/inquiry/controller/InquiryLogController.php index 89af6b0..833be58 100644 --- a/app/inquiry/controller/InquiryLogController.php +++ b/app/inquiry/controller/InquiryLogController.php @@ -25,11 +25,11 @@ class InquiryLogController extends BaseController */ 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; - } + // $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']); @@ -46,7 +46,7 @@ class InquiryLogController extends BaseController if (isset($data['t_time'])) { $data['t_time'] = $data['t_time'] > 0 ? date('Y-m-d H:i:s', $data['t_time']) : 0; } - // var_dump($data); + return $data; } @@ -84,6 +84,23 @@ class InquiryLogController extends BaseController } } + /** + * 重新分配 + * + * @param Request $request + * @return mixed + */ + public function reAllocation(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'allocation'); + $res = $this->service->serviceReAllocation($request, $data); + return $this->success([], '重新分配成功'); + } catch (\Throwable $e) { + return $this->error($e->getMessage()); + } + } + // 导出 // public function export(Request $request) // { diff --git a/app/inquiry/service/InquiryLogService.php b/app/inquiry/service/InquiryLogService.php index 667e41d..10367db 100644 --- a/app/inquiry/service/InquiryLogService.php +++ b/app/inquiry/service/InquiryLogService.php @@ -32,28 +32,7 @@ class InquiryLogService return new $this->validator; } - /** - * 发布 - */ - public function serviceRelease(Request $request, array $data = []) - { - $data = $this->preProcess('Release', $data, $request); - $config = $this->buildQueryConfig($request, 'release'); - $updated = $this->logicUpdateBy($config, $data); - return $this->postProcess('Release', $data); - } - - /** - * 分配 - */ - public function serviceAllocation(Request $request, array $data = []) - { - $data = $this->preProcess('Allocation', $data, $request); - $config = $this->buildQueryConfig($request, 'allocation'); - $updated = $this->logicUpdateBy($config, $data); - return $this->postProcess('Allocation', $data); - } - + /** * 构建查询条件 */ @@ -108,6 +87,7 @@ class InquiryLogService case 'destroy': case 'release': case 'allocation': + case 'reAllocation': $config['where']['id'] = $p['id']; case 'store': break; @@ -116,6 +96,40 @@ class InquiryLogService return $config; } + /** + * 发布 + */ + 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); + } + + /** + * 重新分配 + */ + public function serviceReAllocation(Request $request, array $data = []) + { + $data = $this->preProcess('ReAllocation', $data, $request); + $config = $this->buildQueryConfig($request, 'reAllocation'); + $updated = $this->logicUpdateBy($config, $data); + return $this->postProcess('ReAllocation', $data); + } + + // ====== 前置处理 ====== /** * 添加前置处理 */ @@ -123,7 +137,7 @@ class InquiryLogService { $data['order_no'] = orderNum(); $data['enter_user_id'] = $request->user->uid; - $data['recipient'] = 0; + // $data['recipient'] = 0; $data['email'] = trim($data["email"]); $data['tel'] = trim($data['tel']); $data['whatsapp'] = trim($data['whatsapp']); @@ -143,6 +157,50 @@ class InquiryLogService $data['content'] ); + // 如果添加时填写了接单人 + if (isset($data['recipient']) && $data['recipient']) { + $data['r_time'] = time(); + $data['t_time'] = time(); + $data['status'] = 2; + } + return $data; + } + + /** + * 更新前置处理 + */ + public function beforeServiceUpdate($data, Request $request) + { + $info = $this->findByPk($request->post('id')); + if (!$info) { + throw new BusinessException('数据不存在'); + } + + // 处理富文本 + if (md5(normalizeRichText($data['content'])) !== md5(normalizeRichText($info->content))) { + $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'] + ); + } + + // 如果更新时操作了跟单人 + if ($info->recipient) { + $info->recipient == $data['recipient'] ?: $data['t_time'] = time(); + } else { + $data['r_time'] = time(); + $data['t_time'] = time(); + $data['status'] = 2; + } return $data; } @@ -183,6 +241,20 @@ class InquiryLogService return $data; } + /** + * 重新分配前置处理 + */ + public function beforeServiceReAllocation($data, Request $request) + { + $info = $this->findByPk($request->post('id')); + if (!$info) { + throw new BusinessException('数据不存在'); + } + $data['t_time'] = time(); + return $data; + } + + // ====== 后置处理 ====== /** * 发布后置处理 */ @@ -201,6 +273,34 @@ class InquiryLogService return $res; } + /** + * 添加后置处理 + */ + protected function afterServiceStore($res) + { + RedisQueueRedis::send('inquiry-to-crm', $res); + return $res; + } + + /** + * 更新后置处理 + */ + protected function afterServiceUpdate($res) + { + if ($res['recipient']) { + RedisQueueRedis::send('inquiry-to-crm-update', $res); + } + return $res; + } + + /** + * 重新分配后置处理 + */ + protected function afterServiceReAllocation($res) + { + RedisQueueRedis::send('inquiry-to-crm-update', $res); + return $res; + } /** * 获取询单ById diff --git a/app/process/Emails.php b/app/process/Emails.php index f9641ea..c37d4e2 100644 --- a/app/process/Emails.php +++ b/app/process/Emails.php @@ -28,29 +28,31 @@ class Emails // $email->process(); // }); - // 拉取新邮件 - 每3小时的第0分钟0秒执行 - new Crontab('0 0 */3 * * *', function(){ - $email = new EmailsUtils(); - $email->uids(); - }); + if (getenv('ENVIRONMENT') === 'master') { + // 拉取新邮件 - 每3小时的第0分钟0秒执行 + new Crontab('0 0 */3 * * *', function(){ + $email = new EmailsUtils(); + $email->uids(); + }); - // 邮件详情 - 每3小时的第10分钟0秒执行 - new Crontab('0 10 */3 * * *', function(){ - $email = new EmailsUtils(); - $email->details(); - }); + // 邮件详情 - 每3小时的第10分钟0秒执行 + new Crontab('0 10 */3 * * *', function(){ + $email = new EmailsUtils(); + $email->details(); + }); - // 邮件处理 - 每3小时的第20分钟0秒执行 - new Crontab('0 20 */3 * * *', function(){ - $email = new EmailsUtils(); - $email->process(); - }); + // 邮件处理 - 每3小时的第20分钟0秒执行 + new Crontab('0 20 */3 * * *', function(){ + $email = new EmailsUtils(); + $email->process(); + }); - // 邮件分派 - 每3小时的第30分钟0秒执行 - new Crontab('0 30 */3 * * *', function(){ - $email = new EmailsUtils(); - $email->fenpai(); - }); + // 邮件分派 - 每3小时的第30分钟0秒执行 + new Crontab('0 30 */3 * * *', function(){ + $email = new EmailsUtils(); + $email->fenpai(); + }); + } } } \ No newline at end of file diff --git a/app/queue/redis/InquiryToCrmUpdate.php b/app/queue/redis/InquiryToCrmUpdate.php new file mode 100644 index 0000000..c4e6d02 --- /dev/null +++ b/app/queue/redis/InquiryToCrmUpdate.php @@ -0,0 +1,110 @@ +where('id', $data['id'])->first(); + + $corpId = 0; + if ($info->recipient) { + $uinfo = User::where('id', $info->recipient)->where('status', 1)->first(); + if ($uinfo) { + $corpId = $uinfo->corp_id; + } + } + + // 获取客户信息 + $crmCustomerInfo = CrmCustomer::where('sn', $info->order_no)->first(); + if ($crmCustomerInfo) { + $udata = [ + 'corp_abbr' => $corpId, + 'user_id' => $info->recipient, + 'channel_id' => $info->channel, + 'country_id' => $info->country, + 'magnitude' => $info->quality, + 'email' => $info->email, + 'phone' => $info->tel, + 'WhatsApp' => $info->whatsapp, + 'contacts' => $info->contacts, + 'desc_collect' => $info->content, + ]; + $crmCustomerInfo->update($udata); + } else { + // 同步客户数据 + $crmCustomer = new CrmCustomer(); + $crmCustomer->sn = $info->order_no; + $crmCustomer->corp_abbr = $corpId; + $crmCustomer->user_id = $info->recipient; + $crmCustomer->type = 1; + $crmCustomer->channel_id = $info->channel; + $crmCustomer->country_id = $info->country; + $crmCustomer->magnitude = $info->quality; + $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('同步失败~'); + } + + // 发送邮件消息 + if ($uinfo && $uinfo->email) { + $options = [ + 'id' => $info->id, + 'email' => $uinfo->email + ]; + \Webman\RedisQueue\Redis::send('email-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/config/route.php b/config/route.php index 5023f1f..7ad0f19 100644 --- a/config/route.php +++ b/config/route.php @@ -71,6 +71,14 @@ Route::group('/admin', function () { Route::any('/update', [app\admin\controller\RoleController::class, 'update']); Route::any('/destroy', [app\admin\controller\RoleController::class, 'destroy']); }); + + // 用户 + Route::group('/user', function () { + Route::any('/list', [app\admin\controller\UserController::class, 'index']); + Route::any('/show', [app\admin\controller\UserController::class, 'show']); + Route::any('/update', [app\admin\controller\UserController::class, 'update']); + }); + // 公司 Route::group('/corp', function () { Route::any('/list', [app\admin\controller\CorpController::class, 'index']); @@ -89,6 +97,7 @@ Route::group('/admin', function () { 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']); + Route::any('/reallocation', [app\inquiry\controller\InquiryLogController::class, 'reAllocation']); // 统计 Route::group('/statistics', function () { Route::any('/whole', [app\inquiry\controller\StatisticsController::class, 'whole']); @@ -99,6 +108,17 @@ Route::group('/admin', function () { Route::any('/inquiry/count', [app\inquiry\controller\StatisticsController::class, 'inquiryCount']); }); }); + + // crm + Route::group('/crm', function () { + Route::group('/customer', function () { + Route::any('/list', [app\crmAdmin\controller\CustomerController::class, 'list']); + Route::any('/export', [app\crmAdmin\controller\CustomerController::class, 'export']); + Route::any('/audit', [app\crmAdmin\controller\CustomerController::class, 'audit']); + Route::any('/transfer', [app\crmAdmin\controller\CustomerController::class, 'transfer']); + Route::any('/detail', [app\crmAdmin\controller\CustomerController::class, 'detail']); + }); + }); }); @@ -148,6 +168,7 @@ Route::group('/crm', function () { 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('/customer/report', [app\crm\controller\CustomerController::class, 'report']); Route::any('/tagGroup/list', [app\crm\controller\TagGroupController::class, 'list']); Route::any('/tagGroup/updatePivot', [app\crm\controller\TagGroupController::class, 'updatePivot']);