diff --git a/app/admin/controller/CorpController.php b/app/admin/controller/CorpController.php new file mode 100644 index 0000000..748f79f --- /dev/null +++ b/app/admin/controller/CorpController.php @@ -0,0 +1,40 @@ +service = new CorpService(); + } + + protected function indexFormat($data) + { + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; + } + + /** + * 如果想对某些接口做额外校验或改写, + * 也可以在这里覆盖 trait 提供的方法: + * + * public function store(Request $request) + * { + * // 自定义验证 + * $this->validate(...); + * // 然后再调用标准逻辑 + * return parent::store($request); + * } + */ +} \ No newline at end of file diff --git a/app/admin/controller/RoleController.php b/app/admin/controller/RoleController.php index 59ccb54..691e070 100644 --- a/app/admin/controller/RoleController.php +++ b/app/admin/controller/RoleController.php @@ -2,97 +2,116 @@ namespace app\admin\controller; -use app\common\validate\RoleValidate; -use app\common\service\RoleService; -use support\Request; +use app\common\controller\BaseController; +use app\admin\service\RoleService; +use app\common\traits\crud\CrudControllerTrait; -class RoleController extends \app\common\controller\BaseController +class RoleController extends BaseController { - /** - * 获取角色列表 - */ - public function list(Request $request) + + use CrudControllerTrait; + + protected $service; + + public function __construct() { - try { - $params = $request->all(); - $result = (new RoleService())->getRoleList($params); - // 定义后处理器 - $postProcessor = function($formattedData) { - if (isset($formattedData['created_at'])) - $formattedData['created_at'] = date('Y-m-d H:i:s', $formattedData['created_at']); - if (isset($formattedData['updated_at'])) - $formattedData['updated_at'] = date('Y-m-d H:i:s', $formattedData['updated_at']); - return $formattedData; - }; - return $this->success($this->transformData($result, $postProcessor), '角色列表获取成功'); - } catch (\Throwable $e) { - return $this->error($e->getMessage()); - } + $this->service = new RoleService(); } - /** - * 获取角色详情 - */ - public function detail(Request $request) + protected function indexFormat($data) { - try { - $roleId = $request->get('role_id'); - if (!$roleId) { - return $this->error('缺少角色ID', 422); - } - $result = (new RoleService())->getRoleDetail($roleId); - return $this->success($result, '角色详情获取成功'); - } catch (\Throwable $e) { - return $this->error($e->getMessage()); - } + if (isset($data['created_at'])) + $data['created_at'] = date('Y-m-d H:i:s', $data['created_at']); + if (isset($data['updated_at'])) + $data['updated_at'] = date('Y-m-d H:i:s', $data['updated_at']); + return $data; } - /** - * 增加角色 - */ - public function create(Request $request) - { - try { - $data = $this->validate(new RoleValidate(), $request->post(), 'role_create'); - $result = (new RoleService())->createRole($data); - return $this->success($result, '角色创建成功'); - } catch (\think\exception\ValidateException $e) { - return $this->error($e->getError(), 422); - } catch (\Throwable $e) { - return $this->error($e->getMessage()); - } - } + // /** + // * 获取角色列表 + // */ + // public function list(Request $request) + // { + // try { + // $params = $request->all(); + // $result = (new RoleService())->getRoleList($params); + // // 定义后处理器 + // $postProcessor = function($formattedData) { + // if (isset($formattedData['created_at'])) + // $formattedData['created_at'] = date('Y-m-d H:i:s', $formattedData['created_at']); + // if (isset($formattedData['updated_at'])) + // $formattedData['updated_at'] = date('Y-m-d H:i:s', $formattedData['updated_at']); + // return $formattedData; + // }; + // return $this->success($this->transformData($result, $postProcessor), '角色列表获取成功'); + // } catch (\Throwable $e) { + // return $this->error($e->getMessage()); + // } + // } - /** - * 修改角色 - */ - public function update(Request $request) - { - try { - $data = $this->validate(new RoleValidate(), $request->post(), 'role_update'); - $result = (new RoleService())->updateRole($data); - return $this->success($result, '角色更新成功'); - } catch (\think\exception\ValidateException $e) { - return $this->error($e->getError(), 422); - } catch (\Throwable $e) { - return $this->error($e->getMessage()); - } - } + // /** + // * 获取角色详情 + // */ + // public function detail(Request $request) + // { + // try { + // $roleId = $request->get('role_id'); + // if (!$roleId) { + // return $this->error('缺少角色ID', 422); + // } + // $result = (new RoleService())->getRoleDetail($roleId); + // return $this->success($result, '角色详情获取成功'); + // } catch (\Throwable $e) { + // return $this->error($e->getMessage()); + // } + // } - /** - * 删除角色 - */ - public function delete(Request $request) - { - try { - $roleId = $request->get('role_id'); - if (!$roleId) { - return $this->error('缺少角色ID', 422); - } - (new RoleService())->deleteRole($roleId); - return $this->success([], '角色删除成功'); - } catch (\Throwable $e) { - return $this->error($e->getMessage()); - } - } + // /** + // * 增加角色 + // */ + // public function create(Request $request) + // { + // try { + // $data = $this->validate(new RoleValidate(), $request->post(), 'role_create'); + // $result = (new RoleService())->createRole($data); + // return $this->success($result, '角色创建成功'); + // } catch (\think\exception\ValidateException $e) { + // return $this->error($e->getError(), 422); + // } catch (\Throwable $e) { + // return $this->error($e->getMessage()); + // } + // } + + // /** + // * 修改角色 + // */ + // public function update(Request $request) + // { + // try { + // $data = $this->validate(new RoleValidate(), $request->post(), 'role_update'); + // $result = (new RoleService())->updateRole($data); + // return $this->success($result, '角色更新成功'); + // } catch (\think\exception\ValidateException $e) { + // return $this->error($e->getError(), 422); + // } catch (\Throwable $e) { + // return $this->error($e->getMessage()); + // } + // } + + // /** + // * 删除角色 + // */ + // public function delete(Request $request) + // { + // try { + // $roleId = $request->get('role_id'); + // if (!$roleId) { + // return $this->error('缺少角色ID', 422); + // } + // (new RoleService())->deleteRole($roleId); + // return $this->success([], '角色删除成功'); + // } catch (\Throwable $e) { + // return $this->error($e->getMessage()); + // } + // } } diff --git a/app/admin/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 @@ +validator; + } + + + /** + * 如果需要自定义,比如强制加一个 where['status']=1, + * 只要在这里重写: + */ + // protected function buildQueryConfig(Request $request): array + // { + // $config = []; + // $config['where']['id'] = 2; + // return $config; + // } + + // /** + // * 复杂查询示例: + // * 只要在 URL 里添加 ?complex_query=1, + // * 并传入你需要的任何参数, + // * `serviceList()` 就会走到这里来: + // */ + // protected function complexQuery(Request $request): array + // { + // // 假设前端传了 date_from、date_to + // $from = $request->get('date_from'); + // $to = $request->get('date_to'); + + // // 交给 CrudUtil 执行一个自定义 query + // $pager = $this->logicComplex(function($q) use ($from, $to) { + // $q->where('status', 1); + + // if ($from && $to) { + // $q->whereBetween('created_at', [$from, $to]); + // } + + // // 再加个排序 + // $q->orderBy('created_at','desc'); + + // // 用 paginate 而不是 listAll + // return $q->paginate( + // /* perPage */ (int)request()->get('limit',15), + // ['*'], + // 'page', + // (int)request()->get('page',1) + // ); + // }); + + // // 把 Paginator 转成数组格式 + // return [ + // 'total' => $pager->total(), + // 'data' => $pager->items(), + // ]; + // } + + + // protected function afterServiceList(array $res): array + // { + // // 对列表的 data 做处理 + // $res['data'] = array_map(fn($i)=>[...], $res['data']); + // return $res; + // } +} \ No newline at end of file diff --git a/app/admin/service/RoleService.php b/app/admin/service/RoleService.php new file mode 100644 index 0000000..248bc3d --- /dev/null +++ b/app/admin/service/RoleService.php @@ -0,0 +1,31 @@ +validator; + } +} \ No newline at end of file diff --git a/app/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/Corp.php b/app/common/model/Corp.php index 52c160e..8820c80 100644 --- a/app/common/model/Corp.php +++ b/app/common/model/Corp.php @@ -46,4 +46,14 @@ class Corp extends Model public $timestamps = true; -} + protected $dateFormat = 'U'; + + + protected $fillable = [ + 'name', + 'abbr', + 'tenant_key', + 'sort', + 'status' + ]; +} \ No newline at end of file diff --git a/app/common/model/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/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/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/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/DataTransformer.php b/app/common/traits/DataTransformer.php index 42faf81..4f3abac 100644 --- a/app/common/traits/DataTransformer.php +++ b/app/common/traits/DataTransformer.php @@ -1,47 +1,51 @@ callable 字段名格式化回调,如 fn($f)=>snake_case($f) + * - 'postFormatter' => callable 结果级加工回调,接收整个数组并返回 * - * @return array + * @return array 转换并加工后的纯 PHP 数组 */ protected function transformData( mixed $rawData, - ?callable $postProcessor = null, + ?callable $postFormatter = null, ?callable $fieldFormatter = null ): array { try { $fieldFormatter = $fieldFormatter ?: fn($field) => $field; - if ($rawData instanceof LengthAwarePaginator) { - $formatted = $this->formatPaginated($rawData, $postProcessor, $fieldFormatter); + $arr = $this->formatPaginated($rawData, $postFormatter, $fieldFormatter); } elseif ($rawData instanceof Model) { - $formatted = $this->formatModel($rawData, $fieldFormatter); + $arr = $this->formatModel($rawData, $fieldFormatter); } elseif ($rawData instanceof Collection) { - $formatted = $this->formatCollection($rawData, $fieldFormatter); + $arr = $this->formatCollection($rawData, $fieldFormatter); } elseif (is_array($rawData)) { - $formatted = $this->formatArray($rawData, $fieldFormatter); + $arr = $this->formatArray($rawData, $fieldFormatter); } else { - $formatted = (array) $rawData; + $arr = (array)$rawData; } - if (is_callable($postProcessor)) { - $formatted = $postProcessor($formatted); + if (is_callable($postFormatter)) { + $arr = $postFormatter($arr); } - return $formatted; + return $arr; } catch (\Throwable $e) { return [ 'error' => true, @@ -52,84 +56,96 @@ trait DataTransformer } /** - * 格式化分页数据:将每条记录也进行 transformData 递归处理 + * 格式化分页数据:转换每条记录 * - * @param LengthAwarePaginator $paginator - * @param callable|null $postProcessor - * @param callable $fieldFormatter - * @return array + * @param LengthAwarePaginator $p + * @param callable $fmt + * @return array{records: array, total: int, current: int, size: int} */ protected function formatPaginated( - LengthAwarePaginator $paginator, - ?callable $postProcessor, - callable $fieldFormatter + LengthAwarePaginator $p, + ?callable $postFormatter, + callable $fmt ): array { - $list = $paginator->getCollection()->map(function ($item) use ($postProcessor, $fieldFormatter) { - return $this->transformData($item, $postProcessor, $fieldFormatter); - })->toArray(); + $records = $p->getCollection() + ->map(fn($item) => $this->transformData($item, $postFormatter, $fmt)) + ->toArray(); return [ - 'records' => $list, - 'total' => $paginator->total(), - 'current' => $paginator->currentPage(), - 'size' => $paginator->perPage() + 'records' => $records, + 'total' => $p->total(), + 'current' => $p->currentPage(), + 'size' => $p->perPage(), ]; } /** - * 格式化 Eloquent 模型:递归嵌套关联 + * 格式化 Eloquent 模型:主表属性 + 关联递归 * * @param Model $model - * @param callable $fieldFormatter + * @param callable $fmt * @return array */ - protected function formatModel(Model $model, callable $fieldFormatter): array - { - $formatted = []; - - // 主表字段 + protected function formatModel( + Model $model, + callable $fmt + ): array { + $out = []; + // 1) 主表字段 foreach ($model->getAttributes() as $key => $value) { - $formatted[$fieldFormatter($key)] = $value; + $out[$fmt($key)] = $value; } - - // 关联数据,递归嵌套 - foreach ($model->getRelations() as $relation => $relationData) { - if ($relationData instanceof Model) { - // 单个关联模型 - $formatted[$relation] = $this->formatModel($relationData, $fieldFormatter); - } elseif ($relationData instanceof Collection) { - // 关联集合 - $formatted[$relation] = $relationData - ->map(fn($item) => $this->transformData($item, null, $fieldFormatter)) + // 2) 关联递归 + foreach ($model->getRelations() as $rel => $relData) { + if ($relData instanceof Model) { + $out[$rel] = $this->formatModel($relData, $fmt); + } elseif ($relData instanceof Collection) { + $out[$rel] = $relData + ->map(fn($item) => $this->transformData($item, null, $fmt)) ->toArray(); + } else { + $out[$rel] = $relData; } } - - return $formatted; + return $out; } /** - * 格式化集合 + * 格式化集合数据:对每个元素递归调用 transformData * - * @param Collection $collection - * @param callable $fieldFormatter + * @param Collection $c + * @param callable $fmt * @return array */ - protected function formatCollection(Collection $collection, callable $fieldFormatter): array - { - return $collection->map(fn($item) => $this->transformData($item, null, $fieldFormatter)) - ->toArray(); + protected function formatCollection( + Collection $c, + callable $fmt + ): array { + return $c + ->map(fn($item) => $this->transformData($item, null, $fmt)) + ->toArray(); } /** - * 格式化普通数组 + * 格式化普通数组:递归处理可能出现的模型或集合 * - * @param array $array - * @param callable $fieldFormatter + * @param array $arr + * @param callable $fmt * @return array */ - protected function formatArray(array $array, callable $fieldFormatter): array - { - return array_map(fn($item) => $this->transformData($item, null, $fieldFormatter), $array); + protected function formatArray( + array $arr, + callable $fmt + ): array { + return array_map(function($item) use ($fmt) { + if (is_array($item) + || $item instanceof Model + || $item instanceof Collection + || $item instanceof LengthAwarePaginator + ) { + return $this->transformData($item, null, $fmt); + } + return $item; + }, $arr); } } \ No newline at end of file diff --git a/app/common/traits/FormatKvTrait.php b/app/common/traits/FormatKvTrait.php new file mode 100644 index 0000000..c3045a7 --- /dev/null +++ b/app/common/traits/FormatKvTrait.php @@ -0,0 +1,154 @@ +, + * fields: string[], + * include: string[] + * } + */ + protected function buildQueryConfig(Request $request, string $modelClass): array + { + $all = $request->all(); + + return [ + 'model' => $modelClass, + 'where' => $this->parseFilters($all), + 'order' => $this->parseSort($all['sort'] ?? ''), + 'fields' => $this->parseCsv($all['fields'] ?? ''), + 'include' => $this->parseCsv($all['include'] ?? ''), + ]; + } + + /** + * 把除 sort/fields/include/page/limit 以外的所有参数 + * 智能转换为 where 条件 + * + * 支持: + * - 等值:foo=bar + * - NOT:foo=!bar 或 foo=!bar1,bar2 + * - 范围:foo=2025-07-01,2025-07-15 → between + * - 列表:foo=a,b,c → in + * - NULL:foo=null + * - NOT NULL:foo=!null + * - 通配符:foo=xia* → like 'xia%' + * + * 关联字段直接写 relation.field=xxx + */ + protected function parseFilters(array $params): array + { + $where = []; + $reserved = ['sort','fields','include','page','limit']; + + foreach ($params as $key => $val) { + if (in_array($key, $reserved, true)) { + continue; + } + // NULL / NOT NULL + if ($val === 'null') { + $where[$key] = ['is null']; + continue; + } + if ($val === '!null') { + $where[$key] = ['is not null']; + continue; + } + // NOT equal or NOT IN + if (str_starts_with($val, '!')) { + $v = substr($val, 1); + if (str_contains($v, ',')) { + $list = array_map('trim', explode(',', $v)); + $where[$key] = ['not in', $list]; + } else { + $where[$key] = ['<>', $this->cast($v)]; + } + continue; + } + // BETWEEN or IN + if (str_contains($val, ',')) { + $parts = array_map('trim', explode(',', $val)); + if (count($parts) === 2 && $this->isRangeValue($parts[0], $parts[1])) { + $where[$key] = ['between', $parts]; + } else { + $where[$key] = ['in', $this->castArray($parts)]; + } + continue; + } + // LIKE + if (str_contains($val, '*') || str_contains($val, '%')) { + $pattern = str_replace('*', '%', $val); + $where[$key] = ['like', $pattern]; + continue; + } + // 默认 EQ + $where[$key] = $this->cast($val); + } + + return $where; + } + + /** + * 解析 sort=id,-created_at → ['id'=>'asc','created_at'=>'desc'] + */ + protected function parseSort(string $sort): array + { + $order = []; + foreach (explode(',', $sort) as $piece) { + $f = trim($piece); + if ($f === '') { + continue; + } + if ($f[0] === '-') { + $order[substr($f, 1)] = 'desc'; + } else { + $order[$f] = 'asc'; + } + } + return $order; + } + + /** + * 逗号分割成数组 + */ + protected function parseCsv(string $csv): array + { + if ($csv === '') { + return []; + } + return array_filter(array_map('trim', explode(',', $csv))); + } + + /** 简单类型转换:全数字→int/float,其余保留原字符串 */ + protected function cast(string $v): mixed + { + if (is_numeric($v)) { + return strpos($v, '.') !== false ? (float)$v : (int)$v; + } + return $v; + } + + /** 批量类型转换 */ + protected function castArray(array $arr): array + { + return array_map(fn($v) => $this->cast($v), $arr); + } + + /** 判断两端是否可做 BETWEEN(数字或 YYYY-MM-DD 日期) */ + protected function isRangeValue(string $a, string $b): bool + { + return (is_numeric($a) && is_numeric($b)) + || (preg_match('/^\d{4}-\d{2}-\d{2}$/', $a) + && preg_match('/^\d{4}-\d{2}-\d{2}$/', $b)); + } +} diff --git a/app/common/traits/RequestTrait.php b/app/common/traits/RequestTrait.php new file mode 100644 index 0000000..8fefcc4 --- /dev/null +++ b/app/common/traits/RequestTrait.php @@ -0,0 +1,38 @@ +scene($scene); + } + // 执行数据验证 + if (!$validator->check($data)) { + // 验证失败,抛出业务异常,异常信息为验证器返回的错误信息 + throw new BusinessException($validator->getError()); + } + // 验证成功,返回原始数据 + return $data; + } + + +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudControllerTrait.php b/app/common/traits/crud/CrudControllerTrait.php new file mode 100644 index 0000000..c5cdfdc --- /dev/null +++ b/app/common/traits/crud/CrudControllerTrait.php @@ -0,0 +1,148 @@ +service->serviceList($request); + $data = $this->transformData($raw, method_exists($this, 'indexFormat') + ? [$this, 'indexFormat'] + : null); + return $this->success($data); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 获取详情 + * + * @param Request $request 请求对象 + * @param int|string $id 资源 ID + * @return mixed 返回资源详情数据 + */ + public function show(Request $request) + { + try { + $raw = $this->service->serviceShow($request); + $data = $this->transformData($raw, method_exists($this, 'showFormat') + ? [$this, 'showFormat'] + : null); + return $this->success($data); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 创建 + * + * @param Request $request 请求对象 + * @return mixed 返回创建成功的数据或信息 + */ + public function store(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'create'); + $res = $this->service->serviceStore($request, $data); + return $this->success($res, '创建成功'); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 更新 + * + * @param Request $request 请求对象 + * @return mixed 返回更新成功的数据或信息 + */ + public function update(Request $request) + { + try { + $data = $this->validate($this->service->getValidator(), $request->post(), 'update'); + $res = $this->service->serviceUpdate($request, $data); + return $this->success([], '更新成功'); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } + } + + /** + * 删除 + * + * @param Request $request 请求对象 + * @return mixed 返回删除操作结果 + */ + public function destroy(Request $request) + { + try { + $count = $this->service->serviceDestroy($request); + return $this->success([], "成功删除了 {$count} 条"); + } catch (\Exception $e) { + return $this->error($e->getMessage()); + } + } + + + // ---------------------------------------------------------------- + // 以下三个钩子方法,**在使用此 Trait 的 Controller** 中实现 + // ---------------------------------------------------------------- + + /** + * 字段名格式化:例如全部下划线 + * + * @param string $field + * @return string + */ + // protected function fieldFormatter(string $field): string + // { + // return snake_case($field); + // } + + /** + * 字段级后置加工规则 + * + * @return array + * 键为 “点号嵌套”或 “通配符” 字段路径, + * 值为 fn($value, $row)=>mixed + */ + // protected function getFieldTransforms(): array + // { + // return [ + // 'name' => fn($v,$row)=>strtoupper($v), + // 'orders.*.total' => fn($v,$row)=>number_format($v,2), + // ]; + // } + + /** + * 结果级后置加工:接收整个数组,可任意增删改 + * + * @param array $data + * @return array + */ + // protected function postFormat(array $data): array + // { + // // 例如重命名 records 为 items + // if (isset($data['records'])) { + // $data['items'] = $data['records']; + // unset($data['records']); + // } + // return $data; + // } +} \ No newline at end of file diff --git a/app/common/traits/crud/CrudLogicTrait.php b/app/common/traits/crud/CrudLogicTrait.php new file mode 100644 index 0000000..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..c042b5f --- /dev/null +++ b/app/common/traits/crud/CrudServiceTrait.php @@ -0,0 +1,217 @@ +validator; + } + + /** 分页列表 */ + public function serviceList(Request $request) + { + if (method_exists($this, 'complexQuery')) { + $result = $this->complexQuery($request->get()); + } else { + $config = $this->buildQueryConfig($request, 'index'); + $limit = (int)$request->get('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, array $data = []) + { + // $data = $request->post(); + $dl = $this->getDataLimitFromRequest($request); + $created = $this->logicCreate($data, $dl); + return $this->postProcess('Store', $created); + } + + /** 更新 */ + public function serviceUpdate(Request $request, array $data = []) + { + // $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/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/controller/IndexController.php b/app/controller/IndexController.php index 0d7b7f7..866d3d5 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..a03e632 100644 --- a/app/middleware/AuthCheck.php +++ b/app/middleware/AuthCheck.php @@ -12,8 +12,10 @@ class AuthCheck implements MiddlewareInterface public function process(Request $request, callable $handler) : Response { $controller = new ReflectionClass($request->controller); - $noNeedLogin = $controller->getDefaultProperties()['noNeedLogin'] ?? []; - + $properties = $controller->getDefaultProperties(); + $noNeedLogin = $properties['noNeedLogin'] ?? []; + $noNeedAuth = $properties['noNeedAuth'] ?? []; + if (in_array($request->action, $noNeedLogin)) { // 白名单 return $handler($request); @@ -31,6 +33,7 @@ class AuthCheck implements MiddlewareInterface } $token = $matches[1]; $request->user = (new Jwt())->verifyToken($token); + return $handler($request); } catch (\Throwable $e) { $body = [ 'status' => 'fail', @@ -40,7 +43,8 @@ class AuthCheck implements MiddlewareInterface ]; return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); } - return $handler($request); } + + return $handler($request); } } \ No newline at end of file diff --git a/app/middleware/BasicData.php b/app/middleware/BasicData.php index 472456e..9df57e7 100644 --- a/app/middleware/BasicData.php +++ b/app/middleware/BasicData.php @@ -12,9 +12,9 @@ class BasicData implements MiddlewareInterface public function process(Request $request, callable $handler): Response { $controller = new ReflectionClass($request->controller); - $noNeedHeader = $controller->getDefaultProperties()['noNeedHeader'] ?? []; + $noNeedHeaderData = $controller->getDefaultProperties()['noNeedHeaderData'] ?? []; - if (in_array($request->action, $noNeedHeader) || in_array($request->path(), config('whitelist.basi_cdata'))) { + if (in_array($request->action, $noNeedHeaderData)) { // 白名单 return $handler($request); } else { @@ -35,7 +35,13 @@ class BasicData implements MiddlewareInterface } return $handler($request); } catch (\Throwable $th) { - throw $th; + $body = [ + 'status' => 'fail', + 'code' => 403, + 'message' => $th->getMessage(), + 'data' => [], + ]; + return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']); } } } diff --git a/app/middleware/PermissionMiddleware.php b/app/middleware/PermissionMiddleware.php deleted file mode 100644 index 9642b52..0000000 --- a/app/middleware/PermissionMiddleware.php +++ /dev/null @@ -1,33 +0,0 @@ -permission_code = $permission_code; - } - - public function process(Request $request, callable $next): Response - { - // 假设有 auth() helper 方法能获取当前登录用户ID - $user_id = $request->session()->get('user_id'); - if (!$user_id) { - return json(['code' => 401, 'msg' => '未登录']); - } - - if ($this->permission_code && !AuthService::check($user_id, $this->permission_code)) { - return json(['code' => 403, 'msg' => '没有权限']); - } - - return $next($request); - } -} diff --git a/config/route.php b/config/route.php index d0c9db4..4ed7455 100644 --- a/config/route.php +++ b/config/route.php @@ -26,6 +26,7 @@ use Webman\Route; // POST /admin/user/{user_id}/kickout_all // 一键踢全部 +// 公共 Route::group('/common', function () { Route::group('/auth', function () { Route::any('/login', [app\common\controller\AuthController::class, 'login']); @@ -49,13 +50,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('/store', [app\admin\controller\CorpController::class, 'store']); + Route::any('/update', [app\admin\controller\CorpController::class, 'update']); + Route::any('/destroy', [app\admin\controller\CorpController::class, 'destroy']); }); });