commit 11c177a432b197ea271dc78769cbd8f5690ba82f Author: li Date: Fri Mar 13 19:52:09 2026 +0800 feat: 第一阶段基础框架搭建 后端 (Laravel 10 + Sanctum): - RBAC 四层权限系统 (users→roles→permissions→menus) - 门店隔离中间件 (BelongsToStore Trait + StoreIsolation Middleware) - 操作日志中间件 (自动记录写操作) - 权限检查中间件 (CheckPermission) - 16张数据库表迁移 (系统基础+RBAC+字典+配置) - 11个 Eloquent Model - Auth API (登录/登出/用户信息) - 系统设置模块 CRUD (门店/部门/职务/用户/角色/菜单/权限/字典/日志) - 45条 RESTful API 路由 - InitSeeder 初始数据 (超管/角色/76权限/31菜单) 前端 (Vue 3 + Element Plus + Vite): - Axios 请求封装 + Token 注入 - Pinia 状态管理 (user + permission store) - 动态路由 (服务端菜单→前端路由自动生成) - 后台布局 (侧边栏+顶栏+主内容区) - 登录页 + 仪表盘首页 - 系统设置 7 个 CRUD 页面 技术方案文档 (7卷): - 技术总览/数据库设计/API规范/RBAC设计/模块详设/小程序设计/部署方案 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9334f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Environment +.env +.env.local +.env.*.local + +# Frontend +frontend/node_modules/ +frontend/dist/ + +# Backend (Laravel handles its own via backend/.gitignore) + +# Docker +docker/mysql/data/ +docker/redis/data/ + +# Logs +*.log diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..c0660ea --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,65 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=sqlite +# DB_HOST=127.0.0.1 +# DB_PORT=3306 +# DB_DATABASE=laravel +# DB_USERNAME=root +# DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/backend/.gitattributes b/backend/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/backend/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..b71b1ea --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,24 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..0165a77 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,59 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com)** +- **[Tighten Co.](https://tighten.co)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Redberry](https://redberry.international/laravel-development)** +- **[Active Logic](https://activelogic.com)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/backend/app/Enums/MenuType.php b/backend/app/Enums/MenuType.php new file mode 100644 index 0000000..181cef3 --- /dev/null +++ b/backend/app/Enums/MenuType.php @@ -0,0 +1,19 @@ + '目录', + self::Menu => '菜单', + self::Button => '按钮', + }; + } +} diff --git a/backend/app/Enums/PermissionType.php b/backend/app/Enums/PermissionType.php new file mode 100644 index 0000000..ca20701 --- /dev/null +++ b/backend/app/Enums/PermissionType.php @@ -0,0 +1,19 @@ + '菜单', + self::Button => '按钮', + self::Api => '接口', + }; + } +} diff --git a/backend/app/Enums/UserStatus.php b/backend/app/Enums/UserStatus.php new file mode 100644 index 0000000..94c4d9c --- /dev/null +++ b/backend/app/Enums/UserStatus.php @@ -0,0 +1,19 @@ + '待审核', + self::Active => '正常', + self::Disabled => '禁用', + }; + } +} diff --git a/backend/app/Http/Controllers/Admin/AuthController.php b/backend/app/Http/Controllers/Admin/AuthController.php new file mode 100644 index 0000000..f8f3f95 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/AuthController.php @@ -0,0 +1,88 @@ +validate([ + 'username' => 'required|string', + 'password' => 'required|string', + ]); + + $user = User::withoutGlobalScope('store') + ->where('username', $request->username) + ->first(); + + if (!$user || !Hash::check($request->password, $user->password)) { + return $this->error('用户名或密码错误', 40100, 401); + } + + if ($user->status->value !== 1) { + return $this->error('账号已被禁用或待审核', 40101, 403); + } + + // 清除旧 token + $user->tokens()->delete(); + + $token = $user->createToken('admin')->plainTextToken; + + $user->update(['last_login_at' => now()]); + + return $this->success([ + 'token' => $token, + 'user' => [ + 'id' => $user->id, + 'username' => $user->username, + 'name' => $user->name, + 'avatar' => $user->avatar, + 'store_id' => $user->store_id, + 'is_super' => $user->is_super, + ], + ]); + } + + /** + * 退出登录 + */ + public function logout(Request $request): JsonResponse + { + $request->user()->currentAccessToken()->delete(); + return $this->success(null, '退出成功'); + } + + /** + * 获取当前用户信息 + */ + public function me(Request $request): JsonResponse + { + $user = $request->user(); + $user->load(['department', 'position', 'roles']); + + return $this->success([ + 'id' => $user->id, + 'username' => $user->username, + 'name' => $user->name, + 'phone' => $user->phone, + 'email' => $user->email, + 'avatar' => $user->avatar, + 'store_id' => $user->store_id, + 'is_super' => $user->is_super, + 'department' => $user->department?->name, + 'position' => $user->position?->name, + 'roles' => $user->roles->pluck('name'), + 'permissions' => $user->getAllPermissionCodes(), + 'menus' => $user->getMenuTree(), + ]); + } +} diff --git a/backend/app/Http/Controllers/Admin/System/DepartmentController.php b/backend/app/Http/Controllers/Admin/System/DepartmentController.php new file mode 100644 index 0000000..003db50 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/DepartmentController.php @@ -0,0 +1,80 @@ +get()->toArray(); + $tree = $this->buildTree($departments); + return $this->success($tree); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'parent_id' => 'integer', + 'name' => 'required|string|max:50', + 'sort' => 'integer', + 'status' => 'in:0,1', + ]); + + $data['store_id'] = $data['store_id'] ?? auth()->user()->store_id; + $department = Department::create($data); + return $this->success($department, '创建成功'); + } + + public function update(Request $request, Department $department): JsonResponse + { + $data = $request->validate([ + 'parent_id' => 'integer', + 'name' => 'string|max:50', + 'sort' => 'integer', + 'status' => 'in:0,1', + ]); + + // 不能设自己为上级 + if (isset($data['parent_id']) && $data['parent_id'] == $department->id) { + return $this->error('不能设置自身为上级部门'); + } + + $department->update($data); + return $this->success($department, '更新成功'); + } + + public function destroy(Department $department): JsonResponse + { + if ($department->children()->exists()) { + return $this->error('该部门下还有子部门,无法删除'); + } + if ($department->users()->exists()) { + return $this->error('该部门下还有员工,无法删除'); + } + $department->delete(); + return $this->success(null, '删除成功'); + } + + private function buildTree(array $items, int $parentId = 0): array + { + $tree = []; + foreach ($items as $item) { + if ($item['parent_id'] == $parentId) { + $children = $this->buildTree($items, $item['id']); + if ($children) { + $item['children'] = $children; + } + $tree[] = $item; + } + } + return $tree; + } +} diff --git a/backend/app/Http/Controllers/Admin/System/DictionaryController.php b/backend/app/Http/Controllers/Admin/System/DictionaryController.php new file mode 100644 index 0000000..7ca3e96 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/DictionaryController.php @@ -0,0 +1,91 @@ +orderByDesc('id')->get(); + return $this->success($dictionaries); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'name' => 'required|string|max:50', + 'code' => 'required|string|max:50|unique:dictionaries,code', + 'description' => 'nullable|string|max:255', + ]); + + $dict = Dictionary::create($data); + return $this->success($dict, '创建成功'); + } + + /** + * 字典项列表 + */ + public function items(Dictionary $dictionary): JsonResponse + { + return $this->success($dictionary->items); + } + + /** + * 新建字典项 + */ + public function storeItem(Request $request, Dictionary $dictionary): JsonResponse + { + $data = $request->validate([ + 'label' => 'required|string|max:100', + 'value' => 'required|string|max:100', + 'sort' => 'integer', + 'status' => 'in:0,1', + ]); + + $data['dictionary_id'] = $dictionary->id; + $item = DictionaryItem::create($data); + return $this->success($item, '创建成功'); + } + + /** + * 更新字典项 + */ + public function updateItem(Request $request, DictionaryItem $item): JsonResponse + { + $data = $request->validate([ + 'label' => 'string|max:100', + 'value' => 'string|max:100', + 'sort' => 'integer', + 'status' => 'in:0,1', + ]); + + $item->update($data); + return $this->success($item, '更新成功'); + } + + /** + * 删除字典项 + */ + public function destroyItem(DictionaryItem $item): JsonResponse + { + $item->delete(); + return $this->success(null, '删除成功'); + } + + public function destroy(Dictionary $dictionary): JsonResponse + { + if ($dictionary->is_system) { + return $this->error('系统字典不可删除'); + } + + $dictionary->items()->delete(); + $dictionary->delete(); + return $this->success(null, '删除成功'); + } +} diff --git a/backend/app/Http/Controllers/Admin/System/MenuController.php b/backend/app/Http/Controllers/Admin/System/MenuController.php new file mode 100644 index 0000000..c5e3d08 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/MenuController.php @@ -0,0 +1,71 @@ +success(Menu::tree()); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'parent_id' => 'integer', + 'name' => 'required|string|max:50', + 'path' => 'nullable|string|max:255', + 'component' => 'nullable|string|max:255', + 'icon' => 'nullable|string|max:50', + 'permission_code' => 'nullable|string|max:100', + 'type' => 'required|in:1,2,3', + 'visible' => 'in:0,1', + 'sort' => 'integer', + 'status' => 'in:0,1', + ]); + + $menu = Menu::create($data); + return $this->success($menu, '创建成功'); + } + + public function update(Request $request, Menu $menu): JsonResponse + { + $data = $request->validate([ + 'parent_id' => 'integer', + 'name' => 'string|max:50', + 'path' => 'nullable|string|max:255', + 'component' => 'nullable|string|max:255', + 'icon' => 'nullable|string|max:50', + 'permission_code' => 'nullable|string|max:100', + 'type' => 'in:1,2,3', + 'visible' => 'in:0,1', + 'sort' => 'integer', + 'status' => 'in:0,1', + ]); + + if (isset($data['parent_id']) && $data['parent_id'] == $menu->id) { + return $this->error('不能设置自身为上级菜单'); + } + + $menu->update($data); + return $this->success($menu, '更新成功'); + } + + public function destroy(Menu $menu): JsonResponse + { + if ($menu->children()->exists()) { + return $this->error('该菜单下还有子菜单,无法删除'); + } + + $menu->delete(); + return $this->success(null, '删除成功'); + } +} diff --git a/backend/app/Http/Controllers/Admin/System/OperationLogController.php b/backend/app/Http/Controllers/Admin/System/OperationLogController.php new file mode 100644 index 0000000..e28ff22 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/OperationLogController.php @@ -0,0 +1,39 @@ +user(); + if (!$user->is_super) { + $query->where('store_id', $user->store_id); + } + + if ($request->keyword) { + $query->where(function ($q) use ($request) { + $q->where('user_name', 'like', "%{$request->keyword}%") + ->orWhere('target', 'like', "%{$request->keyword}%"); + }); + } + if ($request->module) { + $query->where('module', $request->module); + } + if ($request->start_date && $request->end_date) { + $query->whereBetween('created_at', [$request->start_date, $request->end_date . ' 23:59:59']); + } + + $logs = $query->orderByDesc('created_at') + ->paginate($request->input('page_size', 20)); + + return $this->paginate($logs); + } +} diff --git a/backend/app/Http/Controllers/Admin/System/PermissionController.php b/backend/app/Http/Controllers/Admin/System/PermissionController.php new file mode 100644 index 0000000..61d7e21 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/PermissionController.php @@ -0,0 +1,62 @@ +success(Permission::tree()); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'parent_id' => 'integer', + 'name' => 'required|string|max:50', + 'code' => 'required|string|max:100|unique:permissions,code', + 'module' => 'nullable|string|max:50', + 'type' => 'required|in:1,2,3', + 'description' => 'nullable|string|max:255', + 'sort' => 'integer', + ]); + + $permission = Permission::create($data); + return $this->success($permission, '创建成功'); + } + + public function update(Request $request, Permission $permission): JsonResponse + { + $data = $request->validate([ + 'parent_id' => 'integer', + 'name' => 'string|max:50', + 'code' => 'string|max:100|unique:permissions,code,' . $permission->id, + 'module' => 'nullable|string|max:50', + 'type' => 'in:1,2,3', + 'description' => 'nullable|string|max:255', + 'sort' => 'integer', + ]); + + $permission->update($data); + return $this->success($permission, '更新成功'); + } + + public function destroy(Permission $permission): JsonResponse + { + if ($permission->children()->exists()) { + return $this->error('该权限下有子权限,无法删除'); + } + + $permission->roles()->detach(); + $permission->delete(); + return $this->success(null, '删除成功'); + } +} diff --git a/backend/app/Http/Controllers/Admin/System/PositionController.php b/backend/app/Http/Controllers/Admin/System/PositionController.php new file mode 100644 index 0000000..98285d3 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/PositionController.php @@ -0,0 +1,48 @@ +paginate($request->input('page_size', 50)); + + return $this->paginate($positions); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'name' => 'required|string|max:50', + 'sort' => 'integer', + ]); + + $data['store_id'] = auth()->user()->store_id; + $position = Position::create($data); + return $this->success($position, '创建成功'); + } + + public function update(Request $request, Position $position): JsonResponse + { + $data = $request->validate([ + 'name' => 'string|max:50', + 'sort' => 'integer', + ]); + + $position->update($data); + return $this->success($position, '更新成功'); + } + + public function destroy(Position $position): JsonResponse + { + $position->delete(); + return $this->success(null, '删除成功'); + } +} diff --git a/backend/app/Http/Controllers/Admin/System/RoleController.php b/backend/app/Http/Controllers/Admin/System/RoleController.php new file mode 100644 index 0000000..4a9f7f8 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/RoleController.php @@ -0,0 +1,98 @@ +keyword) { + $query->where('name', 'like', "%{$request->keyword}%"); + } + + // 门店角色 + 全局角色 + $user = auth()->user(); + if (!$user->is_super) { + $query->where(function ($q) use ($user) { + $q->where('store_id', $user->store_id) + ->orWhereNull('store_id'); + }); + } + + $roles = $query->orderByDesc('id') + ->paginate($request->input('page_size', 20)); + + return $this->paginate($roles); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'name' => 'required|string|max:50', + 'code' => 'required|string|max:50|unique:roles,code', + 'description' => 'nullable|string|max:255', + ]); + + $user = auth()->user(); + $data['store_id'] = $user->is_super ? null : $user->store_id; + + $role = Role::create($data); + return $this->success($role, '创建成功'); + } + + public function show(Role $role): JsonResponse + { + $role->load('permissions'); + return $this->success($role); + } + + public function update(Request $request, Role $role): JsonResponse + { + if ($role->is_system) { + return $this->error('系统内置角色不可编辑'); + } + + $data = $request->validate([ + 'name' => 'string|max:50', + 'description' => 'nullable|string|max:255', + ]); + + $role->update($data); + return $this->success($role, '更新成功'); + } + + /** + * 分配权限 + */ + public function assignPermissions(Request $request, Role $role): JsonResponse + { + $request->validate([ + 'permission_ids' => 'required|array', + 'permission_ids.*' => 'exists:permissions,id', + ]); + + $role->permissions()->sync($request->permission_ids); + return $this->success(null, '权限分配成功'); + } + + public function destroy(Role $role): JsonResponse + { + if ($role->is_system) { + return $this->error('系统内置角色不可删除'); + } + if ($role->users()->exists()) { + return $this->error('该角色下还有用户,无法删除'); + } + + $role->permissions()->detach(); + $role->delete(); + return $this->success(null, '删除成功'); + } +} diff --git a/backend/app/Http/Controllers/Admin/System/StoreController.php b/backend/app/Http/Controllers/Admin/System/StoreController.php new file mode 100644 index 0000000..34a47b2 --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/StoreController.php @@ -0,0 +1,73 @@ +keyword) { + $query->where('name', 'like', "%{$request->keyword}%"); + } + if ($request->has('status')) { + $query->where('status', $request->status); + } + + $stores = $query->orderByDesc('id') + ->paginate($request->input('page_size', 20)); + + return $this->paginate($stores); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'region_id' => 'required|exists:regions,id', + 'name' => 'required|string|max:100', + 'code' => 'nullable|string|max:20|unique:stores,code', + 'address' => 'nullable|string|max:255', + 'phone' => 'nullable|string|max:20', + 'status' => 'in:0,1', + ]); + + $store = Store::create($data); + return $this->success($store, '创建成功'); + } + + public function show(Store $store): JsonResponse + { + $store->load('region'); + return $this->success($store); + } + + public function update(Request $request, Store $store): JsonResponse + { + $data = $request->validate([ + 'region_id' => 'exists:regions,id', + 'name' => 'string|max:100', + 'code' => 'nullable|string|max:20|unique:stores,code,' . $store->id, + 'address' => 'nullable|string|max:255', + 'phone' => 'nullable|string|max:20', + 'status' => 'in:0,1', + ]); + + $store->update($data); + return $this->success($store, '更新成功'); + } + + public function destroy(Store $store): JsonResponse + { + if ($store->users()->exists()) { + return $this->error('该门店下还有员工,无法删除'); + } + $store->delete(); + return $this->success(null, '删除成功'); + } +} diff --git a/backend/app/Http/Controllers/Admin/System/UserController.php b/backend/app/Http/Controllers/Admin/System/UserController.php new file mode 100644 index 0000000..ce3777d --- /dev/null +++ b/backend/app/Http/Controllers/Admin/System/UserController.php @@ -0,0 +1,123 @@ +keyword) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', "%{$request->keyword}%") + ->orWhere('phone', 'like', "%{$request->keyword}%") + ->orWhere('username', 'like', "%{$request->keyword}%"); + }); + } + if ($request->department_id) { + $query->where('department_id', $request->department_id); + } + if ($request->has('status')) { + $query->where('status', $request->status); + } + + $users = $query->orderByDesc('id') + ->paginate($request->input('page_size', 20)); + + return $this->paginate($users); + } + + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'username' => 'required|string|max:50|unique:users,username', + 'password' => 'required|string|min:6', + 'name' => 'required|string|max:50', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'department_id' => 'nullable|exists:departments,id', + 'position_id' => 'nullable|exists:positions,id', + 'status' => 'in:0,1,2', + 'role_ids' => 'array', + 'role_ids.*' => 'exists:roles,id', + ]); + + $roleIds = $data['role_ids'] ?? []; + unset($data['role_ids']); + + $data['store_id'] = auth()->user()->store_id; + $user = User::create($data); + + if ($roleIds) { + $user->roles()->sync($roleIds); + } + + return $this->success($user->load('roles'), '创建成功'); + } + + public function show(User $user): JsonResponse + { + $user->load(['department', 'position', 'roles']); + return $this->success($user); + } + + public function update(Request $request, User $user): JsonResponse + { + $data = $request->validate([ + 'name' => 'string|max:50', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'department_id' => 'nullable|exists:departments,id', + 'position_id' => 'nullable|exists:positions,id', + 'role_ids' => 'array', + 'role_ids.*' => 'exists:roles,id', + ]); + + $roleIds = $data['role_ids'] ?? null; + unset($data['role_ids']); + + $user->update($data); + + if ($roleIds !== null) { + $user->roles()->sync($roleIds); + } + + return $this->success($user->load('roles'), '更新成功'); + } + + /** + * 更新状态 + */ + public function updateStatus(Request $request, User $user): JsonResponse + { + $request->validate(['status' => 'required|in:0,1,2']); + $user->update(['status' => $request->status]); + return $this->success(null, '状态更新成功'); + } + + /** + * 重置密码 + */ + public function resetPassword(Request $request, User $user): JsonResponse + { + $request->validate(['password' => 'required|string|min:6']); + $user->update(['password' => $request->password]); + return $this->success(null, '密码重置成功'); + } + + public function destroy(User $user): JsonResponse + { + if ($user->is_super) { + return $this->error('超级管理员不可删除'); + } + $user->delete(); + return $this->success(null, '删除成功'); + } +} diff --git a/backend/app/Http/Controllers/Controller.php b/backend/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..a4101d3 --- /dev/null +++ b/backend/app/Http/Controllers/Controller.php @@ -0,0 +1,47 @@ +json([ + 'code' => 0, + 'message' => $message, + 'data' => $data, + ]); + } + + /** + * 分页响应 + */ + protected function paginate($paginator): JsonResponse + { + return response()->json([ + 'code' => 0, + 'data' => [ + 'list' => $paginator->items(), + 'total' => $paginator->total(), + 'page' => $paginator->currentPage(), + 'page_size' => $paginator->perPage(), + ], + ]); + } + + /** + * 错误响应 + */ + protected function error(string $message, int $code = 40001, int $httpCode = 400): JsonResponse + { + return response()->json([ + 'code' => $code, + 'message' => $message, + ], $httpCode); + } +} diff --git a/backend/app/Http/Middleware/CheckPermission.php b/backend/app/Http/Middleware/CheckPermission.php new file mode 100644 index 0000000..fa21f71 --- /dev/null +++ b/backend/app/Http/Middleware/CheckPermission.php @@ -0,0 +1,39 @@ +user(); + + if (!$user) { + return response()->json(['code' => 40100, 'message' => '未认证'], 401); + } + + // 超级管理员直接放行 + if ($user->is_super) { + return $next($request); + } + + // 检查权限 + foreach ($permissions as $permission) { + if (!$user->hasPermission($permission)) { + return response()->json([ + 'code' => 40300, + 'message' => '权限不足:' . $permission, + ], 403); + } + } + + return $next($request); + } +} diff --git a/backend/app/Http/Middleware/OperationLog.php b/backend/app/Http/Middleware/OperationLog.php new file mode 100644 index 0000000..5ecb739 --- /dev/null +++ b/backend/app/Http/Middleware/OperationLog.php @@ -0,0 +1,86 @@ +method(), ['POST', 'PUT', 'PATCH', 'DELETE'])) { + return $response; + } + + // 排除白名单 + foreach ($this->except as $pattern) { + if ($request->is($pattern)) { + return $response; + } + } + + $user = $request->user(); + if (!$user) { + return $response; + } + + try { + // 从 URL 推断模块和操作 + $path = $request->path(); + $module = $this->parseModule($path); + $action = $this->parseAction($request->method()); + + OperationLogModel::create([ + 'store_id' => $user->store_id, + 'user_id' => $user->id, + 'user_name' => $user->name, + 'module' => $module, + 'action' => $action, + 'target' => $path, + 'ip' => $request->ip(), + 'method' => $request->method(), + 'url' => $request->fullUrl(), + 'created_at' => now(), + ]); + } catch (\Throwable $e) { + // 日志记录失败不影响业务 + \Log::warning('操作日志记录失败: ' . $e->getMessage()); + } + + return $response; + } + + private function parseModule(string $path): string + { + // api/v1/system/users → system + $segments = explode('/', $path); + return $segments[2] ?? 'unknown'; + } + + private function parseAction(string $method): string + { + return match ($method) { + 'POST' => 'create', + 'PUT', 'PATCH' => 'update', + 'DELETE' => 'delete', + default => $method, + }; + } +} diff --git a/backend/app/Http/Middleware/StoreIsolation.php b/backend/app/Http/Middleware/StoreIsolation.php new file mode 100644 index 0000000..19fc80b --- /dev/null +++ b/backend/app/Http/Middleware/StoreIsolation.php @@ -0,0 +1,33 @@ +user(); + + if (!$user) { + return response()->json(['code' => 40100, 'message' => '未认证'], 401); + } + + // 超级管理员可通过 Header 切换门店 + if ($user->is_super && $request->header('X-Store-Id')) { + $storeId = (int) $request->header('X-Store-Id'); + // 验证门店存在 + if (!\App\Models\Store::where('id', $storeId)->exists()) { + return response()->json(['code' => 40400, 'message' => '门店不存在'], 404); + } + } + + return $next($request); + } +} diff --git a/backend/app/Models/Department.php b/backend/app/Models/Department.php new file mode 100644 index 0000000..10cad49 --- /dev/null +++ b/backend/app/Models/Department.php @@ -0,0 +1,38 @@ + 'integer', + ]; + + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id'); + } + + public function users(): HasMany + { + return $this->hasMany(User::class); + } + + /** + * 递归获取部门树 + */ + public function childrenRecursive(): HasMany + { + return $this->children()->with('childrenRecursive'); + } +} diff --git a/backend/app/Models/Dictionary.php b/backend/app/Models/Dictionary.php new file mode 100644 index 0000000..667ecc7 --- /dev/null +++ b/backend/app/Models/Dictionary.php @@ -0,0 +1,20 @@ + 'boolean', + ]; + + public function items(): HasMany + { + return $this->hasMany(DictionaryItem::class)->orderBy('sort'); + } +} diff --git a/backend/app/Models/DictionaryItem.php b/backend/app/Models/DictionaryItem.php new file mode 100644 index 0000000..5c23a26 --- /dev/null +++ b/backend/app/Models/DictionaryItem.php @@ -0,0 +1,20 @@ + 'integer', + ]; + + public function dictionary(): BelongsTo + { + return $this->belongsTo(Dictionary::class); + } +} diff --git a/backend/app/Models/Menu.php b/backend/app/Models/Menu.php new file mode 100644 index 0000000..ab501e7 --- /dev/null +++ b/backend/app/Models/Menu.php @@ -0,0 +1,55 @@ + MenuType::class, + 'visible' => 'integer', + 'status' => 'integer', + ]; + + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id'); + } + + public function childrenRecursive(): HasMany + { + return $this->children()->with('childrenRecursive')->orderBy('sort'); + } + + /** + * 获取完整菜单树 + */ + public static function tree(): array + { + $all = self::where('status', 1)->orderBy('sort')->get()->toArray(); + return self::buildTree($all); + } + + private static function buildTree(array $items, int $parentId = 0): array + { + $tree = []; + foreach ($items as $item) { + if ($item['parent_id'] == $parentId) { + $children = self::buildTree($items, $item['id']); + if ($children) { + $item['children'] = $children; + } + $tree[] = $item; + } + } + return $tree; + } +} diff --git a/backend/app/Models/OperationLog.php b/backend/app/Models/OperationLog.php new file mode 100644 index 0000000..96aad09 --- /dev/null +++ b/backend/app/Models/OperationLog.php @@ -0,0 +1,28 @@ + 'array', + 'after' => 'array', + 'created_at' => 'datetime', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/backend/app/Models/Permission.php b/backend/app/Models/Permission.php new file mode 100644 index 0000000..01c2853 --- /dev/null +++ b/backend/app/Models/Permission.php @@ -0,0 +1,53 @@ + PermissionType::class, + ]; + + public function children(): HasMany + { + return $this->hasMany(self::class, 'parent_id'); + } + + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'role_permission'); + } + + /** + * 获取权限树 + */ + public static function tree(): array + { + $all = self::orderBy('sort')->get()->toArray(); + return self::buildTree($all); + } + + private static function buildTree(array $items, int $parentId = 0): array + { + $tree = []; + foreach ($items as $item) { + if ($item['parent_id'] == $parentId) { + $children = self::buildTree($items, $item['id']); + if ($children) { + $item['children'] = $children; + } + $tree[] = $item; + } + } + return $tree; + } +} diff --git a/backend/app/Models/Position.php b/backend/app/Models/Position.php new file mode 100644 index 0000000..d546705 --- /dev/null +++ b/backend/app/Models/Position.php @@ -0,0 +1,13 @@ + 'integer', + ]; + + public function stores(): HasMany + { + return $this->hasMany(Store::class); + } +} diff --git a/backend/app/Models/Role.php b/backend/app/Models/Role.php new file mode 100644 index 0000000..0d8ec96 --- /dev/null +++ b/backend/app/Models/Role.php @@ -0,0 +1,27 @@ + 'boolean', + ]; + + public function users(): BelongsToMany + { + return $this->belongsToMany(User::class, 'role_user'); + } + + public function permissions(): BelongsToMany + { + return $this->belongsToMany(Permission::class, 'role_permission'); + } +} diff --git a/backend/app/Models/Store.php b/backend/app/Models/Store.php new file mode 100644 index 0000000..43e071c --- /dev/null +++ b/backend/app/Models/Store.php @@ -0,0 +1,38 @@ + 'integer', + ]; + + public function region(): BelongsTo + { + return $this->belongsTo(Region::class); + } + + public function departments(): HasMany + { + return $this->hasMany(Department::class); + } + + public function users(): HasMany + { + return $this->hasMany(User::class); + } + + public function positions(): HasMany + { + return $this->hasMany(Position::class); + } +} diff --git a/backend/app/Models/SystemConfig.php b/backend/app/Models/SystemConfig.php new file mode 100644 index 0000000..5cdb12d --- /dev/null +++ b/backend/app/Models/SystemConfig.php @@ -0,0 +1,33 @@ +where('store_id', $storeId) + ->first(); + + return $config ? $config->value : $default; + } + + /** + * 设置配置值 + */ + public static function setValue(string $key, $value, ?int $storeId = null, string $group = 'general'): void + { + self::updateOrCreate( + ['key' => $key, 'store_id' => $storeId], + ['value' => $value, 'group' => $group] + ); + } +} diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php new file mode 100644 index 0000000..1b7d86b --- /dev/null +++ b/backend/app/Models/User.php @@ -0,0 +1,118 @@ + UserStatus::class, + 'is_super' => 'boolean', + 'last_login_at' => 'datetime', + 'password' => 'hashed', + ]; + } + + public function department(): BelongsTo + { + return $this->belongsTo(Department::class); + } + + public function position(): BelongsTo + { + return $this->belongsTo(Position::class); + } + + public function roles(): BelongsToMany + { + return $this->belongsToMany(Role::class, 'role_user'); + } + + /** + * 获取用户所有权限标识 + */ + public function getAllPermissionCodes(): array + { + if ($this->is_super) { + return ['*']; + } + + return $this->roles() + ->with('permissions') + ->get() + ->pluck('permissions') + ->flatten() + ->pluck('code') + ->unique() + ->values() + ->toArray(); + } + + /** + * 检查是否有指定权限 + */ + public function hasPermission(string $code): bool + { + if ($this->is_super) { + return true; + } + + return in_array($code, $this->getAllPermissionCodes()); + } + + /** + * 获取用户菜单树 + */ + public function getMenuTree(): array + { + if ($this->is_super) { + $menus = Menu::where('status', 1)->orderBy('sort')->get(); + } else { + $permissionCodes = $this->getAllPermissionCodes(); + $menus = Menu::where('status', 1) + ->where(function ($q) use ($permissionCodes) { + $q->whereIn('permission_code', $permissionCodes) + ->orWhereNull('permission_code'); + }) + ->orderBy('sort') + ->get(); + } + + return self::buildTree($menus->toArray()); + } + + private static function buildTree(array $items, int $parentId = 0): array + { + $tree = []; + foreach ($items as $item) { + if ($item['parent_id'] == $parentId) { + $children = self::buildTree($items, $item['id']); + if ($children) { + $item['children'] = $children; + } + $tree[] = $item; + } + } + return $tree; + } +} diff --git a/backend/app/Providers/AppServiceProvider.php b/backend/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..452e6b6 --- /dev/null +++ b/backend/app/Providers/AppServiceProvider.php @@ -0,0 +1,24 @@ +where($builder->getModel()->getTable() . '.store_id', $storeId); + } + }); + + // 创建时自动填入 store_id + static::creating(function ($model) { + if (empty($model->store_id)) { + $model->store_id = self::getCurrentStoreId(); + } + }); + } + + public function store(): BelongsTo + { + return $this->belongsTo(Store::class); + } + + private static function getCurrentStoreId(): ?int + { + $user = auth()->user(); + if ($user && $user->is_super && request()->header('X-Store-Id')) { + return (int) request()->header('X-Store-Id'); + } + return $user?->store_id; + } +} diff --git a/backend/app/Traits/HasCreator.php b/backend/app/Traits/HasCreator.php new file mode 100644 index 0000000..7749d00 --- /dev/null +++ b/backend/app/Traits/HasCreator.php @@ -0,0 +1,41 @@ +check()) { + if ($model->isFillable('created_by') || in_array('created_by', $model->getFillable())) { + $model->created_by = $model->created_by ?? auth()->id(); + } + } + }); + + static::updating(function ($model) { + if (auth()->check()) { + if ($model->isFillable('updated_by') || in_array('updated_by', $model->getFillable())) { + $model->updated_by = auth()->id(); + } + } + }); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function updater(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } +} diff --git a/backend/artisan b/backend/artisan new file mode 100755 index 0000000..c35e31d --- /dev/null +++ b/backend/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/backend/bootstrap/app.php b/backend/bootstrap/app.php new file mode 100644 index 0000000..13522ca --- /dev/null +++ b/backend/bootstrap/app.php @@ -0,0 +1,26 @@ +withRouting( + api: __DIR__.'/../routes/api.php', + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + apiPrefix: 'api/v1', + ) + ->withMiddleware(function (Middleware $middleware): void { + $middleware->alias([ + 'store' => \App\Http\Middleware\StoreIsolation::class, + 'permission' => \App\Http\Middleware\CheckPermission::class, + 'oplog' => \App\Http\Middleware\OperationLog::class, + ]); + + $middleware->statefulApi(); + }) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->create(); diff --git a/backend/bootstrap/cache/.gitignore b/backend/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/backend/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/backend/bootstrap/providers.php b/backend/bootstrap/providers.php new file mode 100644 index 0000000..fc94ae6 --- /dev/null +++ b/backend/bootstrap/providers.php @@ -0,0 +1,7 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-03-10T16:41:02+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.54.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/325497463e7599cd14224c422c6e5dd2fe832868", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-03-10T20:25:56+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.14", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.14" + }, + "time": "2026-03-01T09:02:38+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.3.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "reference": "e3b85d6e36ad00e5db2d1dcc27c81ffdf15cbf76", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0|^13.0", + "illuminate/contracts": "^11.0|^12.0|^13.0", + "illuminate/database": "^11.0|^12.0|^13.0", + "illuminate/support": "^11.0|^12.0|^13.0", + "php": "^8.2", + "symfony/console": "^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "phpstan/phpstan": "^1.10" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2026-02-07T17:19:31+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-02-20T19:59:49+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.11.1" + }, + "time": "2026-02-06T14:12:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "84b1ca48347efdbe775426f108622a42735a6579" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/84b1ca48347efdbe775426f108622a42735a6579", + "reference": "84b1ca48347efdbe775426f108622a42735a6579", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-05T21:37:03+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.32.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "reference": "254b1595b16b22dbddaaef9ed6ca9fdac4956725", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.32.0" + }, + "time": "2026-02-25T17:01:41+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-01-14T17:24:56+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-01-15T06:54:53+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-03-11T17:23:39+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.3" + }, + "time": "2026-02-13T03:05:33+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.21", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.21" + }, + "time": "2026-03-06T21:21:28+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:46:48+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T14:06:20+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "2a178bf80f05dbbe469a337730eba79d61315262" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a178bf80f05dbbe469a337730eba79d61315262", + "reference": "2a178bf80f05dbbe469a337730eba79d61315262", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T13:07:04+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-20T16:42:42+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:55+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:40:50+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T13:15:18+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", + "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4.1|^7.0.1|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T16:33:18+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "reference": "b02726f39a20bc65e30364f5c750c4ddbf1f58e9", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:50:00+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-05T15:24:09+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "608476f4604102976d687c483ac63a79ba18cc97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:07:59+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/238d749c56b804b31a9bf3e26519d93b65a60938", + "reference": "238d749c56b804b31a9bf3e26519d93b65a60938", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-25T16:50:00+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T10:14:57+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "reference": "13ff19bcf2bea492d3c2fbeaa194dd6f4599ce1b", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-17T13:07:04+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-03T23:30:35+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/045321c440ac18347b136c63d2e9bf28a2dc0291", + "reference": "045321c440ac18347b136c63d2e9bf28a2dc0291", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-15T10:53:20+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-02-09T13:44:54+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", + "laravel-zero/framework": "^12.0.5", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-03-12T15:51:39+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.53.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/e340eaa2bea9b99192570c48ed837155dbf24fbb", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-02-06T12:16:02+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.2", + "laravel/framework": "^11.48.0 || ^12.52.0", + "laravel/pint": "^1.27.1", + "orchestra/testbench-core": "^9.12.0 || ^10.9.0", + "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-02-17T17:33:08+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/5f006c50a981e1630bbb70ad409c5d85f9a716e0", + "reference": "5f006c50a981e1630bbb70ad409c5d85f9a716e0", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T10:14:57+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/backend/config/app.php b/backend/config/app.php new file mode 100644 index 0000000..423eed5 --- /dev/null +++ b/backend/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/backend/config/auth.php b/backend/config/auth.php new file mode 100644 index 0000000..d7568ff --- /dev/null +++ b/backend/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/backend/config/cache.php b/backend/config/cache.php new file mode 100644 index 0000000..b32aead --- /dev/null +++ b/backend/config/cache.php @@ -0,0 +1,117 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/backend/config/cors.php b/backend/config/cors.php new file mode 100644 index 0000000..94f07be --- /dev/null +++ b/backend/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => true, + +]; diff --git a/backend/config/database.php b/backend/config/database.php new file mode 100644 index 0000000..64709ce --- /dev/null +++ b/backend/config/database.php @@ -0,0 +1,184 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/backend/config/filesystems.php b/backend/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/backend/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/backend/config/logging.php b/backend/config/logging.php new file mode 100644 index 0000000..9e998a4 --- /dev/null +++ b/backend/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/backend/config/mail.php b/backend/config/mail.php new file mode 100644 index 0000000..e32e88d --- /dev/null +++ b/backend/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/backend/config/queue.php b/backend/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/backend/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/backend/config/services.php b/backend/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/backend/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/backend/config/session.php b/backend/config/session.php new file mode 100644 index 0000000..5b541b7 --- /dev/null +++ b/backend/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/backend/database/.gitignore b/backend/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/backend/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/backend/database/factories/UserFactory.php b/backend/database/factories/UserFactory.php new file mode 100644 index 0000000..c4ceb07 --- /dev/null +++ b/backend/database/factories/UserFactory.php @@ -0,0 +1,45 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/backend/database/migrations/0001_01_01_000001_create_cache_table.php b/backend/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..ed758bd --- /dev/null +++ b/backend/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/backend/database/migrations/0001_01_01_000002_create_jobs_table.php b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..967fbf5 --- /dev/null +++ b/backend/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,59 @@ +id(); + $table->string('queue'); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + + $table->index(['queue', 'reserved_at', 'available_at']); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000001_create_regions_table.php b/backend/database/migrations/2024_01_01_000001_create_regions_table.php new file mode 100644 index 0000000..dcbd539 --- /dev/null +++ b/backend/database/migrations/2024_01_01_000001_create_regions_table.php @@ -0,0 +1,24 @@ +id(); + $table->string('name', 50)->comment('区域名称'); + $table->integer('sort')->default(0); + $table->tinyInteger('status')->default(1)->comment('1启用 0停用'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('regions'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000002_create_stores_table.php b/backend/database/migrations/2024_01_01_000002_create_stores_table.php new file mode 100644 index 0000000..c3b13c5 --- /dev/null +++ b/backend/database/migrations/2024_01_01_000002_create_stores_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('region_id')->constrained()->comment('所属区域'); + $table->string('name', 100)->comment('门店名称'); + $table->string('code', 20)->unique()->nullable()->comment('门店编码'); + $table->string('address', 255)->nullable(); + $table->string('phone', 20)->nullable(); + $table->string('logo', 255)->nullable(); + $table->tinyInteger('status')->default(1)->comment('1启用 0停用'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('stores'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000003_create_departments_table.php b/backend/database/migrations/2024_01_01_000003_create_departments_table.php new file mode 100644 index 0000000..55c009c --- /dev/null +++ b/backend/database/migrations/2024_01_01_000003_create_departments_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('store_id')->constrained()->comment('所属门店'); + $table->unsignedBigInteger('parent_id')->default(0)->comment('上级部门'); + $table->string('name', 50)->comment('部门名称'); + $table->integer('sort')->default(0); + $table->tinyInteger('status')->default(1)->comment('1启用 0停用'); + $table->timestamps(); + + $table->index('store_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('departments'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000004_create_positions_table.php b/backend/database/migrations/2024_01_01_000004_create_positions_table.php new file mode 100644 index 0000000..6d01452 --- /dev/null +++ b/backend/database/migrations/2024_01_01_000004_create_positions_table.php @@ -0,0 +1,24 @@ +id(); + $table->foreignId('store_id')->constrained()->comment('所属门店'); + $table->string('name', 50)->comment('职务名称'); + $table->integer('sort')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('positions'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000005_create_users_table.php b/backend/database/migrations/2024_01_01_000005_create_users_table.php new file mode 100644 index 0000000..1cabe36 --- /dev/null +++ b/backend/database/migrations/2024_01_01_000005_create_users_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('store_id')->constrained()->comment('所属门店'); + $table->unsignedBigInteger('department_id')->nullable(); + $table->unsignedBigInteger('position_id')->nullable(); + $table->string('username', 50)->unique()->comment('登录名'); + $table->string('password', 255); + $table->string('name', 50)->comment('真实姓名'); + $table->string('phone', 20)->nullable(); + $table->string('email', 100)->nullable(); + $table->string('avatar', 255)->nullable(); + $table->tinyInteger('status')->default(1)->comment('0待审核 1正常 2禁用'); + $table->tinyInteger('is_super')->default(0)->comment('1超级管理员'); + $table->timestamp('last_login_at')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('store_id'); + $table->index('phone'); + }); + } + + public function down(): void + { + Schema::dropIfExists('users'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000006_create_roles_table.php b/backend/database/migrations/2024_01_01_000006_create_roles_table.php new file mode 100644 index 0000000..172ebf2 --- /dev/null +++ b/backend/database/migrations/2024_01_01_000006_create_roles_table.php @@ -0,0 +1,26 @@ +id(); + $table->unsignedBigInteger('store_id')->nullable()->comment('NULL=全局角色'); + $table->string('name', 50)->comment('角色名称'); + $table->string('code', 50)->unique()->comment('角色编码'); + $table->string('description', 255)->nullable(); + $table->tinyInteger('is_system')->default(0)->comment('1系统内置不可删'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('roles'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000007_create_permissions_table.php b/backend/database/migrations/2024_01_01_000007_create_permissions_table.php new file mode 100644 index 0000000..fcb0144 --- /dev/null +++ b/backend/database/migrations/2024_01_01_000007_create_permissions_table.php @@ -0,0 +1,28 @@ +id(); + $table->unsignedBigInteger('parent_id')->default(0)->comment('上级权限'); + $table->string('name', 50)->comment('权限名称'); + $table->string('code', 100)->unique()->comment('权限标识 如 crm.lead.create'); + $table->string('module', 50)->nullable()->comment('所属模块'); + $table->tinyInteger('type')->default(1)->comment('1菜单 2按钮 3接口'); + $table->string('description', 255)->nullable(); + $table->integer('sort')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('permissions'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000008_create_menus_table.php b/backend/database/migrations/2024_01_01_000008_create_menus_table.php new file mode 100644 index 0000000..baccde7 --- /dev/null +++ b/backend/database/migrations/2024_01_01_000008_create_menus_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger('parent_id')->default(0)->comment('上级菜单'); + $table->string('name', 50)->comment('菜单名称'); + $table->string('path', 255)->nullable()->comment('路由路径'); + $table->string('component', 255)->nullable()->comment('组件路径'); + $table->string('icon', 50)->nullable()->comment('图标'); + $table->string('permission_code', 100)->nullable()->comment('关联权限标识'); + $table->tinyInteger('type')->default(1)->comment('1目录 2菜单 3按钮'); + $table->tinyInteger('visible')->default(1)->comment('1显示 0隐藏'); + $table->integer('sort')->default(0); + $table->tinyInteger('status')->default(1)->comment('1启用 0停用'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('menus'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000009_create_role_user_table.php b/backend/database/migrations/2024_01_01_000009_create_role_user_table.php new file mode 100644 index 0000000..9571a1e --- /dev/null +++ b/backend/database/migrations/2024_01_01_000009_create_role_user_table.php @@ -0,0 +1,22 @@ +foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('role_id')->constrained()->onDelete('cascade'); + $table->primary(['user_id', 'role_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('role_user'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000010_create_role_permission_table.php b/backend/database/migrations/2024_01_01_000010_create_role_permission_table.php new file mode 100644 index 0000000..1866133 --- /dev/null +++ b/backend/database/migrations/2024_01_01_000010_create_role_permission_table.php @@ -0,0 +1,22 @@ +foreignId('role_id')->constrained()->onDelete('cascade'); + $table->foreignId('permission_id')->constrained()->onDelete('cascade'); + $table->primary(['role_id', 'permission_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('role_permission'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000011_create_operation_logs_table.php b/backend/database/migrations/2024_01_01_000011_create_operation_logs_table.php new file mode 100644 index 0000000..b6acfbd --- /dev/null +++ b/backend/database/migrations/2024_01_01_000011_create_operation_logs_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('store_id')->constrained(); + $table->unsignedBigInteger('user_id'); + $table->string('user_name', 50); + $table->string('module', 50)->comment('操作模块'); + $table->string('action', 50)->comment('操作类型 create/update/delete'); + $table->string('target', 100)->nullable()->comment('操作对象'); + $table->unsignedBigInteger('target_id')->nullable(); + $table->string('ip', 45)->nullable(); + $table->string('method', 10)->nullable()->comment('HTTP方法'); + $table->string('url', 500)->nullable()->comment('请求URL'); + $table->json('before')->nullable()->comment('变更前数据'); + $table->json('after')->nullable()->comment('变更后数据'); + $table->timestamp('created_at')->nullable(); + + $table->index(['store_id', 'created_at']); + $table->index('user_id'); + $table->index('module'); + }); + } + + public function down(): void + { + Schema::dropIfExists('operation_logs'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000012_create_dictionaries_table.php b/backend/database/migrations/2024_01_01_000012_create_dictionaries_table.php new file mode 100644 index 0000000..9c2a4db --- /dev/null +++ b/backend/database/migrations/2024_01_01_000012_create_dictionaries_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('name', 50)->comment('字典名称'); + $table->string('code', 50)->unique()->comment('字典编码'); + $table->string('description', 255)->nullable(); + $table->tinyInteger('is_system')->default(0)->comment('系统内置不可删'); + $table->timestamps(); + }); + + Schema::create('dictionary_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('dictionary_id')->constrained()->onDelete('cascade'); + $table->string('label', 100)->comment('显示文本'); + $table->string('value', 100)->comment('值'); + $table->integer('sort')->default(0); + $table->tinyInteger('status')->default(1)->comment('1启用 0停用'); + $table->timestamps(); + + $table->index('dictionary_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('dictionary_items'); + Schema::dropIfExists('dictionaries'); + } +}; diff --git a/backend/database/migrations/2024_01_01_000013_create_system_configs_table.php b/backend/database/migrations/2024_01_01_000013_create_system_configs_table.php new file mode 100644 index 0000000..91e326f --- /dev/null +++ b/backend/database/migrations/2024_01_01_000013_create_system_configs_table.php @@ -0,0 +1,28 @@ +id(); + $table->unsignedBigInteger('store_id')->nullable()->comment('NULL=全局配置'); + $table->string('group', 50)->default('general')->comment('配置分组'); + $table->string('key', 100)->comment('配置键'); + $table->text('value')->nullable()->comment('配置值'); + $table->string('description', 255)->nullable(); + $table->timestamps(); + + $table->unique(['store_id', 'key']); + }); + } + + public function down(): void + { + Schema::dropIfExists('system_configs'); + } +}; diff --git a/backend/database/migrations/2026_03_13_114921_create_personal_access_tokens_table.php b/backend/database/migrations/2026_03_13_114921_create_personal_access_tokens_table.php new file mode 100644 index 0000000..40ff706 --- /dev/null +++ b/backend/database/migrations/2026_03_13_114921_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->morphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/backend/database/seeders/DatabaseSeeder.php b/backend/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..6b901f8 --- /dev/null +++ b/backend/database/seeders/DatabaseSeeder.php @@ -0,0 +1,25 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } +} diff --git a/backend/database/seeders/InitSeeder.php b/backend/database/seeders/InitSeeder.php new file mode 100644 index 0000000..1142658 --- /dev/null +++ b/backend/database/seeders/InitSeeder.php @@ -0,0 +1,266 @@ + '默认区域', 'sort' => 0, 'status' => 1]); + + // 2. 创建默认门店 + $store = Store::create([ + 'region_id' => $region->id, + 'name' => '宫中有喜总店', + 'code' => 'HQ001', + 'status' => 1, + ]); + + // 3. 创建默认部门 + Department::create(['store_id' => $store->id, 'name' => '管理层', 'sort' => 0]); + + // 4. 创建超级管理员 + $admin = User::create([ + 'store_id' => $store->id, + 'username' => 'admin', + 'password' => 'admin123', + 'name' => '超级管理员', + 'status' => 1, + 'is_super' => 1, + ]); + + // 5. 创建默认角色 + $superRole = Role::create([ + 'name' => '超级管理员', + 'code' => 'super_admin', + 'description' => '系统最高权限', + 'is_system' => 1, + ]); + + $storeAdmin = Role::create([ + 'store_id' => $store->id, + 'name' => '门店管理员', + 'code' => 'store_admin', + 'description' => '门店最高权限', + 'is_system' => 1, + ]); + + $admin->roles()->attach($superRole->id); + + // 6. 创建权限 + $this->createPermissions(); + + // 7. 创建菜单 + $this->createMenus(); + + // 8. 门店管理员分配所有权限 + $allPermissions = Permission::pluck('id')->toArray(); + $storeAdmin->permissions()->sync($allPermissions); + } + + private function createPermissions(): void + { + $modules = [ + ['name' => 'CRM管理', 'code' => 'crm', 'children' => [ + ['name' => '线索管理', 'code' => 'crm.lead', 'children' => [ + ['name' => '查看', 'code' => 'crm.lead.view', 'type' => 2], + ['name' => '新建', 'code' => 'crm.lead.create', 'type' => 2], + ['name' => '编辑', 'code' => 'crm.lead.update', 'type' => 2], + ['name' => '删除', 'code' => 'crm.lead.delete', 'type' => 2], + ]], + ['name' => '客户管理', 'code' => 'crm.customer', 'children' => [ + ['name' => '查看', 'code' => 'crm.customer.view', 'type' => 2], + ['name' => '新建', 'code' => 'crm.customer.create', 'type' => 2], + ['name' => '编辑', 'code' => 'crm.customer.update', 'type' => 2], + ['name' => '删除', 'code' => 'crm.customer.delete', 'type' => 2], + ]], + ['name' => '合同管理', 'code' => 'crm.contract', 'children' => [ + ['name' => '查看', 'code' => 'crm.contract.view', 'type' => 2], + ['name' => '新建', 'code' => 'crm.contract.create', 'type' => 2], + ['name' => '审批', 'code' => 'crm.contract.approve', 'type' => 2], + ]], + ]], + ['name' => '房务管理', 'code' => 'room', 'children' => [ + ['name' => '房间管理', 'code' => 'room.room', 'children' => [ + ['name' => '查看', 'code' => 'room.room.view', 'type' => 2], + ['name' => '编辑', 'code' => 'room.room.update', 'type' => 2], + ]], + ['name' => '预定管理', 'code' => 'room.reservation', 'children' => [ + ['name' => '查看', 'code' => 'room.reservation.view', 'type' => 2], + ['name' => '新建', 'code' => 'room.reservation.create', 'type' => 2], + ['name' => '操作', 'code' => 'room.reservation.operate', 'type' => 2], + ]], + ]], + ['name' => '护理管理', 'code' => 'care', 'children' => [ + ['name' => '护理档案', 'code' => 'care.profile', 'children' => [ + ['name' => '查看', 'code' => 'care.profile.view', 'type' => 2], + ['name' => '编辑', 'code' => 'care.profile.update', 'type' => 2], + ]], + ['name' => '护理记录', 'code' => 'care.record', 'children' => [ + ['name' => '查看', 'code' => 'care.record.view', 'type' => 2], + ['name' => '新建', 'code' => 'care.record.create', 'type' => 2], + ]], + ]], + ['name' => '月子餐管理', 'code' => 'meal', 'children' => [ + ['name' => '菜品管理', 'code' => 'meal.dish', 'children' => [ + ['name' => '查看', 'code' => 'meal.dish.view', 'type' => 2], + ['name' => '编辑', 'code' => 'meal.dish.update', 'type' => 2], + ]], + ['name' => '排餐管理', 'code' => 'meal.plan', 'children' => [ + ['name' => '查看', 'code' => 'meal.plan.view', 'type' => 2], + ['name' => '编辑', 'code' => 'meal.plan.update', 'type' => 2], + ]], + ]], + ['name' => '服务产康', 'code' => 'service', 'children' => [ + ['name' => '服务项目', 'code' => 'service.item', 'children' => [ + ['name' => '查看', 'code' => 'service.item.view', 'type' => 2], + ['name' => '编辑', 'code' => 'service.item.update', 'type' => 2], + ]], + ]], + ['name' => '月嫂管理', 'code' => 'nanny', 'children' => [ + ['name' => '月嫂档案', 'code' => 'nanny.profile', 'children' => [ + ['name' => '查看', 'code' => 'nanny.profile.view', 'type' => 2], + ['name' => '编辑', 'code' => 'nanny.profile.update', 'type' => 2], + ]], + ]], + ['name' => '进销存', 'code' => 'stock', 'children' => [ + ['name' => '库存管理', 'code' => 'stock.inventory', 'children' => [ + ['name' => '查看', 'code' => 'stock.inventory.view', 'type' => 2], + ['name' => '操作', 'code' => 'stock.inventory.operate', 'type' => 2], + ]], + ]], + ['name' => '财务管理', 'code' => 'finance', 'children' => [ + ['name' => '收支管理', 'code' => 'finance.record', 'children' => [ + ['name' => '查看', 'code' => 'finance.record.view', 'type' => 2], + ['name' => '新建', 'code' => 'finance.record.create', 'type' => 2], + ['name' => '审核', 'code' => 'finance.record.approve', 'type' => 2], + ]], + ]], + ['name' => '人事薪资', 'code' => 'hr', 'children' => [ + ['name' => '员工管理', 'code' => 'hr.employee', 'children' => [ + ['name' => '查看', 'code' => 'hr.employee.view', 'type' => 2], + ['name' => '编辑', 'code' => 'hr.employee.update', 'type' => 2], + ]], + ]], + ['name' => '办公协同', 'code' => 'office', 'children' => [ + ['name' => '审批管理', 'code' => 'office.approval', 'children' => [ + ['name' => '查看', 'code' => 'office.approval.view', 'type' => 2], + ['name' => '审批', 'code' => 'office.approval.operate', 'type' => 2], + ]], + ]], + ['name' => '统计报表', 'code' => 'report', 'children' => [ + ['name' => '查看报表', 'code' => 'report.view', 'type' => 2], + ['name' => '导出报表', 'code' => 'report.export', 'type' => 2], + ]], + ['name' => '知识库', 'code' => 'kb', 'children' => [ + ['name' => '文章管理', 'code' => 'kb.article', 'children' => [ + ['name' => '查看', 'code' => 'kb.article.view', 'type' => 2], + ['name' => '编辑', 'code' => 'kb.article.update', 'type' => 2], + ]], + ]], + ['name' => '系统设置', 'code' => 'system', 'children' => [ + ['name' => '门店管理', 'code' => 'system.store', 'type' => 2], + ['name' => '用户管理', 'code' => 'system.user', 'type' => 2], + ['name' => '角色管理', 'code' => 'system.role', 'type' => 2], + ['name' => '菜单管理', 'code' => 'system.menu', 'type' => 2], + ['name' => '字典管理', 'code' => 'system.dict', 'type' => 2], + ['name' => '操作日志', 'code' => 'system.log', 'type' => 2], + ]], + ]; + + $this->insertPermissions($modules); + } + + private function insertPermissions(array $items, int $parentId = 0, int &$sort = 0): void + { + foreach ($items as $item) { + $children = $item['children'] ?? []; + unset($item['children']); + + $item['parent_id'] = $parentId; + $item['type'] = $item['type'] ?? 1; + $item['module'] = explode('.', $item['code'])[0]; + $item['sort'] = $sort++; + + $permission = Permission::create($item); + + if ($children) { + $this->insertPermissions($children, $permission->id, $sort); + } + } + } + + private function createMenus(): void + { + $menus = [ + ['name' => '首页', 'path' => '/dashboard', 'component' => 'dashboard/index', 'icon' => 'HomeFilled', 'sort' => 0], + ['name' => 'CRM管理', 'icon' => 'User', 'sort' => 1, 'permission_code' => 'crm', 'children' => [ + ['name' => '线索管理', 'path' => '/crm/leads', 'component' => 'crm/leads/index', 'permission_code' => 'crm.lead'], + ['name' => '客户管理', 'path' => '/crm/customers', 'component' => 'crm/customers/index', 'permission_code' => 'crm.customer'], + ['name' => '合同管理', 'path' => '/crm/contracts', 'component' => 'crm/contracts/index', 'permission_code' => 'crm.contract'], + ]], + ['name' => '房务管理', 'icon' => 'House', 'sort' => 2, 'permission_code' => 'room', 'children' => [ + ['name' => '房态看板', 'path' => '/room/board', 'component' => 'room/board/index', 'permission_code' => 'room.room'], + ['name' => '预定管理', 'path' => '/room/reservations', 'component' => 'room/reservations/index', 'permission_code' => 'room.reservation'], + ]], + ['name' => '护理管理', 'icon' => 'FirstAidKit', 'sort' => 3, 'permission_code' => 'care', 'children' => [ + ['name' => '护理档案', 'path' => '/care/profiles', 'component' => 'care/profiles/index', 'permission_code' => 'care.profile'], + ['name' => '护理记录', 'path' => '/care/records', 'component' => 'care/records/index', 'permission_code' => 'care.record'], + ]], + ['name' => '月子餐', 'icon' => 'Bowl', 'sort' => 4, 'permission_code' => 'meal', 'children' => [ + ['name' => '菜品管理', 'path' => '/meal/dishes', 'component' => 'meal/dishes/index', 'permission_code' => 'meal.dish'], + ['name' => '排餐管理', 'path' => '/meal/plans', 'component' => 'meal/plans/index', 'permission_code' => 'meal.plan'], + ]], + ['name' => '服务产康', 'icon' => 'Promotion', 'sort' => 5, 'permission_code' => 'service'], + ['name' => '月嫂管理', 'icon' => 'Avatar', 'sort' => 6, 'permission_code' => 'nanny'], + ['name' => '进销存', 'icon' => 'Box', 'sort' => 7, 'permission_code' => 'stock'], + ['name' => '财务管理', 'icon' => 'Money', 'sort' => 8, 'permission_code' => 'finance'], + ['name' => '人事薪资', 'icon' => 'Stamp', 'sort' => 9, 'permission_code' => 'hr'], + ['name' => '办公协同', 'icon' => 'ChatDotRound', 'sort' => 10, 'permission_code' => 'office'], + ['name' => '统计报表', 'icon' => 'DataAnalysis', 'sort' => 11, 'permission_code' => 'report'], + ['name' => '知识库', 'icon' => 'Reading', 'sort' => 12, 'permission_code' => 'kb'], + ['name' => '系统设置', 'icon' => 'Setting', 'sort' => 99, 'permission_code' => 'system', 'children' => [ + ['name' => '门店管理', 'path' => '/system/stores', 'component' => 'system/stores/index', 'permission_code' => 'system.store'], + ['name' => '部门管理', 'path' => '/system/departments', 'component' => 'system/departments/index', 'permission_code' => 'system.store'], + ['name' => '职务管理', 'path' => '/system/positions', 'component' => 'system/positions/index', 'permission_code' => 'system.store'], + ['name' => '用户管理', 'path' => '/system/users', 'component' => 'system/users/index', 'permission_code' => 'system.user'], + ['name' => '角色管理', 'path' => '/system/roles', 'component' => 'system/roles/index', 'permission_code' => 'system.role'], + ['name' => '菜单管理', 'path' => '/system/menus', 'component' => 'system/menus/index', 'permission_code' => 'system.menu'], + ['name' => '字典管理', 'path' => '/system/dictionaries', 'component' => 'system/dictionaries/index', 'permission_code' => 'system.dict'], + ['name' => '操作日志', 'path' => '/system/logs', 'component' => 'system/logs/index', 'permission_code' => 'system.log'], + ]], + ]; + + $this->insertMenus($menus); + } + + private function insertMenus(array $items, int $parentId = 0): void + { + foreach ($items as $index => $item) { + $children = $item['children'] ?? []; + unset($item['children']); + + $item['parent_id'] = $parentId; + $item['type'] = isset($item['component']) ? 2 : 1; // 有 component = 菜单,否则目录 + $item['sort'] = $item['sort'] ?? $index; + $item['visible'] = 1; + $item['status'] = 1; + + $menu = Menu::create($item); + + if ($children) { + $this->insertMenus($children, $menu->id); + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..7686b29 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.7" + } +} diff --git a/backend/phpunit.xml b/backend/phpunit.xml new file mode 100644 index 0000000..e7f0a48 --- /dev/null +++ b/backend/phpunit.xml @@ -0,0 +1,36 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + diff --git a/backend/public/.htaccess b/backend/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/backend/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/backend/public/favicon.ico b/backend/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/backend/public/index.php b/backend/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/backend/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/backend/public/robots.txt b/backend/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/backend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/backend/resources/css/app.css b/backend/resources/css/app.css new file mode 100644 index 0000000..3e6abea --- /dev/null +++ b/backend/resources/css/app.css @@ -0,0 +1,11 @@ +@import 'tailwindcss'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../**/*.blade.php'; +@source '../**/*.js'; + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; +} diff --git a/backend/resources/js/app.js b/backend/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/backend/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/backend/resources/js/bootstrap.js b/backend/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/backend/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/backend/resources/views/welcome.blade.php b/backend/resources/views/welcome.blade.php new file mode 100644 index 0000000..b7355d7 --- /dev/null +++ b/backend/resources/views/welcome.blade.php @@ -0,0 +1,277 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + +
+ @if (Route::has('login')) + + @endif +
+
+
+
+

