From 228408149b1456886e5b2bc82e460e8642d44469 Mon Sep 17 00:00:00 2001 From: mzl Date: Fri, 18 Jul 2025 17:32:42 +0800 Subject: [PATCH 1/3] crud --- app/admin/controller/CorpController.php | 40 ++ app/admin/logic/CorpLogic.php | 17 + app/admin/service/CorpService.php | 122 ++++ app/common/controller/AuthController.php | 2 +- app/common/controller/BaseController.php | 44 ++ app/common/logic/AuthLogic_bak.php | 593 ------------------ app/common/logic/BaseLogic.php | 86 --- app/common/logic/CrudLogic_bak.php | 106 ++++ app/common/model/UserFeishuApp_bak.php | 69 -- app/common/service/AuthService_bak.php | 155 ----- app/common/service/CrudService.php | 143 +++++ app/common/traits/DataTransformer.php | 144 +++-- app/common/traits/FormatKvTrait.php | 154 +++++ .../traits/crud/CrudControllerTrait.php | 126 ++++ app/common/traits/crud/CrudLogicTrait.php | 96 +++ app/common/traits/crud/CrudServiceTrait.php | 209 ++++++ .../traits/crud/CrudServiceTrait_bak2.php | 270 ++++++++ app/common/utils/CrudUtil.php | 406 ++++++++++++ app/controller/IndexController.php | 3 + app/middleware/AuthCheck.php | 62 +- app/middleware/PermissionMiddleware.php | 33 - config/middleware.php | 1 - config/route.php | 7 + 23 files changed, 1880 insertions(+), 1008 deletions(-) create mode 100644 app/admin/controller/CorpController.php create mode 100644 app/admin/logic/CorpLogic.php create mode 100644 app/admin/service/CorpService.php delete mode 100644 app/common/logic/AuthLogic_bak.php delete mode 100644 app/common/logic/BaseLogic.php create mode 100644 app/common/logic/CrudLogic_bak.php delete mode 100644 app/common/model/UserFeishuApp_bak.php delete mode 100644 app/common/service/AuthService_bak.php create mode 100644 app/common/service/CrudService.php create mode 100644 app/common/traits/FormatKvTrait.php create mode 100644 app/common/traits/crud/CrudControllerTrait.php create mode 100644 app/common/traits/crud/CrudLogicTrait.php create mode 100644 app/common/traits/crud/CrudServiceTrait.php create mode 100644 app/common/traits/crud/CrudServiceTrait_bak2.php create mode 100644 app/common/utils/CrudUtil.php delete mode 100644 app/middleware/PermissionMiddleware.php 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/logic/CorpLogic.php b/app/admin/logic/CorpLogic.php new file mode 100644 index 0000000..99fa0ee --- /dev/null +++ b/app/admin/logic/CorpLogic.php @@ -0,0 +1,17 @@ +logicComplex(function($q) use ($params) { + // // 自定义 join、where、select + // return $q->where('status', 1) + // ->orderBy('created_at','desc') + // ->paginate($params['limit'] ?? 15); + // })->toArray(); + // } + + // /** + // * 复杂查询示例: + // * 只要在 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; + // } + + // protected function afterServiceShow($item) + // { + // // 对详情对象处理 + // $item['extra']=123; + // return $item; + // } + + // afterServiceStore/Update/Destroy 同理 + + + // protected function afterServiceList(mixed $result): array + // { + // if ($result['records']) { + // $result['records'] = array_map(fn($i) => [...$i], $result['records']); + // } + + // return $result; + // } +} \ 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/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/CrudLogic_bak.php b/app/common/logic/CrudLogic_bak.php new file mode 100644 index 0000000..91a4c16 --- /dev/null +++ b/app/common/logic/CrudLogic_bak.php @@ -0,0 +1,106 @@ +modelClass = $modelClass; + } + + /** + * 列表(带分页) + * @param array $config CrudUtil::buildQuery 接受的配置 + * @param int $limit + * @param int $page + */ + public function list(array $config, int $limit, int $page) + { + $config['model'] = $this->modelClass; + $query = CrudUtil::buildQuery($config); + return CrudUtil::paginate($query, $limit, $page); + } + + /** + * 列表(不分页) + */ + public function listAll(array $config) + { + $config['model'] = $this->modelClass; + $query = CrudUtil::buildQuery($config); + return CrudUtil::getAll($query); + } + + /** + * 根据主键查询单条 + */ + public function show($id, array $with = []) + { + return CrudUtil::findByPk($this->modelClass, $id, $with); + } + + /** + * 根据任意条件查询单条(可带 dataLimit) + */ + public function showBy(array $where, array $with = [], array $dataLimit = []) + { + return CrudUtil::findOneBy($this->modelClass, $where, $with, $dataLimit); + } + + /** + * 创建(可带 dataLimit) + */ + public function create(array $data, array $dataLimit = []) + { + return CrudUtil::create($this->modelClass, $data, $dataLimit); + } + + /** + * 主键更新(可带 dataLimit) + */ + public function updateByPk($id, array $data, array $dataLimit = []) + { + return CrudUtil::updateByPk($this->modelClass, $id, $data, $dataLimit); + } + + /** + * 任意条件更新(可带 dataLimit) + */ + public function updateBy(array $where, array $data, array $dataLimit = []) + { + return CrudUtil::updateBy($this->modelClass, $where, $data, $dataLimit); + } + + /** + * 主键删除 + */ + public function deleteByPk(array $ids) + { + return CrudUtil::deleteByPk($this->modelClass, $ids); + } + + /** + * 任意条件删除(可带 dataLimit) + */ + public function deleteBy(array $where, array $dataLimit = []) + { + return CrudUtil::deleteBy($this->modelClass, $where, $dataLimit); + } + + /** + * 复杂查询回调 + * @param callable $cb function(Builder $q){ ... } + */ + public function complex(callable $cb) + { + return CrudUtil::executeComplex($this->modelClass, $cb); + } +} \ 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 @@ -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/CrudService.php b/app/common/service/CrudService.php new file mode 100644 index 0000000..8d66a37 --- /dev/null +++ b/app/common/service/CrudService.php @@ -0,0 +1,143 @@ +logic = $logic; + } + + /** + * 列表接口 + * 支持前端直接传递: + * - filter_* → where + * - sort_by, order + * - fields (逗号分隔) + * - with (逗号分隔) + * - withFields (relation:field,field;...) + * - dataLimitType, dataLimitField + * - limit, page + * - complex_query=1 则调用 complexQuery() + */ + public function list(Request $request): array + { + $p = $request->get(); + + // 复杂查询开关 + if (!empty($p['complex_query']) && method_exists($this, 'complexQuery')) { + return $this->complexQuery($p); + } + + // 1. 构造 where + $where = []; + foreach ($p as $k => $v) { + if (strpos($k, 'filter_') === 0 && $v !== '') { + $col = substr($k, 7); + // 简单等值或 like + $where[$col] = strpos($v, '%') !== false ? ['like', $v] : $v; + } + } + + // 2. 构造其他配置 + $config = [ + 'where' => $where, + 'order' => [($p['sort_by'] ?? 'id') => ($p['order'] ?? 'desc')], + 'fields' => !empty($p['fields']) ? explode(',', $p['fields']) : [], + 'with' => !empty($p['with']) ? explode(',', $p['with']) : [], + 'withFields' => $this->parseWithFields($p['withFields'] ?? ''), + 'dataLimit' => !empty($p['dataLimitType']) ? [ + 'type' => $p['dataLimitType'], + 'field' => $p['dataLimitField'] ?? 'admin_id' + ] : [], + ]; + + $limit = (int)($p['limit'] ?? 15); + $page = (int)($p['page'] ?? 1); + + $pager = $this->logic->list($config, $limit, $page); + return ['total' => $pager->total(), 'data' => $pager->items()]; + } + + /** + * 详情 + */ + public function show($id) + { + return $this->logic->show($id, []); + } + + /** + * 创建 + */ + public function store(Request $request) + { + $data = $request->post(); + $dataLimit = $this->getDataLimitFromRequest($request); + return $this->logic->create($data, $dataLimit); + } + + /** + * 更新 + */ + public function update(Request $request) + { + $key = $this->logic->modelClass::make()->getKeyName(); + $id = (int)$request->post($key); + $data= $request->post(); + $dataLimit = $this->getDataLimitFromRequest($request); + return $this->logic->updateByPk($id, $data, $dataLimit); + } + + /** + * 删除 + */ + public function destroy(Request $request) + { + $key = $this->logic->modelClass::make()->getKeyName(); + $ids = (array)$request->post($key, []); + return $this->logic->deleteByPk($ids); + } + + /** ---------- helpers ---------- */ + + /** + * 解析 withFields 参数 "rel:field,field;rel2:..." + */ + 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/Field + */ + protected function getDataLimitFromRequest(Request $req): array + { + $type = $req->get('dataLimitType'); + if (!$type) return []; + return [ + 'type' => $type, + 'field' => $req->get('dataLimitField', 'admin_id'), + ]; + } + + /** + * 子类可重写,做复杂联表查询等 + */ + protected function complexQuery(array $params): array + { + throw new \LogicException('请在子类实现 complexQuery 方法'); + } +} \ 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/crud/CrudControllerTrait.php b/app/common/traits/crud/CrudControllerTrait.php new file mode 100644 index 0000000..0fd4a0d --- /dev/null +++ b/app/common/traits/crud/CrudControllerTrait.php @@ -0,0 +1,126 @@ +service->serviceList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } + + /** + * 获取指定资源详情(GET /resource/{id}) + * + * @param Request $request 请求对象 + * @param int|string $id 资源 ID + * @return mixed 返回资源详情数据 + */ + public function show(Request $request) + { + $raw = $this->service->serviceShow($request); + $data = $this->transformData($raw, method_exists($this, 'showFormat') + ? [$this, 'showFormat'] + : null); + return $this->success($data); + } + + /** + * 创建新资源(POST /resource) + * + * @param Request $request 请求对象 + * @return mixed 返回创建成功的数据或信息 + */ + public function store(Request $request) + { + $res = $this->service->serviceStore($request); + return $this->success($res, '创建成功'); + } + + /** + * 更新指定资源(PUT/PATCH /resource/{id}) + * + * @param Request $request 请求对象 + * @return mixed 返回更新成功的数据或信息 + */ + public function update(Request $request) + { + $res = $this->service->serviceUpdate($request); + return $this->success($res, '更新成功'); + } + + /** + * 删除指定资源(DELETE /resource/{id}) + * + * @param Request $request 请求对象 + * @return mixed 返回删除操作结果 + */ + public function destroy(Request $request) + { + $count = $this->service->serviceDestroy($request); + return $this->success([], "删除了 {$count} 条"); + } + + + // ---------------------------------------------------------------- + // 以下三个钩子方法,**在使用此 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..4e2b794 --- /dev/null +++ b/app/common/traits/crud/CrudLogicTrait.php @@ -0,0 +1,96 @@ +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 logicShowBy(array $where, array $with = [], array $dataLimit = []) + // { + // return CrudUtil::findOneBy($this->modelClass, $where, $with, $dataLimit); + // } + + /** + * 创建(可带 dataLimit) + */ + public function logicCreate(array $data, array $dataLimit = []) + { + return CrudUtil::create($this->modelClass, $data, $dataLimit); + } + + /** + * 主键更新(可带 dataLimit) + */ + // public function logicUpdateByPk($id, array $data, array $dataLimit = []) + // { + // return CrudUtil::updateByPk($this->modelClass, $id, $data, $dataLimit); + // } + + /** + * 任意条件更新(可带 dataLimit) + */ + public function logicUpdateBy(array $config, array $data) + { + return CrudUtil::updateBy(CrudUtil::buildQuery($config), $data); + } + + /** + * 主键删除(批量) + */ + // public function logicDeleteByPk(array $ids) + // { + // return CrudUtil::deleteByPk($this->modelClass, $ids); + // } + + /** + * 任意条件删除(可带 dataLimit) + */ + public function logicDeleteBy(array $config) + { + return CrudUtil::deleteBy(CrudUtil::buildQuery($config)); + } + + /** + * 复杂查询回调 + */ + public function logicComplex(callable $callback) + { + return CrudUtil::executeComplex($this->modelClass, $callback); + } +} diff --git a/app/common/traits/crud/CrudServiceTrait.php b/app/common/traits/crud/CrudServiceTrait.php new file mode 100644 index 0000000..765042f --- /dev/null +++ b/app/common/traits/crud/CrudServiceTrait.php @@ -0,0 +1,209 @@ +complexQuery($request->get()); + } else { + $config = $this->buildQueryConfig($request, 'index'); + $limit = (int)$request->get('limit', 15); + $page = (int)$request->get('page', 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) + { + $data = $request->post(); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); + return $this->postProcess('Store', $created); + } + + /** 更新 */ + public function serviceUpdate(Request $request) + { + $data= $request->post(); + $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 的查询配置1 + * + * 前端可以通过以下参数控制: + * - filter_= → where + * - sort_by=&order= + * - fields=id,name,email → select + * - with=roles,posts → 关联 + * - withFields=roles:id,name;... → 关联字段 + * - dataLimitType=personal|auth + * - dataLimitField=admin_id + */ + 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; + + // // 1. where 从 filter_ 前缀自动映射 + // $where = []; + // foreach ($p as $key => $val) { + // if (strpos($key, 'filter_') === 0 && $val !== '') { + // $col = substr($key, 7); + // // 支持 %like% 查询 + // $where[$col] = strpos($val, '%') !== false + // ? ['like', $val] + // : $val; + // } + // } + + // // 2. 其余配置 + // return [ + // 'model' => $this->modelClass, + // 'where' => $where, + // 'order' => [ + // $p['sort_by'] ?? 'id' => $p['order'] ?? 'desc' + // ], + // 'fields' => !empty($p['fields']) + // ? explode(',', $p['fields']) + // : [], + // 'with' => !empty($p['with']) + // ? explode(',', $p['with']) + // : [], + // 'withFields' => $this->parseWithFields( + // $p['withFields'] ?? '' + // ), + // 'dataLimit' => $this->getDataLimitFromRequest($request), + // ]; + } + + /** 解析 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'), + ]; + } + + /** + * 处理复杂查询 + * 覆盖此方法,实现复杂查询逻辑 + */ + // protected function complexQuery(array $params): array + // { + // throw new \LogicException('请在子类实现 complexQuery'); + // } + + /** + * 根据操作类型,调用对应的后置钩子 + * + * @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; + } +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudServiceTrait_bak2.php b/app/common/traits/crud/CrudServiceTrait_bak2.php new file mode 100644 index 0000000..a75b28f --- /dev/null +++ b/app/common/traits/crud/CrudServiceTrait_bak2.php @@ -0,0 +1,270 @@ +get('complex_query') && method_exists($this, 'complexQuery')) { + $data = $this->complexQuery($request->get()); + } else { + $cfg = $this->buildQueryConfig($request, 'list'); + $limit = (int)$request->get('limit', 15); + $page = (int)$request->get('page', 1); + $data = $this->logicList($cfg, $limit, $page); + } + return $this->postProcess('List', $data); + } + + /** 查看单条 */ + public function serviceShow(Request $request, $id) + { + $item = $this->logicShow($id, []); + return $this->postProcess('Show', $item); + } + + /** 创建 */ + public function serviceStore(Request $request) + { + $data = $request->post(); + $dl = $this->getDataLimitFromRequest($request); + $item = $this->logicCreate($data, $dl); + return $this->postProcess('Store', $item); + } + + /** 更新 */ + public function serviceUpdate(Request $request) + { + $key = (new $this->modelClass)->getKeyName(); + $id = (int)$request->post($key); + $data = $request->post(); + $dl = $this->getDataLimitFromRequest($request); + $item = $this->logicUpdateByPk($id, $data, $dl); + return $this->postProcess('Update', $item); + } + + /** 删除 */ + public function serviceDestroy(Request $request) + { + $key = (new $this->modelClass)->getKeyName(); + $ids = (array)$request->post($key, []); + $result = $this->logicDeleteByPk($ids); + return $this->postProcess('Destroy', $result); + } + + // ---------------- 统一后置钩子 ---------------- // + + /** + * 各操作后触发 afterService{Op}() + * @param string $op List|Show|Store|Update|Destroy + */ + protected function postProcess(string $op, mixed $data): mixed + { + $hook = "afterService{$op}"; + return method_exists($this, $hook) + ? $this->{$hook}($data) + : $data; + } + + // ---------------- 复杂查询入口 ---------------- // + + /** + * 子类 override 实现完全定制的复杂查询 + * @param array $params 全部请求参数 + */ + protected function complexQuery(array $params) + { + throw new \LogicException('请在子类实现 complexQuery()'); + } + + // ------------- 辅助:数据权限 & withFields 解析 ------------- // + + /** 从请求读取 dataLimitType & dataLimitField → ['type'=>..,'field'=>..] */ + protected function getDataLimitFromRequest(Request $req): array + { + $type = $req->get('dataLimitType'); + if (!$type) { + return []; + } + return [ + 'type' => $type, + 'field' => $req->get('dataLimitField', 'admin_id'), + ]; + } + + /** 解析 withFields 字符串 "rel:field,field;rel2:..." */ + 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; + } + + // ------------- 扁平化 Request → 查询配置 ------------- // + + /** + * 构建完整配置 + * 返回 [ + * where=>[], with=>[], order=>[], withFields=>?, fields=>?, dataLimit=>[] + * ] + */ + protected function buildQueryConfig(Request $request, string $scene = 'list'): array + { + return [ + 'where' => $this->buildFilters($request, $scene), + 'with' => $this->buildRelations($request, $scene), + 'order' => $this->buildOrder($request, $scene), + 'withFields' => $this->buildWithFields($request,$scene), + 'fields' => $this->buildFields($request, $scene), + 'dataLimit' => $this->buildDataLimit($request, $scene), + ]; + } + + /** + * 通用合并方法:取公共配置 + 场景专属配置,若均为 array 则 array_merge + * + * @param mixed $common 公共配置 + * @param Request $request + * @param string $scene + * @param string $type Filters|Relations|Order|WithFields|Fields|DataLimit + */ + private function applySceneConfig( + mixed $common, + Request $request, + string $scene, + string $type + ): mixed { + $method = 'apply' . ucfirst($scene) . $type; + if (! method_exists($this, $method)) { + return $common; + } + $sceneCfg = $this->{$method}($common, $request); + if (is_array($common) && is_array($sceneCfg)) { + return array_merge($common, $sceneCfg); + } + return $sceneCfg; + } + + /** 构建 WHERE:commonFilters + apply{Scene}Filters */ + protected function buildFilters(Request $r, string $scene): array + { + $common = $this->commonFilters($r); + return (array)$this->applySceneConfig($common, $r, $scene, 'Filters'); + } + + /** 构建 with 关联 */ + protected function buildRelations(Request $r, string $scene): array + { + $common = $this->commonRelations(); + return (array)$this->applySceneConfig($common, $r, $scene, 'Relations'); + } + + /** 构建 order 排序 */ + protected function buildOrder(Request $r, string $scene): array + { + $common = $this->commonOrder(); + return (array)$this->applySceneConfig($common, $r, $scene, 'Order'); + } + + /** 构建 withFields(关联模型字段白名单) */ + protected function buildWithFields(Request $r, string $scene): ?array + { + $common = []; + return $this->applySceneConfig($common, $r, $scene, 'WithFields'); + } + + /** 构建 fields(主表字段白名单) */ + protected function buildFields(Request $r, string $scene): ?array + { + $common = []; + return $this->applySceneConfig($common, $r, $scene, 'Fields'); + } + + /** 构建 dataLimit(数据权限) */ + protected function buildDataLimit(Request $r, string $scene): array + { + $common = []; + return (array)$this->applySceneConfig($common, $r, $scene, 'DataLimit'); + } + + + // ------------- 可重写的“公共 + 场景” 方法 ------------- // + + // --- common* 系列(全局默认) --- + protected function commonFilters(Request $r): array { return []; } + protected function commonRelations(): array { return []; } + protected function commonOrder(): array { return []; } + + // // --- list* 系列 --- + // protected function applyListFilters(array $c, Request $r): array { return $this->listFilters($r); } + // protected function listFilters(Request $r): array { return []; } + + // protected function applyListRelations(array $c, Request $r): array { return $this->listRelations(); } + // protected function listRelations(): array { return []; } + + // protected function applyListOrder(array $c, Request $r): array { return $this->listOrder($r); } + // protected function listOrder(Request $r): array { return ['created_at'=>'desc']; } + + // protected function applyListWithFields(array $c, Request $r): array{ return $this->listWithFields($r); } + // protected function listWithFields(Request $r): array { return []; } + + // protected function applyListFields(array $c, Request $r): array { return $this->listFields($r); } + // protected function listFields(Request $r): array { return []; } + + // protected function applyListDataLimit(array $c, Request $r): array { return $this->listDataLimit($r); } + // protected function listDataLimit(Request $r): array { return []; } + + // // --- detail* 系列 --- + // protected function applyDetailFilters(array $c, Request $r): array { return $this->detailFilters($r); } + // protected function detailFilters(Request $r): array { return ['id'=>$r->get('id')]; } + + // protected function applyDetailRelations(array $c, Request $r): array { return $this->detailRelations(); } + // protected function detailRelations(): array { return []; } + + // protected function applyDetailOrder(array $c, Request $r): array { return $this->detailOrder($r); } + // protected function detailOrder(Request $r): array { return []; } + + // protected function applyDetailWithFields(array $c, Request $r): array{ return $this->detailWithFields($r); } + // protected function detailWithFields(Request $r): array { return []; } + + // protected function applyDetailFields(array $c, Request $r): array { return $this->detailFields($r); } + // protected function detailFields(Request $r): array { return []; } + + // protected function applyDetailDataLimit(array $c, Request $r): array { return $this->listDataLimit($r); } + + // // --- export* 系列(如有需要可 override) --- + // protected function applyExportFilters(array $c, Request $r): array { return $this->exportFilters($r); } + // protected function exportFilters(Request $r): array { return []; } +} \ 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..58e8302 --- /dev/null +++ b/app/common/utils/CrudUtil.php @@ -0,0 +1,406 @@ + 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(); + + // 1. eager load + if (!empty($config['with'])) { + $q->with($config['with']); + } + + // 2. 关联表字段选择 + if (!empty($config['withFields'])) { + foreach ($config['withFields'] as $rel => $fields) { + $q->with([$rel => function ($qr) use ($fields) { + $fk = $qr->getModel()->getForeignKey(); + $qr->select(array_unique(array_merge($fields, [$fk]))); + }]); + } + } + + // 3. 本表字段选择 + if (!empty($config['fields'])) { + $q->select($config['fields']); + } + + // 4. where 条件(支持 dot-notation 关联过滤) + if (!empty($config['where'])) { + foreach ($config['where'] as $col => $val) { + if (strpos($col, '.') !== false) { + [$rel, $relCol] = explode('.', $col, 2); + $q->whereHas($rel, function ($qr) use ($relCol, $val) { + self::applyWhere($qr, $relCol, $val); + }); + } else { + self::applyWhere($q, $col, $val); + } + } + } + + // 5. 排序 + if (!empty($config['order'])) { + foreach ($config['order'] as $col => $dir) { + $q->orderBy($col, strtolower($dir) === 'asc' ? 'asc' : 'desc'); + } + } + + // 6. 数据权限过滤 + 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 string $modelClass + * @param array $where + * @param array $with + * @param array $dataLimit + * @return Model + * @throws BusinessException + */ + // public static function findOneBy( + // string $modelClass, + // array $where, + // array $with = [], + // array $dataLimit = [] + // ): Model { + // $config = compact('modelClass'); + // $config['model'] = $modelClass; + // $config['where'] = $where; + // $config['with'] = $with; + // $config['dataLimit'] = $dataLimit; + + // $m = self::buildQuery($config)->first(); + // if (!$m) { + // throw new BusinessException("未找到符合条件的记录", 404); + // } + // return $m; + // } + 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 string $modelClass + * @param array $where + * @param array $data + * @param array $dataLimit + * @return int 受影响行数 + */ + // public static function updateBy( + // string $modelClass, + // array $where, + // array $data, + // array $dataLimit = [] + // ): int { + // $config = ['model'=>$modelClass, 'where'=>$where, 'dataLimit'=>$dataLimit]; + // return self::buildQuery($config)->update($data); + // } + 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 string $modelClass + * @param array $where + * @param array $dataLimit + * @return int 删除条数 + */ + // public static function deleteBy( + // string $modelClass, + // array $where, + // array $dataLimit = [] + // ): int { + // $config = ['model'=>$modelClass, 'where'=>$where, 'dataLimit'=>$dataLimit]; + // return self::buildQuery($config)->delete(); + // } + public static function deleteBy(Builder $q): int + { + return $q->delete(); + } + + + //=================== Internal Methods ===================// + + /** + * 应用单字段 where 过滤 + * + * @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) && count($val) === 2) { + [$op, $v] = $val; + 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); + } + } else { + $q->where($col, $val); + } + } + + /** + * 应用数据权限过滤到查询(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/controller/IndexController.php b/app/controller/IndexController.php index f9b026d..79d65c7 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -13,6 +13,8 @@ class IndexController { protected $noNeedLogin = ['index', 'view', 'perm']; + + protected $noNeedHeaderData = ['index', 'view', 'perm']; public function index(Request $request) { @@ -58,6 +60,7 @@ class IndexController Cache::delete("user_resources_{$data['user_id']}"); return json(['code' => 0, 'msg' => 'success']); } catch (\Throwable $e) { + var_dump($e); return json(['code' => 1, 'msg' => $e->getMessage()]); } } diff --git a/app/middleware/AuthCheck.php b/app/middleware/AuthCheck.php index c085901..3c504aa 100644 --- a/app/middleware/AuthCheck.php +++ b/app/middleware/AuthCheck.php @@ -6,18 +6,50 @@ use ReflectionClass; use Webman\MiddlewareInterface; use Webman\Http\Response; use Webman\Http\Request; +use app\common\service\BasicDataService; 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'] ?? []; + $noNeedHeaderData = $properties['noNeedHeaderData'] ?? []; - if (in_array($request->action, $noNeedLogin)) { - // 白名单 - return $handler($request); - } else { + // header数据 + if (!in_array($request->action, $noNeedHeaderData)) { //如果需要header数据 + try { + $corpAbbr = $request->header('Fdesk-Corp-Abbr'); + $systemCode = $request->header('Fdesk-System-Code'); + $loginType = $request->header('Fdesk-Login-Type'); + $basicDataService = new BasicDataService(); + if ($basicDataService->checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr)) { + $request->login_type = $loginType; + $request->system_code = $systemCode; + $request->corp_abbr = $corpAbbr; + $request->basic_data = [ + 'login_type' => $loginType, + 'system_code' => $systemCode, + 'corp_abbr' => $corpAbbr + ]; + } + return $handler($request); + } catch (\Throwable $th) { + throw $th; + $body = [ + 'status' => 'fail', + 'code' => 401, + 'message' => $th->getMessage(), + 'data' => [], + ]; + return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); + } + } + + // 不登陆&不鉴权 + if (!in_array($request->action, $noNeedLogin)) { //如果需要登陆&鉴权 try { $authHeader = $request->header('Fdesk-Auth-Token'); if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) { @@ -31,6 +63,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 +73,24 @@ class AuthCheck implements MiddlewareInterface ]; return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); } - return $handler($request); } + + // 登陆&不鉴权 + // if (!in_array($request->action, $noNeedAuth)) { //如果需要登陆&不鉴权 + // try { + + // return $handler($request); + // } catch (\Throwable $e) { + // $body = [ + // 'status' => 'fail', + // 'code' => 401, + // 'message' => $e->getMessage(), + // 'data' => [], + // ]; + // return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); + // } + // } + + return $handler($request); } } \ No newline at end of file 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/config/middleware.php b/config/middleware.php index ae0ad6e..6447d2c 100644 --- a/config/middleware.php +++ b/config/middleware.php @@ -16,7 +16,6 @@ return [ '' => [ \app\middleware\AccessControl::class, - \app\middleware\BasicData::class, \app\middleware\AuthCheck::class ] ]; \ No newline at end of file diff --git a/config/route.php b/config/route.php index d970528..e9cc611 100644 --- a/config/route.php +++ b/config/route.php @@ -58,6 +58,13 @@ Route::group('/admin', function () { Route::any('/update', [app\admin\controller\RoleController::class, 'update']); Route::any('/delete', [app\admin\controller\RoleController::class, 'delete']); }); + Route::group('/corp', function () { + Route::any('/list', [app\admin\controller\CorpController::class, 'index']); + Route::any('/show', [app\admin\controller\CorpController::class, 'show']); + // Route::any('/create', [app\admin\controller\CorpController::class, 'store']); + Route::any('/update', [app\admin\controller\CorpController::class, 'update']); + // Route::any('/delete', [app\admin\controller\CorpController::class, 'destroy']); + }); }); From 45679bc5e4364c1c9816f419da392083eb05838b Mon Sep 17 00:00:00 2001 From: chenlan <18339827479@163.com> Date: Mon, 21 Jul 2025 15:09:06 +0800 Subject: [PATCH 2/3] =?UTF-8?q?crud=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/controller/RoleController.php | 185 ++++++++++-------- app/admin/service/CorpService.php | 61 ++---- app/admin/service/RoleService.php | 31 +++ app/common/model/Corp.php | 12 +- app/common/service/BasicDataService.php | 2 +- app/common/service/RoleService.php | 55 ------ app/common/traits/RequestTrait.php | 38 ++++ .../traits/crud/CrudControllerTrait.php | 64 ++++-- app/common/traits/crud/CrudServiceTrait.php | 24 ++- app/common/validate/CorpValidate.php | 35 ++++ app/common/validate/RoleValidate.php | 4 +- app/middleware/AuthCheck.php | 56 +----- app/middleware/BasicData.php | 12 +- config/middleware.php | 1 + config/route.php | 16 +- 15 files changed, 318 insertions(+), 278 deletions(-) create mode 100644 app/admin/service/RoleService.php delete mode 100644 app/common/service/RoleService.php create mode 100644 app/common/traits/RequestTrait.php create mode 100644 app/common/validate/CorpValidate.php 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/service/CorpService.php b/app/admin/service/CorpService.php index b1cc0bf..5767ea6 100644 --- a/app/admin/service/CorpService.php +++ b/app/admin/service/CorpService.php @@ -2,9 +2,9 @@ namespace app\admin\service; +use app\common\validate\CorpValidate; use app\common\traits\crud\CrudServiceTrait; use app\common\model\Corp; -use support\Request; class CorpService { @@ -15,6 +15,19 @@ class CorpService */ protected string $modelClass = Corp::class; + /** + * 验证器 + */ + protected $validator = CorpValidate::class; + + /** + * 获取验证器 + */ + public function getValidator() + { + return new $this->validator; + } + /** * 如果需要自定义,比如强制加一个 where['status']=1, @@ -27,33 +40,6 @@ class CorpService // return $config; // } - /** - * 如果需要自定义“哪些字段可筛选”或额外联表, - * 只需重写 buildQueryConfig: - */ - // protected function buildQueryConfig(Request $request): array - // { - // $config = parent::buildQueryConfig($request); - // // 强制只看 status=1 的记录 - // $config['where']['status'] = 1; - // // 自动 eager load company relation - // $config['with'][] = 'company'; - // return $config; - // } - - /** - * 复杂查询,覆盖 complexQuery 方法 - */ - // protected function complexQuery(array $params): array - // { - // return $this->logicComplex(function($q) use ($params) { - // // 自定义 join、where、select - // return $q->where('status', 1) - // ->orderBy('created_at','desc') - // ->paginate($params['limit'] ?? 15); - // })->toArray(); - // } - // /** // * 复杂查询示例: // * 只要在 URL 里添加 ?complex_query=1, @@ -100,23 +86,4 @@ class CorpService // $res['data'] = array_map(fn($i)=>[...], $res['data']); // return $res; // } - - // protected function afterServiceShow($item) - // { - // // 对详情对象处理 - // $item['extra']=123; - // return $item; - // } - - // afterServiceStore/Update/Destroy 同理 - - - // protected function afterServiceList(mixed $result): array - // { - // if ($result['records']) { - // $result['records'] = array_map(fn($i) => [...$i], $result['records']); - // } - - // return $result; - // } } \ 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/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/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/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/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 index 0fd4a0d..c5cdfdc 100644 --- a/app/common/traits/crud/CrudControllerTrait.php +++ b/app/common/traits/crud/CrudControllerTrait.php @@ -11,22 +11,26 @@ use support\Request; trait CrudControllerTrait { /** - * 获取资源列表(GET /resource) + * 获取列表 * * @param Request $request 请求对象 * @return mixed 返回分页数据及总数 */ public function index(Request $request) { - $raw = $this->service->serviceList($request); - $data = $this->transformData($raw, method_exists($this, 'indexFormat') - ? [$this, 'indexFormat'] - : null); - return $this->success($data); + try { + $raw = $this->service->serviceList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } } /** - * 获取指定资源详情(GET /resource/{id}) + * 获取详情 * * @param Request $request 请求对象 * @param int|string $id 资源 ID @@ -34,47 +38,65 @@ trait CrudControllerTrait */ public function show(Request $request) { - $raw = $this->service->serviceShow($request); - $data = $this->transformData($raw, method_exists($this, 'showFormat') - ? [$this, 'showFormat'] - : null); - return $this->success($data); + try { + $raw = $this->service->serviceShow($request); + $data = $this->transformData($raw, method_exists($this, 'showFormat') + ? [$this, 'showFormat'] + : null); + return $this->success($data); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } } /** - * 创建新资源(POST /resource) + * 创建 * * @param Request $request 请求对象 * @return mixed 返回创建成功的数据或信息 */ public function store(Request $request) { - $res = $this->service->serviceStore($request); - return $this->success($res, '创建成功'); + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'create'); + $res = $this->service->serviceStore($request, $data); + return $this->success($res, '创建成功'); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } } /** - * 更新指定资源(PUT/PATCH /resource/{id}) + * 更新 * * @param Request $request 请求对象 * @return mixed 返回更新成功的数据或信息 */ public function update(Request $request) { - $res = $this->service->serviceUpdate($request); - return $this->success($res, '更新成功'); + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'update'); + $res = $this->service->serviceUpdate($request, $data); + return $this->success([], '更新成功'); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } } /** - * 删除指定资源(DELETE /resource/{id}) + * 删除 * * @param Request $request 请求对象 * @return mixed 返回删除操作结果 */ public function destroy(Request $request) { - $count = $this->service->serviceDestroy($request); - return $this->success([], "删除了 {$count} 条"); + try { + $count = $this->service->serviceDestroy($request); + return $this->success([], "成功删除了 {$count} 条"); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } } diff --git a/app/common/traits/crud/CrudServiceTrait.php b/app/common/traits/crud/CrudServiceTrait.php index 765042f..c042b5f 100644 --- a/app/common/traits/crud/CrudServiceTrait.php +++ b/app/common/traits/crud/CrudServiceTrait.php @@ -17,6 +17,14 @@ trait CrudServiceTrait /** 具体 Service 里定义:protected string $modelClass = YourModel::class; */ + /** + * 获取验证器 + */ + public function getValidator() + { + return $this->validator; + } + /** 分页列表 */ public function serviceList(Request $request) { @@ -52,18 +60,18 @@ trait CrudServiceTrait } /** 创建 */ - public function serviceStore(Request $request) + public function serviceStore(Request $request, array $data = []) { - $data = $request->post(); + // $data = $request->post(); $dl = $this->getDataLimitFromRequest($request); $created = $this->logicCreate($data, $dl); return $this->postProcess('Store', $created); } /** 更新 */ - public function serviceUpdate(Request $request) + public function serviceUpdate(Request $request, array $data = []) { - $data= $request->post(); + // $data= $request->post(); $config = $this->buildQueryConfig($request, 'update'); $updated = $this->logicUpdateBy($config, $data); return $this->postProcess('Update', $updated); @@ -100,13 +108,13 @@ trait CrudServiceTrait switch ($scene) { case 'default': $config['where']['status'] = 1; - $config['order']['sort'] = ['desc']; - $config['order']['id'] = ['desc']; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; break; case 'index': $config['where']['status'] = 1; - $config['order']['sort'] = ['desc']; - $config['order']['id'] = ['desc']; + $config['order']['sort'] = 'desc'; + $config['order']['id'] = 'desc'; break; case 'show': $config['where']['id'] = $p['id']; diff --git a/app/common/validate/CorpValidate.php b/app/common/validate/CorpValidate.php new file mode 100644 index 0000000..5889ef9 --- /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 = [ + 'create' => ['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/RoleValidate.php b/app/common/validate/RoleValidate.php index 330fae6..afd69cc 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'], + 'create' => ['system_id', 'name', 'desc', 'sort'], + 'update' => ['id', 'system_id', 'name', 'desc', 'sort', 'status'], ]; } diff --git a/app/middleware/AuthCheck.php b/app/middleware/AuthCheck.php index 3c504aa..a03e632 100644 --- a/app/middleware/AuthCheck.php +++ b/app/middleware/AuthCheck.php @@ -6,7 +6,6 @@ use ReflectionClass; use Webman\MiddlewareInterface; use Webman\Http\Response; use Webman\Http\Request; -use app\common\service\BasicDataService; class AuthCheck implements MiddlewareInterface { @@ -16,40 +15,11 @@ class AuthCheck implements MiddlewareInterface $properties = $controller->getDefaultProperties(); $noNeedLogin = $properties['noNeedLogin'] ?? []; $noNeedAuth = $properties['noNeedAuth'] ?? []; - $noNeedHeaderData = $properties['noNeedHeaderData'] ?? []; - - // header数据 - if (!in_array($request->action, $noNeedHeaderData)) { //如果需要header数据 - try { - $corpAbbr = $request->header('Fdesk-Corp-Abbr'); - $systemCode = $request->header('Fdesk-System-Code'); - $loginType = $request->header('Fdesk-Login-Type'); - $basicDataService = new BasicDataService(); - if ($basicDataService->checkAndCacheBaseParams($loginType, $systemCode, $corpAbbr)) { - $request->login_type = $loginType; - $request->system_code = $systemCode; - $request->corp_abbr = $corpAbbr; - $request->basic_data = [ - 'login_type' => $loginType, - 'system_code' => $systemCode, - 'corp_abbr' => $corpAbbr - ]; - } - return $handler($request); - } catch (\Throwable $th) { - throw $th; - $body = [ - 'status' => 'fail', - 'code' => 401, - 'message' => $th->getMessage(), - 'data' => [], - ]; - return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); - } - } - - // 不登陆&不鉴权 - if (!in_array($request->action, $noNeedLogin)) { //如果需要登陆&鉴权 + + if (in_array($request->action, $noNeedLogin)) { + // 白名单 + return $handler($request); + } else { try { $authHeader = $request->header('Fdesk-Auth-Token'); if (!$authHeader || !preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) { @@ -75,22 +45,6 @@ class AuthCheck implements MiddlewareInterface } } - // 登陆&不鉴权 - // if (!in_array($request->action, $noNeedAuth)) { //如果需要登陆&不鉴权 - // try { - - // return $handler($request); - // } catch (\Throwable $e) { - // $body = [ - // 'status' => 'fail', - // 'code' => 401, - // 'message' => $e->getMessage(), - // 'data' => [], - // ]; - // return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); - // } - // } - 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/config/middleware.php b/config/middleware.php index 6447d2c..ae0ad6e 100644 --- a/config/middleware.php +++ b/config/middleware.php @@ -16,6 +16,7 @@ return [ '' => [ \app\middleware\AccessControl::class, + \app\middleware\BasicData::class, \app\middleware\AuthCheck::class ] ]; \ No newline at end of file diff --git a/config/route.php b/config/route.php index e9cc611..700fb4b 100644 --- a/config/route.php +++ b/config/route.php @@ -28,6 +28,7 @@ use Webman\Route; // 调试 Route::any('/perm', [app\controller\IndexController::class, 'perm']); +// 公共 Route::group('/common', function () { Route::group('/auth', function () { Route::any('/login', [app\common\controller\AuthController::class, 'login']); @@ -50,20 +51,23 @@ Route::group('/common', function () { }); }); +// 后台管理 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('/create', [app\admin\controller\CorpController::class, 'store']); + Route::any('/store', [app\admin\controller\CorpController::class, 'store']); Route::any('/update', [app\admin\controller\CorpController::class, 'update']); - // Route::any('/delete', [app\admin\controller\CorpController::class, 'destroy']); + Route::any('/destroy', [app\admin\controller\CorpController::class, 'destroy']); }); }); From 149de9d6fb1ed6429a46b6d121cf1ce29e6a07ce Mon Sep 17 00:00:00 2001 From: mzl Date: Mon, 21 Jul 2025 15:10:14 +0800 Subject: [PATCH 3/3] Merge branch 'develop' into feature/mzl_dev_curd --- app/common/controller/UserController.php | 53 +++ app/common/logic/PermLogic.php | 2 +- app/common/logic/UserLogic.php | 29 ++ app/common/model/Role.php | 2 +- app/common/model/User.php | 7 +- app/common/service/AuthService.php | 11 +- app/common/service/UserService.php | 62 +++ app/controller/IndexController.php | 10 + app/crm/controller/BaseController.php | 51 +++ app/crm/controller/CarTypeController.php | 34 ++ app/crm/controller/CustomerController.php | 527 ++++++++++++++++++++++ app/crm/controller/FollowController.php | 141 ++++++ app/crm/controller/TagGroupController.php | 80 ++++ app/crm/logic/CustomerLogic.php | 15 + app/crm/logic/FollowLogic.php | 8 + app/crm/model/CarType.php | 36 ++ app/crm/model/Channel.php | 48 ++ app/crm/model/Country.php | 41 ++ app/crm/model/Customer.php | 82 ++++ app/crm/model/CustomerHis.php | 68 +++ app/crm/model/CustomerTag.php | 36 ++ app/crm/model/Follow.php | 50 ++ app/crm/model/Tag.php | 55 +++ app/crm/model/TagGroup.php | 50 ++ app/crm/service/CustomerService.php | 163 +++++++ app/crm/service/FollowService.php | 65 +++ app/crm/utils/Util.php | 121 +++++ app/crm/validate/FollowValidate.php | 21 + app/ticket/service/CategorieService.php | 2 +- config/constants/Constants.php | 100 ++++ config/route.php | 34 +- resource/translations/ar/messages.php | 31 ++ resource/translations/en/messages.php | 31 ++ resource/translations/ru/messages.php | 31 ++ resource/translations/zh/messages.php | 31 ++ 35 files changed, 2112 insertions(+), 16 deletions(-) create mode 100644 app/crm/controller/BaseController.php create mode 100644 app/crm/controller/CarTypeController.php create mode 100644 app/crm/controller/CustomerController.php create mode 100644 app/crm/controller/FollowController.php create mode 100644 app/crm/controller/TagGroupController.php create mode 100644 app/crm/logic/CustomerLogic.php create mode 100644 app/crm/logic/FollowLogic.php create mode 100644 app/crm/model/CarType.php create mode 100644 app/crm/model/Channel.php create mode 100644 app/crm/model/Country.php create mode 100644 app/crm/model/Customer.php create mode 100644 app/crm/model/CustomerHis.php create mode 100644 app/crm/model/CustomerTag.php create mode 100644 app/crm/model/Follow.php create mode 100644 app/crm/model/Tag.php create mode 100644 app/crm/model/TagGroup.php create mode 100644 app/crm/service/CustomerService.php create mode 100644 app/crm/service/FollowService.php create mode 100644 app/crm/utils/Util.php create mode 100644 app/crm/validate/FollowValidate.php diff --git a/app/common/controller/UserController.php b/app/common/controller/UserController.php index efab4a1..2a22145 100644 --- a/app/common/controller/UserController.php +++ b/app/common/controller/UserController.php @@ -22,4 +22,57 @@ class UserController extends BaseController } } + /** + * 用户信息 + */ + 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/PermLogic.php b/app/common/logic/PermLogic.php index b30e6f8..674fbc8 100644 --- a/app/common/logic/PermLogic.php +++ b/app/common/logic/PermLogic.php @@ -102,7 +102,7 @@ class PermLogic // component 字段规则: // 1. 存在子菜单,固定 '/index/index' // 2. 否则为当前完整路径(或其它你想要的) - $componentValue = $hasChildrenMenu ? '/index/index' : $currentPath; + $componentValue = ($hasChildrenMenu && !$node->url && $node->type !== 'directory') ? '/index/index' : $currentPath; $tree[] = [ 'id' => $node->id, diff --git a/app/common/logic/UserLogic.php b/app/common/logic/UserLogic.php index 1d6dc93..4b58247 100644 --- a/app/common/logic/UserLogic.php +++ b/app/common/logic/UserLogic.php @@ -24,4 +24,33 @@ 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; + } + } \ No newline at end of file diff --git a/app/common/model/Role.php b/app/common/model/Role.php index c9eaabd..ea84200 100644 --- a/app/common/model/Role.php +++ b/app/common/model/Role.php @@ -53,6 +53,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/User.php b/app/common/model/User.php index e9b4627..5d1b8f0 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', ]; // 用户所有角色 diff --git a/app/common/service/AuthService.php b/app/common/service/AuthService.php index 6de72a2..aa8f4eb 100644 --- a/app/common/service/AuthService.php +++ b/app/common/service/AuthService.php @@ -31,7 +31,7 @@ class AuthService return match ($loginType) { 'feishu' => $logic->feishuLogin($data), - // 'qrcode' => $logic->qrCodeLogin($data), + 'qrcode' => $logic->qrCodeLogin($data), default => $logic->uPwdLogin($data), }; } @@ -86,13 +86,4 @@ class AuthService { (new AuthLogic())->blockToken($token, $systemCode); } - - - // public function perms() - // { - // $permService = new PermService(); - // $menuTree = $permService->getMenuTree($uid); - // $permissionCodes = $permService->getPermissionCodes($uid); - // return (new PermService())->getMenuTree($this->userId) - // } } \ No newline at end of file diff --git a/app/common/service/UserService.php b/app/common/service/UserService.php index 98375bc..85684d3 100644 --- a/app/common/service/UserService.php +++ b/app/common/service/UserService.php @@ -3,6 +3,10 @@ namespace app\common\service; use app\common\logic\UserLogic; +use app\common\model\Corp; +use app\common\model\User; +use app\crm\model\Country; +use support\exception\BusinessException; class UserService { @@ -37,4 +41,62 @@ 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; + } } \ No newline at end of file diff --git a/app/controller/IndexController.php b/app/controller/IndexController.php index 79d65c7..866d3d5 100644 --- a/app/controller/IndexController.php +++ b/app/controller/IndexController.php @@ -64,4 +64,14 @@ class IndexController return json(['code' => 1, 'msg' => $e->getMessage()]); } } + + + // test function + public function test(Request $request) + { + $user = User::find(1); + $user->roles()->sync([1]); + $user->systems()->sync([1]); + return json(['code' => 0, 'msg' => '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..8eee3ac --- /dev/null +++ b/app/crm/controller/CustomerController.php @@ -0,0 +1,527 @@ + $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('page', 1); + $perPage = $request->input('per_page', 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')); + } + + $data = [ + 'user_id' => $request->user->uid, + 'corp_abbr' => $request->basic_data['corp_abbr'], + '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)->distinct('customer_id')->count(); + + //跟进数 + $whereVisit = $where; + $whereVisit[] = ['type', '=', 1]; + $follow = Follow::where($whereVisit)->distinct('customer_id')->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..a23749c --- /dev/null +++ b/app/crm/controller/FollowController.php @@ -0,0 +1,141 @@ + $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')); + } + $picture = Util::getParam($request, 'picture'); + if (Util::isEmpty($picture)) { + return $this->error(trans('picture_not_null')); + } + + $data = [ + 'user_id' => $request->user->uid, + 'corp_abbr' => $request->basic_data['corp_abbr'], + 'customer_id' => $customer_id, + 'type' => $type, + 'picture' => $picture, + 'remark' => Util::getParam($request, 'remark', ''), + 'audio' => Util::getParam($request, 'audio', ''), + 'audio_text' => Util::getParam($request, '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..e411984 --- /dev/null +++ b/app/crm/service/CustomerService.php @@ -0,0 +1,163 @@ +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']; + if(empty($user_id) || $user_id == 0){ + $where[] = ['user_id', '=', $user_id]; + } + + $seas = $params['seas'] ?? ''; + if($seas == '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($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' || $seas == 'seas') { + $query->onlyTrashed(); + } + + $customer = Customer::where($where)->paginate($perPage, ['*'], 'page', $page); + foreach ($customer as &$item) { + $item = $this->getCustomer($item); + if($seas == 'seas'){ + $item['seas_time'] = time()-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..0d3fd8e --- /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..db6743b --- /dev/null +++ b/app/crm/utils/Util.php @@ -0,0 +1,121 @@ +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; + } + +} \ 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/ticket/service/CategorieService.php b/app/ticket/service/CategorieService.php index 4954c85..67d1d74 100644 --- a/app/ticket/service/CategorieService.php +++ b/app/ticket/service/CategorieService.php @@ -14,7 +14,7 @@ class CategorieService public function list($params) { $result = (new CategorieLogic())->list($params); - $value=$result[0]->toArray(); +// $value=$result[0]->toArray(); foreach ($result as $key => &$value) { $value['name'] = isset($value['translation']) ? $value['translation']['name'] : ''; unset($value['translation']); 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/route.php b/config/route.php index 700fb4b..4ed7455 100644 --- a/config/route.php +++ b/config/route.php @@ -25,8 +25,6 @@ use Webman\Route; // POST /admin/user/{user_id}/kickout // body: {"tokens": ["..."]} // 踢下线 // POST /admin/user/{user_id}/kickout_all // 一键踢全部 -// 调试 -Route::any('/perm', [app\controller\IndexController::class, 'perm']); // 公共 Route::group('/common', function () { @@ -43,6 +41,7 @@ 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 () { @@ -98,3 +97,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/resource/translations/ar/messages.php b/resource/translations/ar/messages.php index a701f87..7430697 100644 --- a/resource/translations/ar/messages.php +++ b/resource/translations/ar/messages.php @@ -2,4 +2,35 @@ 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' => ' لا يمكن أن تكون الصورة فارغة', + '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..342ffad 100644 --- a/resource/translations/en/messages.php +++ b/resource/translations/en/messages.php @@ -2,4 +2,35 @@ 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', + '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..3a1839f 100644 --- a/resource/translations/ru/messages.php +++ b/resource/translations/ru/messages.php @@ -2,4 +2,35 @@ 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' => ' Фотографии не могут быть пустыми', + '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..96c3cb7 100644 --- a/resource/translations/zh/messages.php +++ b/resource/translations/zh/messages.php @@ -2,4 +2,35 @@ 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' => ' 图片不能为空', + '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