Let's get started

+

Laravel has an incredibly rich ecosystem.
We suggest starting with the following.

+ + +
+
+ {{-- Laravel Logo --}} + + + + + + + + + + + {{-- Light Mode 12 SVG --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{-- Dark Mode 12 SVG --}} + +
+
+
+
+ + @if (Route::has('login')) + + @endif + + diff --git a/backend/routes/api.php b/backend/routes/api.php new file mode 100644 index 0000000..5b0bb29 --- /dev/null +++ b/backend/routes/api.php @@ -0,0 +1,69 @@ +group(function () { + + // --- 认证 --- + Route::post('/auth/logout', [AuthController::class, 'logout']); + Route::get('/auth/me', [AuthController::class, 'me']); + + // --- 系统设置模块 --- + Route::prefix('system')->group(function () { + + // 门店管理 + Route::apiResource('stores', StoreController::class); + + // 部门管理 + Route::apiResource('departments', DepartmentController::class)->except(['show']); + + // 职务管理 + Route::apiResource('positions', PositionController::class)->except(['show']); + + // 用户管理 + Route::apiResource('users', UserController::class); + Route::put('users/{user}/status', [UserController::class, 'updateStatus']); + Route::put('users/{user}/password', [UserController::class, 'resetPassword']); + + // 角色管理 + Route::apiResource('roles', RoleController::class); + Route::put('roles/{role}/permissions', [RoleController::class, 'assignPermissions']); + + // 菜单管理 + Route::apiResource('menus', MenuController::class)->except(['show']); + + // 权限管理 + Route::apiResource('permissions', PermissionController::class)->except(['show']); + + // 数据字典 + Route::apiResource('dictionaries', DictionaryController::class)->except(['show', 'update']); + Route::get('dictionaries/{dictionary}/items', [DictionaryController::class, 'items']); + Route::post('dictionaries/{dictionary}/items', [DictionaryController::class, 'storeItem']); + Route::put('dictionary-items/{item}', [DictionaryController::class, 'updateItem']); + Route::delete('dictionary-items/{item}', [DictionaryController::class, 'destroyItem']); + + // 操作日志 + Route::get('operation-logs', [OperationLogController::class, 'index']); + }); +}); diff --git a/backend/routes/console.php b/backend/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/backend/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/backend/routes/web.php b/backend/routes/web.php new file mode 100644 index 0000000..86a06c5 --- /dev/null +++ b/backend/routes/web.php @@ -0,0 +1,7 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/backend/tests/TestCase.php b/backend/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/backend/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/backend/vite.config.js b/backend/vite.config.js new file mode 100644 index 0000000..f35b4e7 --- /dev/null +++ b/backend/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + tailwindcss(), + ], + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +}); diff --git a/docs/01-technical-overview.md b/docs/01-technical-overview.md new file mode 100644 index 0000000..5655c1d --- /dev/null +++ b/docs/01-technical-overview.md @@ -0,0 +1,199 @@ +# 宫中有喜 — 技术方案总览 + +## 一、项目概述 + +| 项 | 值 | +|---|---| +| 项目名称 | 宫中有喜月子会所综合管理系统 | +| 系统类型 | 多门店 SaaS 管理平台 + 客户端小程序 | +| 后端 | PHP 8.1+ / Laravel 10 | +| 数据库 | MySQL 8.0 | +| 前端(后台) | Vue 3 + Element Plus(SPA,自适应) | +| 前端(客户端) | 微信小程序(uni-app,可复用 Vue 组件) | +| API 风格 | RESTful JSON,统一前缀 `/api/v1/` | +| 部署 | Nginx + PHP-FPM / Docker Compose | + +## 二、框架选型理由 + +### 后端:Laravel 10 +- **Eloquent ORM**:模型关联、软删除、Scope 过滤(门店隔离天然支持) +- **中间件体系**:RBAC 权限、门店隔离、操作日志 一站式拦截 +- **队列 & 事件**:短信、推送、异步报表 开箱即用 +- **Sanctum**:后台 SPA token + 小程序 API token 双模认证 +- **Migration & Seeder**:数据库版本控制,多人协作无冲突 +- **生态成熟**:社区、文档、第三方包(Excel 导出、PDF、微信SDK)齐全 + +### 前端:Vue 3 + Element Plus +- 与 Laravel 前后分离配合成熟 +- Element Plus 提供完整后台组件(表格、表单、日历、审批流) +- 响应式布局满足 Web + Mobile 自适应需求 + +### 客户端:uni-app +- 一套代码编译微信小程序 + H5 +- Vue 3 语法,与后台前端技术栈统一,降低学习成本 +- 丰富的插件市场(微信支付、地图、视频播放等) + +## 三、系统架构图 + +``` +┌─────────────────────────────────────────────────┐ +│ 客户端 │ +│ ┌──────────────┐ ┌──────────────────────────┐ │ +│ │ 微信小程序 │ │ 后台管理 SPA (Vue3) │ │ +│ │ (uni-app) │ │ Element Plus │ │ +│ └──────┬───────┘ └───────────┬──────────────┘ │ +└─────────┼──────────────────────┼────────────────┘ + │ HTTPS │ HTTPS + ▼ ▼ +┌─────────────────────────────────────────────────┐ +│ Nginx 反向代理 / 负载均衡 │ +│ 静态资源 + API 路由 + WebSocket │ +└─────────────────────┬───────────────────────────┘ + │ +┌─────────────────────▼───────────────────────────┐ +│ Laravel 10 API 层 │ +│ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐ │ +│ │ 认证 │ │ RBAC │ │ 门店 │ │ 操作日志 │ │ +│ │中间件 │ │中间件 │ │隔离 │ │中间件 │ │ +│ └───┬────┘ └───┬────┘ └───┬────┘ └────┬─────┘ │ +│ └──────────┴──────────┴────────────┘ │ +│ ┌──────────────────────────────────────────┐ │ +│ │ Controller 层 (13模块) │ │ +│ │ CRM | 房务 | 护理 | 月子餐 | 产康 │ │ +│ │ 月嫂 | 进销存 | 财务 | 人事 | 协同 │ │ +│ │ 报表 | 知识库 | 系统设置 │ │ +│ └──────────────────┬───────────────────────┘ │ +│ ┌──────────────────▼───────────────────────┐ │ +│ │ Service 层 (业务逻辑) │ │ +│ └──────────────────┬───────────────────────┘ │ +│ ┌──────────────────▼───────────────────────┐ │ +│ │ Model 层 (Eloquent ORM) │ │ +│ └──────────────────┬───────────────────────┘ │ +└─────────────────────┼───────────────────────────┘ + │ +┌─────────────────────▼───────────────────────────┐ +│ 数据层 │ +│ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ MySQL 8 │ │ Redis │ │ 文件存储 │ │ +│ │ 主业务库 │ │ 缓存/队列│ │ (本地/OSS) │ │ +│ └──────────┘ └──────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────┘ +``` + +## 四、项目目录结构 + +``` +saas/ +├── backend/ # Laravel 后端 +│ ├── app/ +│ │ ├── Http/ +│ │ │ ├── Controllers/ +│ │ │ │ ├── Admin/ # 后台管理接口 +│ │ │ │ │ ├── Crm/ # 5.1 CRM模块 +│ │ │ │ │ ├── Room/ # 5.2 房务模块 +│ │ │ │ │ ├── Care/ # 5.3 护理模块 +│ │ │ │ │ ├── Meal/ # 5.4 月子餐模块 +│ │ │ │ │ ├── Service/# 5.5 服务产康 +│ │ │ │ │ ├── Nanny/ # 5.6 月嫂模块 +│ │ │ │ │ ├── Stock/ # 5.7 进销存 +│ │ │ │ │ ├── Finance/# 5.8 财务模块 +│ │ │ │ │ ├── Hr/ # 5.9 人事薪资 +│ │ │ │ │ ├── Office/ # 5.10 办公协同 +│ │ │ │ │ ├── Report/ # 5.11 统计报表 +│ │ │ │ │ ├── Kb/ # 5.12 知识库 +│ │ │ │ │ └── System/ # 5.13 系统设置 +│ │ │ │ └── Client/ # 微客宝客户端接口 +│ │ │ ├── Middleware/ +│ │ │ │ ├── StoreIsolation.php +│ │ │ │ ├── CheckPermission.php +│ │ │ │ └── OperationLog.php +│ │ │ └── Requests/ # 表单验证 +│ │ ├── Models/ # Eloquent 模型 +│ │ ├── Services/ # 业务逻辑层 +│ │ ├── Enums/ # 枚举常量 +│ │ ├── Events/ # 事件 +│ │ ├── Listeners/ # 监听器 +│ │ ├── Jobs/ # 队列任务 +│ │ └── Traits/ # 公用 Trait +│ ├── database/ +│ │ ├── migrations/ # 数据库迁移 +│ │ └── seeders/ # 种子数据 +│ ├── routes/ +│ │ ├── api.php # 后台 API 路由 +│ │ └── client.php # 客户端 API 路由 +│ └── config/ +│ +├── frontend/ # Vue 3 后台前端 +│ ├── src/ +│ │ ├── views/ # 页面(按模块分目录) +│ │ ├── components/ # 公共组件 +│ │ ├── api/ # 接口封装 +│ │ ├── stores/ # Pinia 状态管理 +│ │ ├── router/ # 路由(动态菜单) +│ │ ├── utils/ # 工具函数 +│ │ └── assets/ # 静态资源 +│ └── vite.config.js +│ +├── miniapp/ # uni-app 微信小程序 +│ ├── pages/ # 页面 +│ ├── components/ # 组件 +│ ├── api/ # 接口 +│ ├── stores/ # 状态 +│ └── static/ # 静态资源 +│ +├── docs/ # 技术文档 +│ ├── 01-technical-overview.md +│ ├── 02-database-design.md +│ ├── 03-api-specification.md +│ ├── 04-rbac-design.md +│ ├── 05-module-details.md +│ ├── 06-miniapp-design.md +│ └── 07-deployment.md +│ +├── docker-compose.yml +└── README.md +``` + +## 五、开发阶段规划 + +### 第一阶段:基础框架(2周) +- Laravel 项目搭建 + 数据库迁移框架 +- RBAC 权限系统(用户、角色、权限、菜单) +- 门店隔离中间件 +- 操作日志中间件 +- 审批流引擎基础 +- Vue 3 后台框架 + 动态路由 + 登录认证 +- 系统设置模块(门店、部门、角色、用户) + +### 第二阶段:核心业务(4周) +- CRM 模块(线索、客户、签约、问卷) +- 房务模块(房态、预定、入离住) +- 护理模块(档案、计划、记录、异常、指标) +- 月子餐模块(排餐、送餐、评价) + +### 第三阶段:增值 & 供应链(3周) +- 服务与产康模块 +- 月嫂模块 +- 进销存模块 + +### 第四阶段:财务 & 人事(2周) +- 财务模块 +- 人事薪资模块 + +### 第五阶段:协同 & 报表(2周) +- 办公协同模块 +- 统计报表模块 +- 知识库模块 + +### 第六阶段:微客宝小程序(3周) +- uni-app 项目搭建 +- 首页(商城、点餐、学堂、月嫂、活动) +- 共伴成长(护理记录、数据图表) +- 个人中心(账户、订单、服务、评价) + +### 第七阶段:联调 & 部署(2周) +- 前后端联调 +- 微信支付对接 +- 性能优化 +- Docker 部署 +- 上线验收 diff --git a/docs/02-database-design.md b/docs/02-database-design.md new file mode 100644 index 0000000..e5d061c --- /dev/null +++ b/docs/02-database-design.md @@ -0,0 +1,1284 @@ +# 宫中有喜 — 数据库设计 + +## 一、设计原则 + +1. **门店隔离**:所有业务表均包含 `store_id` 字段,查询自动过滤 +2. **软删除**:关键业务表使用 `deleted_at` 软删除 +3. **审计字段**:所有表包含 `created_by`、`updated_by`、`created_at`、`updated_at` +4. **枚举用 tinyint**:状态字段用 tinyint + 代码层枚举类,不用 enum 类型 +5. **金额用 decimal(12,2)**:财务金额统一精度 +6. **索引策略**:外键自动索引 + 常用查询组合索引 + +## 二、核心表清单(按模块) + +### 2.1 系统基础(RBAC + 门店) + +```sql +-- 区域 +CREATE TABLE regions ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(50) NOT NULL COMMENT '区域名称', + sort INT DEFAULT 0, + status TINYINT DEFAULT 1 COMMENT '1启用 0停用', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='区域表'; + +-- 门店 +CREATE TABLE stores ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + region_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(100) NOT NULL COMMENT '门店名称', + code VARCHAR(20) UNIQUE COMMENT '门店编码', + address VARCHAR(255), + phone VARCHAR(20), + logo VARCHAR(255), + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_region (region_id) +) COMMENT='门店表'; + +-- 部门 +CREATE TABLE departments ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + parent_id BIGINT UNSIGNED DEFAULT 0, + name VARCHAR(50) NOT NULL, + sort INT DEFAULT 0, + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store (store_id) +) COMMENT='部门表'; + +-- 职务 +CREATE TABLE positions ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL, + sort INT DEFAULT 0, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='职务表'; + +-- 用户(员工) +CREATE TABLE users ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + department_id BIGINT UNSIGNED NULL, + position_id BIGINT UNSIGNED NULL, + username VARCHAR(50) NOT NULL, + password VARCHAR(255) NOT NULL, + name VARCHAR(50) NOT NULL COMMENT '真实姓名', + phone VARCHAR(20), + email VARCHAR(100), + avatar VARCHAR(255), + status TINYINT DEFAULT 0 COMMENT '0待审核 1正常 2禁用', + last_login_at TIMESTAMP NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + UNIQUE INDEX idx_username (username), + INDEX idx_store (store_id), + INDEX idx_phone (phone) +) COMMENT='用户表'; + +-- 角色 +CREATE TABLE roles ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NULL COMMENT 'NULL=全局角色', + name VARCHAR(50) NOT NULL, + code VARCHAR(50) NOT NULL COMMENT '角色编码', + description VARCHAR(255), + is_system TINYINT DEFAULT 0 COMMENT '系统内置不可删', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + UNIQUE INDEX idx_code (code) +) COMMENT='角色表'; + +-- 权限 +CREATE TABLE permissions ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + parent_id BIGINT UNSIGNED DEFAULT 0, + name VARCHAR(100) NOT NULL COMMENT '权限名称', + code VARCHAR(100) NOT NULL COMMENT '权限编码 如 crm.lead.create', + type TINYINT NOT NULL COMMENT '1菜单 2按钮 3接口', + path VARCHAR(255) COMMENT '前端路由/API路径', + icon VARCHAR(50), + sort INT DEFAULT 0, + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + UNIQUE INDEX idx_code (code) +) COMMENT='权限表'; + +-- 角色-权限关联 +CREATE TABLE role_permissions ( + role_id BIGINT UNSIGNED NOT NULL, + permission_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (role_id, permission_id) +) COMMENT='角色权限关联表'; + +-- 用户-角色关联 +CREATE TABLE user_roles ( + user_id BIGINT UNSIGNED NOT NULL, + role_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (user_id, role_id) +) COMMENT='用户角色关联表'; + +-- 操作日志 +CREATE TABLE operation_logs ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED, + user_id BIGINT UNSIGNED, + module VARCHAR(50) COMMENT '模块名', + action VARCHAR(50) COMMENT '动作', + target_type VARCHAR(50) COMMENT '操作对象类型', + target_id BIGINT UNSIGNED COMMENT '操作对象ID', + before_data JSON COMMENT '变更前', + after_data JSON COMMENT '变更后', + ip VARCHAR(45), + user_agent VARCHAR(500), + created_at TIMESTAMP NULL, + INDEX idx_store_user (store_id, user_id), + INDEX idx_created (created_at) +) COMMENT='操作日志表'; +``` + +### 2.2 CRM 模块 + +```sql +-- 渠道来源 +CREATE TABLE channels ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL, + type TINYINT COMMENT '1线上 2线下 3转介绍', + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='渠道来源表'; + +-- 线索 +CREATE TABLE leads ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + channel_id BIGINT UNSIGNED, + name VARCHAR(50) NOT NULL COMMENT '客户姓名', + phone VARCHAR(20) NOT NULL, + wechat VARCHAR(50), + expected_date DATE COMMENT '预产期', + source VARCHAR(50) COMMENT '具体来源', + status TINYINT DEFAULT 1 COMMENT '1新建 2跟进中 3已转化 4无效', + owner_id BIGINT UNSIGNED COMMENT '负责销售', + remark TEXT, + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store_status (store_id, status), + INDEX idx_owner (owner_id), + INDEX idx_phone (phone) +) COMMENT='线索表'; + +-- 线索跟进记录 +CREATE TABLE lead_follows ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + lead_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + type TINYINT COMMENT '1电话 2微信 3到店 4其他', + content TEXT, + next_follow_at TIMESTAMP NULL COMMENT '下次跟进时间', + created_at TIMESTAMP NULL, + INDEX idx_lead (lead_id) +) COMMENT='线索跟进记录'; + +-- 客户 +CREATE TABLE customers ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + lead_id BIGINT UNSIGNED COMMENT '来源线索', + name VARCHAR(50) NOT NULL, + phone VARCHAR(20) NOT NULL, + id_card VARCHAR(20), + wechat VARCHAR(50), + birthday DATE, + expected_date DATE COMMENT '预产期', + actual_date DATE COMMENT '实际生产日期', + baby_count TINYINT DEFAULT 1, + tags JSON COMMENT '标签数组', + status TINYINT DEFAULT 1 COMMENT '1潜在 2签约 3在住 4离店 5无效', + owner_id BIGINT UNSIGNED COMMENT '负责人', + remark TEXT, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + deleted_at TIMESTAMP NULL, + INDEX idx_store_status (store_id, status), + INDEX idx_phone (phone) +) COMMENT='客户表'; + +-- 客户家属 +CREATE TABLE customer_families ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + customer_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50), + phone VARCHAR(20), + relation VARCHAR(20) COMMENT '关系:丈夫/母亲/婆婆等', + is_emergency TINYINT DEFAULT 0, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_customer (customer_id) +) COMMENT='客户家属表'; + +-- 合同 +CREATE TABLE contracts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + contract_no VARCHAR(50) UNIQUE COMMENT '合同编号', + package_id BIGINT UNSIGNED COMMENT '套餐ID', + package_name VARCHAR(100), + total_amount DECIMAL(12,2) NOT NULL COMMENT '合同总额', + discount_amount DECIMAL(12,2) DEFAULT 0 COMMENT '优惠金额', + actual_amount DECIMAL(12,2) NOT NULL COMMENT '实收金额', + days INT COMMENT '入住天数', + check_in_date DATE, + check_out_date DATE, + status TINYINT DEFAULT 0 COMMENT '0待审 1通过 2无效 3已退', + sign_type TINYINT COMMENT '1线下 2网签', + audit_user_id BIGINT UNSIGNED, + audit_at TIMESTAMP NULL, + audit_remark VARCHAR(255), + remark TEXT, + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store (store_id), + INDEX idx_customer (customer_id) +) COMMENT='合同表'; + +-- 问卷模板 +CREATE TABLE questionnaire_templates ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED, + title VARCHAR(100) NOT NULL, + description TEXT, + questions JSON COMMENT '题目定义JSON', + type TINYINT COMMENT '1入住前 2入住中 3离店 4回访', + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='问卷模板表'; + +-- 问卷回收 +CREATE TABLE questionnaire_answers ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + template_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + answers JSON, + score DECIMAL(5,2), + status TINYINT DEFAULT 0 COMMENT '0未完成 1已提交', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_template (template_id), + INDEX idx_customer (customer_id) +) COMMENT='问卷回收表'; + +-- 投诉 +CREATE TABLE complaints ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + type TINYINT COMMENT '1服务 2膳食 3环境 4其他', + content TEXT NOT NULL, + images JSON, + status TINYINT DEFAULT 0 COMMENT '0待处理 1处理中 2已解决 3已关闭', + handler_id BIGINT UNSIGNED, + handle_result TEXT, + handle_at TIMESTAMP NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store_status (store_id, status) +) COMMENT='投诉表'; +``` + +### 2.3 房务模块 + +```sql +-- 房型 +CREATE TABLE room_types ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL, + price DECIMAL(12,2) COMMENT '日单价', + description TEXT, + images JSON, + facilities JSON COMMENT '设施列表', + sort INT DEFAULT 0, + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='房型表'; + +-- 房间 +CREATE TABLE rooms ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + room_type_id BIGINT UNSIGNED NOT NULL, + floor VARCHAR(10) COMMENT '楼层', + number VARCHAR(20) NOT NULL COMMENT '房号', + status TINYINT DEFAULT 1 COMMENT '1空房 2已预定 3入住 4维修 5清洁', + sort INT DEFAULT 0, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + UNIQUE INDEX idx_store_number (store_id, number) +) COMMENT='房间表'; + +-- 预定/入住记录 +CREATE TABLE reservations ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + contract_id BIGINT UNSIGNED, + room_id BIGINT UNSIGNED NOT NULL, + check_in_date DATE NOT NULL, + check_out_date DATE NOT NULL, + actual_check_in TIMESTAMP NULL, + actual_check_out TIMESTAMP NULL, + status TINYINT DEFAULT 0 COMMENT '0预定 1已入住 2已退房 3已取消', + remark TEXT, + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_room_date (room_id, check_in_date, check_out_date), + INDEX idx_customer (customer_id) +) COMMENT='预定/入住记录表'; + +-- 客户外出记录 +CREATE TABLE customer_outings ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + reservation_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + out_at TIMESTAMP NOT NULL, + expected_back_at TIMESTAMP, + actual_back_at TIMESTAMP NULL, + reason TEXT, + risk_note TEXT COMMENT '风险提醒说明', + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='客户外出记录'; + +-- 呼叫记录 +CREATE TABLE call_records ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + room_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED, + type TINYINT COMMENT '1护理 2清洁 3送水 4其他', + status TINYINT DEFAULT 0 COMMENT '0待处理 1处理中 2已完成', + handler_id BIGINT UNSIGNED, + handle_at TIMESTAMP NULL, + remark VARCHAR(255), + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store_status (store_id, status) +) COMMENT='呼叫记录表'; + +-- 场地 +CREATE TABLE venues ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL COMMENT 'SPA房/瑜伽房/活动室', + capacity INT, + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='场地表'; + +-- 场地预约 +CREATE TABLE venue_bookings ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + venue_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED, + booking_date DATE NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, + purpose VARCHAR(100), + status TINYINT DEFAULT 0 COMMENT '0预约 1使用中 2已结束 3已取消', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_venue_date (venue_id, booking_date) +) COMMENT='场地预约表'; +``` + +### 2.4 护理模块 + +```sql +-- 护理档案 +CREATE TABLE care_profiles ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + customer_id BIGINT UNSIGNED NOT NULL, + reservation_id BIGINT UNSIGNED, + type TINYINT COMMENT '1妈妈 2宝宝', + baby_name VARCHAR(50), + baby_gender TINYINT COMMENT '1男 2女', + baby_birthday DATETIME, + birth_weight DECIMAL(5,2), + birth_method TINYINT COMMENT '1顺产 2剖宫产', + allergies TEXT COMMENT '过敏信息', + medical_history TEXT COMMENT '既往病史', + risk_level TINYINT DEFAULT 0 COMMENT '0正常 1低风险 2高风险', + assessment JSON COMMENT '入所评估结果', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_customer (customer_id) +) COMMENT='护理档案表'; + +-- 护理计划 +CREATE TABLE care_plans ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + care_profile_id BIGINT UNSIGNED NOT NULL, + plan_date DATE NOT NULL, + stage VARCHAR(50) COMMENT '阶段:产后第X天', + items JSON COMMENT '计划项目列表', + nurse_id BIGINT UNSIGNED COMMENT '责任护士', + status TINYINT DEFAULT 0 COMMENT '0待执行 1执行中 2已完成', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_profile_date (care_profile_id, plan_date) +) COMMENT='护理计划表'; + +-- 护理记录 +CREATE TABLE care_records ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + care_profile_id BIGINT UNSIGNED NOT NULL, + care_plan_id BIGINT UNSIGNED, + type TINYINT COMMENT '1妈妈护理 2宝宝护理', + items JSON COMMENT '执行的护理项目与结果', + remark TEXT, + images JSON COMMENT '护理照片', + nurse_id BIGINT UNSIGNED NOT NULL, + recorded_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_profile (care_profile_id), + INDEX idx_date (recorded_at) +) COMMENT='护理记录表'; + +-- 护理异常 +CREATE TABLE care_exceptions ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + care_profile_id BIGINT UNSIGNED NOT NULL, + reporter_id BIGINT UNSIGNED NOT NULL, + level TINYINT COMMENT '1一般 2重要 3紧急', + description TEXT NOT NULL, + images JSON, + status TINYINT DEFAULT 0 COMMENT '0上报 1处理中 2已解决', + handler_id BIGINT UNSIGNED, + handle_result TEXT, + handle_at TIMESTAMP NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_profile (care_profile_id) +) COMMENT='护理异常表'; + +-- 健康指标记录 +CREATE TABLE health_metrics ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + care_profile_id BIGINT UNSIGNED NOT NULL, + metric_type VARCHAR(30) NOT NULL COMMENT 'temperature/weight/jaundice/blood_pressure', + value DECIMAL(8,2) NOT NULL, + unit VARCHAR(10), + recorded_at TIMESTAMP NOT NULL, + recorder_id BIGINT UNSIGNED, + remark VARCHAR(255), + created_at TIMESTAMP NULL, + INDEX idx_profile_type (care_profile_id, metric_type), + INDEX idx_recorded (recorded_at) +) COMMENT='健康指标记录表'; +``` + +### 2.5 月子餐模块 + +```sql +-- 菜品库 +CREATE TABLE dishes ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL, + category VARCHAR(30) COMMENT '分类:汤/主食/配菜/甜品', + description TEXT, + image VARCHAR(255), + ingredients JSON COMMENT '主要食材', + contraindications JSON COMMENT '禁忌分类', + price DECIMAL(8,2) DEFAULT 0, + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store (store_id) +) COMMENT='菜品库'; + +-- 排餐模板 +CREATE TABLE meal_plan_templates ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL, + stage VARCHAR(30) COMMENT '适用阶段:产后1-7天/8-14天等', + meals JSON COMMENT '每日餐次与菜品配置', + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='排餐模板表'; + +-- 每日排餐 +CREATE TABLE daily_meal_plans ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + plan_date DATE NOT NULL, + meal_type TINYINT NOT NULL COMMENT '1早餐 2午餐 3下午茶 4晚餐 5宵夜', + dishes JSON COMMENT '菜品列表', + special_note TEXT COMMENT '特殊要求', + status TINYINT DEFAULT 0 COMMENT '0待备餐 1已备餐 2已送达 3未用', + deliver_at TIMESTAMP NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_customer_date (customer_id, plan_date), + INDEX idx_store_date (store_id, plan_date) +) COMMENT='每日排餐表'; + +-- 膳食评价 +CREATE TABLE meal_reviews ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + daily_meal_plan_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + score TINYINT COMMENT '1-5星', + content TEXT, + created_at TIMESTAMP NULL, + INDEX idx_plan (daily_meal_plan_id) +) COMMENT='膳食评价表'; +``` + +### 2.6 服务与产康模块 + +```sql +-- 服务项目 +CREATE TABLE service_items ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(100) NOT NULL, + category TINYINT COMMENT '1产康 2零售 3加餐 4其他', + price DECIMAL(10,2) NOT NULL, + duration INT COMMENT '服务时长(分钟)', + description TEXT, + image VARCHAR(255), + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store_cat (store_id, category) +) COMMENT='服务项目表'; + +-- 服务包/套餐 +CREATE TABLE service_packages ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(100) NOT NULL, + items JSON COMMENT '包含项目与次数', + price DECIMAL(10,2) NOT NULL, + validity_days INT COMMENT '有效天数', + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='服务包表'; + +-- 服务订单 +CREATE TABLE service_orders ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + order_no VARCHAR(50) UNIQUE, + type TINYINT COMMENT '1单项 2服务包 3零售 4加餐', + items JSON COMMENT '订单项明细', + total_amount DECIMAL(12,2), + discount_amount DECIMAL(12,2) DEFAULT 0, + actual_amount DECIMAL(12,2), + status TINYINT DEFAULT 0 COMMENT '0待付 1已付 2已完成 3已取消 4已退', + pay_method TINYINT COMMENT '1微信 2支付宝 3现金 4挂账', + paid_at TIMESTAMP NULL, + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store (store_id), + INDEX idx_customer (customer_id) +) COMMENT='服务订单表'; + +-- 服务执行记录 +CREATE TABLE service_executions ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + service_order_id BIGINT UNSIGNED NOT NULL, + service_item_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + technician_id BIGINT UNSIGNED COMMENT '技师/产康师', + scheduled_at TIMESTAMP COMMENT '预约时间', + started_at TIMESTAMP NULL, + ended_at TIMESTAMP NULL, + status TINYINT DEFAULT 0 COMMENT '0待执行 1执行中 2已完成 3已取消', + remark TEXT, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_order (service_order_id), + INDEX idx_technician (technician_id) +) COMMENT='服务执行记录表'; + +-- 服务评价 +CREATE TABLE service_reviews ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + service_order_id BIGINT UNSIGNED, + service_execution_id BIGINT UNSIGNED, + customer_id BIGINT UNSIGNED NOT NULL, + score TINYINT COMMENT '1-5星', + content TEXT, + created_at TIMESTAMP NULL, + INDEX idx_order (service_order_id) +) COMMENT='服务评价表'; +``` + +### 2.7 月嫂模块 + +```sql +-- 月嫂档案 +CREATE TABLE nannies ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL, + phone VARCHAR(20), + id_card VARCHAR(20), + avatar VARCHAR(255), + level TINYINT COMMENT '1初级 2中级 3高级 4金牌', + skills JSON COMMENT '技能标签', + experience_years INT, + health_cert VARCHAR(255) COMMENT '健康证', + cooperation_type TINYINT COMMENT '1自有 2合作 3兼职', + base_salary DECIMAL(10,2), + introduction TEXT, + status TINYINT DEFAULT 1 COMMENT '1空闲 2服务中 3休假 4停用', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store_status (store_id, status) +) COMMENT='月嫂档案表'; + +-- 月嫂订单 +CREATE TABLE nanny_orders ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + nanny_id BIGINT UNSIGNED, + order_no VARCHAR(50) UNIQUE, + service_type TINYINT COMMENT '1月嫂 2育儿嫂', + start_date DATE, + end_date DATE, + days INT, + price DECIMAL(12,2), + status TINYINT DEFAULT 0 COMMENT '0待匹配 1待确认 2已签约 3服务中 4已完成 5已取消', + match_candidates JSON COMMENT '匹配候选月嫂', + remark TEXT, + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store (store_id), + INDEX idx_nanny (nanny_id) +) COMMENT='月嫂订单表'; + +-- 月嫂排班 +CREATE TABLE nanny_schedules ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nanny_id BIGINT UNSIGNED NOT NULL, + nanny_order_id BIGINT UNSIGNED, + schedule_date DATE NOT NULL, + type TINYINT COMMENT '1服务 2休息 3培训', + remark VARCHAR(255), + created_at TIMESTAMP NULL, + INDEX idx_nanny_date (nanny_id, schedule_date) +) COMMENT='月嫂排班表'; + +-- 月嫂评价 +CREATE TABLE nanny_reviews ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + nanny_order_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + nanny_id BIGINT UNSIGNED NOT NULL, + score TINYINT COMMENT '1-5', + content TEXT, + created_at TIMESTAMP NULL, + INDEX idx_nanny (nanny_id) +) COMMENT='月嫂评价表'; +``` + +### 2.8 进销存模块 + +```sql +-- 仓库 +CREATE TABLE warehouses ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL, + manager_id BIGINT UNSIGNED, + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='仓库表'; + +-- 供应商 +CREATE TABLE suppliers ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(100) NOT NULL, + contact VARCHAR(50), + phone VARCHAR(20), + address VARCHAR(255), + bank_info VARCHAR(255), + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='供应商表'; + +-- 物资(SKU) +CREATE TABLE materials ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(100) NOT NULL, + code VARCHAR(50) COMMENT 'SKU编码', + category VARCHAR(50), + unit VARCHAR(20) COMMENT '单位', + spec VARCHAR(100) COMMENT '规格', + min_stock INT DEFAULT 0 COMMENT '安全库存', + shelf_life_days INT COMMENT '保质天数', + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store (store_id), + INDEX idx_code (code) +) COMMENT='物资表'; + +-- 库存 +CREATE TABLE inventories ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + warehouse_id BIGINT UNSIGNED NOT NULL, + material_id BIGINT UNSIGNED NOT NULL, + quantity DECIMAL(12,2) DEFAULT 0, + batch_no VARCHAR(50), + expire_date DATE, + updated_at TIMESTAMP NULL, + UNIQUE INDEX idx_wh_mat (warehouse_id, material_id, batch_no) +) COMMENT='库存表'; + +-- 采购单 +CREATE TABLE purchase_orders ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + order_no VARCHAR(50) UNIQUE, + supplier_id BIGINT UNSIGNED, + total_amount DECIMAL(12,2), + status TINYINT DEFAULT 0 COMMENT '0草稿 1待审批 2已审批 3已入库 4已取消', + items JSON COMMENT '采购明细', + audit_user_id BIGINT UNSIGNED, + audit_at TIMESTAMP NULL, + remark TEXT, + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='采购单表'; + +-- 出入库单 +CREATE TABLE stock_movements ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + warehouse_id BIGINT UNSIGNED NOT NULL, + movement_no VARCHAR(50) UNIQUE, + type TINYINT NOT NULL COMMENT '1入库 2出库 3调拨 4盘点', + direction TINYINT COMMENT '1入 2出', + items JSON COMMENT '物资明细', + related_order VARCHAR(50) COMMENT '关联单号', + status TINYINT DEFAULT 0 COMMENT '0草稿 1已确认', + operator_id BIGINT UNSIGNED, + remark TEXT, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store_type (store_id, type) +) COMMENT='出入库单表'; +``` + +### 2.9 财务模块 + +```sql +-- 资金账户 +CREATE TABLE fund_accounts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL COMMENT '账户名称', + type TINYINT COMMENT '1银行 2微信 3支付宝 4现金', + balance DECIMAL(14,2) DEFAULT 0, + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='资金账户表'; + +-- 收支记录 +CREATE TABLE financial_records ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + account_id BIGINT UNSIGNED NOT NULL, + record_no VARCHAR(50) UNIQUE, + direction TINYINT NOT NULL COMMENT '1收入 2支出', + category VARCHAR(50) COMMENT '分类:套餐/产康/零售/押金/工资/采购...', + amount DECIMAL(12,2) NOT NULL, + customer_id BIGINT UNSIGNED, + related_type VARCHAR(50) COMMENT '关联单据类型', + related_id BIGINT UNSIGNED COMMENT '关联单据ID', + pay_method TINYINT COMMENT '1微信 2支付宝 3现金 4转账 5POS', + status TINYINT DEFAULT 1 COMMENT '1已确认 2待确认 3已冲正', + remark TEXT, + operator_id BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store_dir (store_id, direction), + INDEX idx_customer (customer_id), + INDEX idx_date (created_at) +) COMMENT='收支记录表'; + +-- 客户账户 +CREATE TABLE customer_accounts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + customer_id BIGINT UNSIGNED NOT NULL, + store_id BIGINT UNSIGNED NOT NULL, + type TINYINT COMMENT '1套餐账 2会员账 3押金账 4预存卡', + total_amount DECIMAL(12,2) DEFAULT 0 COMMENT '总额', + used_amount DECIMAL(12,2) DEFAULT 0 COMMENT '已用', + balance DECIMAL(12,2) DEFAULT 0 COMMENT '余额', + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_customer (customer_id) +) COMMENT='客户账户表'; + +-- 客户账户流水 +CREATE TABLE customer_account_logs ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + customer_account_id BIGINT UNSIGNED NOT NULL, + direction TINYINT COMMENT '1充值/存入 2消费/扣除 3退还', + amount DECIMAL(12,2) NOT NULL, + balance_after DECIMAL(12,2), + related_type VARCHAR(50), + related_id BIGINT UNSIGNED, + remark VARCHAR(255), + operator_id BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + INDEX idx_account (customer_account_id) +) COMMENT='客户账户流水表'; + +-- 服务卡/代金卡 +CREATE TABLE voucher_cards ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + card_no VARCHAR(50) UNIQUE, + type TINYINT COMMENT '1服务卡 2代金卡', + total_amount DECIMAL(10,2), + balance DECIMAL(10,2), + expire_date DATE, + status TINYINT DEFAULT 1 COMMENT '1正常 2冻结 3已退 4已过期', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_customer (customer_id) +) COMMENT='服务卡/代金卡表'; +``` + +### 2.10 人事薪资模块 + +```sql +-- 员工档案(扩展 users 表的人事信息) +CREATE TABLE employee_profiles ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL UNIQUE, + hire_date DATE, + regular_date DATE COMMENT '转正日期', + contract_start DATE, + contract_end DATE, + id_card VARCHAR(20), + bank_card VARCHAR(30), + bank_name VARCHAR(50), + education VARCHAR(20), + emergency_contact VARCHAR(50), + emergency_phone VARCHAR(20), + attachments JSON COMMENT '证件附件', + remark TEXT, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='员工档案表'; + +-- 班次模板 +CREATE TABLE shift_templates ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL, + start_time TIME, + end_time TIME, + color VARCHAR(10) COMMENT '颜色标识', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='班次模板表'; + +-- 排班 +CREATE TABLE schedules ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + shift_template_id BIGINT UNSIGNED, + schedule_date DATE NOT NULL, + remark VARCHAR(255), + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_user_date (user_id, schedule_date), + INDEX idx_store_date (store_id, schedule_date) +) COMMENT='排班表'; + +-- 业绩记录 +CREATE TABLE performances ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + month VARCHAR(7) COMMENT '2024-01', + metric_type VARCHAR(50) COMMENT '指标类型', + metric_value DECIMAL(12,2), + remark VARCHAR(255), + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_user_month (user_id, month) +) COMMENT='业绩记录表'; + +-- 提成方案 +CREATE TABLE commission_plans ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(100), + rules JSON COMMENT '提成规则', + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='提成方案表'; + +-- 薪资记录 +CREATE TABLE salary_records ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + month VARCHAR(7) NOT NULL, + base_salary DECIMAL(10,2), + commission DECIMAL(10,2) DEFAULT 0, + bonus DECIMAL(10,2) DEFAULT 0, + deduction DECIMAL(10,2) DEFAULT 0, + total DECIMAL(10,2), + status TINYINT DEFAULT 0 COMMENT '0待发 1已发', + paid_at TIMESTAMP NULL, + remark TEXT, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_user_month (user_id, month) +) COMMENT='薪资记录表'; +``` + +### 2.11 办公协同模块 + +```sql +-- 审批流模板 +CREATE TABLE approval_templates ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED, + name VARCHAR(100) NOT NULL, + type VARCHAR(50) COMMENT '请假/报销/采购/优惠/退款...', + flow JSON COMMENT '审批节点配置', + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='审批流模板表'; + +-- 审批单 +CREATE TABLE approvals ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + template_id BIGINT UNSIGNED, + approval_no VARCHAR(50) UNIQUE, + type VARCHAR(50), + title VARCHAR(200), + content JSON COMMENT '表单数据', + applicant_id BIGINT UNSIGNED NOT NULL, + current_step INT DEFAULT 0, + status TINYINT DEFAULT 0 COMMENT '0待审批 1审批中 2已通过 3已拒绝 4已撤回', + related_type VARCHAR(50), + related_id BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_store_status (store_id, status), + INDEX idx_applicant (applicant_id) +) COMMENT='审批单表'; + +-- 审批记录 +CREATE TABLE approval_records ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + approval_id BIGINT UNSIGNED NOT NULL, + step INT, + approver_id BIGINT UNSIGNED NOT NULL, + action TINYINT COMMENT '1同意 2拒绝 3转审', + opinion TEXT, + created_at TIMESTAMP NULL, + INDEX idx_approval (approval_id) +) COMMENT='审批记录表'; + +-- 通知/公告 +CREATE TABLE notices ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED, + title VARCHAR(200) NOT NULL, + content TEXT, + type TINYINT COMMENT '1公告 2通知 3系统消息', + target_type TINYINT COMMENT '1全员 2指定角色 3指定人', + target_ids JSON, + publish_at TIMESTAMP NULL, + status TINYINT DEFAULT 0 COMMENT '0草稿 1已发布', + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='通知公告表'; + +-- 交班记录 +CREATE TABLE shift_handovers ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + department VARCHAR(50), + handover_date DATE NOT NULL, + from_user_id BIGINT UNSIGNED NOT NULL, + to_user_id BIGINT UNSIGNED, + content TEXT NOT NULL, + items JSON COMMENT '交接事项清单', + status TINYINT DEFAULT 0 COMMENT '0待接 1已接', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='交班记录表'; +``` + +### 2.12 知识库模块 + +```sql +-- 知识分类 +CREATE TABLE kb_categories ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + parent_id BIGINT UNSIGNED DEFAULT 0, + name VARCHAR(50) NOT NULL, + target TINYINT COMMENT '1员工 2客户 3通用', + sort INT DEFAULT 0, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='知识分类表'; + +-- 知识内容 +CREATE TABLE kb_articles ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + category_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(200) NOT NULL, + content LONGTEXT, + media_type TINYINT COMMENT '1图文 2视频 3文档', + media_url VARCHAR(500), + cover VARCHAR(255), + view_count INT DEFAULT 0, + status TINYINT DEFAULT 0 COMMENT '0草稿 1已发布 2已下架', + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_category (category_id) +) COMMENT='知识内容表'; + +-- 学习任务 +CREATE TABLE learning_tasks ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(200), + articles JSON COMMENT '关联文章/视频ID列表', + target_roles JSON COMMENT '目标角色', + deadline DATE, + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='学习任务表'; + +-- 学习记录 +CREATE TABLE learning_records ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + learning_task_id BIGINT UNSIGNED, + user_id BIGINT UNSIGNED NOT NULL, + article_id BIGINT UNSIGNED NOT NULL, + progress TINYINT DEFAULT 0 COMMENT '0-100', + completed_at TIMESTAMP NULL, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + INDEX idx_user (user_id) +) COMMENT='学习记录表'; + +-- 考试 +CREATE TABLE exams ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(200), + questions JSON, + pass_score INT DEFAULT 60, + time_limit INT COMMENT '分钟', + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='考试表'; + +-- 考试记录 +CREATE TABLE exam_records ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + exam_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + answers JSON, + score INT, + is_pass TINYINT, + started_at TIMESTAMP, + submitted_at TIMESTAMP NULL, + created_at TIMESTAMP NULL, + INDEX idx_exam_user (exam_id, user_id) +) COMMENT='考试记录表'; +``` + +### 2.13 微客宝专用表 + +```sql +-- 客户微信绑定 +CREATE TABLE customer_wechat ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + customer_id BIGINT UNSIGNED NOT NULL, + open_id VARCHAR(64) NOT NULL, + union_id VARCHAR(64), + nickname VARCHAR(100), + avatar VARCHAR(255), + session_key VARCHAR(255), + phone VARCHAR(20), + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, + UNIQUE INDEX idx_openid (open_id), + INDEX idx_customer (customer_id) +) COMMENT='客户微信绑定表'; + +-- 优惠券定义 +CREATE TABLE coupon_templates ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(100) NOT NULL, + type TINYINT COMMENT '1满减 2折扣 3代金', + value DECIMAL(10,2) COMMENT '面值/折扣值', + min_amount DECIMAL(10,2) DEFAULT 0 COMMENT '使用门槛', + applicable JSON COMMENT '适用范围', + total_count INT COMMENT '总发行量', + issued_count INT DEFAULT 0, + start_date DATE, + end_date DATE, + status TINYINT DEFAULT 1, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='优惠券模板表'; + +-- 客户优惠券 +CREATE TABLE customer_coupons ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + template_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + code VARCHAR(50) UNIQUE, + status TINYINT DEFAULT 0 COMMENT '0未使用 1已使用 2已过期', + used_at TIMESTAMP NULL, + used_order_id BIGINT UNSIGNED, + expire_date DATE, + created_at TIMESTAMP NULL, + INDEX idx_customer (customer_id) +) COMMENT='客户优惠券表'; + +-- 活动 +CREATE TABLE activities ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(200) NOT NULL, + content LONGTEXT, + cover VARCHAR(255), + activity_date DATETIME, + location VARCHAR(200), + max_participants INT, + current_participants INT DEFAULT 0, + status TINYINT DEFAULT 0 COMMENT '0草稿 1报名中 2已满 3已结束', + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='活动表'; + +-- 活动报名 +CREATE TABLE activity_registrations ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + activity_id BIGINT UNSIGNED NOT NULL, + customer_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(50), + phone VARCHAR(20), + expected_date DATE COMMENT '预产期', + status TINYINT DEFAULT 0 COMMENT '0已报名 1已签到 2已取消', + created_at TIMESTAMP NULL, + INDEX idx_activity (activity_id) +) COMMENT='活动报名表'; + +-- 母婴圈动态 +CREATE TABLE community_posts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + store_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(200), + content TEXT, + images JSON, + type TINYINT COMMENT '1官方 2活动花絮 3课程', + view_count INT DEFAULT 0, + like_count INT DEFAULT 0, + status TINYINT DEFAULT 1, + created_by BIGINT UNSIGNED, + created_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL +) COMMENT='母婴圈动态表'; +``` + +## 三、ER 关系要点 + +``` +stores 1──N rooms +stores 1──N users +stores 1──N customers (通过业务表关联) + +customers 1──N contracts +customers 1──N reservations +customers 1──N care_profiles +customers 1──N service_orders +customers 1──N nanny_orders +customers 1──N customer_accounts +customers 1──1 customer_wechat + +contracts 1──1 reservations +reservations N──1 rooms + +care_profiles 1──N care_plans +care_profiles 1──N care_records +care_profiles 1──N health_metrics + +users N──N roles (user_roles) +roles N──N permissions (role_permissions) +``` + +## 四、索引与性能策略 + +1. **门店隔离索引**:所有业务表 `store_id` 为第一索引字段 +2. **时间范围查询**:日期字段加索引(`plan_date`、`schedule_date`、`created_at`) +3. **外键关联索引**:`customer_id`、`user_id`、`order_id` 等 +4. **读写分离预留**:Laravel 配置支持读写分离 +5. **JSON 字段**:用于灵活配置(标签、表单、明细),不做条件查询的场景 diff --git a/docs/03-api-specification.md b/docs/03-api-specification.md new file mode 100644 index 0000000..77657a6 --- /dev/null +++ b/docs/03-api-specification.md @@ -0,0 +1,792 @@ +# 宫中有喜 — API 接口规范 + +## 一、全局约定 + +### 1.1 基础信息 + +| 项 | 值 | +|---|---| +| Base URL | `https://{domain}/api/v1/` | +| 客户端 Base URL | `https://{domain}/api/v1/client/` | +| Content-Type | `application/json` | +| 认证方式 | Bearer Token(Sanctum) | +| 时区 | Asia/Shanghai | +| 编码 | UTF-8 | + +### 1.2 认证 + +``` +# 后台登录 +POST /api/v1/auth/login +Body: { "username": "admin", "password": "xxx" } +Response: { "code": 0, "data": { "token": "xxx", "user": {...} } } + +# 小程序登录 +POST /api/v1/client/auth/wechat-login +Body: { "code": "wx_code", "encrypted_data": "...", "iv": "..." } + +# 退出 +POST /api/v1/auth/logout +Header: Authorization: Bearer {token} + +# 刷新用户信息 +GET /api/v1/auth/me +``` + +### 1.3 统一响应格式 + +```json +// 成功 +{ + "code": 0, + "message": "success", + "data": { ... } +} + +// 分页 +{ + "code": 0, + "data": { + "list": [...], + "total": 100, + "page": 1, + "page_size": 20 + } +} + +// 失败 +{ + "code": 40001, + "message": "参数校验失败", + "errors": { + "phone": ["手机号格式不正确"] + } +} +``` + +### 1.4 错误码规范 + +| 范围 | 含义 | +|------|------| +| 0 | 成功 | +| 40001–40099 | 参数校验错误 | +| 40100–40199 | 认证错误(token 无效/过期) | +| 40300–40399 | 权限不足 | +| 40400–40499 | 资源不存在 | +| 42200–42299 | 业务逻辑冲突 | +| 50000–50099 | 服务器内部错误 | + +### 1.5 分页与筛选 + +``` +GET /api/v1/xxx?page=1&page_size=20&keyword=张&status=1&sort_by=created_at&sort_order=desc +``` + +| 参数 | 类型 | 说明 | +|------|------|------| +| page | int | 页码,默认 1 | +| page_size | int | 每页数量,默认 20,最大 100 | +| keyword | string | 模糊搜索 | +| sort_by | string | 排序字段 | +| sort_order | string | asc / desc | +| store_id | int | 门店过滤(超级管理员可用) | +| start_date / end_date | string | 日期范围 YYYY-MM-DD | + +### 1.6 通用 Header + +| Header | 说明 | +|--------|------| +| Authorization | `Bearer {token}` | +| X-Store-Id | 当前门店 ID(切换门店时) | +| Accept-Language | zh-CN | + +--- + +## 二、后台管理 API(Admin) + +### 2.1 系统设置模块 `/system/` + +``` +# 门店管理 +GET /system/stores # 门店列表 +POST /system/stores # 新建门店 +GET /system/stores/{id} # 门店详情 +PUT /system/stores/{id} # 编辑门店 +DELETE /system/stores/{id} # 删除门店 + +# 部门管理 +GET /system/departments # 部门树 +POST /system/departments # 新建部门 +PUT /system/departments/{id} # 编辑部门 +DELETE /system/departments/{id} # 删除部门 + +# 职务管理 +GET /system/positions # 职务列表 +POST /system/positions # 新建职务 +PUT /system/positions/{id} # 编辑 +DELETE /system/positions/{id} # 删除 + +# 用户管理 +GET /system/users # 用户列表(支持部门筛选) +POST /system/users # 新建用户 +GET /system/users/{id} # 用户详情 +PUT /system/users/{id} # 编辑用户 +PUT /system/users/{id}/status # 启用/禁用 +PUT /system/users/{id}/password # 重置密码 +DELETE /system/users/{id} # 删除用户 + +# 角色管理 +GET /system/roles # 角色列表 +POST /system/roles # 新建角色 +GET /system/roles/{id} # 角色详情(含权限树) +PUT /system/roles/{id} # 编辑角色 +DELETE /system/roles/{id} # 删除角色 +PUT /system/roles/{id}/permissions # 分配权限 + +# 菜单管理 +GET /system/menus # 菜单树 +POST /system/menus # 新建菜单 +PUT /system/menus/{id} # 编辑菜单 +DELETE /system/menus/{id} # 删除菜单 + +# 权限管理 +GET /system/permissions # 权限列表 +POST /system/permissions # 新建权限 + +# 操作日志 +GET /system/operation-logs # 操作日志列表 + +# 数据字典 +GET /system/dictionaries # 字典类型列表 +POST /system/dictionaries # 新建字典类型 +GET /system/dictionaries/{id}/items # 字典项列表 +POST /system/dictionaries/{id}/items # 新建字典项 + +# 系统配置 +GET /system/configs # 获取系统配置 +PUT /system/configs # 更新系统配置 +``` + +### 2.2 CRM 模块 `/crm/` + +``` +# 线索管理 +GET /crm/leads # 线索列表 +POST /crm/leads # 新建线索 +GET /crm/leads/{id} # 线索详情 +PUT /crm/leads/{id} # 编辑线索 +POST /crm/leads/{id}/assign # 分配线索 +POST /crm/leads/{id}/convert # 线索转客户 +POST /crm/leads/import # 批量导入 +GET /crm/leads/export # 导出 Excel + +# 跟进记录 +GET /crm/leads/{id}/follow-ups # 跟进记录列表 +POST /crm/leads/{id}/follow-ups # 新建跟进记录 + +# 客户管理 +GET /crm/customers # 客户列表 +POST /crm/customers # 新建客户 +GET /crm/customers/{id} # 客户详情(含跟进、合同) +PUT /crm/customers/{id} # 编辑客户 +DELETE /crm/customers/{id} # 删除客户 + +# 合同/签约 +GET /crm/contracts # 合同列表 +POST /crm/contracts # 新建合同 +GET /crm/contracts/{id} # 合同详情 +PUT /crm/contracts/{id} # 编辑合同 +POST /crm/contracts/{id}/approve # 审批合同 +GET /crm/contracts/{id}/pdf # 导出合同 PDF + +# 满意度问卷 +GET /crm/surveys # 问卷模板列表 +POST /crm/surveys # 新建问卷 +GET /crm/surveys/{id}/results # 问卷结果 + +# CRM 仪表盘 +GET /crm/dashboard # CRM 统计数据 +``` + +### 2.3 房务模块 `/room/` + +``` +# 楼层/房型 +GET /room/floors # 楼层列表 +POST /room/floors # 新建楼层 +GET /room/room-types # 房型列表 +POST /room/room-types # 新建房型 + +# 房间管理 +GET /room/rooms # 房间列表(含状态) +POST /room/rooms # 新建房间 +PUT /room/rooms/{id} # 编辑房间 +GET /room/rooms/board # 房态看板(日历视图数据) + +# 预定管理 +GET /room/reservations # 预定列表 +POST /room/reservations # 新建预定 +PUT /room/reservations/{id} # 编辑预定 +POST /room/reservations/{id}/cancel # 取消预定 +POST /room/reservations/{id}/checkin # 办理入住 + +# 入住管理 +GET /room/check-ins # 在住列表 +GET /room/check-ins/{id} # 入住详情 +POST /room/check-ins/{id}/extend # 延住 +POST /room/check-ins/{id}/change-room # 换房 +POST /room/check-ins/{id}/checkout # 办理离店 + +# 房间日志 +GET /room/rooms/{id}/logs # 房间操作日志 +``` + +### 2.4 护理模块 `/care/` + +``` +# 护理档案 +GET /care/profiles # 档案列表 +POST /care/profiles # 新建档案 +GET /care/profiles/{id} # 档案详情 +PUT /care/profiles/{id} # 编辑档案 + +# 护理计划 +GET /care/plans # 计划列表 +POST /care/plans # 新建计划(含模板) +GET /care/plans/{id} # 计划详情 +PUT /care/plans/{id} # 编辑计划 +POST /care/plans/{id}/activate # 激活计划 + +# 护理记录 +GET /care/records # 记录列表 +POST /care/records # 新建记录 +GET /care/records/{id} # 记录详情 + +# 异常登记 +POST /care/alerts # 新建异常 +GET /care/alerts # 异常列表 +PUT /care/alerts/{id}/resolve # 处理异常 + +# 健康指标 +GET /care/health-metrics # 指标记录列表 +POST /care/health-metrics # 录入指标 +GET /care/health-metrics/chart # 指标趋势图数据 + +# 护理模板 +GET /care/templates # 护理模板列表 +POST /care/templates # 新建模板 +``` + +### 2.5 月子餐模块 `/meal/` + +``` +# 菜品管理 +GET /meal/dishes # 菜品列表 +POST /meal/dishes # 新建菜品 +PUT /meal/dishes/{id} # 编辑菜品 +DELETE /meal/dishes/{id} # 删除菜品 + +# 餐单模板 +GET /meal/templates # 餐单模板列表 +POST /meal/templates # 新建餐单模板 +GET /meal/templates/{id} # 模板详情 + +# 每日排餐 +GET /meal/daily-plans # 每日排餐列表 +POST /meal/daily-plans # 新建/复制排餐 +PUT /meal/daily-plans/{id} # 编辑排餐 +POST /meal/daily-plans/batch-generate # 批量生成排餐 + +# 送餐记录 +GET /meal/deliveries # 送餐记录列表 +POST /meal/deliveries/{id}/confirm # 确认送达 +POST /meal/deliveries/{id}/feedback # 餐品反馈 + +# 餐品评价 +GET /meal/reviews # 评价列表 +GET /meal/reviews/stats # 评价统计 +``` + +### 2.6 服务与产康模块 `/service/` + +``` +# 服务项目 +GET /service/items # 服务项目列表 +POST /service/items # 新建项目 +PUT /service/items/{id} # 编辑项目 + +# 服务套餐 +GET /service/packages # 套餐列表 +POST /service/packages # 新建套餐 +PUT /service/packages/{id} # 编辑套餐 + +# 服务预约/订单 +GET /service/orders # 订单列表 +POST /service/orders # 新建预约 +PUT /service/orders/{id} # 编辑 +POST /service/orders/{id}/confirm # 确认 +POST /service/orders/{id}/complete # 完成 +POST /service/orders/{id}/cancel # 取消 + +# 技师排班 +GET /service/therapist-schedules # 排班列表 +POST /service/therapist-schedules # 新建排班 +GET /service/therapist-schedules/board # 排班看板 +``` + +### 2.7 月嫂模块 `/nanny/` + +``` +# 月嫂档案 +GET /nanny/nannies # 月嫂列表 +POST /nanny/nannies # 新建月嫂 +GET /nanny/nannies/{id} # 月嫂详情 +PUT /nanny/nannies/{id} # 编辑月嫂 +PUT /nanny/nannies/{id}/status # 更新状态 + +# 月嫂订单 +GET /nanny/orders # 订单列表 +POST /nanny/orders # 新建订单 +GET /nanny/orders/{id} # 订单详情 +POST /nanny/orders/{id}/assign # 指派月嫂 +POST /nanny/orders/{id}/complete # 完成 + +# 月嫂评价 +GET /nanny/reviews # 评价列表 +POST /nanny/reviews # 新建评价 +GET /nanny/nannies/{id}/reviews # 某月嫂评价 + +# 月嫂排班 +GET /nanny/schedules # 排班列表 +POST /nanny/schedules # 新建排班 +``` + +### 2.8 进销存模块 `/stock/` + +``` +# 仓库管理 +GET /stock/warehouses # 仓库列表 +POST /stock/warehouses # 新建仓库 + +# 物资管理 +GET /stock/materials # 物资列表 +POST /stock/materials # 新建物资 +PUT /stock/materials/{id} # 编辑物资 +GET /stock/materials/categories # 物资分类 + +# 库存查询 +GET /stock/inventories # 库存列表 +GET /stock/inventories/alerts # 库存预警 + +# 采购订单 +GET /stock/purchase-orders # 采购单列表 +POST /stock/purchase-orders # 新建采购单 +GET /stock/purchase-orders/{id} # 详情 +POST /stock/purchase-orders/{id}/approve # 审批 +POST /stock/purchase-orders/{id}/receive # 入库 + +# 出入库 +POST /stock/in-records # 入库记录 +POST /stock/out-records # 出库记录 +GET /stock/records # 出入库流水 + +# 供应商 +GET /stock/suppliers # 供应商列表 +POST /stock/suppliers # 新建供应商 +PUT /stock/suppliers/{id} # 编辑 +``` + +### 2.9 财务模块 `/finance/` + +``` +# 资金账户 +GET /finance/accounts # 账户列表 +POST /finance/accounts # 新建账户 +PUT /finance/accounts/{id} # 编辑 + +# 收支记录 +GET /finance/records # 收支流水 +POST /finance/records # 新建收支 +GET /finance/records/{id} # 详情 +POST /finance/records/{id}/approve # 审核 + +# 客户账户 +GET /finance/customer-accounts # 客户账户列表 +GET /finance/customer-accounts/{id} # 客户账户详情 +POST /finance/customer-accounts/{id}/recharge # 充值 +POST /finance/customer-accounts/{id}/consume # 消费 +GET /finance/customer-accounts/{id}/bills # 账单流水 + +# 储值卡/优惠券 +GET /finance/voucher-cards # 储值卡列表 +POST /finance/voucher-cards # 发行储值卡 + +# 发票管理 +GET /finance/invoices # 发票列表 +POST /finance/invoices # 开票申请 +POST /finance/invoices/{id}/confirm # 确认开票 + +# 财务报表 +GET /finance/reports/daily # 日报 +GET /finance/reports/monthly # 月报 +GET /finance/reports/category # 分类汇总 +``` + +### 2.10 人事薪资模块 `/hr/` + +``` +# 员工档案 +GET /hr/employees # 员工列表 +POST /hr/employees # 新建员工 +GET /hr/employees/{id} # 员工详情 +PUT /hr/employees/{id} # 编辑 + +# 排班管理 +GET /hr/schedules # 排班列表 +POST /hr/schedules # 新建排班 +POST /hr/schedules/batch # 批量排班 +GET /hr/schedules/board # 排班日历 + +# 考勤 +GET /hr/attendances # 考勤列表 +POST /hr/attendances/clock # 打卡 +GET /hr/attendances/monthly # 月度汇总 + +# 薪资 +GET /hr/salaries # 工资单列表 +POST /hr/salaries/calculate # 计算工资 +POST /hr/salaries/{id}/confirm # 确认工资单 +GET /hr/salaries/export # 导出工资单 + +# 请假/加班 +GET /hr/leaves # 请假列表 +POST /hr/leaves # 申请请假 +POST /hr/leaves/{id}/approve # 审批 +``` + +### 2.11 办公协同模块 `/office/` + +``` +# 公告 +GET /office/announcements # 公告列表 +POST /office/announcements # 发布公告 +GET /office/announcements/{id} # 公告详情 + +# 审批流 +GET /office/approvals # 审批列表(我的待办/已办) +POST /office/approvals # 发起审批 +GET /office/approvals/{id} # 审批详情 +POST /office/approvals/{id}/approve # 通过 +POST /office/approvals/{id}/reject # 驳回 + +# 审批模板 +GET /office/approval-templates # 模板列表 +POST /office/approval-templates # 新建模板 + +# 交接班 +GET /office/handovers # 交接班列表 +POST /office/handovers # 新建交接 +POST /office/handovers/{id}/confirm # 确认交接 + +# 消息/通知 +GET /office/notifications # 通知列表 +PUT /office/notifications/{id}/read # 标记已读 +PUT /office/notifications/read-all # 全部已读 +GET /office/notifications/unread-count # 未读数量 +``` + +### 2.12 统计报表模块 `/report/` + +``` +# 经营概览 +GET /report/overview # 经营总览仪表盘 + +# 营收报表 +GET /report/revenue # 营收统计 +GET /report/revenue/trend # 营收趋势 + +# 入住报表 +GET /report/occupancy # 入住率统计 +GET /report/occupancy/trend # 入住率趋势 + +# CRM 报表 +GET /report/crm/conversion # 转化率统计 +GET /report/crm/source # 来源分析 +GET /report/crm/staff # 员工业绩 + +# 护理报表 +GET /report/care/workload # 护理工作量 +GET /report/care/quality # 护理质量 + +# 满意度报表 +GET /report/satisfaction # 满意度统计 + +# 自定义报表 +GET /report/custom # 自定义报表列表 +POST /report/custom # 创建自定义报表 +GET /report/custom/{id}/data # 获取报表数据 +GET /report/custom/{id}/export # 导出报表 +``` + +### 2.13 知识库模块 `/kb/` + +``` +# 分类 +GET /kb/categories # 分类树 +POST /kb/categories # 新建分类 +PUT /kb/categories/{id} # 编辑分类 + +# 文章 +GET /kb/articles # 文章列表 +POST /kb/articles # 新建文章 +GET /kb/articles/{id} # 文章详情 +PUT /kb/articles/{id} # 编辑文章 +DELETE /kb/articles/{id} # 删除文章 +POST /kb/articles/{id}/publish # 发布 +GET /kb/articles/search # 全文搜索 +``` + +--- + +## 三、客户端 API(Client / 微客宝小程序) + +### 3.1 认证 + +``` +POST /client/auth/wechat-login # 微信登录 +POST /client/auth/bindPhone # 绑定手机号 +GET /client/auth/profile # 获取个人信息 +PUT /client/auth/profile # 更新个人信息 +``` + +### 3.2 首页 + +``` +GET /client/home/banners # 轮播图 +GET /client/home/notices # 公告 +GET /client/home/services # 服务推荐 +GET /client/home/activities # 活动列表 +``` + +### 3.3 商城/服务 + +``` +GET /client/services # 服务列表 +GET /client/services/{id} # 服务详情 +GET /client/packages # 套餐列表 +GET /client/packages/{id} # 套餐详情 +POST /client/orders # 下单 +GET /client/orders # 我的订单 +GET /client/orders/{id} # 订单详情 +POST /client/orders/{id}/pay # 支付 +POST /client/orders/{id}/cancel # 取消 +``` + +### 3.4 月子餐 + +``` +GET /client/meals/today # 今日餐单 +GET /client/meals/week # 本周餐单 +POST /client/meals/feedback # 餐品反馈 +GET /client/meals/dishes # 可选菜品 +POST /client/meals/custom-order # 自选点餐 +``` + +### 3.5 共伴成长 + +``` +GET /client/baby/care-records # 宝宝护理记录 +GET /client/baby/health-chart # 健康数据图表 +GET /client/baby/milestones # 成长里程碑 +GET /client/mom/care-records # 妈妈护理记录 +GET /client/mom/health-chart # 妈妈健康图表 +``` + +### 3.6 月嫂 + +``` +GET /client/nannies # 月嫂列表 +GET /client/nannies/{id} # 月嫂详情 +POST /client/nanny-orders # 预约月嫂 +GET /client/nanny-orders # 我的月嫂订单 +POST /client/nanny-reviews # 评价月嫂 +``` + +### 3.7 学堂 + +``` +GET /client/articles # 知识文章列表 +GET /client/articles/{id} # 文章详情 +GET /client/articles/categories # 分类 +POST /client/articles/{id}/like # 点赞 +POST /client/articles/{id}/collect # 收藏 +``` + +### 3.8 社区 + +``` +GET /client/community/posts # 帖子列表 +POST /client/community/posts # 发帖 +GET /client/community/posts/{id} # 帖子详情 +POST /client/community/posts/{id}/comment # 评论 +POST /client/community/posts/{id}/like # 点赞 +``` + +### 3.9 个人中心 + +``` +GET /client/my/account # 账户余额/储值卡 +GET /client/my/coupons # 优惠券列表 +GET /client/my/orders # 全部订单 +GET /client/my/reviews # 我的评价 +GET /client/my/collections # 我的收藏 +GET /client/my/messages # 消息通知 +``` + +### 3.10 支付 + +``` +POST /client/pay/wechat # 微信支付下单 +POST /client/pay/callback # 支付回调(后端内部) +GET /client/pay/status/{order_no} # 查询支付状态 +``` + +--- + +## 四、文件上传 + +``` +# 通用文件上传 +POST /api/v1/upload/image # 上传图片(返回 URL) +POST /api/v1/upload/file # 上传文件(Excel/PDF) +POST /api/v1/upload/avatar # 上传头像(自动裁剪) +``` + +请求格式:`multipart/form-data` + +| 参数 | 类型 | 说明 | +|------|------|------| +| file | File | 文件 | +| type | string | 业务类型(avatar/contract/care_record 等) | + +--- + +## 五、WebSocket 事件(可选) + +``` +# 连接 +ws://{domain}/ws?token={bearer_token} + +# 事件 +notification.new — 新通知推送 +approval.pending — 待审批提醒 +care.alert — 护理异常告警 +room.status_change — 房态变更通知 +``` + +--- + +## 六、接口安全 + +### 6.1 限流策略 + +| 接口类型 | 限制 | +|----------|------| +| 登录接口 | 5 次/分钟/IP | +| 普通接口 | 60 次/分钟/用户 | +| 上传接口 | 10 次/分钟/用户 | +| 导出接口 | 5 次/分钟/用户 | +| 支付回调 | 不限(白名单IP) | + +### 6.2 数据权限 + +- 所有业务接口自动注入 `store_id` 过滤 +- 超级管理员可通过 `X-Store-Id` Header 切换门店 +- 敏感操作(删除、审批)记录操作日志 + +### 6.3 输入校验 + +- 所有写入接口使用 Laravel FormRequest 校验 +- XSS 过滤:HTMLPurifier 处理富文本 +- SQL 注入:Eloquent 参数绑定(禁止 raw SQL 拼接) +- 文件上传:类型白名单 + 大小限制(图片 5MB,文件 20MB) + +--- + +## 七、典型接口示例 + +### 7.1 新建线索 + +```http +POST /api/v1/crm/leads +Authorization: Bearer xxx +Content-Type: application/json + +{ + "name": "张女士", + "phone": "13800138000", + "source": "wechat", + "expected_date": "2024-06-15", + "expected_room_type": 2, + "expected_days": 28, + "remark": "朋友推荐,预算10万左右" +} +``` + +Response: +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1001, + "name": "张女士", + "phone": "13800138000", + "source": "wechat", + "status": "new", + "assigned_to": null, + "created_at": "2024-03-15 10:30:00" + } +} +``` + +### 7.2 房态看板 + +```http +GET /api/v1/room/rooms/board?date=2024-03-15&range=week +Authorization: Bearer xxx +``` + +Response: +```json +{ + "code": 0, + "data": { + "floors": [ + { + "id": 1, + "name": "3楼", + "rooms": [ + { + "id": 101, + "number": "301", + "type": "豪华套房", + "status": "occupied", + "customer": "李女士", + "check_in": "2024-03-10", + "check_out": "2024-04-07" + } + ] + } + ], + "summary": { + "total": 30, + "occupied": 22, + "available": 5, + "maintenance": 2, + "reserved": 1, + "occupancy_rate": "73.3%" + } + } +} +``` diff --git a/docs/04-rbac-design.md b/docs/04-rbac-design.md new file mode 100644 index 0000000..fee98f4 --- /dev/null +++ b/docs/04-rbac-design.md @@ -0,0 +1,1392 @@ +# 宫中有喜 — RBAC 权限系统设计 v1.0 + +## 一、设计目标 + +| 目标 | 说明 | +|------|------| +| 多门店数据隔离 | 每条业务数据强绑 `store_id`,中间件层自动注入过滤 | +| 四层权限模型 | User → Role → Permission → Menu,支持按钮级控制 | +| 灵活审批流 | 可视化配置多级审批,支持会签 / 或签 / 条件分支 | +| 完整审计链 | 所有写操作自动记录,满足医疗/月子行业合规要求 | +| 最小权限原则 | 默认拒绝,显式授权,禁止越权访问 | + +--- + +## 二、RBAC 四层模型 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ 权限判定流程 │ +│ │ +│ Request │ +│ │ │ +│ ▼ │ +│ Auth Middleware (Sanctum Token) │ +│ │ │ +│ ▼ │ +│ StoreIsolation Middleware │ +│ │ 注入 store_id → 全局 Scope 过滤 │ +│ ▼ │ +│ CheckPermission Middleware │ +│ │ User → user_roles → roles → role_permissions │ +│ │ → permissions → 比对路由所需权限标识 │ +│ ▼ │ +│ Controller (业务逻辑) │ +│ │ │ +│ ▼ │ +│ OperationLog Middleware (响应后异步写日志) │ +│ │ +└──────────────────────────────────────────────────────────────┘ +``` + +### 2.1 四层关系 + +``` +用户 (users) + │ N:M + ▼ +角色 (roles) + │ N:M N:M + ├──────────► 权限 (permissions) + │ + └──────────► 菜单 (menus) +``` + +- **用户**:系统登录主体,含内部员工与客户 +- **角色**:权限集合的逻辑分组,绑定门店 +- **权限**:最小授权单元,对应 API 路由 + 操作标识 +- **菜单**:前端路由 + 页面按钮,控制界面可见性 + +### 2.2 权限标识命名规范 + +``` +{module}:{sub_module}:{action} + +示例: + crm:customer:list # CRM - 客户列表 + crm:customer:create # CRM - 新建客户 + crm:customer:update # CRM - 编辑客户 + crm:customer:delete # CRM - 删除客户 + crm:customer:export # CRM - 导出客户 + room:booking:approve # 房务 - 预定审批 + care:record:create # 护理 - 新增记录 + finance:bill:void # 财务 - 作废账单 + system:role:assign # 系统 - 角色分配 +``` + +### 2.3 数据权限范围(data_scope) + +| 值 | 含义 | SQL 过滤条件 | +|----|------|-------------| +| `all` | 全部数据 | 不追加过滤(仅平台超管) | +| `store` | 本门店全部 | `WHERE store_id = ?` | +| `department` | 本部门 | `WHERE department_id IN (?)` | +| `self` | 仅本人 | `WHERE created_by = ?` | +| `custom` | 自定义部门 | `WHERE department_id IN (配置列表)` | + +--- + +## 三、系统角色清单 + +### 3.1 平台级角色 + +| 角色 | 代码 | data_scope | 说明 | +|------|------|-----------|------| +| 平台超级管理员 | `platform_super_admin` | `all` | 跨门店全权,不可删除 | + +### 3.2 门店级角色 + +| 角色 | 代码 | data_scope | 典型权限 | +|------|------|-----------|----------| +| 门店管理员/店长 | `store_manager` | `store` | 本店全部功能 + 审批终审 | +| 销售/客服 | `sales` | `department` | CRM 全模块 + 签约 | +| 前台/房务 | `front_desk` | `store` | 预约、入住、退房、房态 | +| 护理护士 | `nurse` | `department` | 护理档案、记录、医嘱 | +| 膳食/厨房 | `kitchen` | `department` | 排餐、送餐、食材管理 | +| 服务技师/产康师 | `therapist` | `self` | 服务预约、产康记录 | +| 仓库/采购 | `warehouse` | `department` | 进销存全流程 | +| 财务 | `finance` | `store` | 收付款、账单、报表 | +| 人事 | `hr` | `store` | 员工档案、考勤、薪资 | +| 月嫂中心管理员 | `nanny_center_admin` | `store` | 月嫂调度、评价、合同 | +| 月嫂督导 | `nanny_supervisor` | `department` | 月嫂排班、质检 | +| 月嫂个人 | `nanny` | `self` | 个人排班、服务记录 | + +### 3.3 客户角色 + +| 角色 | 代码 | data_scope | 说明 | +|------|------|-----------|------| +| 客户 | `client` | `self` | 微信小程序访问,仅查看本人数据 | + +--- + +## 四、数据库表设计 + +### 4.1 门店表 (stores) + +```sql +CREATE TABLE `stores` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '门店ID', + `name` VARCHAR(100) NOT NULL COMMENT '门店名称', + `code` VARCHAR(20) NOT NULL COMMENT '门店编码(唯一标识)', + `contact_person` VARCHAR(50) NULL COMMENT '联系人', + `contact_phone` VARCHAR(20) NULL COMMENT '联系电话', + `province` VARCHAR(50) NULL COMMENT '省', + `city` VARCHAR(50) NULL COMMENT '市', + `district` VARCHAR(50) NULL COMMENT '区', + `address` VARCHAR(255) NULL COMMENT '详细地址', + `logo` VARCHAR(500) NULL COMMENT '门店 Logo URL', + `business_license` VARCHAR(500) NULL COMMENT '营业执照图片 URL', + `license_no` VARCHAR(50) NULL COMMENT '营业执照号', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用 2=试用', + `expires_at` DATE NULL COMMENT 'SaaS 服务到期日', + `max_users` INT UNSIGNED NOT NULL DEFAULT 50 COMMENT '最大用户数', + `settings` JSON NULL COMMENT '门店个性化配置(JSON)', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序', + `created_by` BIGINT UNSIGNED NULL COMMENT '创建人', + `updated_by` BIGINT UNSIGNED NULL COMMENT '更新人', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` TIMESTAMP NULL COMMENT '软删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_status` (`status`), + KEY `idx_deleted_at` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店表'; +``` + +### 4.2 部门表 (departments) + +```sql +CREATE TABLE `departments` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '部门ID', + `store_id` BIGINT UNSIGNED NOT NULL COMMENT '所属门店ID', + `parent_id` BIGINT UNSIGNED NULL DEFAULT 0 COMMENT '上级部门ID(0=顶级)', + `name` VARCHAR(100) NOT NULL COMMENT '部门名称', + `code` VARCHAR(50) NULL COMMENT '部门编码', + `leader_id` BIGINT UNSIGNED NULL COMMENT '部门负责人用户ID', + `phone` VARCHAR(20) NULL COMMENT '部门电话', + `email` VARCHAR(100) NULL COMMENT '部门邮箱', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用', + `created_by` BIGINT UNSIGNED NULL COMMENT '创建人', + `updated_by` BIGINT UNSIGNED NULL COMMENT '更新人', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` TIMESTAMP NULL COMMENT '软删除时间', + PRIMARY KEY (`id`), + KEY `idx_store_id` (`store_id`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_leader_id` (`leader_id`), + KEY `idx_deleted_at` (`deleted_at`), + CONSTRAINT `fk_departments_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门表'; +``` + +### 4.3 职务表 (positions) + +```sql +CREATE TABLE `positions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '职务ID', + `store_id` BIGINT UNSIGNED NOT NULL COMMENT '所属门店ID', + `name` VARCHAR(100) NOT NULL COMMENT '职务名称', + `code` VARCHAR(50) NULL COMMENT '职务编码', + `level` TINYINT NOT NULL DEFAULT 1 COMMENT '职级:1=普通 2=主管 3=经理 4=总监 5=总经理', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用', + `created_by` BIGINT UNSIGNED NULL COMMENT '创建人', + `updated_by` BIGINT UNSIGNED NULL COMMENT '更新人', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` TIMESTAMP NULL COMMENT '软删除时间', + PRIMARY KEY (`id`), + KEY `idx_store_id` (`store_id`), + KEY `idx_deleted_at` (`deleted_at`), + CONSTRAINT `fk_positions_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职务表'; +``` + +### 4.4 用户表 (users) + +```sql +CREATE TABLE `users` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `store_id` BIGINT UNSIGNED NULL COMMENT '所属门店ID(NULL=平台级用户)', + `department_id` BIGINT UNSIGNED NULL COMMENT '所属部门ID', + `position_id` BIGINT UNSIGNED NULL COMMENT '职务ID', + `username` VARCHAR(50) NOT NULL COMMENT '登录账号', + `password` VARCHAR(255) NOT NULL COMMENT '密码(bcrypt)', + `name` VARCHAR(50) NOT NULL COMMENT '真实姓名', + `employee_no` VARCHAR(30) NULL COMMENT '工号', + `avatar` VARCHAR(500) NULL COMMENT '头像 URL', + `gender` TINYINT NOT NULL DEFAULT 0 COMMENT '性别:0=未知 1=男 2=女', + `phone` VARCHAR(20) NULL COMMENT '手机号', + `email` VARCHAR(100) NULL COMMENT '邮箱', + `id_card` VARCHAR(20) NULL COMMENT '身份证号(加密存储)', + `user_type` TINYINT NOT NULL DEFAULT 1 COMMENT '用户类型:1=内部员工 2=客户 3=月嫂', + `wx_openid` VARCHAR(64) NULL COMMENT '微信小程序 OpenID', + `wx_unionid` VARCHAR(64) NULL COMMENT '微信 UnionID', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用 2=锁定', + `last_login_at` TIMESTAMP NULL COMMENT '最后登录时间', + `last_login_ip` VARCHAR(45) NULL COMMENT '最后登录IP', + `password_changed_at` TIMESTAMP NULL COMMENT '密码最后修改时间', + `login_fail_count` TINYINT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数', + `locked_until` TIMESTAMP NULL COMMENT '锁定截止时间', + `created_by` BIGINT UNSIGNED NULL COMMENT '创建人', + `updated_by` BIGINT UNSIGNED NULL COMMENT '更新人', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` TIMESTAMP NULL COMMENT '软删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`), + UNIQUE KEY `uk_phone` (`phone`), + UNIQUE KEY `uk_wx_openid` (`wx_openid`), + KEY `idx_store_id` (`store_id`), + KEY `idx_department_id` (`department_id`), + KEY `idx_position_id` (`position_id`), + KEY `idx_user_type` (`user_type`), + KEY `idx_status` (`status`), + KEY `idx_employee_no` (`employee_no`), + KEY `idx_deleted_at` (`deleted_at`), + CONSTRAINT `fk_users_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_users_department` FOREIGN KEY (`department_id`) REFERENCES `departments` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_users_position` FOREIGN KEY (`position_id`) REFERENCES `positions` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; +``` + +### 4.5 角色表 (roles) + +```sql +CREATE TABLE `roles` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '角色ID', + `store_id` BIGINT UNSIGNED NULL COMMENT '所属门店ID(NULL=平台级角色)', + `name` VARCHAR(100) NOT NULL COMMENT '角色名称', + `code` VARCHAR(50) NOT NULL COMMENT '角色编码(英文标识)', + `description` VARCHAR(255) NULL COMMENT '角色描述', + `data_scope` VARCHAR(20) NOT NULL DEFAULT 'self' COMMENT '数据权限范围:all/store/department/self/custom', + `is_system` TINYINT NOT NULL DEFAULT 0 COMMENT '是否系统内置角色:0=否 1=是(不可删除)', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用', + `created_by` BIGINT UNSIGNED NULL COMMENT '创建人', + `updated_by` BIGINT UNSIGNED NULL COMMENT '更新人', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` TIMESTAMP NULL COMMENT '软删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_store_code` (`store_id`, `code`), + KEY `idx_store_id` (`store_id`), + KEY `idx_status` (`status`), + KEY `idx_deleted_at` (`deleted_at`), + CONSTRAINT `fk_roles_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表'; +``` + +### 4.6 权限表 (permissions) + +```sql +CREATE TABLE `permissions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '权限ID', + `parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父权限ID(0=顶级模块)', + `name` VARCHAR(100) NOT NULL COMMENT '权限名称', + `code` VARCHAR(100) NOT NULL COMMENT '权限标识(module:sub:action)', + `type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1=模块 2=菜单 3=操作/按钮 4=API', + `method` VARCHAR(10) NULL COMMENT 'HTTP 方法:GET/POST/PUT/DELETE', + `path` VARCHAR(255) NULL COMMENT 'API 路径(如 /api/v1/customers)', + `description` VARCHAR(255) NULL COMMENT '权限描述', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_type` (`type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表'; +``` + +> **说明**:权限表为全局表,不绑 `store_id`。权限定义全局统一,通过 `role_permissions` 关联到角色实现门店级差异化授权。 + +### 4.7 菜单表 (menus) + +```sql +CREATE TABLE `menus` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父菜单ID(0=顶级)', + `name` VARCHAR(100) NOT NULL COMMENT '菜单名称', + `code` VARCHAR(100) NOT NULL COMMENT '菜单标识(与前端路由 name 对应)', + `type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1=目录 2=菜单 3=按钮', + `icon` VARCHAR(100) NULL COMMENT '图标 class(Element Plus icon)', + `path` VARCHAR(255) NULL COMMENT '前端路由路径', + `component` VARCHAR(255) NULL COMMENT '前端组件路径', + `redirect` VARCHAR(255) NULL COMMENT '重定向地址', + `permission` VARCHAR(100) NULL COMMENT '所需权限标识(关联 permissions.code)', + `is_visible` TINYINT NOT NULL DEFAULT 1 COMMENT '是否在菜单栏显示:0=隐藏 1=显示', + `is_cache` TINYINT NOT NULL DEFAULT 0 COMMENT '是否缓存(keep-alive):0=否 1=是', + `is_external` TINYINT NOT NULL DEFAULT 0 COMMENT '是否外链:0=否 1=是', + `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + KEY `idx_parent_id` (`parent_id`), + KEY `idx_type` (`type`), + KEY `idx_sort` (`sort_order`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表'; +``` + +> **说明**:菜单表同样为全局表。通过 `role_menus` 实现不同角色看到不同菜单。前端登录后请求 `/api/v1/menus/user` 获取动态路由。 + +### 4.8 角色-权限关联表 (role_permissions) + +```sql +CREATE TABLE `role_permissions` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID', + `permission_id` BIGINT UNSIGNED NOT NULL COMMENT '权限ID', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`), + KEY `idx_permission_id` (`permission_id`), + CONSTRAINT `fk_rp_role` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_rp_permission` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-权限关联表'; +``` + +### 4.9 用户-角色关联表 (user_roles) + +```sql +CREATE TABLE `user_roles` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', + `role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_role` (`user_id`, `role_id`), + KEY `idx_role_id` (`role_id`), + CONSTRAINT `fk_ur_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_ur_role` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户-角色关联表'; +``` + +### 4.10 角色-菜单关联表 (role_menus) + +```sql +CREATE TABLE `role_menus` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID', + `menu_id` BIGINT UNSIGNED NOT NULL COMMENT '菜单ID', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`), + KEY `idx_menu_id` (`menu_id`), + CONSTRAINT `fk_rm_role` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_rm_menu` FOREIGN KEY (`menu_id`) REFERENCES `menus` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单关联表'; +``` + +### 4.11 角色-自定义部门关联表 (role_departments) + +```sql +CREATE TABLE `role_departments` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID', + `department_id` BIGINT UNSIGNED NOT NULL COMMENT '部门ID', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_department` (`role_id`, `department_id`), + KEY `idx_department_id` (`department_id`), + CONSTRAINT `fk_rd_role` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_rd_department` FOREIGN KEY (`department_id`) REFERENCES `departments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-自定义部门关联表(data_scope=custom时生效)'; +``` + +### 4.12 审批流配置表 (approval_flows) + +```sql +CREATE TABLE `approval_flows` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '审批流ID', + `store_id` BIGINT UNSIGNED NOT NULL COMMENT '所属门店ID', + `name` VARCHAR(100) NOT NULL COMMENT '审批流名称', + `code` VARCHAR(50) NOT NULL COMMENT '审批流编码(如 contract_sign, purchase_order)', + `module` VARCHAR(50) NOT NULL COMMENT '所属模块(crm/room/finance/stock 等)', + `description` VARCHAR(255) NULL COMMENT '描述', + `trigger_condition` JSON NULL COMMENT '触发条件(金额阈值、类别等 JSON 配置)', + `is_enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用:0=否 1=是', + `version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '版本号(每次修改递增)', + `created_by` BIGINT UNSIGNED NULL COMMENT '创建人', + `updated_by` BIGINT UNSIGNED NULL COMMENT '更新人', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` TIMESTAMP NULL COMMENT '软删除时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_store_code` (`store_id`, `code`), + KEY `idx_module` (`module`), + KEY `idx_deleted_at` (`deleted_at`), + CONSTRAINT `fk_af_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流配置表'; +``` + +**trigger_condition JSON 示例**: + +```json +{ + "conditions": [ + {"field": "amount", "operator": ">=", "value": 5000, "flow_variant": "high_amount"}, + {"field": "amount", "operator": "<", "value": 5000, "flow_variant": "normal"} + ] +} +``` + +### 4.13 审批步骤表 (approval_flow_steps) + +```sql +CREATE TABLE `approval_flow_steps` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '步骤ID', + `flow_id` BIGINT UNSIGNED NOT NULL COMMENT '审批流ID', + `step_number` INT UNSIGNED NOT NULL COMMENT '步骤序号(从1开始)', + `name` VARCHAR(100) NOT NULL COMMENT '步骤名称', + `approve_type` TINYINT NOT NULL DEFAULT 1 COMMENT '审批类型:1=指定用户 2=指定角色 3=部门负责人 4=发起人上级', + `approve_mode` TINYINT NOT NULL DEFAULT 1 COMMENT '审批模式:1=或签(任一通过) 2=会签(全部通过)', + `approver_ids` JSON NULL COMMENT '审批人ID列表(approve_type=1时)', + `approver_role_id` BIGINT UNSIGNED NULL COMMENT '审批角色ID(approve_type=2时)', + `timeout_hours` INT UNSIGNED NULL COMMENT '超时小时数(NULL=不限)', + `timeout_action` TINYINT NULL COMMENT '超时动作:1=自动通过 2=自动驳回 3=转交上级', + `flow_variant` VARCHAR(50) NULL COMMENT '流程分支标识(对应 trigger_condition 的 flow_variant)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_flow_variant_step` (`flow_id`, `flow_variant`, `step_number`), + KEY `idx_approver_role` (`approver_role_id`), + CONSTRAINT `fk_afs_flow` FOREIGN KEY (`flow_id`) REFERENCES `approval_flows` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批步骤表'; +``` + +### 4.14 审批记录表 (approval_records) + +```sql +CREATE TABLE `approval_records` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '审批记录ID', + `store_id` BIGINT UNSIGNED NOT NULL COMMENT '所属门店ID', + `flow_id` BIGINT UNSIGNED NOT NULL COMMENT '审批流ID', + `flow_version` INT UNSIGNED NOT NULL COMMENT '发起时的审批流版本', + `record_no` VARCHAR(30) NOT NULL COMMENT '审批单号(如 APR-20260313-0001)', + `title` VARCHAR(200) NOT NULL COMMENT '审批标题', + `business_type` VARCHAR(50) NOT NULL COMMENT '业务类型(如 contract/purchase/leave)', + `business_id` BIGINT UNSIGNED NOT NULL COMMENT '关联业务记录ID', + `business_data` JSON NULL COMMENT '提交时的业务数据快照(JSON)', + `applicant_id` BIGINT UNSIGNED NOT NULL COMMENT '申请人ID', + `current_step` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '当前审批步骤序号', + `status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=待审批 1=审批中 2=已通过 3=已驳回 4=已撤回 5=已取消', + `result_remark` TEXT NULL COMMENT '最终审批意见', + `completed_at` TIMESTAMP NULL COMMENT '审批完成时间', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_record_no` (`record_no`), + KEY `idx_store_id` (`store_id`), + KEY `idx_flow_id` (`flow_id`), + KEY `idx_business` (`business_type`, `business_id`), + KEY `idx_applicant` (`applicant_id`), + KEY `idx_status` (`status`), + KEY `idx_created_at` (`created_at`), + CONSTRAINT `fk_ar_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_ar_flow` FOREIGN KEY (`flow_id`) REFERENCES `approval_flows` (`id`) ON UPDATE CASCADE, + CONSTRAINT `fk_ar_applicant` FOREIGN KEY (`applicant_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录表'; +``` + +### 4.15 审批记录明细表 (approval_record_details) + +```sql +CREATE TABLE `approval_record_details` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '明细ID', + `record_id` BIGINT UNSIGNED NOT NULL COMMENT '审批记录ID', + `step_number` INT UNSIGNED NOT NULL COMMENT '步骤序号', + `step_name` VARCHAR(100) NOT NULL COMMENT '步骤名称', + `approver_id` BIGINT UNSIGNED NOT NULL COMMENT '审批人ID', + `action` TINYINT NOT NULL DEFAULT 0 COMMENT '审批动作:0=待处理 1=通过 2=驳回 3=转交 4=自动通过(超时)', + `remark` TEXT NULL COMMENT '审批意见', + `attachments` JSON NULL COMMENT '附件列表(JSON 数组)', + `assigned_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分配时间', + `handled_at` TIMESTAMP NULL COMMENT '处理时间', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_record_id` (`record_id`), + KEY `idx_approver_id` (`approver_id`), + KEY `idx_action` (`action`), + KEY `idx_step` (`record_id`, `step_number`), + CONSTRAINT `fk_ard_record` FOREIGN KEY (`record_id`) REFERENCES `approval_records` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `fk_ard_approver` FOREIGN KEY (`approver_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录明细表'; +``` + +### 4.16 操作日志表 (operation_logs) + +```sql +CREATE TABLE `operation_logs` ( + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '日志ID', + `store_id` BIGINT UNSIGNED NULL COMMENT '门店ID(NULL=平台级操作)', + `user_id` BIGINT UNSIGNED NULL COMMENT '操作人ID(NULL=系统自动操作)', + `user_name` VARCHAR(50) NULL COMMENT '操作人姓名(冗余,防止用户删除后无法溯源)', + `module` VARCHAR(50) NOT NULL COMMENT '功能模块(crm/room/care/finance 等)', + `action` VARCHAR(50) NOT NULL COMMENT '操作动作(create/update/delete/export/login/logout 等)', + `title` VARCHAR(200) NOT NULL COMMENT '操作描述', + `method` VARCHAR(10) NOT NULL COMMENT 'HTTP 方法', + `url` VARCHAR(500) NOT NULL COMMENT '请求 URL', + `route_name` VARCHAR(100) NULL COMMENT 'Laravel 路由名称', + `request_body` JSON NULL COMMENT '请求参数(脱敏后,排除 password 等)', + `response_code` INT NULL COMMENT 'HTTP 响应状态码', + `response_body` JSON NULL COMMENT '响应摘要(可选,仅记录关键字段)', + `diff_data` JSON NULL COMMENT '数据变更对比({field: {old, new}})', + `ip` VARCHAR(45) NOT NULL COMMENT '客户端 IP(支持 IPv6)', + `user_agent` VARCHAR(500) NULL COMMENT '浏览器 User-Agent', + `duration_ms` INT UNSIGNED NULL COMMENT '请求耗时(毫秒)', + `status` TINYINT NOT NULL DEFAULT 1 COMMENT '操作结果:0=失败 1=成功', + `error_message` TEXT NULL COMMENT '失败时的错误信息', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `idx_store_id` (`store_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_module_action` (`module`, `action`), + KEY `idx_created_at` (`created_at`), + KEY `idx_ip` (`ip`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表'; +``` + +> **日志保留策略**:生产环境建议按月分表或定期归档至冷存储,保留至少 180 天热数据。 + +--- + +## 五、权限控制原则与查询逻辑 + +### 5.1 门店隔离三原则 + +| # | 原则 | 实现 | +|---|------|------| +| 1 | 所有业务表必须包含 `store_id` 字段 | Migration 检查 | +| 2 | 所有 Model 查询自动附加 `store_id` 条件 | Global Scope | +| 3 | 跨门店查询必须经过平台超管身份校验 | 中间件白名单 | + +### 5.2 数据过滤 SQL 逻辑(伪代码) + +```sql +-- 基础查询(以客户列表为例) +SELECT c.* FROM customers c +WHERE c.deleted_at IS NULL + -- 第一层:门店隔离(StoreIsolation 中间件自动注入) + AND c.store_id = :current_user_store_id + -- 第二层:数据权限范围(根据角色 data_scope) + AND ( + CASE :data_scope + WHEN 'all' THEN 1=1 -- 平台超管 + WHEN 'store' THEN c.store_id = :current_user_store_id -- 本店全部(已由第一层覆盖) + WHEN 'department' THEN c.department_id = :current_user_dept_id -- 本部门 + WHEN 'self' THEN c.created_by = :current_user_id -- 仅本人 + WHEN 'custom' THEN c.department_id IN (:custom_dept_ids) -- 自定义部门 + END + ) + -- 第三层:功能权限(CheckPermission 中间件在路由层已拦截,此处无需重复) + -- 第四层:负责人绑定(业务级过滤,部分模块适用) + AND ( + :require_assignee = FALSE + OR c.assignee_id = :current_user_id + ) +ORDER BY c.created_at DESC; +``` + +### 5.3 权限判定流程图 + +``` +接收请求 + │ + ├─ 1. Auth:Token 有效? ──── 否 → 401 + │ + ├─ 2. StoreIsolation: + │ 用户 store_id 与请求资源 store_id 匹配? + │ ├── 不匹配且非超管 → 403 + │ └── 匹配或超管 → 继续 + │ + ├─ 3. CheckPermission: + │ 路由标注的权限标识 是否在用户权限集中? + │ ├── 不在 → 403 + │ └── 在 → 继续 + │ + ├─ 4. DataScope(Service/Model 层): + │ 根据角色 data_scope 自动追加查询条件 + │ + └─ 5. OperationLog(响应后): + 记录本次操作到 operation_logs +``` + +--- + +## 六、Laravel 中间件实现方案 + +### 6.1 StoreIsolation 中间件 + +**文件路径**:`app/Http/Middleware/StoreIsolation.php` + +```php +user(); + + if (!$user) { + abort(401, 'Unauthenticated'); + } + + // 平台超管可跨门店操作 + if ($user->isPlatformSuperAdmin()) { + // 如果请求中指定了 store_id(超管切换门店视角),使用请求中的值 + $storeId = $request->header('X-Store-Id') ?? $request->input('store_id'); + if ($storeId) { + app()->instance('current_store_id', (int) $storeId); + } + return $next($request); + } + + // 普通用户必须属于某门店 + if (!$user->store_id) { + abort(403, '用户未绑定门店'); + } + + // 注入当前门店ID到容器,供 Global Scope 使用 + app()->instance('current_store_id', $user->store_id); + + return $next($request); + } +} +``` + +**配套 Global Scope**(所有含 `store_id` 的 Model 使用 `StoreScope` Trait): + +```php +bound('current_store_id')) { + $builder->where($builder->getModel()->getTable() . '.store_id', app('current_store_id')); + } + }); + + // 创建时自动填充 store_id + static::creating(function ($model) { + if (!$model->store_id && app()->bound('current_store_id')) { + $model->store_id = app('current_store_id'); + } + }); + } +} +``` + +### 6.2 CheckPermission 中间件 + +**文件路径**:`app/Http/Middleware/CheckPermission.php` + +```php +middleware('permission:crm:customer:list'); + * + * 执行逻辑: + * 1. 从路由中间件参数提取所需权限标识 + * 2. 平台超管拥有全部权限,直接放行 + * 3. 从 Redis 缓存获取用户权限集(缓存 key: permissions:user:{id}) + * 4. 缓存未命中则查库:user → user_roles → roles → role_permissions → permissions + * 5. 比对是否包含所需权限,不通过返回 403 + */ + public function handle(Request $request, Closure $next, string ...$permissions): Response + { + $user = $request->user(); + + if (!$user) { + abort(401, 'Unauthenticated'); + } + + // 平台超管拥有全部权限 + if ($user->isPlatformSuperAdmin()) { + return $next($request); + } + + // 获取用户权限集(带缓存) + $userPermissions = $this->getUserPermissions($user->id); + + // 检查是否拥有所需权限(任一匹配即可) + foreach ($permissions as $permission) { + if (in_array($permission, $userPermissions)) { + return $next($request); + } + } + + abort(403, '没有操作权限'); + } + + /** + * 获取用户权限集(Redis 缓存 + 数据库回源) + * 缓存时间:30 分钟,角色/权限变更时主动清除 + */ + protected function getUserPermissions(int $userId): array + { + $cacheKey = "permissions:user:{$userId}"; + + return Cache::remember($cacheKey, 1800, function () use ($userId) { + return \App\Models\User::find($userId) + ->roles() + ->where('roles.status', 1) + ->with(['permissions' => fn($q) => $q->where('permissions.status', 1)]) + ->get() + ->pluck('permissions') + ->flatten() + ->pluck('code') + ->unique() + ->values() + ->toArray(); + }); + } +} +``` + +### 6.3 OperationLog 中间件 + +**文件路径**:`app/Http/Middleware/OperationLog.php` + +```php +middleware('operation.log:crm,create,新建客户'); + */ + public function handle(Request $request, Closure $next, string $module = '', string $action = '', string $title = ''): Response + { + $startTime = microtime(true); + + $response = $next($request); + + // 仅记录写操作,GET 请求默认跳过(除非显式标注) + if ($request->isMethod('GET') && empty($module)) { + return $response; + } + + // 异步写入日志(使用 Laravel Job 队列) + dispatch(new \App\Jobs\WriteOperationLog([ + 'store_id' => app()->bound('current_store_id') ? app('current_store_id') : null, + 'user_id' => $request->user()?->id, + 'user_name' => $request->user()?->name, + 'module' => $module ?: $this->guessModule($request), + 'action' => $action ?: strtolower($request->method()), + 'title' => $title ?: $this->guessTitle($request), + 'method' => $request->method(), + 'url' => $request->fullUrl(), + 'route_name' => $request->route()?->getName(), + 'request_body' => $this->sanitizeParams($request->all()), + 'response_code' => $response->getStatusCode(), + 'ip' => $request->ip(), + 'user_agent' => $request->userAgent(), + 'duration_ms' => (int) ((microtime(true) - $startTime) * 1000), + 'status' => $response->getStatusCode() < 400 ? 1 : 0, + ])); + + return $response; + } + + /** + * 参数脱敏:移除敏感字段 + */ + protected function sanitizeParams(array $params): array + { + $sensitiveKeys = ['password', 'password_confirmation', 'token', 'secret', 'id_card', 'credit_card']; + foreach ($sensitiveKeys as $key) { + if (isset($params[$key])) { + $params[$key] = '[REDACTED]'; + } + } + return $params; + } + + protected function guessModule(Request $request): string + { + // 从路由路径推断模块:/api/v1/crm/customers → crm + $segments = $request->segments(); + return $segments[2] ?? 'system'; + } + + protected function guessTitle(Request $request): string + { + $method = $request->method(); + return match ($method) { + 'POST' => '新建记录', + 'PUT', 'PATCH' => '更新记录', + 'DELETE' => '删除记录', + default => '操作', + }; + } +} +``` + +### 6.4 中间件注册 + +```php +// bootstrap/app.php (Laravel 10/11) +// 或 app/Http/Kernel.php + +// 路由中间件别名 +protected $middlewareAliases = [ + // ... + 'store.isolation' => \App\Http\Middleware\StoreIsolation::class, + 'permission' => \App\Http\Middleware\CheckPermission::class, + 'operation.log' => \App\Http\Middleware\OperationLog::class, +]; + +// 中间件组 +protected $middlewareGroups = [ + 'admin' => [ + 'auth:sanctum', + 'store.isolation', + 'operation.log', + ], +]; +``` + +### 6.5 路由示例 + +```php +// routes/api.php + +Route::prefix('v1')->middleware('admin')->group(function () { + + // CRM 模块 + Route::prefix('crm')->group(function () { + Route::get('/customers', [CustomerController::class, 'index']) + ->middleware('permission:crm:customer:list') + ->middleware('operation.log:crm,list,查看客户列表'); + + Route::post('/customers', [CustomerController::class, 'store']) + ->middleware('permission:crm:customer:create') + ->middleware('operation.log:crm,create,新建客户'); + + Route::put('/customers/{id}', [CustomerController::class, 'update']) + ->middleware('permission:crm:customer:update') + ->middleware('operation.log:crm,update,更新客户'); + + Route::delete('/customers/{id}', [CustomerController::class, 'destroy']) + ->middleware('permission:crm:customer:delete') + ->middleware('operation.log:crm,delete,删除客户'); + + Route::post('/customers/export', [CustomerController::class, 'export']) + ->middleware('permission:crm:customer:export') + ->middleware('operation.log:crm,export,导出客户'); + }); + + // 系统管理 + Route::prefix('system')->group(function () { + Route::apiResource('roles', RoleController::class) + ->middleware('permission:system:role:list,system:role:create,system:role:update,system:role:delete'); + + Route::post('/roles/{id}/permissions', [RoleController::class, 'assignPermissions']) + ->middleware('permission:system:role:assign'); + + Route::get('/menus/user', [MenuController::class, 'userMenus']); // 获取当前用户菜单(无需额外权限) + }); +}); +``` + +--- + +## 七、DataScope 查询 Trait + +```php +dataScope()->paginate(); + */ + public function scopeDataScope(Builder $query): Builder + { + $user = Auth::user(); + + if (!$user || $user->isPlatformSuperAdmin()) { + return $query; + } + + // 获取用户所有角色中最宽的 data_scope + $dataScope = $user->getWidestDataScope(); + + return match ($dataScope) { + 'all' => $query, + 'store' => $query, // StoreIsolation 已处理 + 'department' => $query->where($this->getTable() . '.department_id', $user->department_id), + 'self' => $query->where($this->getTable() . '.created_by', $user->id), + 'custom' => $query->whereIn( + $this->getTable() . '.department_id', + $user->getCustomDepartmentIds() + ), + default => $query->where($this->getTable() . '.created_by', $user->id), // 兜底:仅本人 + }; + } +} +``` + +--- + +## 八、审批流引擎设计 + +### 8.1 审批流程图 + +``` +发起申请 + │ + ├─ 1. 匹配审批流:按 module + code + trigger_condition 定位 + │ + ├─ 2. 创建 approval_records(状态=0待审批) + │ + ├─ 3. 生成 approval_record_details(按步骤创建待审批明细) + │ + ├─ 4. 通知审批人(站内消息 + 微信模板消息) + │ + ├─ 5. 审批人操作: + │ ├── 通过 → 检查当前步骤是否全部完成 + │ │ ├── 或签模式:任一通过即进入下一步 + │ │ └── 会签模式:全部通过才进入下一步 + │ ├── 驳回 → 审批结束,状态=3已驳回 + │ └── 转交 → 更新审批人,重新通知 + │ + ├─ 6. 进入下一步 → 重复步骤 4-5 + │ + └─ 7. 最后一步通过 → 审批结束,状态=2已通过 + └── 触发业务回调(如:合同生效、采购单下发) +``` + +### 8.2 审批服务核心方法 + +```php += 5000 需要审批 + if ($contract->amount >= 5000) { + app(ApprovalService::class)->submit( + flowCode: 'contract_sign', + businessId: $contract->id, + businessType: 'contract', + title: "合同审批:{$contract->customer_name} ¥{$contract->amount}", + businessData: $contract->toArray() + ); + $contract->update(['status' => Contract::STATUS_PENDING_APPROVAL]); + } + + return $contract; + } +} +``` + +--- + +## 九、前端动态路由与按钮权限 + +### 9.1 登录后菜单获取 + +``` +前端登录成功 + │ + ├─ 1. POST /api/v1/auth/login → 获取 Token + 用户基础信息 + │ + ├─ 2. GET /api/v1/menus/user → 获取用户可访问的菜单树 + │ 后端查询链:user → user_roles → roles → role_menus → menus + │ 返回:树形菜单 JSON(含 path、component、permission) + │ + ├─ 3. 前端 vue-router 动态注册路由 + │ router.addRoute(menuToRoute(menuItem)) + │ + └─ 4. GET /api/v1/permissions/user → 获取用户权限标识列表 + 前端存入 Pinia Store,用于按钮级 v-permission 指令 +``` + +### 9.2 按钮级权限指令 + +```vue + + +``` + +```javascript +// src/directives/permission.js +export default { + mounted(el, binding) { + const userPermissions = usePermissionStore().permissions; // string[] + const required = binding.value; + if (!userPermissions.includes(required)) { + el.parentNode?.removeChild(el); + } + }, +}; +``` + +--- + +## 十、权限缓存策略 + +| 缓存 Key | 内容 | TTL | 清除时机 | +|-----------|------|-----|----------| +| `permissions:user:{id}` | 用户权限标识列表 | 30 min | 角色变更 / 权限变更 / 用户角色变更 | +| `menus:user:{id}` | 用户菜单树 | 30 min | 菜单变更 / 角色菜单变更 | +| `roles:store:{store_id}` | 门店角色列表 | 60 min | 角色增删改 | +| `data_scope:user:{id}` | 用户数据权限范围 | 30 min | 角色变更 | + +**缓存清除方式**:通过 Laravel Event + Listener 在权限/角色/用户关联变更时主动清除相关缓存。 + +```php +// 示例:角色权限变更时清除关联用户缓存 +class RolePermissionChanged +{ + public function handle(RolePermissionUpdated $event): void + { + $roleId = $event->roleId; + $userIds = UserRole::where('role_id', $roleId)->pluck('user_id'); + + foreach ($userIds as $userId) { + Cache::forget("permissions:user:{$userId}"); + Cache::forget("menus:user:{$userId}"); + Cache::forget("data_scope:user:{$userId}"); + } + } +} +``` + +--- + +## 十一、安全加固措施 + +### 11.1 登录安全 + +| 策略 | 实现 | +|------|------| +| 密码强度 | 最低 8 位,含大小写字母 + 数字 | +| 登录失败锁定 | 连续 5 次失败锁定 30 分钟 | +| 密码有效期 | 90 天强制修改(可配置) | +| 敏感操作二次验证 | 删除数据、修改权限需输入密码或短信验证码 | + +### 11.2 API 安全 + +| 策略 | 实现 | +|------|------| +| Token 有效期 | Sanctum Token 默认 8 小时,可配置 | +| 并发登录限制 | 同一账号仅保留最新 Token(可配置多端登录) | +| 接口限流 | 全局 60 次/分钟,敏感接口 10 次/分钟(throttle 中间件) | +| 传输加密 | 全站 HTTPS + HSTS | + +### 11.3 数据安全 + +| 策略 | 实现 | +|------|------| +| 敏感字段加密 | 身份证号、银行卡号使用 AES-256-CBC 加密存储 | +| SQL 注入防护 | Eloquent ORM 参数绑定 + 禁止 `DB::raw()` 裸拼接 | +| 越权防护 | Model 层 Global Scope + 中间件双重检查 | +| 日志脱敏 | 请求日志自动过滤 password / token / secret 字段 | + +--- + +## 十二、种子数据(Seeder) + +系统初始化时需要预置以下数据: + +### 12.1 默认权限树(部分示例) + +``` +system # 系统管理 +├── system:store # 门店管理 +│ ├── system:store:list +│ ├── system:store:create +│ ├── system:store:update +│ └── system:store:delete +├── system:department # 部门管理 +├── system:position # 职务管理 +├── system:user # 用户管理 +│ ├── system:user:list +│ ├── system:user:create +│ ├── system:user:update +│ ├── system:user:delete +│ ├── system:user:reset_password +│ └── system:user:assign_role +├── system:role # 角色管理 +│ ├── system:role:list +│ ├── system:role:create +│ ├── system:role:update +│ ├── system:role:delete +│ └── system:role:assign +├── system:menu # 菜单管理 +├── system:log # 日志管理 +└── system:approval_flow # 审批流管理 + +crm # CRM 客户管理 +├── crm:lead # 线索管理 +├── crm:customer # 客户管理 +├── crm:contract # 签约管理 +├── crm:followup # 跟进记录 +└── crm:questionnaire # 问卷调查 + +room # 房务管理 +├── room:room # 房间管理 +├── room:booking # 预定管理 +├── room:checkin # 入住管理 +└── room:housekeeping # 保洁管理 + +care # 护理管理 +├── care:archive # 护理档案 +├── care:plan # 护理计划 +├── care:record # 护理记录 +├── care:alert # 异常告警 +└── care:indicator # 健康指标 + +meal # 月子餐管理 +├── meal:menu_plan # 排餐计划 +├── meal:delivery # 送餐管理 +├── meal:ingredient # 食材管理 +└── meal:review # 餐食评价 + +service # 服务与产康 +├── service:project # 服务项目 +├── service:booking # 服务预约 +├── service:record # 服务记录 +└── service:package # 套餐管理 + +nanny # 月嫂管理 +├── nanny:profile # 月嫂档案 +├── nanny:schedule # 排班调度 +├── nanny:evaluation # 质检评价 +├── nanny:contract # 月嫂合同 +└── nanny:settlement # 结算管理 + +stock # 进销存 +├── stock:product # 商品管理 +├── stock:inventory # 库存管理 +├── stock:purchase # 采购管理 +├── stock:inbound # 入库管理 +└── stock:outbound # 出库管理 + +finance # 财务管理 +├── finance:bill # 账单管理 +├── finance:payment # 收付款 +├── finance:invoice # 发票管理 +├── finance:report # 财务报表 +└── finance:settlement # 结算管理 + +hr # 人事管理 +├── hr:employee # 员工档案 +├── hr:attendance # 考勤管理 +├── hr:salary # 薪资管理 +└── hr:performance # 绩效管理 + +report # 统计报表 +├── report:dashboard # 数据看板 +├── report:business # 业务报表 +└── report:export # 报表导出 +``` + +### 12.2 默认角色与权限映射 + +```php +// database/seeders/RolePermissionSeeder.php + +$rolePermissionMap = [ + 'store_manager' => ['*'], // 本门店全部权限 + 'sales' => ['crm:*', 'room:booking:list', 'report:dashboard:list'], + 'front_desk' => ['room:*', 'crm:customer:list', 'crm:customer:update'], + 'nurse' => ['care:*', 'meal:delivery:list'], + 'kitchen' => ['meal:*', 'stock:ingredient:*'], + 'therapist' => ['service:*'], + 'warehouse' => ['stock:*'], + 'finance' => ['finance:*', 'report:*'], + 'hr' => ['hr:*', 'system:user:list', 'system:department:list'], + 'nanny_center_admin' => ['nanny:*'], + 'nanny_supervisor' => ['nanny:schedule:*', 'nanny:evaluation:*'], + 'nanny' => ['nanny:profile:self', 'nanny:schedule:self'], + 'client' => [], // 客户端走独立路由,不经后台权限体系 +]; +``` + +--- + +## 十三、ER 关系总览 + +``` +stores ─────────────────────────────────────────────────┐ + │ 1:N │ + ├── departments (parent_id 自引用形成树) │ + │ │ 1:N │ + │ └── users │ + │ │ N:M │ + │ └── user_roles ── roles │ + │ │ N:M │ N:M │ + │ ├── role_permissions │ + │ │ └── permissions │ + │ ├── role_menus │ + │ │ └── menus │ + │ └── role_departments │ + │ └── departments │ + │ │ + ├── positions │ + │ │ + ├── approval_flows │ + │ │ 1:N │ + │ └── approval_flow_steps │ + │ │ + ├── approval_records │ + │ │ 1:N │ + │ └── approval_record_details │ + │ │ + └── operation_logs │ + │ + permissions (全局表,无 store_id) ────────────────────┘ + menus (全局表,无 store_id) +``` + +--- + +## 十四、部署检查清单 + +- [ ] `permissions` 和 `menus` 种子数据已完整导入 +- [ ] 平台超管账号已创建(`platform_super_admin` 角色) +- [ ] 各门店默认角色已初始化(Seeder 按 `stores` 表遍历创建) +- [ ] Redis 已配置并可连接(用于权限缓存) +- [ ] 操作日志队列 Worker 已启动(`php artisan queue:work --queue=logs`) +- [ ] 审批超时定时任务已注册(`schedule:run` 每分钟执行) +- [ ] HTTPS 证书已部署,HTTP 自动跳转 HTTPS +- [ ] `operation_logs` 表已配置按月归档策略 +- [ ] 敏感字段加密密钥 `APP_KEY` 已安全存储 +- [ ] 登录失败锁定策略参数已确认(次数、时长) diff --git a/docs/05-module-details.md b/docs/05-module-details.md new file mode 100644 index 0000000..75e720a --- /dev/null +++ b/docs/05-module-details.md @@ -0,0 +1,546 @@ +# 宫中有喜 — 模块详细设计 + +## 一、模块总览 + +| 编号 | 模块 | 核心实体 | 关键流程 | +|------|------|----------|----------| +| 5.1 | CRM | 线索、客户、合同 | 线索→跟进→签约→入住 | +| 5.2 | 房务 | 房间、预定、入住 | 预定→入住→在住→离店 | +| 5.3 | 护理 | 档案、计划、记录 | 建档→制定计划→执行→异常处理 | +| 5.4 | 月子餐 | 菜品、排餐、送餐 | 排餐→备餐→送餐→反馈 | +| 5.5 | 服务产康 | 项目、套餐、订单 | 预约→排班→服务→评价 | +| 5.6 | 月嫂 | 月嫂、订单、评价 | 选嫂→派嫂→服务→评价 | +| 5.7 | 进销存 | 物资、库存、采购 | 采购→入库→领用→盘点 | +| 5.8 | 财务 | 账户、收支、发票 | 收款→记账→对账→报表 | +| 5.9 | 人事薪资 | 员工、排班、工资 | 入职→排班→考勤→算薪 | +| 5.10 | 办公协同 | 审批、公告、交接 | 发起→审批→通知→归档 | +| 5.11 | 统计报表 | 报表、图表 | 采集→聚合→可视化→导出 | +| 5.12 | 知识库 | 文章、分类 | 创建→审核→发布→检索 | +| 5.13 | 系统设置 | 门店、用户、角色 | 配置→分配→生效 | + +--- + +## 二、5.1 CRM 客户关系管理 + +### 2.1 业务流程 + +``` +线索来源(微信/电话/转介/活动/小程序) + ↓ +[线索池] → 自动/手动分配 → [跟进中] + ↓ ↓ + 无效关闭 多次跟进记录 + ↓ + [意向确认] → 签约 + ↓ + [合同] → 审批 → 生效 + ↓ + [客户] → 关联入住/服务 +``` + +### 2.2 线索状态机 + +| 状态 | 值 | 可流转至 | +|------|-----|----------| +| 新线索 | new | following, invalid | +| 跟进中 | following | intended, invalid | +| 有意向 | intended | contracted, invalid | +| 已签约 | contracted | — | +| 无效 | invalid | new(可回收) | + +### 2.3 核心业务规则 + +- 线索分配规则:轮询分配 / 按区域分配 / 手动指定 +- 线索回收:超过 N 天未跟进自动回收至公海池 +- 跟进提醒:距上次跟进 3/7 天自动提醒 +- 签约流程:需主管审批 → 财务确认 → 合同生效 +- 客户标签:VIP / 重点关注 / 转介绍来源 等自定义标签 +- 问卷评分:入住中期 + 离店时各一次满意度问卷 + +### 2.4 关键 Service 方法 + +```php +// LeadService +assignLead(leadId, userId) // 分配线索 +recycleLead(leadId) // 回收至公海 +convertToCustomer(leadId, data) // 线索转客户+建合同 +batchImport(file) // Excel批量导入 + +// ContractService +createContract(data) // 创建合同 +approveContract(contractId, action) // 审批 +calculateAmount(items) // 计算合同金额 +generatePdf(contractId) // 生成PDF +``` + +--- + +## 三、5.2 房务管理 + +### 3.1 房态状态机 + +``` +空闲(available) ←→ 已预定(reserved) + ↓ ↓ + 已入住(occupied) ←── 办理入住 + ↓ + 待清洁(cleaning) ← 办理离店 + ↓ + 维修中(maintenance) + ↓ + 空闲(available) +``` + +### 3.2 核心业务规则 + +- 房间编号规则:楼层号 + 序号(如 301、302) +- 房型定价:基础日价 × 天数,支持季节浮动系数 +- 预定冲突检测:同一房间日期区间不可重叠 +- 入住办理:预定 → 验证身份 → 分配房间 → 入住登记 +- 换房规则:在住期间可换房,自动调整账单 +- 延住规则:检查目标日期房间是否可用 → 调整账单 +- 房态看板:日历视图,支持按周/月切换,颜色区分状态 + +### 3.3 关键 Service 方法 + +```php +// RoomService +checkAvailability(roomId, startDate, endDate) // 检查可用性 +getRoomBoard(date, range) // 房态看板数据 + +// ReservationService +createReservation(data) // 新建预定 +cancelReservation(id, reason) // 取消预定 +checkIn(reservationId, data) // 办理入住 + +// CheckInService +changeRoom(checkInId, newRoomId) // 换房 +extend(checkInId, newEndDate) // 延住 +checkOut(checkInId) // 离店 +``` + +--- + +## 四、5.3 护理管理 + +### 4.1 护理流程 + +``` +入住 → 建立护理档案(产妇+新生儿) + ↓ +评估体质/需求 → 制定个性化护理计划 + ↓ +每日执行护理任务(按计划模板) + ↓ +记录护理项目 + 健康指标 + ↓ +异常登记 → 处理 → 闭环 + ↓ +离店总结 → 护理报告 +``` + +### 4.2 护理档案内容 + +**产妇档案**:基本信息、分娩信息(日期/方式/胎次)、过敏史、饮食禁忌、体质评估 + +**新生儿档案**:出生信息、体重、身长、Apgar 评分、特殊情况 + +### 4.3 健康指标追踪 + +| 对象 | 指标 | 频率 | +|------|------|------| +| 产妇 | 体温、血压、恶露、伤口 | 日/2次 | +| 产妇 | 体重、情绪评估 | 周/1次 | +| 新生儿 | 体温、体重、身长 | 日/1次 | +| 新生儿 | 喂奶量、排便次数、黄疸 | 日/多次 | + +### 4.4 核心业务规则 + +- 护理计划模板:按阶段(第1周、第2周...)预设任务 +- 任务打卡:护理人员执行后打卡确认,支持拍照 +- 异常三级:一般(记录即可)/ 注意(通知主管)/ 紧急(立即通知家属+医生) +- 交接班:上一班护理记录自动同步至下一班 + +### 4.5 关键 Service 方法 + +```php +// CareProfileService +createProfile(checkInId) // 入住时自动建档 +generateReport(profileId) // 生成护理报告 + +// CarePlanService +createFromTemplate(profileId, templateId) // 从模板创建计划 +activatePlan(planId) // 激活计划 +getTasksForDate(profileId, date) // 获取当日任务 + +// CareRecordService +createRecord(data) // 记录护理项目 +recordHealthMetric(data) // 录入健康指标 +getMetricChart(profileId, metric, range) // 指标趋势 + +// CareAlertService +createAlert(data) // 创建异常 +resolveAlert(alertId, resolution) // 处理异常 +``` + +--- + +## 五、5.4 月子餐管理 + +### 5.1 排餐流程 + +``` +菜品库维护 → 餐单模板制定 + ↓ +每日排餐(可从模板复制) + ↓ +按时段送餐(早/早点/午/午点/晚/晚点) + ↓ +送达确认 → 客户反馈 +``` + +### 5.2 核心业务规则 + +- 六餐制:早餐、早点心、午餐、午点心、晚餐、晚点心 +- 体质匹配:根据产妇体质推荐菜品,标记禁忌 +- 过敏预警:菜品含过敏原时自动告警 +- 换餐:入住客户可在规定时间前申请换餐 +- 评价统计:菜品好评率影响后续推荐 + +### 5.3 关键 Service 方法 + +```php +// MealPlanService +generateDailyPlan(date, templateId) // 生成日排餐 +batchGenerate(startDate, endDate, templateId) // 批量生成 +customizePlan(planId, customerId, items) // 个性化调整 + +// MealDeliveryService +createDeliveryTasks(date) // 生成送餐任务 +confirmDelivery(deliveryId) // 确认送达 +recordFeedback(deliveryId, data) // 记录反馈 +``` + +--- + +## 六、5.5 服务与产康 + +### 6.1 业务流程 + +``` +服务项目配置(产后瑜伽/盆底修复/催乳/满月汗蒸...) + ↓ +套餐组合(单项/套餐/卡次) + ↓ +客户预约 → 技师排班匹配 + ↓ +服务执行 → 服务记录 + ↓ +客户评价 +``` + +### 6.2 核心业务规则 + +- 服务分类:产康项目 / 母婴护理 / 增值服务 +- 计费模式:单次 / 套餐 / 次卡 / 月卡 +- 排班冲突:同一技师同时段不可安排两个服务 +- 服务时长:每个项目有标准时长,影响排班 +- 次卡消费:每次服务自动扣减次数 + +--- + +## 七、5.6 月嫂管理 + +### 7.1 业务流程 + +``` +月嫂入驻 → 建档(证书/经验/等级/日薪) + ↓ +客户选嫂(列表展示/筛选) + ↓ +下单 → 会所审核 → 确认指派 + ↓ +月嫂上户 → 服务期间 + ↓ +服务结束 → 客户评价 → 更新档案评分 +``` + +### 7.2 月嫂等级 + +| 等级 | 条件 | 日薪参考 | +|------|------|----------| +| 初级 | 持证 + <1年经验 | 300-500 | +| 中级 | 持证 + 1-3年 | 500-800 | +| 高级 | 持证 + 3-5年 + 好评率>90% | 800-1200 | +| 金牌 | 持证 + >5年 + 好评率>95% | 1200+ | + +### 7.3 核心业务规则 + +- 月嫂状态:待岗 / 服务中 / 休息 / 离职 +- 排班冲突:同一月嫂同时段只能服务一个客户 +- 会所抽佣:订单金额 × 佣金比例 +- 评价影响:综合评分影响等级和排序 + +--- + +## 八、5.7 进销存 + +### 8.1 业务流程 + +``` +物资分类维护 → 供应商管理 + ↓ +采购申请 → 审批 → 采购执行 + ↓ +到货验收 → 入库 + ↓ +领用申请 → 出库 + ↓ +定期盘点 → 差异处理 + ↓ +库存预警 → 自动触发采购 +``` + +### 8.2 核心业务规则 + +- 库存预警:低于安全库存自动提醒 +- 先进先出:按批次管理,临期品优先出库 +- 采购审批:金额 > N 元需主管审批 +- 出库关联:与护理/月子餐模块联动(如消耗护理用品) +- 盘点:支持全盘、抽盘,生成盘盈亏报表 + +--- + +## 九、5.8 财务管理 + +### 9.1 业务流程 + +``` +收入类:签约收款 / 服务消费 / 充值 / 月嫂佣金 + ↓ +支出类:采购付款 / 工资 / 运营费用 + ↓ +记账 → 审核 → 入账 + ↓ +日/月结算 → 报表 + ↓ +发票管理 +``` + +### 9.2 客户账户体系 + +``` +客户账户 +├── 现金余额(充值/退款) +├── 储值卡余额(卡种 × N) +├── 积分 +└── 消费明细流水 +``` + +### 9.3 核心业务规则 + +- 收支分类:预设分类树(一级→二级→三级) +- 审核流程:金额 > N 元需财务主管审核 +- 客户充值:支持微信支付 / 线下转账 +- 储值卡:预设面额、折扣率、有效期 +- 退款:原路退回 / 退回余额,需审批 +- 发票:电子发票 / 纸质发票,关联订单 + +--- + +## 十、5.9 人事薪资 + +### 10.1 业务流程 + +``` +员工入职 → 建档 → 分配角色/部门 + ↓ +排班管理(固定班/轮班) + ↓ +每日考勤(打卡/补卡/请假) + ↓ +月度汇总 → 工资计算 + ↓ +工资确认 → 发放 +``` + +### 10.2 工资计算公式 + +``` +实发 = 基本工资 + 岗位工资 + 绩效 + + 加班费 + 补贴 + - 社保 - 公积金 - 个税 + - 缺勤扣款 - 其他扣款 +``` + +### 10.3 核心业务规则 + +- 排班类型:早班/中班/晚班/夜班,支持跨天 +- 考勤规则:迟到(≤30min) / 早退 / 旷工 / 加班 +- 请假类型:年假/事假/病假/调休/产假/婚假 +- 加班费:工作日1.5倍 / 周末2倍 / 节假日3倍 +- 绩效:月度考核分数 × 绩效基数 + +--- + +## 十一、5.10 办公协同 + +### 11.1 审批引擎 + +``` +审批模板定义(表单字段 + 流程节点) + ↓ +发起审批(填写表单) + ↓ +按流程流转(串行/并行/条件分支) + ↓ +审批人 通过/驳回 + ↓ +结果通知 + 归档 +``` + +### 11.2 审批流程类型 + +| 流程 | 触发 | 审批链 | +|------|------|--------| +| 请假申请 | 人事模块 | 主管→人事 | +| 采购申请 | 进销存模块 | 部门主管→财务 | +| 合同审批 | CRM模块 | 销售主管→总经理 | +| 退款申请 | 财务模块 | 客服→财务→总经理 | +| 换房申请 | 房务模块 | 客服→房务主管 | + +### 11.3 核心功能 + +- 公告管理:发布、置顶、已读统计 +- 交接班:上一班记录 → 下一班确认,确保信息连续 +- 消息通知:站内消息 + 微信模板消息推送 +- 待办汇总:首页展示所有待处理事项 + +--- + +## 十二、5.11 统计报表 + +### 12.1 预设报表 + +| 报表 | 维度 | 指标 | +|------|------|------| +| 经营概览 | 门店/日期 | 营收、入住率、客户数、满意度 | +| 营收分析 | 门店/月/类别 | 签约额、实收额、退款额、应收 | +| 入住统计 | 门店/月/房型 | 入住率、平均入住天数、房型分布 | +| CRM转化 | 来源/月/员工 | 线索数、转化率、签约率 | +| 护理质量 | 门店/月 | 异常率、满意度、任务完成率 | +| 餐品评价 | 菜品/月 | 好评率、投诉率 | +| 库存报表 | 仓库/月 | 采购额、消耗额、盘亏率 | +| 人力成本 | 部门/月 | 人工成本、人效比 | + +### 12.2 核心功能 + +- 数据钻取:点击指标可下钻到明细 +- 对比分析:同环比、多门店对比 +- 导出功能:Excel / PDF +- 自定义报表:拖拽选择维度和指标 +- 数据权限:按门店/部门过滤 + +--- + +## 十三、5.12 知识库 + +### 13.1 功能设计 + +- 分类管理:树形分类(如:产妇护理/新生儿护理/营养学/政策法规) +- 文章管理:富文本编辑器,支持图片/视频/附件 +- 发布流程:草稿→审核→发布 +- 全文检索:标题+内容全文搜索 +- 权限控制:内部文章(员工可见)/ 公开文章(小程序可见) +- 阅读统计:浏览量、点赞量、收藏量 +- 版本管理:文章修改历史 + +--- + +## 十四、5.13 系统设置 + +### 14.1 功能清单 + +| 功能 | 说明 | +|------|------| +| 门店管理 | 新建/编辑/启停门店 | +| 组织架构 | 区域 → 门店 → 部门 → 职务 | +| 用户管理 | 员工账号创建、角色分配 | +| 角色管理 | 角色定义、权限分配 | +| 菜单管理 | 动态菜单配置 | +| 字典管理 | 通用数据字典(下拉选项等) | +| 系统参数 | 全局配置项(如回收天数、审批金额阈值) | +| 操作日志 | 全局操作审计 | + +### 14.2 多门店体系 + +``` +总部(超级管理员) +├── 区域A +│ ├── 门店1(独立数据) +│ └── 门店2(独立数据) +└── 区域B + └── 门店3(独立数据) +``` + +- 总部可查看所有门店数据 +- 门店只能看到本店数据 +- 部分配置(如字典、模板)可总部统一下发 + +--- + +## 十五、跨模块联动 + +| 触发 | 联动模块 | 动作 | +|------|----------|------| +| CRM签约 | 房务 | 自动创建预定 | +| 房务入住 | 护理 | 自动建立护理档案 | +| 房务入住 | 月子餐 | 自动生成排餐任务 | +| 护理异常(紧急) | 办公协同 | 推送通知 | +| 服务消费 | 财务 | 自动生成收支记录 | +| 月嫂订单 | 财务 | 佣金记录 | +| 采购入库 | 财务 | 付款记录 | +| 请假审批通过 | 人事 | 更新考勤 | +| 各模块 | 统计报表 | 数据采集 | + +### 联动实现方式 + +使用 Laravel Event/Listener 解耦: + +```php +// 事件定义 +ContractSigned::class → CreateReservationListener +CheckedIn::class → CreateCareProfileListener + → GenerateMealPlanListener +CareAlertCreated::class → NotifyStaffListener +ServiceCompleted::class → CreateFinanceRecordListener +PurchaseReceived::class → CreateFinanceRecordListener +LeaveApproved::class → UpdateAttendanceListener +``` + +--- + +## 十六、通用功能 + +### 16.1 导入导出 + +- 导入:Excel 上传 → 校验 → 预览 → 确认导入 +- 导出:异步队列生成 → 下载链接(大数据量) +- 模板下载:每个可导入功能提供 Excel 模板 + +### 16.2 操作日志 + +```php +// 自动记录 +[时间] [用户] [模块] [操作] [对象] [变更内容] +// 示例 +2024-03-15 10:30 张护士 护理模块 新建 护理记录#1001 { ... } +``` + +### 16.3 消息通知 + +| 渠道 | 场景 | +|------|------| +| 站内消息 | 审批、任务提醒、异常告警 | +| 微信模板消息 | 客户:入住通知、餐单推送、服务提醒 | +| 短信 | 重要通知备选渠道 | diff --git a/docs/06-miniapp-design.md b/docs/06-miniapp-design.md new file mode 100644 index 0000000..cdf4df8 --- /dev/null +++ b/docs/06-miniapp-design.md @@ -0,0 +1,505 @@ +# 宫中有喜 — 微客宝小程序设计 + +## 一、概述 + +| 项 | 值 | +|---|---| +| 产品名称 | 宫中有喜·微客宝 | +| 类型 | 微信小程序 | +| 框架 | uni-app (Vue 3) | +| 目标用户 | 在住/潜在客户(产妇及家属) | +| 核心功能 | 服务商城、月子餐、共伴成长、月嫂预约、学堂社区 | + +## 二、页面架构 + +### 2.1 Tab 导航(底部 5 Tab) + +``` +┌──────────────────────────────────┐ +│ 微客宝小程序 │ +├────┬────┬────┬────┬────┤ +│ 首页 │ 服务 │ 成长 │ 学堂 │ 我的 │ +└────┴────┴────┴────┴────┘ +``` + +### 2.2 完整页面树 + +``` +pages/ +├── index/ # Tab1: 首页 +│ └── index.vue # 轮播 + 快捷入口 + 推荐 + 活动 +│ +├── service/ # Tab2: 服务 +│ ├── index.vue # 服务分类列表 +│ ├── detail.vue # 服务/套餐详情 +│ ├── booking.vue # 预约下单 +│ ├── nanny/ +│ │ ├── list.vue # 月嫂列表 +│ │ ├── detail.vue # 月嫂详情 +│ │ └── order.vue # 月嫂预约 +│ └── meal/ +│ ├── today.vue # 今日餐单 +│ ├── week.vue # 本周餐单 +│ └── custom.vue # 自选点餐 +│ +├── growth/ # Tab3: 共伴成长 +│ ├── index.vue # 成长首页(宝宝+妈妈) +│ ├── baby/ +│ │ ├── records.vue # 宝宝护理记录 +│ │ ├── chart.vue # 宝宝数据图表 +│ │ └── milestone.vue # 成长里程碑 +│ └── mom/ +│ ├── records.vue # 妈妈护理记录 +│ └── chart.vue # 妈妈健康图表 +│ +├── study/ # Tab4: 学堂 +│ ├── index.vue # 文章列表 +│ ├── detail.vue # 文章详情 +│ └── community/ +│ ├── index.vue # 社区帖子列表 +│ ├── detail.vue # 帖子详情 +│ └── post.vue # 发帖 +│ +├── mine/ # Tab5: 我的 +│ ├── index.vue # 个人中心首页 +│ ├── orders/ +│ │ ├── list.vue # 我的订单 +│ │ └── detail.vue # 订单详情 +│ ├── account.vue # 账户余额/储值卡 +│ ├── coupons.vue # 优惠券 +│ ├── reviews.vue # 我的评价 +│ ├── collections.vue # 我的收藏 +│ ├── messages.vue # 消息通知 +│ └── settings.vue # 设置 +│ +├── auth/ # 认证 +│ ├── login.vue # 微信授权登录 +│ └── bindPhone.vue # 绑定手机号 +│ +├── pay/ # 支付 +│ ├── cashier.vue # 收银台 +│ └── result.vue # 支付结果 +│ +└── common/ # 公共页面 + ├── webview.vue # 内嵌网页 + └── preview.vue # 图片预览 +``` + +--- + +## 三、核心页面设计 + +### 3.1 首页 (index) + +``` +┌────────────────────────────┐ +│ [轮播图 Banner] │ +├────────────────────────────┤ +│ 📋公告栏(滚动) │ +├────┬────┬────┬────┤ +│ 商城 │ 点餐 │ 月嫂 │ 更多 │ ← 快捷入口 +├────┴────┴────┴────┤ +│ ⭐ 热门服务推荐 │ +│ ┌─────┐ ┌─────┐ │ +│ │ 项目1│ │ 项目2│ │ +│ └─────┘ └─────┘ │ +├────────────────────────────┤ +│ 🎉 精彩活动 │ +│ ┌──────────────────┐ │ +│ │ 活动卡片 │ │ +│ └──────────────────┘ │ +├────────────────────────────┤ +│ 📖 孕妈课堂(文章推荐) │ +│ ┌──────────────────┐ │ +│ │ 文章卡片 │ │ +│ └──────────────────┘ │ +└────────────────────────────┘ +``` + +数据来源: +- 轮播图:`GET /client/home/banners` +- 公告:`GET /client/home/notices` +- 推荐服务:`GET /client/home/services` +- 活动:`GET /client/home/activities` +- 文章推荐:`GET /client/articles?recommend=1&limit=3` + +### 3.2 共伴成长页 + +``` +┌────────────────────────────┐ +│ [宝宝头像] 小明 出生第15天 │ +│ 体重: 3.5kg 身长: 50cm │ +├────────────────────────────┤ +│ Tab: [护理记录] [数据图表] [里程碑] │ +├────────────────────────────┤ +│ 今日护理记录 │ +│ 09:00 喂奶 120ml ✓ │ +│ 09:30 换尿布 ✓ │ +│ 10:00 体温测量 36.8°C ✓ │ +│ 11:00 洗澡 ✓ │ +│ 12:00 喂奶 100ml ✓ │ +│ ... │ +├────────────────────────────┤ +│ 切换至 [妈妈] 查看 │ +└────────────────────────────┘ +``` + +图表使用 uCharts 或 ECharts 小程序版,展示: +- 体重增长曲线 +- 喂奶量趋势 +- 睡眠时长统计 +- 健康指标折线图 + +### 3.3 月子餐页 + +``` +┌────────────────────────────┐ +│ 📅 3月15日 周五 今日餐单 │ +├────────────────────────────┤ +│ 🌅 早餐 07:30 │ +│ ┌────────────────────┐ │ +│ │ [图] 红枣小米粥 │ │ +│ │ [图] 蒸蛋 │ │ +│ │ [图] 核桃酥 │ │ +│ └────────────────────┘ │ +│ [👍已送达] [💬反馈] │ +├────────────────────────────┤ +│ 🍵 早点心 10:00 │ +│ ┌────────────────────┐ │ +│ │ [图] 银耳莲子汤 │ │ +│ └────────────────────┘ │ +│ [⏳待送达] │ +├────────────────────────────┤ +│ 🌞 午餐 12:00 │ +│ ┌────────────────────┐ │ +│ │ [图] 黄花菜炖鸡 │ │ +│ │ [图] 时令蔬菜 │ │ +│ │ [图] 乌鸡汤 │ │ +│ └────────────────────┘ │ +│ [🔄申请换餐] │ +├────────────────────────────┤ +│ 🍵🌙🌙 午点/晚餐/晚点... │ +├────────────────────────────┤ +│ [查看本周餐单] [自选点餐] │ +└────────────────────────────┘ +``` + +--- + +## 四、组件设计 + +### 4.1 公共组件 + +``` +components/ +├── NavBar.vue # 自定义导航栏 +├── TabBar.vue # 底部导航(自定义实现) +├── EmptyState.vue # 空状态占位 +├── LoadMore.vue # 加载更多/到底了 +├── PriceTag.vue # 价格显示(¥ 格式化) +├── StarRating.vue # 评分星级 +├── CountDown.vue # 倒计时 +├── ImageUploader.vue # 图片上传 +├── DatePicker.vue # 日期选择 +├── OrderCard.vue # 订单卡片 +├── ServiceCard.vue # 服务项目卡片 +├── ArticleCard.vue # 文章卡片 +├── NannyCard.vue # 月嫂卡片 +├── MealCard.vue # 菜品卡片 +└── StatusTag.vue # 状态标签 +``` + +### 4.2 业务组件 + +``` +components/business/ +├── PayButton.vue # 支付按钮(封装微信支付) +├── PhoneAuth.vue # 手机号授权组件 +├── ShareCard.vue # 分享卡片生成 +├── CouponSelector.vue # 优惠券选择 +├── ServiceBooking.vue # 服务预约表单 +└── ReviewForm.vue # 评价表单(星级+文字+图片) +``` + +--- + +## 五、状态管理 (Pinia) + +``` +stores/ +├── user.js # 用户信息、Token、登录状态 +├── cart.js # 购物车/预约车 +├── order.js # 订单状态 +└── app.js # 全局配置(门店信息、系统参数) +``` + +### 5.1 用户状态 + +```javascript +// stores/user.js +export const useUserStore = defineStore('user', { + state: () => ({ + token: '', + userInfo: null, // 微信用户信息 + customerInfo: null, // 客户业务信息 + isLoggedIn: false, + storeId: null, // 所属门店 + }), + actions: { + async wechatLogin(code) { ... }, + async bindPhone(encryptedData, iv) { ... }, + async fetchProfile() { ... }, + logout() { ... }, + } +}) +``` + +--- + +## 六、API 封装 + +``` +api/ +├── request.js # 请求封装(baseURL, 拦截器, Token注入) +├── auth.js # 登录/注册 +├── home.js # 首页数据 +├── service.js # 服务/套餐 +├── meal.js # 月子餐 +├── nanny.js # 月嫂 +├── growth.js # 共伴成长 +├── order.js # 订单 +├── pay.js # 支付 +├── article.js # 文章/学堂 +├── community.js # 社区 +└── user.js # 个人中心 +``` + +### 6.1 请求封装 + +```javascript +// api/request.js +const BASE_URL = 'https://xxx.com/api/v1/client' + +const request = (options) => { + const userStore = useUserStore() + return new Promise((resolve, reject) => { + uni.request({ + url: BASE_URL + options.url, + method: options.method || 'GET', + data: options.data, + header: { + 'Authorization': `Bearer ${userStore.token}`, + 'Content-Type': 'application/json' + }, + success: (res) => { + if (res.data.code === 0) { + resolve(res.data.data) + } else if (res.data.code >= 40100 && res.data.code < 40200) { + // Token 过期,跳转登录 + userStore.logout() + uni.navigateTo({ url: '/pages/auth/login' }) + reject(res.data) + } else { + uni.showToast({ title: res.data.message, icon: 'none' }) + reject(res.data) + } + }, + fail: reject + }) + }) +} + +export default request +``` + +--- + +## 七、微信能力集成 + +### 7.1 登录流程 + +``` +用户打开小程序 + ↓ +wx.login() 获取 code + ↓ +POST /client/auth/wechat-login { code } + ↓ +后端:code → openid/unionid → 查找/创建用户 → 返回 token + ↓ +若无手机号 → 弹出绑定手机号 + ↓ +getPhoneNumber → 解密 → 绑定 + ↓ +登录完成,进入首页 +``` + +### 7.2 微信支付流程 + +``` +用户点击支付 + ↓ +POST /client/pay/wechat { order_id, ... } + ↓ +后端调用微信统一下单 → 返回支付参数 + ↓ +前端 wx.requestPayment(payParams) + ↓ +支付成功/失败 → 跳转结果页 + ↓ +后端接收微信回调 → 更新订单状态 +``` + +### 7.3 分享 + +```javascript +// 页面分享配置 +onShareAppMessage() { + return { + title: '宫中有喜·月子会所', + path: '/pages/index/index?inviter=' + userId, + imageUrl: '/static/share-cover.png' + } +} + +onShareTimeline() { + return { + title: '宫中有喜·专业月子护理', + imageUrl: '/static/share-cover.png' + } +} +``` + +### 7.4 订阅消息 + +| 模板 | 触发场景 | +|------|----------| +| 订单状态通知 | 下单成功、支付成功、服务完成 | +| 服务预约提醒 | 预约确认、服务前1小时提醒 | +| 餐单推送 | 每日餐单发布 | +| 护理日报 | 每日护理小结 | +| 活动通知 | 新活动上线 | + +### 7.5 其他微信能力 + +| 能力 | 用途 | +|------|------| +| 地图 | 门店定位、导航 | +| 客服 | 在线客服对话 | +| 图片选择/预览 | 评价上传图片 | +| 扫码 | 扫码签到、扫码支付 | +| 生物认证 | 敏感操作验证 | + +--- + +## 八、UI 设计规范 + +### 8.1 色彩 + +| 色彩 | 色值 | 用途 | +|------|------|------| +| 主色 | #E8A87C | 温暖橘粉,品牌色 | +| 辅色 | #D4A574 | 暖棕,辅助强调 | +| 成功 | #67C23A | 完成/送达 | +| 警告 | #E6A23C | 提醒/待处理 | +| 危险 | #F56C6C | 异常/取消 | +| 文字主色 | #303133 | 正文 | +| 文字次色 | #909399 | 辅助说明 | +| 背景色 | #F5F5F5 | 页面背景 | +| 卡片色 | #FFFFFF | 内容卡片 | + +### 8.2 字体 + +| 场景 | 大小 | 权重 | +|------|------|------| +| 大标题 | 36rpx | bold | +| 标题 | 32rpx | bold | +| 正文 | 28rpx | normal | +| 辅助文字 | 24rpx | normal | +| 价格 | 36rpx | bold | + +### 8.3 间距 + +| 场景 | 值 | +|------|-----| +| 页面边距 | 32rpx | +| 卡片间距 | 24rpx | +| 卡片内边距 | 24rpx | +| 元素间距 | 16rpx | +| 紧凑间距 | 8rpx | + +### 8.4 圆角 + +| 场景 | 值 | +|------|-----| +| 按钮 | 40rpx(胶囊) | +| 卡片 | 16rpx | +| 图片 | 12rpx | +| 标签 | 8rpx | + +--- + +## 九、性能优化 + +| 策略 | 说明 | +|------|------| +| 分包加载 | 主包 < 2MB,子包按 Tab 分 | +| 图片懒加载 | 列表页图片按需加载 | +| 数据缓存 | 首页数据缓存 5 分钟 | +| 骨架屏 | 列表页加载时显示骨架 | +| 防抖节流 | 搜索输入防抖、按钮点击节流 | +| 虚拟列表 | 长列表使用 recycle-view | + +### 9.1 分包配置 + +```json +{ + "pages": [ + "pages/index/index", + "pages/service/index", + "pages/growth/index", + "pages/study/index", + "pages/mine/index" + ], + "subPackages": [ + { + "root": "pages/service", + "pages": ["detail", "booking", "nanny/list", "nanny/detail", "nanny/order", "meal/today", "meal/week", "meal/custom"] + }, + { + "root": "pages/growth", + "pages": ["baby/records", "baby/chart", "baby/milestone", "mom/records", "mom/chart"] + }, + { + "root": "pages/study", + "pages": ["detail", "community/index", "community/detail", "community/post"] + }, + { + "root": "pages/mine", + "pages": ["orders/list", "orders/detail", "account", "coupons", "reviews", "collections", "messages", "settings"] + }, + { + "root": "pages/auth", + "pages": ["login", "bindPhone"] + }, + { + "root": "pages/pay", + "pages": ["cashier", "result"] + } + ] +} +``` + +--- + +## 十、安全设计 + +| 措施 | 说明 | +|------|------| +| Token 存储 | uni.setStorageSync 加密存储 | +| 接口签名 | 敏感接口请求签名(timestamp + nonce + sign) | +| HTTPS | 所有接口强制 HTTPS | +| 输入校验 | 前端校验 + 后端校验双重保障 | +| 敏感数据 | 手机号脱敏显示(138****8000) | +| 支付安全 | 服务端生成支付参数,前端不接触密钥 | +| 防重复提交 | 支付/下单按钮加 loading + 接口幂等 | diff --git a/docs/07-deployment.md b/docs/07-deployment.md new file mode 100644 index 0000000..c53c750 --- /dev/null +++ b/docs/07-deployment.md @@ -0,0 +1,692 @@ +# 宫中有喜 — 部署方案 + +## 一、部署架构 + +``` +┌─────────────────────────────────────────┐ +│ CDN (可选) │ +│ 静态资源加速 │ +└──────────────────┬──────────────────────┘ + │ +┌──────────────────▼──────────────────────┐ +│ Nginx (反向代理) │ +│ 80/443 → 静态资源 / API / WebSocket │ +└────┬──────────────┬─────────────────────┘ + │ │ + ▼ ▼ +┌─────────┐ ┌──────────────┐ +│ Vue SPA │ │ PHP-FPM │ +│ (静态) │ │ Laravel App │ +└─────────┘ └──────┬───────┘ + │ + ┌───────────┼───────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌────────┐ ┌─────────┐ + │ MySQL 8 │ │ Redis │ │ Storage │ + │ │ │ │ │ (文件) │ + └──────────┘ └────────┘ └─────────┘ +``` + +--- + +## 二、环境要求 + +| 组件 | 最低版本 | 推荐 | +|------|----------|------| +| OS | Ubuntu 20.04 / CentOS 8 | Ubuntu 22.04 | +| PHP | 8.1 | 8.2 | +| MySQL | 8.0 | 8.0 | +| Redis | 6.0 | 7.0 | +| Nginx | 1.18 | 1.24 | +| Node.js | 16 | 18 LTS | +| Composer | 2.x | 2.x | +| Docker | 20.10 | 24.x | +| Docker Compose | 2.x | 2.x | + +### PHP 扩展 + +``` +php-fpm php-mysql php-redis php-mbstring php-xml +php-curl php-gd php-zip php-bcmath php-intl +php-opcache php-fileinfo +``` + +--- + +## 三、Docker Compose 部署 + +### 3.1 docker-compose.yml + +```yaml +version: '3.8' + +services: + # Nginx 反向代理 + nginx: + image: nginx:1.24-alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./docker/nginx/conf.d:/etc/nginx/conf.d + - ./docker/nginx/ssl:/etc/nginx/ssl + - ./frontend/dist:/var/www/frontend + - ./backend/public:/var/www/backend/public + - ./backend/storage/app/public:/var/www/backend/storage/app/public + depends_on: + - php + networks: + - app-network + restart: unless-stopped + + # PHP-FPM + php: + build: + context: ./docker/php + dockerfile: Dockerfile + volumes: + - ./backend:/var/www/backend + depends_on: + - mysql + - redis + networks: + - app-network + restart: unless-stopped + + # Laravel 队列 Worker + queue: + build: + context: ./docker/php + dockerfile: Dockerfile + command: php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 + volumes: + - ./backend:/var/www/backend + depends_on: + - mysql + - redis + networks: + - app-network + restart: unless-stopped + + # Laravel 定时任务 + scheduler: + build: + context: ./docker/php + dockerfile: Dockerfile + command: /bin/sh -c "while true; do php /var/www/backend/artisan schedule:run --verbose --no-interaction >> /dev/null 2>&1; sleep 60; done" + volumes: + - ./backend:/var/www/backend + depends_on: + - mysql + - redis + networks: + - app-network + restart: unless-stopped + + # MySQL + mysql: + image: mysql:8.0 + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_USER: ${DB_USERNAME} + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - mysql-data:/var/lib/mysql + - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf + - ./docker/mysql/init:/docker-entrypoint-initdb.d + networks: + - app-network + restart: unless-stopped + + # Redis + redis: + image: redis:7-alpine + ports: + - "6379:6379" + command: redis-server --requirepass ${REDIS_PASSWORD} + volumes: + - redis-data:/data + networks: + - app-network + restart: unless-stopped + +volumes: + mysql-data: + redis-data: + +networks: + app-network: + driver: bridge +``` + +### 3.2 PHP Dockerfile + +```dockerfile +# docker/php/Dockerfile +FROM php:8.2-fpm-alpine + +# 安装扩展 +RUN apk add --no-cache \ + libpng-dev libjpeg-turbo-dev freetype-dev \ + libzip-dev icu-dev oniguruma-dev \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install \ + pdo_mysql mbstring xml curl gd zip bcmath intl opcache pcntl \ + && pecl install redis \ + && docker-php-ext-enable redis + +# Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# PHP 配置 +COPY php.ini /usr/local/etc/php/conf.d/custom.ini + +WORKDIR /var/www/backend + +# 权限 +RUN chown -R www-data:www-data /var/www/backend + +USER www-data + +EXPOSE 9000 +CMD ["php-fpm"] +``` + +### 3.3 PHP 配置 + +```ini +; docker/php/php.ini +upload_max_filesize = 20M +post_max_size = 25M +memory_limit = 256M +max_execution_time = 60 +date.timezone = Asia/Shanghai + +; OPcache +opcache.enable=1 +opcache.memory_consumption=128 +opcache.interned_strings_buffer=8 +opcache.max_accelerated_files=10000 +opcache.validate_timestamps=0 +opcache.revalidate_freq=0 +``` + +### 3.4 Nginx 配置 + +```nginx +# docker/nginx/conf.d/default.conf + +# 后台管理 SPA +server { + listen 80; + server_name admin.example.com; + + root /var/www/frontend; + index index.html; + + # SPA 路由 + location / { + try_files $uri $uri/ /index.html; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + + # Gzip + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml; + gzip_min_length 1024; +} + +# API 服务 +server { + listen 80; + server_name api.example.com; + + root /var/www/backend/public; + index index.php; + + # Laravel 路由 + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # PHP-FPM + location ~ \.php$ { + fastcgi_pass php:9000; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 60; + } + + # 文件上传大小 + client_max_body_size 25m; + + # 静态资源 + location /storage { + alias /var/www/backend/storage/app/public; + expires 30d; + } + + # 禁止访问隐藏文件 + location ~ /\. { + deny all; + } +} +``` + +### 3.5 MySQL 配置 + +```ini +# docker/mysql/my.cnf +[mysqld] +character-set-server = utf8mb4 +collation-server = utf8mb4_unicode_ci +default-time-zone = '+08:00' + +# InnoDB +innodb_buffer_pool_size = 512M +innodb_log_file_size = 128M +innodb_flush_log_at_trx_commit = 2 + +# 连接 +max_connections = 200 +wait_timeout = 600 + +# 慢查询 +slow_query_log = 1 +slow_query_log_file = /var/log/mysql/slow.log +long_query_time = 2 + +# 二进制日志(用于备份恢复) +log-bin = mysql-bin +binlog_expire_logs_seconds = 604800 +binlog_format = ROW + +[client] +default-character-set = utf8mb4 +``` + +--- + +## 四、环境变量 (.env) + +```bash +# .env (生产环境示例) +APP_NAME="宫中有喜" +APP_ENV=production +APP_KEY=base64:xxxx +APP_DEBUG=false +APP_URL=https://api.example.com + +# 数据库 +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=gongzhongyouxi +DB_USERNAME=appuser +DB_PASSWORD=strong_password_here +DB_ROOT_PASSWORD=root_strong_password + +# Redis +REDIS_HOST=redis +REDIS_PASSWORD=redis_password +REDIS_PORT=6379 + +# 缓存/队列/会话 +CACHE_DRIVER=redis +QUEUE_CONNECTION=redis +SESSION_DRIVER=redis + +# 文件存储 +FILESYSTEM_DISK=local +# 或 OSS: +# FILESYSTEM_DISK=oss +# OSS_ACCESS_KEY_ID=xxx +# OSS_ACCESS_KEY_SECRET=xxx +# OSS_BUCKET=xxx +# OSS_ENDPOINT=xxx + +# 微信小程序 +WECHAT_MINI_APP_ID=wxXXXXXXXXXXX +WECHAT_MINI_SECRET=xxxxxxxxxx + +# 微信支付 +WECHAT_PAY_MCH_ID=xxxxxxxxxx +WECHAT_PAY_KEY=xxxxxxxxxx +WECHAT_PAY_CERT_PATH=/path/to/cert +WECHAT_PAY_KEY_PATH=/path/to/key +WECHAT_PAY_NOTIFY_URL=https://api.example.com/api/v1/client/pay/callback + +# 短信 +SMS_DRIVER=aliyun +SMS_KEY=xxx +SMS_SECRET=xxx +SMS_SIGN=宫中有喜 + +# 日志 +LOG_CHANNEL=daily +LOG_LEVEL=warning +``` + +--- + +## 五、部署流程 + +### 5.1 首次部署 + +```bash +# 1. 克隆代码 +git clone /opt/saas +cd /opt/saas + +# 2. 配置环境变量 +cp .env.example .env +vim .env # 填入生产配置 + +# 3. 构建前端 +cd frontend +npm install +npm run build +cd .. + +# 4. 启动容器 +docker compose up -d + +# 5. 初始化 Laravel +docker compose exec php composer install --no-dev --optimize-autoloader +docker compose exec php php artisan key:generate +docker compose exec php php artisan migrate --force +docker compose exec php php artisan db:seed --class=InitSeeder +docker compose exec php php artisan storage:link +docker compose exec php php artisan config:cache +docker compose exec php php artisan route:cache +docker compose exec php php artisan view:cache + +# 6. 验证 +curl https://api.example.com/api/v1/health +``` + +### 5.2 更新部署 + +```bash +# 1. 拉取代码 +cd /opt/saas +git pull origin main + +# 2. 更新后端依赖 +docker compose exec php composer install --no-dev --optimize-autoloader + +# 3. 数据库迁移 +docker compose exec php php artisan migrate --force + +# 4. 清除缓存 +docker compose exec php php artisan config:cache +docker compose exec php php artisan route:cache +docker compose exec php php artisan view:cache +docker compose exec php php artisan queue:restart + +# 5. 更新前端(如有变更) +cd frontend +npm install +npm run build +cd .. + +# 6. 重载 Nginx +docker compose exec nginx nginx -s reload +``` + +### 5.3 零停机部署(可选) + +```bash +# 使用 Laravel 维护模式 +docker compose exec php php artisan down --retry=60 + +# 执行部署步骤... + +docker compose exec php php artisan up +``` + +--- + +## 六、备份策略 + +### 6.1 数据库备份 + +```bash +#!/bin/bash +# scripts/backup-db.sh +BACKUP_DIR="/opt/backups/mysql" +DATE=$(date +%Y%m%d_%H%M) +KEEP_DAYS=30 + +mkdir -p $BACKUP_DIR + +# 备份 +docker compose exec -T mysql mysqldump \ + -u root -p"${DB_ROOT_PASSWORD}" \ + --single-transaction \ + --routines --triggers \ + ${DB_DATABASE} | gzip > "${BACKUP_DIR}/db_${DATE}.sql.gz" + +# 清理旧备份 +find $BACKUP_DIR -name "*.sql.gz" -mtime +$KEEP_DAYS -delete + +echo "[$(date)] Backup completed: db_${DATE}.sql.gz" +``` + +### 6.2 文件备份 + +```bash +#!/bin/bash +# scripts/backup-files.sh +BACKUP_DIR="/opt/backups/files" +DATE=$(date +%Y%m%d) + +mkdir -p $BACKUP_DIR + +# 备份上传文件 +tar -czf "${BACKUP_DIR}/storage_${DATE}.tar.gz" \ + -C /opt/saas/backend storage/app/public + +# 清理 30 天前 +find $BACKUP_DIR -name "*.tar.gz" -mtime +30 -delete +``` + +### 6.3 定时任务 + +```bash +# crontab -e +# 每天凌晨 3 点备份数据库 +0 3 * * * /opt/saas/scripts/backup-db.sh >> /var/log/backup.log 2>&1 +# 每周日凌晨 4 点备份文件 +0 4 * * 0 /opt/saas/scripts/backup-files.sh >> /var/log/backup.log 2>&1 +``` + +--- + +## 七、监控与日志 + +### 7.1 健康检查 + +```php +// routes/api.php +Route::get('/health', function () { + try { + DB::connection()->getPdo(); + Redis::ping(); + return response()->json([ + 'status' => 'healthy', + 'timestamp' => now()->toISOString(), + 'services' => [ + 'database' => 'ok', + 'redis' => 'ok', + 'storage' => is_writable(storage_path()) ? 'ok' : 'error', + ] + ]); + } catch (\Exception $e) { + return response()->json(['status' => 'unhealthy', 'error' => $e->getMessage()], 500); + } +}); +``` + +### 7.2 日志管理 + +```php +// config/logging.php — 生产配置 +'channels' => [ + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => 'warning', + 'days' => 14, + ], + 'business' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/business.log'), + 'level' => 'info', + 'days' => 30, + ], +], +``` + +### 7.3 监控检查清单 + +| 监控项 | 方法 | 告警条件 | +|--------|------|----------| +| 服务存活 | /health 接口 | 连续 3 次失败 | +| CPU 使用率 | 系统监控 | > 80% 持续 5 分钟 | +| 内存使用率 | 系统监控 | > 85% | +| 磁盘使用率 | 系统监控 | > 80% | +| MySQL 连接数 | SHOW STATUS | > 150 | +| MySQL 慢查询 | 慢查询日志 | > 10 条/小时 | +| Redis 内存 | INFO memory | > 80% maxmemory | +| 队列积压 | queue:monitor | > 100 条待处理 | +| Laravel 日志 | 日志文件 | ERROR 级别 | +| 响应时间 | Nginx access_log | P95 > 2s | +| SSL 证书 | 定期检查 | 到期前 30 天 | + +--- + +## 八、安全加固 + +### 8.1 服务器 + +```bash +# 防火墙 — 仅开放必要端口 +ufw allow 22/tcp # SSH +ufw allow 80/tcp # HTTP +ufw allow 443/tcp # HTTPS +ufw enable + +# SSH 加固 +# /etc/ssh/sshd_config +PermitRootLogin no +PasswordAuthentication no +PubkeyAuthentication yes +``` + +### 8.2 Docker 安全 + +- 不使用 root 运行容器(PHP Dockerfile 已配置 www-data) +- MySQL/Redis 端口不暴露到公网(仅限 Docker 内部网络) +- 定期更新基础镜像 +- 使用 `.dockerignore` 排除敏感文件 + +### 8.3 应用安全 + +| 措施 | 配置 | +|------|------| +| HTTPS 强制 | Nginx redirect 80→443 | +| CORS | 限定域名白名单 | +| CSRF | Sanctum SPA 模式自动处理 | +| Rate Limiting | Laravel throttle 中间件 | +| SQL 注入 | Eloquent 参数绑定 | +| XSS | HTMLPurifier + CSP Header | +| 文件上传 | 白名单 + 大小限制 + 病毒扫描 | +| 敏感数据 | .env 不入 Git,密钥加密存储 | + +### 8.4 SSL 证书 + +```bash +# Let's Encrypt 免费证书 +apt install certbot python3-certbot-nginx +certbot --nginx -d admin.example.com -d api.example.com + +# 自动续期 +certbot renew --dry-run +# crontab +0 0 1 */2 * certbot renew --quiet +``` + +--- + +## 九、性能优化 + +### 9.1 Laravel 优化 + +```bash +# 配置/路由/视图缓存 +php artisan config:cache +php artisan route:cache +php artisan view:cache + +# Composer 自动加载优化 +composer install --optimize-autoloader --no-dev +``` + +### 9.2 MySQL 优化 + +- 索引覆盖常用查询(见 02-database-design.md 索引策略) +- 开启查询缓存(Redis 层面) +- 大表分页使用 cursor 分页 +- 定期 ANALYZE TABLE + +### 9.3 Redis 缓存策略 + +| 缓存键 | TTL | 场景 | +|--------|-----|------| +| user:{id} | 30min | 用户信息 | +| menu:{role_id} | 60min | 角色菜单 | +| room:board:{store_id}:{date} | 5min | 房态看板 | +| config:{key} | 60min | 系统配置 | +| dict:{type} | 24h | 数据字典 | + +### 9.4 前端优化 + +- Vite 构建:代码分割 + Tree Shaking +- 路由懒加载:按模块动态 import +- 图片压缩:WebP 格式 + CDN +- Gzip:Nginx 开启 gzip 压缩 + +--- + +## 十、目录结构(Docker 相关) + +``` +saas/ +├── docker/ +│ ├── nginx/ +│ │ ├── conf.d/ +│ │ │ └── default.conf +│ │ └── ssl/ # SSL 证书 +│ ├── php/ +│ │ ├── Dockerfile +│ │ └── php.ini +│ └── mysql/ +│ ├── my.cnf +│ └── init/ # 初始化 SQL +├── scripts/ +│ ├── backup-db.sh +│ ├── backup-files.sh +│ └── deploy.sh +├── docker-compose.yml +├── .env +└── .env.example +``` diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..1511959 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,5 @@ +# Vue 3 + Vite + +This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..8ec8f05 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2141 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.13.6", + "element-plus": "^2.13.5", + "nprogress": "^0.2.0", + "pinia": "^3.0.4", + "vue": "^3.5.30", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.5", + "sass": "^1.98.0", + "vite": "^8.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", + "integrity": "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.5.tgz", + "integrity": "sha512-bL3AxKuQySfk1iGcBsQnoRVexTPJq0Z/ixFVM8OhVJAP6ZXXXLtM7NFKWhLl30Kg7uTBqIaPXbh+nuQCuBDedg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz", + "integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.30", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz", + "integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz", + "integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.30", + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz", + "integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz", + "integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz", + "integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/shared": "3.5.30" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz", + "integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.30", + "@vue/runtime-core": "3.5.30", + "@vue/shared": "3.5.30", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz", + "integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "vue": "3.5.30" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz", + "integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.13.5", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.5.tgz", + "integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "12.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.30", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz", + "integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.30", + "@vue/compiler-sfc": "3.5.30", + "@vue/runtime-dom": "3.5.30", + "@vue/server-renderer": "3.5.30", + "@vue/shared": "3.5.30" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..afb8357 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.13.6", + "element-plus": "^2.13.5", + "nprogress": "^0.2.0", + "pinia": "^3.0.4", + "vue": "^3.5.30", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.5", + "sass": "^1.98.0", + "vite": "^8.0.0" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..98240ae --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js new file mode 100644 index 0000000..0d022f7 --- /dev/null +++ b/frontend/src/api/auth.js @@ -0,0 +1,23 @@ +import request from '@/utils/request.js' + +/** + * Login + * @param {{ username: string, password: string }} data + */ +export function login(data) { + return request.post('/auth/login', data) +} + +/** + * Logout + */ +export function logout() { + return request.post('/auth/logout') +} + +/** + * Get current authenticated user info + */ +export function getMe() { + return request.get('/auth/me') +} diff --git a/frontend/src/api/system.js b/frontend/src/api/system.js new file mode 100644 index 0000000..6b78b80 --- /dev/null +++ b/frontend/src/api/system.js @@ -0,0 +1,109 @@ +import request from '@/utils/request.js' + +// ─── Stores ──────────────────────────────────────────────────────────────── + +export const storeApi = { + getList: (params) => request.get('/system/stores', { params }), + getDetail: (id) => request.get(`/system/stores/${id}`), + create: (data) => request.post('/system/stores', data), + update: (id, data) => request.put(`/system/stores/${id}`, data), + delete: (id) => request.delete(`/system/stores/${id}`) +} + +// ─── Departments ──────────────────────────────────────────────────────────── + +export const departmentApi = { + getList: (params) => request.get('/system/departments', { params }), + getDetail: (id) => request.get(`/system/departments/${id}`), + create: (data) => request.post('/system/departments', data), + update: (id, data) => request.put(`/system/departments/${id}`, data), + delete: (id) => request.delete(`/system/departments/${id}`), + getTree: () => request.get('/system/departments/tree') +} + +// ─── Positions ────────────────────────────────────────────────────────────── + +export const positionApi = { + getList: (params) => request.get('/system/positions', { params }), + getDetail: (id) => request.get(`/system/positions/${id}`), + create: (data) => request.post('/system/positions', data), + update: (id, data) => request.put(`/system/positions/${id}`, data), + delete: (id) => request.delete(`/system/positions/${id}`) +} + +// ─── Users ────────────────────────────────────────────────────────────────── + +export const userApi = { + getList: (params) => request.get('/system/users', { params }), + getDetail: (id) => request.get(`/system/users/${id}`), + create: (data) => request.post('/system/users', data), + update: (id, data) => request.put(`/system/users/${id}`, data), + delete: (id) => request.delete(`/system/users/${id}`), + resetPassword: (id, data) => request.post(`/system/users/${id}/reset-password`, data), + updateStatus: (id, status) => request.patch(`/system/users/${id}/status`, { status }) +} + +// ─── Roles ────────────────────────────────────────────────────────────────── + +export const roleApi = { + getList: (params) => request.get('/system/roles', { params }), + getDetail: (id) => request.get(`/system/roles/${id}`), + create: (data) => request.post('/system/roles', data), + update: (id, data) => request.put(`/system/roles/${id}`, data), + delete: (id) => request.delete(`/system/roles/${id}`), + assignPermissions: (id, permissionIds) => + request.post(`/system/roles/${id}/permissions`, { permission_ids: permissionIds }), + getPermissions: (id) => request.get(`/system/roles/${id}/permissions`) +} + +// ─── Menus ────────────────────────────────────────────────────────────────── + +export const menuApi = { + getList: (params) => request.get('/system/menus', { params }), + getTree: () => request.get('/system/menus/tree'), + getDetail: (id) => request.get(`/system/menus/${id}`), + create: (data) => request.post('/system/menus', data), + update: (id, data) => request.put(`/system/menus/${id}`, data), + delete: (id) => request.delete(`/system/menus/${id}`) +} + +// ─── Permissions ──────────────────────────────────────────────────────────── + +export const permissionApi = { + getList: (params) => request.get('/system/permissions', { params }), + getTree: () => request.get('/system/permissions/tree'), + getDetail: (id) => request.get(`/system/permissions/${id}`), + create: (data) => request.post('/system/permissions', data), + update: (id, data) => request.put(`/system/permissions/${id}`, data), + delete: (id) => request.delete(`/system/permissions/${id}`) +} + +// ─── Dictionaries ──────────────────────────────────────────────────────────── + +export const dictApi = { + // Dictionary types + getTypeList: (params) => request.get('/system/dict-types', { params }), + getTypeDetail: (id) => request.get(`/system/dict-types/${id}`), + createType: (data) => request.post('/system/dict-types', data), + updateType: (id, data) => request.put(`/system/dict-types/${id}`, data), + deleteType: (id) => request.delete(`/system/dict-types/${id}`), + + // Dictionary items + getItemList: (typeId, params) => + request.get(`/system/dict-types/${typeId}/items`, { params }), + getItemDetail: (typeId, itemId) => + request.get(`/system/dict-types/${typeId}/items/${itemId}`), + createItem: (typeId, data) => + request.post(`/system/dict-types/${typeId}/items`, data), + updateItem: (typeId, itemId, data) => + request.put(`/system/dict-types/${typeId}/items/${itemId}`, data), + deleteItem: (typeId, itemId) => + request.delete(`/system/dict-types/${typeId}/items/${itemId}`) +} + +// ─── Operation Logs ───────────────────────────────────────────────────────── + +export const logApi = { + getList: (params) => request.get('/system/operation-logs', { params }), + getDetail: (id) => request.get(`/system/operation-logs/${id}`) +} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/assets/vue.svg b/frontend/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..3a7b808 --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,93 @@ + + + diff --git a/frontend/src/layout/components/Header.vue b/frontend/src/layout/components/Header.vue new file mode 100644 index 0000000..5abdd44 --- /dev/null +++ b/frontend/src/layout/components/Header.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/frontend/src/layout/components/Sidebar.vue b/frontend/src/layout/components/Sidebar.vue new file mode 100644 index 0000000..9cd7485 --- /dev/null +++ b/frontend/src/layout/components/Sidebar.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/frontend/src/layout/index.vue b/frontend/src/layout/index.vue new file mode 100644 index 0000000..95ac759 --- /dev/null +++ b/frontend/src/layout/index.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..2967cdb --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,21 @@ +import { createApp } from 'vue' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import { createPinia } from 'pinia' +import router from './router/index.js' +import App from './App.vue' +import '@/styles/index.scss' + +const app = createApp(App) + +// Register all Element Plus icons globally +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus) + +app.mount('#app') diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..c0e530e --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,69 @@ +import { createRouter, createWebHistory } from 'vue-router' +import NProgress from 'nprogress' +import 'nprogress/nprogress.css' +import { constantRoutes } from '@/router/routes.js' + +NProgress.configure({ showSpinner: false }) + +const router = createRouter({ + history: createWebHistory(), + routes: constantRoutes, + scrollBehavior(to, from, savedPosition) { + if (savedPosition) { + return savedPosition + } + return { top: 0 } + } +}) + +// Whitelist paths that don't require authentication +const WHITE_LIST = ['/login', '/404'] + +router.beforeEach(async (to, from, next) => { + NProgress.start() + + // Lazily import stores to avoid circular dependency at module init time + const { useUserStore } = await import('@/stores/user.js') + const { usePermissionStore } = await import('@/stores/permission.js') + const userStore = useUserStore() + const permissionStore = usePermissionStore() + + const token = userStore.token + + if (token) { + if (to.path === '/login') { + // Already logged in — redirect to home + next({ path: '/' }) + } else { + // Check if user info has been fetched + if (!userStore.userInfo) { + try { + await userStore.getInfo() + const accessRoutes = permissionStore.generateRoutes(userStore.menus) + accessRoutes.forEach((route) => router.addRoute(route)) + // Replace so the new routes are active + next({ ...to, replace: true }) + } catch (err) { + // Token invalid — clear and redirect + await userStore.logout() + next(`/login?redirect=${to.path}`) + } + } else { + next() + } + } + } else { + // No token + if (WHITE_LIST.includes(to.path)) { + next() + } else { + next(`/login?redirect=${to.path}`) + } + } +}) + +router.afterEach(() => { + NProgress.done() +}) + +export default router diff --git a/frontend/src/router/routes.js b/frontend/src/router/routes.js new file mode 100644 index 0000000..61283dc --- /dev/null +++ b/frontend/src/router/routes.js @@ -0,0 +1,17 @@ +/** + * Static routes — always available regardless of authentication or permissions. + */ +export const constantRoutes = [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/login/index.vue'), + meta: { title: '登录', hidden: true } + }, + { + path: '/404', + name: 'NotFound', + component: () => import('@/views/error/404.vue'), + meta: { title: '页面不存在', hidden: true } + } +] diff --git a/frontend/src/stores/permission.js b/frontend/src/stores/permission.js new file mode 100644 index 0000000..ef1f1a0 --- /dev/null +++ b/frontend/src/stores/permission.js @@ -0,0 +1,114 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { constantRoutes } from '@/router/routes.js' + +// Layout wrapper for authenticated pages +const Layout = () => import('@/layout/index.vue') + +/** + * Convert a single server menu node to a vue-router route object. + * Recursive — handles children at any depth. + */ +function menuToRoute(menu) { + const route = { + path: menu.path, + name: menu.name || menu.path, + meta: { + title: menu.title, + icon: menu.icon || '', + id: menu.id, + hidden: menu.visible === 0 + } + } + + if (menu.component === 'Layout') { + route.component = Layout + } else if (menu.component) { + // Dynamic import from views directory + const comp = menu.component + route.component = () => import(`@/views/${comp}.vue`) + } + + if (menu.children && menu.children.length > 0) { + route.children = menu.children.map(menuToRoute) + // If this is a top-level layout node with children, add redirect + if (!route.redirect && route.children.length > 0) { + route.redirect = route.children[0].path + } + } + + return route +} + +export const usePermissionStore = defineStore('permission', () => { + const routes = ref([]) + const dynamicRoutes = ref([]) + + /** + * Generate vue-router routes from the server menu tree. + * Returns the array of dynamic routes to be added via router.addRoute(). + */ + function generateRoutes(menus) { + const generated = [] + + // Wrap all menu roots under the Layout component + const layoutRoute = { + path: '/', + name: 'Root', + component: Layout, + redirect: '/dashboard', + children: [] + } + + // Flatten top-level menu items into Layout's children + for (const menu of menus) { + if (menu.component === 'Layout' && menu.children) { + // This menu node IS the layout wrapper — add its children directly + for (const child of menu.children) { + layoutRoute.children.push(menuToRoute(child)) + } + } else { + layoutRoute.children.push(menuToRoute(menu)) + } + } + + // Always ensure dashboard is accessible + const hasDashboard = layoutRoute.children.some( + (r) => r.path === '/dashboard' || r.path === 'dashboard' + ) + if (!hasDashboard) { + layoutRoute.children.unshift({ + path: 'dashboard', + name: 'Dashboard', + component: () => import('@/views/dashboard/index.vue'), + meta: { title: '工作台', icon: 'Odometer' } + }) + } + + generated.push(layoutRoute) + + // Catch-all 404 — must be last + generated.push({ + path: '/:pathMatch(.*)*', + redirect: '/404', + meta: { hidden: true } + }) + + dynamicRoutes.value = generated + routes.value = [...constantRoutes, ...generated] + + return generated + } + + function resetRoutes() { + routes.value = [] + dynamicRoutes.value = [] + } + + return { + routes, + dynamicRoutes, + generateRoutes, + resetRoutes + } +}) diff --git a/frontend/src/stores/user.js b/frontend/src/stores/user.js new file mode 100644 index 0000000..ed380fb --- /dev/null +++ b/frontend/src/stores/user.js @@ -0,0 +1,66 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { login as loginApi, logout as logoutApi, getMe } from '@/api/auth.js' + +export const useUserStore = defineStore('user', () => { + const token = ref(localStorage.getItem('token') || '') + const userInfo = ref(null) + const permissions = ref([]) + const menus = ref([]) + + // Getters + const isLoggedIn = computed(() => !!token.value) + const isSuper = computed(() => userInfo.value?.is_super === true || userInfo.value?.is_super === 1) + + // Actions + async function login(credentials) { + const res = await loginApi(credentials) + const { token: newToken, user } = res.data + token.value = newToken + userInfo.value = user + localStorage.setItem('token', newToken) + return res + } + + async function getInfo() { + const res = await getMe() + const { user, permissions: perms, menus: menuList } = res.data + userInfo.value = user + permissions.value = perms || [] + menus.value = menuList || [] + return res + } + + async function logout() { + try { + await logoutApi() + } catch { + // Ignore logout API errors — clear local state regardless + } finally { + token.value = '' + userInfo.value = null + permissions.value = [] + menus.value = [] + localStorage.removeItem('token') + localStorage.removeItem('userInfo') + } + } + + function hasPermission(permCode) { + if (isSuper.value) return true + return permissions.value.includes(permCode) + } + + return { + token, + userInfo, + permissions, + menus, + isLoggedIn, + isSuper, + login, + getInfo, + logout, + hasPermission + } +}) diff --git a/frontend/src/style.css b/frontend/src/style.css new file mode 100644 index 0000000..527d4fb --- /dev/null +++ b/frontend/src/style.css @@ -0,0 +1,296 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +body { + margin: 0; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} + +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#app { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss new file mode 100644 index 0000000..fecda31 --- /dev/null +++ b/frontend/src/styles/index.scss @@ -0,0 +1,239 @@ +// Brand color variables +:root { + --color-primary: #E8A87C; + --color-primary-light: #F5C9A0; + --color-primary-lighter: #FBE8D5; + --color-primary-dark: #D4875A; + --color-primary-darker: #B86B3A; + --color-secondary: #C97B5E; + --color-bg: #F7F3EF; + --color-bg-sidebar: #2D1B0E; + --color-bg-sidebar-light: #4A2E1A; + --color-text-primary: #2C2C2C; + --color-text-secondary: #666666; + --color-text-placeholder: #AAAAAA; + --color-border: #EAEAEA; + --color-success: #67C23A; + --color-warning: #E6A23C; + --color-danger: #F56C6C; + --color-info: #909399; + --sidebar-width: 220px; + --sidebar-collapsed-width: 64px; + --header-height: 56px; +} + +// Element Plus primary color override +:root { + --el-color-primary: #E8A87C; + --el-color-primary-light-3: #F0BB97; + --el-color-primary-light-5: #F5C9A0; + --el-color-primary-light-7: #F8D8BB; + --el-color-primary-light-8: #FBEACE; + --el-color-primary-light-9: #FDF4EC; + --el-color-primary-dark-2: #D4875A; +} + +// Global reset +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', + 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 14px; + color: var(--color-text-primary); + background-color: var(--color-bg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#app { + height: 100%; +} + +a { + color: var(--color-primary); + text-decoration: none; + &:hover { + color: var(--color-primary-dark); + } +} + +// Custom scrollbar +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: #cccccc; + border-radius: 3px; + &:hover { + background: #aaaaaa; + } +} + +// Utility classes +.flex { + display: flex; +} + +.flex-1 { + flex: 1; +} + +.flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +.flex-between { + display: flex; + align-items: center; + justify-content: space-between; +} + +.flex-col { + display: flex; + flex-direction: column; +} + +.w-full { + width: 100%; +} + +.h-full { + height: 100%; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-primary { + color: var(--color-primary); +} + +.text-danger { + color: var(--color-danger); +} + +.text-success { + color: var(--color-success); +} + +.text-secondary { + color: var(--color-text-secondary); +} + +.mt-4 { margin-top: 4px; } +.mt-8 { margin-top: 8px; } +.mt-12 { margin-top: 12px; } +.mt-16 { margin-top: 16px; } +.mt-24 { margin-top: 24px; } +.mb-4 { margin-bottom: 4px; } +.mb-8 { margin-bottom: 8px; } +.mb-12 { margin-bottom: 12px; } +.mb-16 { margin-bottom: 16px; } +.mb-24 { margin-bottom: 24px; } +.ml-8 { margin-left: 8px; } +.mr-8 { margin-right: 8px; } + +.page-container { + padding: 16px; + height: 100%; + overflow-y: auto; +} + +.card { + background: #fff; + border-radius: 8px; + padding: 20px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); +} + +.search-bar { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 16px; + padding: 16px 20px; + background: #fff; + border-radius: 8px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); +} + +.table-container { + background: #fff; + border-radius: 8px; + padding: 16px 20px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); +} + +.table-actions { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +// NProgress bar color +#nprogress .bar { + background: var(--color-primary) !important; +} + +#nprogress .peg { + box-shadow: 0 0 10px var(--color-primary), 0 0 5px var(--color-primary) !important; +} + +// Element Plus overrides +.el-button--primary { + background-color: var(--color-primary); + border-color: var(--color-primary); + + &:hover, + &:focus { + background-color: var(--color-primary-light); + border-color: var(--color-primary-light); + } + + &:active { + background-color: var(--color-primary-dark); + border-color: var(--color-primary-dark); + } +} + +.el-table { + .el-table__header th { + background-color: #FAFAFA; + color: var(--color-text-secondary); + font-weight: 600; + } +} + +.el-pagination { + margin-top: 16px; + justify-content: flex-end; +} + +.el-tag { + border-radius: 4px; +} diff --git a/frontend/src/utils/request.js b/frontend/src/utils/request.js new file mode 100644 index 0000000..772b4d6 --- /dev/null +++ b/frontend/src/utils/request.js @@ -0,0 +1,83 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' + +const request = axios.create({ + baseURL: '/api/v1', + timeout: 15000, + headers: { + 'Content-Type': 'application/json' + } +}) + +// Request interceptor — inject Authorization token +request.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token') + if (token) { + config.headers['Authorization'] = `Bearer ${token}` + } + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// Response interceptor — handle business codes and auth errors +request.interceptors.response.use( + (response) => { + const res = response.data + + // Success: code === 0 + if (res.code === 0) { + return res + } + + // Auth error: 40100–40199 → clear token and redirect to login + if (res.code >= 40100 && res.code <= 40199) { + localStorage.removeItem('token') + localStorage.removeItem('userInfo') + ElMessage.error('登录已过期,请重新登录') + // Avoid circular import by using window.location + window.location.href = '/login' + return Promise.reject(new Error(res.message || '未授权')) + } + + // Other business error + ElMessage.error(res.message || '请求失败') + return Promise.reject(new Error(res.message || '请求失败')) + }, + (error) => { + // Network / HTTP errors + if (error.response) { + const status = error.response.status + if (status === 401) { + localStorage.removeItem('token') + localStorage.removeItem('userInfo') + ElMessage.error('登录已过期,请重新登录') + window.location.href = '/login' + return Promise.reject(error) + } + if (status === 403) { + ElMessage.error('无权限访问') + return Promise.reject(error) + } + if (status === 404) { + ElMessage.error('请求的资源不存在') + return Promise.reject(error) + } + if (status >= 500) { + ElMessage.error('服务器内部错误,请稍后重试') + return Promise.reject(error) + } + } + if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) { + ElMessage.error('请求超时,请检查网络连接') + return Promise.reject(error) + } + ElMessage.error(error.message || '网络错误') + return Promise.reject(error) + } +) + +export default request diff --git a/frontend/src/views/dashboard/index.vue b/frontend/src/views/dashboard/index.vue new file mode 100644 index 0000000..e991338 --- /dev/null +++ b/frontend/src/views/dashboard/index.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/frontend/src/views/error/404.vue b/frontend/src/views/error/404.vue new file mode 100644 index 0000000..a2fff75 --- /dev/null +++ b/frontend/src/views/error/404.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/frontend/src/views/login/index.vue b/frontend/src/views/login/index.vue new file mode 100644 index 0000000..0ce4a16 --- /dev/null +++ b/frontend/src/views/login/index.vue @@ -0,0 +1,220 @@ + + + + + diff --git a/frontend/src/views/system/departments/index.vue b/frontend/src/views/system/departments/index.vue new file mode 100644 index 0000000..5b20799 --- /dev/null +++ b/frontend/src/views/system/departments/index.vue @@ -0,0 +1,280 @@ + + + diff --git a/frontend/src/views/system/dictionaries/index.vue b/frontend/src/views/system/dictionaries/index.vue new file mode 100644 index 0000000..c931e64 --- /dev/null +++ b/frontend/src/views/system/dictionaries/index.vue @@ -0,0 +1,561 @@ + + + + + diff --git a/frontend/src/views/system/menus/index.vue b/frontend/src/views/system/menus/index.vue new file mode 100644 index 0000000..c5374d9 --- /dev/null +++ b/frontend/src/views/system/menus/index.vue @@ -0,0 +1,323 @@ + + + diff --git a/frontend/src/views/system/roles/index.vue b/frontend/src/views/system/roles/index.vue new file mode 100644 index 0000000..01a2ff4 --- /dev/null +++ b/frontend/src/views/system/roles/index.vue @@ -0,0 +1,327 @@ + + + + + diff --git a/frontend/src/views/system/stores/index.vue b/frontend/src/views/system/stores/index.vue new file mode 100644 index 0000000..e3335fa --- /dev/null +++ b/frontend/src/views/system/stores/index.vue @@ -0,0 +1,303 @@ + + + diff --git a/frontend/src/views/system/users/index.vue b/frontend/src/views/system/users/index.vue new file mode 100644 index 0000000..88a61f2 --- /dev/null +++ b/frontend/src/views/system/users/index.vue @@ -0,0 +1,486 @@ + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..d41a255 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { fileURLToPath, URL } from 'node:url' + +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + secure: false + } + } + } +})