first commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
# XinAdmin
|
||||
|
||||
XinAdmin is a full-stack development framework: PHP8.2 + Laravel12 + React19 + TypeScript + Ant Design6 + Zustand + Tailwind CSS4. Licensed under MIT, free for commercial use without authorization.
|
||||
|
||||
## Built-in Features
|
||||
|
||||
- Dashboard: Echarts-based dashboards with demo pages
|
||||
- Administrators: Backend user management with groups, permissions, and settings
|
||||
- Role & Department Management: Role-based menu permission control, enterprise org structure
|
||||
- System Settings: Visual form-based server variable configuration
|
||||
- File Management: Backend file manager with folders, multi-select, grouping
|
||||
- Dictionary Management: Maintenance of frequently used static data
|
||||
- Mail & Storage Configuration: Visual config and testing for Laravel mail/filesystem
|
||||
- AI Configuration: Visual config and testing for Laravel AI SDK
|
||||
- Frontend Members: Permission, grouping, lists, balance records
|
||||
|
||||
# AnnoRoute
|
||||
|
||||
AnnoRoute is a PHP 8 Attribute-based route registration module. Routes are declared via controller annotations — no manual route files needed. Sanctum auth and permission verification are auto-integrated.
|
||||
|
||||
## Usage
|
||||
|
||||
- `#[GetRoute]` / `#[PostRoute]` / `#[PutRoute]` / `#[DeleteRoute]` on methods declare HTTP routes
|
||||
- The `authorize` parameter controls access: `'query'` becomes ability `prefix.query`, `false` makes a route public
|
||||
- `#[RequestAttribute]` params: `routePrefix`, `abilitiesPrefix`, `middleware` (string|array), `authGuard` (?string)
|
||||
- Method attributes share: `route` (path appended to prefix), `authorize` (string|bool), `middleware`, `where` (regex array)
|
||||
- Final route = `routePrefix + route`; final ability = `abilitiesPrefix + '.' + authorize`
|
||||
- When `authorize` is not falsy, `auth:sanctum` + `authGuard` + `abilities:` middleware are auto-assembled
|
||||
- Extend `Modules\Common\Http\Controllers\BaseController` for `success()` / `error()` response helpers
|
||||
|
||||
# Frontend
|
||||
|
||||
Source lives in `web/`. Bundled with Vite, outputting to `public/`. Package manager: pnpm.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `web/api/` | Typed Axios wrappers per backend module |
|
||||
| `web/components/` | Reusable UI: `AuthButton`, `DictTag`, `IconFont`, `XinForm`, `XinTable` |
|
||||
| `web/domain/` | TypeScript interfaces for API models |
|
||||
| `web/hooks/` | `useAuth`, `useLanguage`, `useMobile`, `useRequest` |
|
||||
| `web/layout/` | Layout engine (4 modes), menu, header, breadcrumbs, theme |
|
||||
| `web/locales/` | i18n (i18next), zh_CN + en_US |
|
||||
| `web/pages/` | Auto-discovered page components (file-system routing) |
|
||||
| `web/router/` | React Router v7 `createBrowserRouter` |
|
||||
| `web/stores/` | Zustand: `global` (app/theme), `user` (auth/perms), `dict` (cache) |
|
||||
| `web/utils/` | Axios instance with dedup, auth headers, error handling |
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Path alias `@/` → `web/`
|
||||
- Zustand stores: `State` + `Actions` → `persist` + `devtools` → localStorage
|
||||
- Access stores via selector: `useXxxStore(state => state.field)`
|
||||
- All user-facing text via `useTranslation()` (react-i18next)
|
||||
- Locale files mirror page paths: `pages/system/user.tsx` → `locales/zh_CN/system/user.ts`, key prefix `system.user`
|
||||
- All keys dot-separated, double quotes, 2-space indent. See `@skill:xinadmin-development` for details
|
||||
- Permission checks: `<AuthButton auth="system.user.create">` or `useAuth().auth('permission')`
|
||||
- HTTP client auto-attaches `Authorization: Bearer`, `User-Language`, handles 401 auto-logout
|
||||
- Pages in `web/pages/` are auto-routed; `index.tsx` maps to parent directory; root `/` → `/dashboard/analysis`
|
||||
- Pages outside layout: add to `excludePaths` array in router config
|
||||
|
||||
# Antd
|
||||
|
||||
Ant Design 6 is the UI component library. Components are imported from `antd` and themed via `<ConfigProvider>` with tokens from `web/layout/theme.ts`.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Use `antd` MCP tools (`antd_info`, `antd_doc`, `antd_demo`) to verify component APIs before writing code
|
||||
- Never use deprecated props or components — check with `antd_changelog` when upgrading or referencing older examples
|
||||
- Theme tokens flow: `web/layout/theme.ts` → `<ConfigProvider theme={...}>` → Ant Design components
|
||||
- Common components: `Table`, `Form`, `Modal`, `Drawer`, `Button`, `Input`, `Select`, `DatePicker`, `Switch`, `Tag`, `Card`, `App` ...
|
||||
|
||||
# Layout
|
||||
|
||||
Wraps authenticated pages. Supports 4 modes set via global store: `side` (default), `top`, `mix`, `columns`.
|
||||
|
||||
Menus are fetched from `/system/menu` and stored in `LayoutContext` (React Context). Menu type: `'menu'` (folder), `'route'` (page), `'rule'` (perm-only). Server filters by user role — no client-side filtering needed. Labels support i18n via `node.local`.
|
||||
|
||||
Theme tokens (20+ properties) are managed in `web/layout/theme.ts`, applied via Ant Design `<ConfigProvider>`, persisted to localStorage under `global-storage`.
|
||||
|
||||
# XinForm And XinTable
|
||||
|
||||
Two declarative JSON-driven CRUD components. Define columns once with metadata — the same definition drives table display, search form, and create/edit forms.
|
||||
|
||||
## XinForm
|
||||
|
||||
Use for settings/config pages or standalone forms. Supports 3 layout modes: `'Form'` (inline), `'ModalForm'`, `'DrawerForm'`.
|
||||
|
||||
```tsx
|
||||
<XinForm
|
||||
columns={[
|
||||
{ dataIndex: 'username', title: 'Username', valueType: 'text', rules: [{ required: true }] },
|
||||
{ dataIndex: 'role_id', title: 'Role', valueType: 'select', fieldProps: { options: roleOptions } },
|
||||
]}
|
||||
layoutType="ModalForm"
|
||||
grid
|
||||
trigger={<Button type="primary">New</Button>}
|
||||
modalProps={{ title: 'Create User', width: 600 }}
|
||||
onFinish={async (values) => { await save(values); return true; }}
|
||||
/>
|
||||
```
|
||||
|
||||
Key `FormColumn` fields: `dataIndex` (supports nested paths `['a','b']`), `valueType` (26 types: `text`, `password`, `select`, `date`, `switch`, etc.), `fieldProps`, `fieldRender` (custom render), `dependency` (field linkage: `{ dependencies, visible?, disabled?, fieldProps? }`), `hideIn*` visibility flags.
|
||||
|
||||
`formRef` exposes `open()`, `close()`, `isOpen()`, `setLoading()` plus all Ant Design `FormInstance` methods.
|
||||
|
||||
## XinTable
|
||||
|
||||
Use for standard CRUD pages (list + create + update + delete). Auto-handles API calls, permissions, search, and toolbar.
|
||||
|
||||
```tsx
|
||||
<XinTable<ISysUser>
|
||||
api="/system/user"
|
||||
columns={[
|
||||
{ title: 'ID', dataIndex: 'id', hideInForm: true, width: 80 },
|
||||
{ title: 'Username', dataIndex: 'username', valueType: 'text', rules: [{ required: true }] },
|
||||
{ title: 'Status', dataIndex: 'status', valueType: 'radio', render: (v) => <Tag>{v === 1 ? 'Active' : 'Inactive'}</Tag> },
|
||||
]}
|
||||
rowKey="id"
|
||||
accessName="system.user"
|
||||
formProps={{ grid: true, colProps: { span: 12 }, layout: 'vertical' }}
|
||||
modalProps={{ width: 800 }}
|
||||
/>
|
||||
```
|
||||
|
||||
Required props: `api` (REST endpoint), `accessName` (permission prefix), `rowKey` (PK field), `columns`.
|
||||
|
||||
Default REST behavior — `GET {api}` for list, `POST {api}` for create, `PUT {api}/{id}` for update, `DELETE {api}/{id}` for delete. Add/edit/delete buttons auto-wrapped in `<AuthButton auth={accessName + '.create'}>`.
|
||||
|
||||
Customize with: `handleRequest` (full custom fetch), `requestParams` (transform before send), `handleFinish` (custom submit), `actionBarRender` / `toolBarRender` / `operateRender` (slot overrides).
|
||||
|
||||
## Choosing Between Them
|
||||
|
||||
- **XinTable**: Full CRUD pages (users, roles, dicts, files)
|
||||
- **XinForm** (inline/ModalForm): Settings pages with a single form (mail, storage, AI config)
|
||||
- **XinForm** (ModalForm + trigger): Add/edit without a table (dept management)
|
||||
- Use `hideInForm` / `hideInTable` / `hideInSearch` to control per-context visibility
|
||||
|
||||
# Development Workflow
|
||||
|
||||
When building a new CRUD feature, follow this four-phase workflow. See `@skill:xinadmin-development` for complete details.
|
||||
|
||||
1. **Database Migration** — create table structure, indexes, foreign keys
|
||||
2. **Backend** — Controller (AnnoRoute attributes), Model, FormRequest
|
||||
3. **Frontend** — Page (file-system routing), Domain types, API wrappers, i18n
|
||||
4. **Menu & Permissions** — seeder menu entry + rules, menu translation keys
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: xinadmin-development
|
||||
description: "TRIGGER when building a new CRUD module or feature in XinAdmin. Covers the full-stack development flow: database migrations, backend controller/model/form-request with AnnoRoute attribute routing, XinForm/XinTable frontend pages, and menu rules and permissions in seeder. Also activate when the user references AnnoRoute attribute routing, #[GetRoute]/#[PostRoute]/#[PutRoute]/#[DeleteRoute] attributes, XinAdmin controller patterns, or XinAdmin CRUD page development."
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# XinAdmin Development
|
||||
|
||||
Best practices for building features in XinAdmin, organized by topic. Each rule teaches what to do and why.
|
||||
|
||||
## Consistency First
|
||||
|
||||
Before applying any rule, check what the application already does. XinAdmin has established patterns — the best choice is the one the codebase already uses, even if another pattern would be theoretically better.
|
||||
|
||||
Check sibling controllers, related pages, or existing seed data for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### 1. CRUD Development Workflow → `rules/crud-workflow.md`
|
||||
|
||||
End-to-end flow for building a new feature in four phases:
|
||||
|
||||
- **Phase 1:** Database migration — `php artisan make:migration`
|
||||
- **Phase 2:** Backend — Controller with AnnoRoute attributes, Eloquent model, FormRequest
|
||||
- **Phase 3:** Frontend — Domain types, API wrappers, i18n, XinTable page component
|
||||
- **Phase 4:** Menu routes, permission rules in `SysUserSeeder`, menu translations
|
||||
|
||||
### 2. AnnoRoute Attribute Routing → `rules/annoroute.md`
|
||||
|
||||
- `#[RequestAttribute]` on the controller class sets shared prefix and permission prefix
|
||||
- `#[GetRoute]` / `#[PostRoute]` / `#[PutRoute]` / `#[DeleteRoute]` on methods declare HTTP routes
|
||||
- `authorize` parameter controls access: `'query'` → ability `prefix.query`, `false` → public route
|
||||
- `where` parameter for route parameter regex constraints
|
||||
- Route registration via `AnnoRoute->register(path)` in ServiceProvider `boot()`
|
||||
- Controller MUST have `#[RequestAttribute]` to be discovered
|
||||
- Inherit `BaseController` for `success()` / `error()` response helpers
|
||||
|
||||
### 3. i18n Locale Conventions → `rules/i18n-conventions.md`
|
||||
|
||||
- Locale files mirror page paths: `pages/system/user.tsx` → `locales/zh_CN/system/user.ts`, prefix `system.user`
|
||||
- `index.tsx` pages drop `/index`: `pages/ai/chat/index.tsx` → `locales/zh_CN/ai/chat.ts`, prefix `ai.chat`
|
||||
- Shared component translations go in `components/` directory: `xin.form.*`, `xin.table.*`, `xin.crud.*`
|
||||
- Layout translations go in `layout/` directory: `layout.*`
|
||||
- Standalone files (`menu.ts`, `login.ts`) stay at locale root
|
||||
- All keys dot-separated, double quotes, 2-space indent
|
||||
|
||||
## How to Apply
|
||||
|
||||
Always use a sub-agent to read rule files and explore this skill's content.
|
||||
|
||||
1. Identify what you're building (new CRUD → workflow + annoroute; routing only → annoroute)
|
||||
2. Check sibling files for existing patterns — follow those first per Consistency First
|
||||
3. Work through the phases in order — each builds on the last
|
||||
@@ -0,0 +1,213 @@
|
||||
# AnnoRoute Attribute Routing
|
||||
|
||||
AnnoRoute is a PHP 8 Attribute-based route registration module built into XinAdmin. Routes are declared via controller annotations, with automatic Sanctum authentication and permission verification integration.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Route Composition
|
||||
|
||||
The final route URL is: `RequestAttribute.routePrefix` + `methodAttribute.route`
|
||||
|
||||
```php
|
||||
#[RequestAttribute('/system/user', 'system.user')]
|
||||
class SysUserController extends BaseController
|
||||
{
|
||||
#[GetRoute('/role', 'role')]
|
||||
public function role(): JsonResponse { }
|
||||
}
|
||||
// Final route: GET /system/user/role
|
||||
```
|
||||
|
||||
The final permission string is: `RequestAttribute.abilitiesPrefix` + `.` + `methodAttribute.authorize`
|
||||
|
||||
```php
|
||||
// abilitiesPrefix = "system.user", authorize = "role"
|
||||
// Final ability: "system.user.role"
|
||||
```
|
||||
|
||||
### Route Registration
|
||||
|
||||
Routes are registered by scanning directories for `*Controller.php` files. In your ServiceProvider:
|
||||
|
||||
```php
|
||||
use Modules\AnnoRoute\AnnoRoute;
|
||||
|
||||
public function boot(AnnoRoute $annoRoute): void
|
||||
{
|
||||
$annoRoute->register(base_path('modules/YourModule/Http/Controllers'));
|
||||
}
|
||||
```
|
||||
|
||||
The scanner reads each controller file, extracts namespace + class name, then uses reflection to find and register attributes. Only classes with `#[RequestAttribute]` are registered.
|
||||
|
||||
## Attribute Reference
|
||||
|
||||
### Class-Level: `#[RequestAttribute]`
|
||||
|
||||
Defines the shared prefix and auth configuration for all routes within the controller.
|
||||
|
||||
```php
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
|
||||
#[RequestAttribute(
|
||||
routePrefix: '/admin/user',
|
||||
abilitiesPrefix: 'admin.user',
|
||||
middleware: 'log', // string or array — additional middleware for all routes
|
||||
authGuard: 'admin', // optional — Sanctum guard provider
|
||||
)]
|
||||
class UserController { }
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-------------------|------------------|---------|--------------------------------------------------|
|
||||
| `routePrefix` | `string` | `''` | URL prefix shared by all routes in this controller |
|
||||
| `abilitiesPrefix` | `string` | `''` | Prefix for permission ability strings |
|
||||
| `middleware` | `string\|array` | `''` | Additional middleware applied to every route |
|
||||
| `authGuard` | `?string` | `null` | Sanctum auth guard provider name |
|
||||
|
||||
### Method-Level Attributes
|
||||
|
||||
#### `#[GetRoute]` / `#[PostRoute]` / `#[PutRoute]` / `#[DeleteRoute]`
|
||||
|
||||
All method attributes share an identical constructor signature:
|
||||
|
||||
```php
|
||||
use Modules\AnnoRoute\Attribute\{GetRoute, PostRoute, PutRoute, DeleteRoute};
|
||||
|
||||
#[GetRoute(
|
||||
route: '/{id}',
|
||||
authorize: 'update',
|
||||
middleware: 'throttle:10,1',
|
||||
where: ['id' => '[0-9]+'],
|
||||
)]
|
||||
public function show(int $id): JsonResponse { }
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|--------------|------------------|---------|----------------------------------------------------------------|
|
||||
| `route` | `string` | `''` | Route path appended to the class `routePrefix` |
|
||||
| `authorize` | `string\|bool` | `true` | Permission ability string; `false` disables auth entirely |
|
||||
| `middleware` | `string\|array` | `''` | Route-specific middleware |
|
||||
| `where` | `array` | `[]` | Regex constraints for route parameters, e.g. `['id' => '[0-9]+']` |
|
||||
|
||||
### Authorization Behavior
|
||||
|
||||
When `authorize` is not `false` or empty, these middleware are automatically added:
|
||||
|
||||
1. `auth:sanctum` — Sanctum authentication
|
||||
2. `authGuard:{guard}` — guard check (or `authGuard` without a specific guard)
|
||||
3. `abilities:{prefix}.{authorize}` — permission check
|
||||
|
||||
When `authorize` is `false`, no auth middleware is applied (public route).
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic CRUD Controller
|
||||
|
||||
```php
|
||||
use Modules\AnnoRoute\Attribute\{RequestAttribute, GetRoute, PostRoute, PutRoute, DeleteRoute};
|
||||
|
||||
#[RequestAttribute('/system/dict', 'system.dict')]
|
||||
class SysDictController extends BaseController
|
||||
{
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
// GET /system/dict — ability: system.dict.query
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(FormRequest $request): JsonResponse
|
||||
{
|
||||
// POST /system/dict — ability: system.dict.create
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function update(int $id, FormRequest $request): JsonResponse
|
||||
{
|
||||
// PUT /system/dict/123 — ability: system.dict.update
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
|
||||
public function delete(int $id): JsonResponse
|
||||
{
|
||||
// DELETE /system/dict/123 — ability: system.dict.delete
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Public Routes (No Auth)
|
||||
|
||||
```php
|
||||
#[GetRoute('/public-data', authorize: false)]
|
||||
public function publicData(): JsonResponse
|
||||
{
|
||||
return $this->success($data);
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Route Path
|
||||
|
||||
When the method route is empty string (default), the controller routePrefix is the full route:
|
||||
|
||||
```php
|
||||
#[RequestAttribute('/dashboard', 'dashboard')]
|
||||
class DashboardController extends BaseController
|
||||
{
|
||||
#[GetRoute(authorize: 'index')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
// GET /dashboard — ability: dashboard.index
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Additional Middleware
|
||||
|
||||
```php
|
||||
#[GetRoute('/export', 'export', middleware: 'throttle:5,1')]
|
||||
public function export(): JsonResponse { }
|
||||
|
||||
#[PostRoute('/batch', 'batch', middleware: ['log', 'transaction'])]
|
||||
public function batchProcess(): JsonResponse { }
|
||||
```
|
||||
|
||||
### Route with Multiple Parameters
|
||||
|
||||
```php
|
||||
#[GetRoute(
|
||||
route: '/{deptId}/user/{userId}',
|
||||
authorize: 'detail',
|
||||
where: ['deptId' => '[0-9]+', 'userId' => '[0-9]+'],
|
||||
)]
|
||||
public function detail(int $deptId, int $userId): JsonResponse { }
|
||||
```
|
||||
|
||||
## Key Rules
|
||||
|
||||
- Namespace: `Modules\AnnoRoute\Attribute\`
|
||||
- Controller classes MUST use `#[RequestAttribute]` to be discovered; methods are only registered when the class has this attribute
|
||||
- Method attributes: `GetRoute`, `PostRoute`, `PutRoute`, `DeleteRoute`
|
||||
- Route registration: `AnnoRoute->register(path)` in ServiceProvider `boot()`
|
||||
- Scanner only looks for `*Controller.php` files
|
||||
- All auth middleware is auto-assembled — only declare `authorize` strings, not auth middleware directly
|
||||
- Inherit `BaseController` for the `success()` / `error()` response helpers
|
||||
- Controllers must return `Illuminate\Http\JsonResponse`
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Forgetting #[RequestAttribute] on the Class
|
||||
|
||||
If the class attribute is missing, no routes from that controller will be registered — regardless of method attributes.
|
||||
|
||||
### Duplicate Route Prefix
|
||||
|
||||
The final route is `routePrefix + route`. Convention is leading slash on both — they concatenate directly (no double slash).
|
||||
|
||||
### authorize vs abilitiesPrefix
|
||||
|
||||
The full permission string is `abilitiesPrefix.authorize`. If `abilitiesPrefix` is empty, the raw `authorize` value is used. Omitting `abilitiesPrefix` means you must pass the full ability string in each method's `authorize`.
|
||||
@@ -0,0 +1,205 @@
|
||||
# CRUD Development Workflow
|
||||
|
||||
The standard end-to-end workflow for building a new feature in XinAdmin follows four phases. Each phase builds on the last — always work in order.
|
||||
|
||||
## Phase 1: Database Migration
|
||||
|
||||
Create the database table structure.
|
||||
|
||||
```
|
||||
php artisan make:migration create_xxx_table
|
||||
```
|
||||
|
||||
- Define columns, indexes, and foreign keys in the migration file
|
||||
- Use `$table->id()` for auto-increment or `$table->string('id', 36)->primary()` for UUIDs
|
||||
- Use `constrained()` for foreign keys referencing other tables
|
||||
- Run `php artisan migrate` to apply
|
||||
|
||||
## Phase 2: Backend — Controller, Model, FormRequest
|
||||
|
||||
### Controller
|
||||
|
||||
Create the controller in the appropriate module under `modules/{Module}/Http/Controllers/`. Use AnnoRoute attributes for routing and authorization:
|
||||
|
||||
```php
|
||||
#[RequestAttribute('/system/xxx', 'system.xxx')]
|
||||
class XxxController extends BaseController
|
||||
{
|
||||
protected array $searchField = ['name' => 'like'];
|
||||
protected array $quickSearchField = ['name'];
|
||||
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$perPage = (int) ($params['pageSize'] ?? 10);
|
||||
$data = $this->buildSearch($params, Model::query())
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($perPage);
|
||||
return $this->success($data->toArray());
|
||||
}
|
||||
|
||||
#[PostRoute(authorize: 'create')]
|
||||
public function create(FormRequest $request): JsonResponse { }
|
||||
|
||||
#[PutRoute(route: '/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function update(int $id, FormRequest $request): JsonResponse { }
|
||||
|
||||
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[0-9]+'])]
|
||||
public function delete(int $id): JsonResponse { }
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- `#[RequestAttribute]` sets route prefix and permission prefix — AnnoRoute auto-registers routes (see `rules/annoroute.md`)
|
||||
- Always use `->paginate($perPage)->toArray()` and pass to `$this->success()` — the PaginationProvider returns `{ data, total, pageSize, current }` which XinTable expects
|
||||
- Use `$this->buildSearch()` for filter/keyword/sort query building
|
||||
- Return `$this->success()` or `$this->error()` from BaseController
|
||||
|
||||
### Model
|
||||
|
||||
Create or reuse the Eloquent model:
|
||||
|
||||
```php
|
||||
class XxxModel extends Model
|
||||
{
|
||||
protected $table = 'xxx';
|
||||
protected $fillable = ['name', 'status', /* ... */];
|
||||
protected $casts = ['status' => 'integer'];
|
||||
}
|
||||
```
|
||||
|
||||
### FormRequest
|
||||
|
||||
Create for validation on create/update:
|
||||
|
||||
```bash
|
||||
php artisan make:request XxxFormRequest
|
||||
```
|
||||
|
||||
Place in `modules/{Module}/Http/Requests/`. Define `rules()` and `messages()`. Inject via controller method parameter for auto-validation.
|
||||
|
||||
## Phase 3: Frontend — Page, Domain, API, i18n
|
||||
|
||||
### Domain (`web/domain/`)
|
||||
|
||||
TypeScript interfaces matching backend model fields:
|
||||
|
||||
```typescript
|
||||
export interface IXxx {
|
||||
id?: number;
|
||||
name?: string;
|
||||
status?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### API (`web/api/{module}/`)
|
||||
|
||||
Typed Axios wrappers for each endpoint. Use `createAxios` from `@/utils/request`:
|
||||
|
||||
```typescript
|
||||
import createAxios from '@/utils/request';
|
||||
|
||||
export async function getList(params?: Record<string, any>) {
|
||||
return createAxios<PaginatorData<IXxx>>({ url: '/system/xxx', method: 'get', params });
|
||||
}
|
||||
```
|
||||
|
||||
XinTable handles list/create/update/delete automatically — custom API functions are only needed for additional endpoints.
|
||||
|
||||
### i18n (`web/locales/{zh_CN,en_US}/`)
|
||||
|
||||
Translation keys for the page. Each feature gets its own file:
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
'xxx.page.title': 'XXX Management',
|
||||
'xxx.id': 'ID',
|
||||
'xxx.name': 'Name',
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Register in `web/locales/{zh_CN,en_US}/index.ts` by importing and spreading into the default export.
|
||||
|
||||
### Page (`web/pages/{module}/xxx/index.tsx`)
|
||||
|
||||
Use `<XinTable>` for standard CRUD list pages:
|
||||
|
||||
```tsx
|
||||
import XinTable from '@/components/XinTable';
|
||||
import type { XinTableColumn } from '@/components/XinTable/typings';
|
||||
import type { IXxx } from '@/domain/xxx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function XxxPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const columns: XinTableColumn<IXxx>[] = [
|
||||
{ title: t('xxx.id'), dataIndex: 'id', hideInForm: true, width: 80 },
|
||||
{ title: t('xxx.name'), dataIndex: 'name', valueType: 'text',
|
||||
rules: [{ required: true, message: t('xxx.name.required') }] },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title level={3}>{t('xxx.page.title')}</Title>
|
||||
<XinTable<IXxx>
|
||||
api="/system/xxx"
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
accessName="system.xxx"
|
||||
formProps={{ grid: true, colProps: { span: 12 }, layout: 'vertical' }}
|
||||
modalProps={{ width: 800 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
File-system routing auto-maps: `web/pages/system/xxx/index.tsx` → `/system/xxx`
|
||||
|
||||
## Phase 4: Menu Routes and Permissions
|
||||
|
||||
Add the menu entry in `database/seeders/SysUserSeeder.php` under the appropriate parent menu:
|
||||
|
||||
```php
|
||||
[
|
||||
'type' => "route",
|
||||
'key' => "system.xxx",
|
||||
'name' => "XXX Management",
|
||||
"path" => "/system/xxx",
|
||||
'local' => "menu.system.xxx",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '查询列表', 'key' => 'system.xxx.query'],
|
||||
['type' => 'rule', 'name' => '新增', 'key' => 'system.xxx.create'],
|
||||
['type' => 'rule', 'name' => '更新', 'key' => 'system.xxx.update'],
|
||||
['type' => 'rule', 'name' => '删除', 'key' => 'system.xxx.delete'],
|
||||
]
|
||||
],
|
||||
```
|
||||
|
||||
Add the menu translation key in `web/locales/{zh_CN,en_US}/menu.ts`:
|
||||
|
||||
```text
|
||||
"menu.system.xxx": "XXX Management",
|
||||
```
|
||||
|
||||
After seeding, the super admin role (role_id=1) automatically gets all permissions via the seeder's auto-assignment logic.
|
||||
|
||||
Run `php artisan db:seed --class=SysUserSeeder` to apply.
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
1. Migration → `database/migrations/`
|
||||
2. Controller → `modules/{Module}/Http/Controllers/` (with AnnoRoute attributes)
|
||||
3. Model → `modules/{Module}/Models/` or vendor model
|
||||
4. FormRequest → `modules/{Module}/Http/Requests/` (for create/update validation)
|
||||
5. Domain types → `web/domain/xxx.ts`
|
||||
6. API wrappers → `web/api/{module}/xxx.ts`
|
||||
7. i18n files → `web/locales/{zh_CN,en_US}/xxx.ts` + register in `index.ts`
|
||||
8. Page component → `web/pages/{module}/xxx/index.tsx`
|
||||
9. Menu entry + rules → `database/seeders/SysUserSeeder.php`
|
||||
10. Menu translation → `web/locales/{zh_CN,en_US}/menu.ts`
|
||||
@@ -0,0 +1,101 @@
|
||||
# i18n Conventions
|
||||
|
||||
Locale files must follow a consistent directory structure and naming convention. All keys are dot-separated, organized by module prefix matching the page path.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Locale files mirror page paths, with special directories for components and layout:
|
||||
|
||||
```
|
||||
web/locales/zh_CN/ (en_US mirrors exactly)
|
||||
├── index.ts # Aggregates all modules
|
||||
├── menu.ts # Standalone files (no page path prefix)
|
||||
├── login.ts
|
||||
├── dashboard/ # dashboard/*.tsx pages
|
||||
│ ├── analysis.ts
|
||||
│ ├── monitor.ts
|
||||
│ └── workplace.ts
|
||||
├── system/ # system/*.tsx pages
|
||||
│ ├── info.ts
|
||||
│ ├── user.ts
|
||||
│ ├── rule.ts
|
||||
│ └── ...
|
||||
├── ai/ # ai/*.tsx pages
|
||||
│ ├── chat.ts
|
||||
│ ├── conversation.ts
|
||||
│ └── agent.ts
|
||||
├── user/ # user/*.tsx pages
|
||||
│ └── profile.ts
|
||||
├── components/ # Shared component translations
|
||||
│ ├── xin-form.ts
|
||||
│ ├── xin-table.ts
|
||||
│ └── xin-crud.ts
|
||||
└── layout/ # Layout translations
|
||||
└── layout.ts
|
||||
```
|
||||
|
||||
## Key Naming Rules
|
||||
|
||||
### 1. Page translations follow page path
|
||||
|
||||
The key prefix equals the page file path (without extension, `/index` dropped):
|
||||
|
||||
| Page file | Locale file | Key prefix |
|
||||
|-----------|-------------|------------|
|
||||
| `pages/system/info.tsx` | `locales/zh_CN/system/info.ts` | `system.info` |
|
||||
| `pages/system/user.tsx` | `locales/zh_CN/system/user.ts` | `system.user` |
|
||||
| `pages/system/dict/index.tsx` | `locales/zh_CN/system/dict.ts` | `system.dict` |
|
||||
| `pages/ai/chat/index.tsx` | `locales/zh_CN/ai/chat.ts` | `ai.chat` |
|
||||
| `pages/dashboard/analysis.tsx` | `locales/zh_CN/dashboard/analysis.ts` | `dashboard.analysis` |
|
||||
|
||||
### 2. `index.tsx` pages drop "/index"
|
||||
|
||||
`pages/ai/chat/index.tsx` → file goes at `ai/chat.ts`, not `ai/chat/index.ts`.
|
||||
|
||||
### 3. Components go in `components/` directory
|
||||
|
||||
| Component | Locale file | Key prefix |
|
||||
|-----------|-------------|------------|
|
||||
| `XinForm` + sub-components | `components/xin-form.ts` | `xin.form` |
|
||||
| `XinTable` + sub-components | `components/xin-table.ts` | `xin.table` |
|
||||
| XinCrud shared keys | `components/xin-crud.ts` | `xin.crud` |
|
||||
|
||||
### 4. Layout goes in `layout/` directory
|
||||
|
||||
Layout translations are in `layout/layout.ts` with prefix `layout.*`.
|
||||
|
||||
### 5. Standalone files stay at root
|
||||
|
||||
Files without a page path prefix (like `menu.ts`, `login.ts`) stay at the root of the locale directory.
|
||||
|
||||
## File Format
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
// Section comment (Chinese in zh_CN, English in en_US)
|
||||
"prefix.page.title": "页面标题",
|
||||
"prefix.field.name": "字段名",
|
||||
"prefix.field.name.required": "字段名为必填项",
|
||||
};
|
||||
```
|
||||
|
||||
- **Quotes**: Always double quotes for keys and values
|
||||
- **Indentation**: 2 spaces
|
||||
- **Trailing commas**: Yes (after last entry)
|
||||
- **Comments**: Section comments in the locale's own language
|
||||
- **No `index.ts` files** inside subdirectories — only at the language root for aggregation
|
||||
|
||||
## Registering New Files
|
||||
|
||||
Import and spread new locale files in the language root `index.ts`:
|
||||
|
||||
```typescript
|
||||
import moduleName from "./path/to/file";
|
||||
|
||||
export default {
|
||||
...moduleName,
|
||||
// ... other modules
|
||||
};
|
||||
```
|
||||
|
||||
Both `zh_CN/index.ts` and `en_US/index.ts` must be updated identically.
|
||||
@@ -0,0 +1,24 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{ts,tsx,js,jsx,mjs,cjs}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{css,scss,sass,less}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{html,htm}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
@@ -0,0 +1,51 @@
|
||||
# 网站基本配置
|
||||
APP_NAME=XinAdmin
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://localhost:8000
|
||||
|
||||
VITE_BASE_URL=$APP_URL
|
||||
|
||||
# 密码加密算法因子
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
# 日志配置
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# 数据库配置
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=laravel
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=root
|
||||
|
||||
# redis配置
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
# 队列配置
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
# 系统设置
|
||||
SETTING_CACHE_KEY=settings
|
||||
|
||||
# 邮件配置
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_FROM_ADDRESS=
|
||||
MAIL_FROM_NAME=
|
||||
@@ -0,0 +1 @@
|
||||
VITE_BASE_URL=/index.php
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/dist
|
||||
/.idea
|
||||
/.vscode
|
||||
/vendor
|
||||
/dist-ssr
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/.claude
|
||||
|
||||
hot
|
||||
.env
|
||||
*.log
|
||||
*.local
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"laravel-boost": {
|
||||
"command": "php",
|
||||
"args": [
|
||||
"artisan",
|
||||
"boost:mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<laravel-boost-guidelines>
|
||||
=== .ai/xinadmin rules ===
|
||||
|
||||
# XinAdmin
|
||||
|
||||
XinAdmin is a full-stack development framework: PHP8.2 + Laravel12 + React19 + TypeScript + Ant Design6 + Zustand + Tailwind CSS4. Licensed under MIT, free for commercial use without authorization.
|
||||
|
||||
## Built-in Features
|
||||
|
||||
- Dashboard: Echarts-based dashboards with demo pages
|
||||
- Administrators: Backend user management with groups, permissions, and settings
|
||||
- Role & Department Management: Role-based menu permission control, enterprise org structure
|
||||
- System Settings: Visual form-based server variable configuration
|
||||
- File Management: Backend file manager with folders, multi-select, grouping
|
||||
- Dictionary Management: Maintenance of frequently used static data
|
||||
- Mail & Storage Configuration: Visual config and testing for Laravel mail/filesystem
|
||||
- AI Configuration: Visual config and testing for Laravel AI SDK
|
||||
- Frontend Members: Permission, grouping, lists, balance records
|
||||
|
||||
# AnnoRoute
|
||||
|
||||
AnnoRoute is a PHP 8 Attribute-based route registration module. Routes are declared via controller annotations — no manual route files needed. Sanctum auth and permission verification are auto-integrated.
|
||||
|
||||
## Usage
|
||||
|
||||
- `#[GetRoute]` / `#[PostRoute]` / `#[PutRoute]` / `#[DeleteRoute]` on methods declare HTTP routes
|
||||
- The `authorize` parameter controls access: `'query'` becomes ability `prefix.query`, `false` makes a route public
|
||||
- `#[RequestAttribute]` params: `routePrefix`, `abilitiesPrefix`, `middleware` (string|array), `authGuard` (?string)
|
||||
- Method attributes share: `route` (path appended to prefix), `authorize` (string|bool), `middleware`, `where` (regex array)
|
||||
- Final route = `routePrefix + route`; final ability = `abilitiesPrefix + '.' + authorize`
|
||||
- When `authorize` is not falsy, `auth:sanctum` + `authGuard` + `abilities:` middleware are auto-assembled
|
||||
- Extend `Modules\Common\Http\Controllers\BaseController` for `success()` / `error()` response helpers
|
||||
|
||||
# Frontend
|
||||
|
||||
Source lives in `web/`. Bundled with Vite, outputting to `public/`. Package manager: pnpm.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
| Directory | Purpose |
|
||||
|-----------|---------|
|
||||
| `web/api/` | Typed Axios wrappers per backend module |
|
||||
| `web/components/` | Reusable UI: `AuthButton`, `DictTag`, `IconFont`, `XinForm`, `XinTable` |
|
||||
| `web/domain/` | TypeScript interfaces for API models |
|
||||
| `web/hooks/` | `useAuth`, `useLanguage`, `useMobile`, `useRequest` |
|
||||
| `web/layout/` | Layout engine (4 modes), menu, header, breadcrumbs, theme |
|
||||
| `web/locales/` | i18n (i18next), zh_CN + en_US |
|
||||
| `web/pages/` | Auto-discovered page components (file-system routing) |
|
||||
| `web/router/` | React Router v7 `createBrowserRouter` |
|
||||
| `web/stores/` | Zustand: `global` (app/theme), `user` (auth/perms), `dict` (cache) |
|
||||
| `web/utils/` | Axios instance with dedup, auth headers, error handling |
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Path alias `@/` → `web/`
|
||||
- Zustand stores: `State` + `Actions` → `persist` + `devtools` → localStorage
|
||||
- Access stores via selector: `useXxxStore(state => state.field)`
|
||||
- All user-facing text via `useTranslation()` (react-i18next)
|
||||
- Locale files mirror page paths: `pages/system/user.tsx` → `locales/zh_CN/system/user.ts`, key prefix `system.user`
|
||||
- All keys dot-separated, double quotes, 2-space indent. See `@skill:xinadmin-development` for details
|
||||
- Permission checks: `<AuthButton auth="system.user.create">` or `useAuth().auth('permission')`
|
||||
- HTTP client auto-attaches `Authorization: Bearer`, `User-Language`, handles 401 auto-logout
|
||||
- Pages in `web/pages/` are auto-routed; `index.tsx` maps to parent directory; root `/` → `/dashboard/analysis`
|
||||
- Pages outside layout: add to `excludePaths` array in router config
|
||||
|
||||
# Antd
|
||||
|
||||
Ant Design 6 is the UI component library. Components are imported from `antd` and themed via `<ConfigProvider>` with tokens from `web/layout/theme.ts`.
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Use `antd` MCP tools (`antd_info`, `antd_doc`, `antd_demo`) to verify component APIs before writing code
|
||||
- Never use deprecated props or components — check with `antd_changelog` when upgrading or referencing older examples
|
||||
- Theme tokens flow: `web/layout/theme.ts` → `<ConfigProvider theme={...}>` → Ant Design components
|
||||
- Common components: `Table`, `Form`, `Modal`, `Drawer`, `Button`, `Input`, `Select`, `DatePicker`, `Switch`, `Tag`, `Card`, `App` ...
|
||||
|
||||
# Layout
|
||||
|
||||
Wraps authenticated pages. Supports 4 modes set via global store: `side` (default), `top`, `mix`, `columns`.
|
||||
|
||||
Menus are fetched from `/system/menu` and stored in `LayoutContext` (React Context). Menu type: `'menu'` (folder), `'route'` (page), `'rule'` (perm-only). Server filters by user role — no client-side filtering needed. Labels support i18n via `node.local`.
|
||||
|
||||
Theme tokens (20+ properties) are managed in `web/layout/theme.ts`, applied via Ant Design `<ConfigProvider>`, persisted to localStorage under `global-storage`.
|
||||
|
||||
# XinForm And XinTable
|
||||
|
||||
Two declarative JSON-driven CRUD components. Define columns once with metadata — the same definition drives table display, search form, and create/edit forms.
|
||||
|
||||
## XinForm
|
||||
|
||||
Use for settings/config pages or standalone forms. Supports 3 layout modes: `'Form'` (inline), `'ModalForm'`, `'DrawerForm'`.
|
||||
|
||||
```tsx
|
||||
<XinForm
|
||||
columns={[
|
||||
{ dataIndex: 'username', title: 'Username', valueType: 'text', rules: [{ required: true }] },
|
||||
{ dataIndex: 'role_id', title: 'Role', valueType: 'select', fieldProps: { options: roleOptions } },
|
||||
]}
|
||||
layoutType="ModalForm"
|
||||
grid
|
||||
trigger={<Button type="primary">New</Button>}
|
||||
modalProps={{ title: 'Create User', width: 600 }}
|
||||
onFinish={async (values) => { await save(values); return true; }}
|
||||
/>
|
||||
```
|
||||
|
||||
Key `FormColumn` fields: `dataIndex` (supports nested paths `['a','b']`), `valueType` (26 types: `text`, `password`, `select`, `date`, `switch`, etc.), `fieldProps`, `fieldRender` (custom render), `dependency` (field linkage: `{ dependencies, visible?, disabled?, fieldProps? }`), `hideIn*` visibility flags.
|
||||
|
||||
`formRef` exposes `open()`, `close()`, `isOpen()`, `setLoading()` plus all Ant Design `FormInstance` methods.
|
||||
|
||||
## XinTable
|
||||
|
||||
Use for standard CRUD pages (list + create + update + delete). Auto-handles API calls, permissions, search, and toolbar.
|
||||
|
||||
```tsx
|
||||
<XinTable<ISysUser>
|
||||
api="/system/user"
|
||||
columns={[
|
||||
{ title: 'ID', dataIndex: 'id', hideInForm: true, width: 80 },
|
||||
{ title: 'Username', dataIndex: 'username', valueType: 'text', rules: [{ required: true }] },
|
||||
{ title: 'Status', dataIndex: 'status', valueType: 'radio', render: (v) => <Tag>{v === 1 ? 'Active' : 'Inactive'}</Tag> },
|
||||
]}
|
||||
rowKey="id"
|
||||
accessName="system.user"
|
||||
formProps={{ grid: true, colProps: { span: 12 }, layout: 'vertical' }}
|
||||
modalProps={{ width: 800 }}
|
||||
/>
|
||||
```
|
||||
|
||||
Required props: `api` (REST endpoint), `accessName` (permission prefix), `rowKey` (PK field), `columns`.
|
||||
|
||||
Default REST behavior — `GET {api}` for list, `POST {api}` for create, `PUT {api}/{id}` for update, `DELETE {api}/{id}` for delete. Add/edit/delete buttons auto-wrapped in `<AuthButton auth={accessName + '.create'}>`.
|
||||
|
||||
Customize with: `handleRequest` (full custom fetch), `requestParams` (transform before send), `handleFinish` (custom submit), `actionBarRender` / `toolBarRender` / `operateRender` (slot overrides).
|
||||
|
||||
## Choosing Between Them
|
||||
|
||||
- **XinTable**: Full CRUD pages (users, roles, dicts, files)
|
||||
- **XinForm** (inline/ModalForm): Settings pages with a single form (mail, storage, AI config)
|
||||
- **XinForm** (ModalForm + trigger): Add/edit without a table (dept management)
|
||||
- Use `hideInForm` / `hideInTable` / `hideInSearch` to control per-context visibility
|
||||
|
||||
# Development Workflow
|
||||
|
||||
When building a new CRUD feature, follow this four-phase workflow. See `@skill:xinadmin-development` for complete details.
|
||||
|
||||
1. **Database Migration** — create table structure, indexes, foreign keys
|
||||
2. **Backend** — Controller (AnnoRoute attributes), Model, FormRequest
|
||||
3. **Frontend** — Page (file-system routing), Domain types, API wrappers, i18n
|
||||
4. **Menu & Permissions** — seeder menu entry + rules, menu translation keys
|
||||
|
||||
=== foundation rules ===
|
||||
|
||||
# Laravel Boost Guidelines
|
||||
|
||||
The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
|
||||
|
||||
## Foundational Context
|
||||
|
||||
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
|
||||
|
||||
- php - 8.3
|
||||
- laravel/ai (AI) - v0
|
||||
- laravel/framework (LARAVEL) - v13
|
||||
- laravel/prompts (PROMPTS) - v0
|
||||
- laravel/sanctum (SANCTUM) - v4
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/sail (SAIL) - v1
|
||||
- phpunit/phpunit (PHPUNIT) - v12
|
||||
- react (REACT) - v19
|
||||
- eslint (ESLINT) - v9
|
||||
- tailwindcss (TAILWINDCSS) - v4
|
||||
|
||||
## Skills Activation
|
||||
|
||||
This project has domain-specific skills available in `**/skills/**`. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
|
||||
|
||||
## Conventions
|
||||
|
||||
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
|
||||
- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`.
|
||||
- Check for existing components to reuse before writing a new one.
|
||||
|
||||
## Verification Scripts
|
||||
|
||||
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
|
||||
|
||||
## Application Structure & Architecture
|
||||
|
||||
- Stick to existing directory structure; don't create new base folders without approval.
|
||||
- Do not change the application's dependencies without approval.
|
||||
|
||||
## Frontend Bundling
|
||||
|
||||
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `pnpm run build`, `pnpm run dev`, or `composer run dev`. Ask them.
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- You must only create documentation files if explicitly requested by the user.
|
||||
|
||||
## Replies
|
||||
|
||||
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
|
||||
|
||||
=== boost rules ===
|
||||
|
||||
# Laravel Boost
|
||||
|
||||
## Tools
|
||||
|
||||
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
|
||||
- Use `database-query` to run read-only queries against the database instead of writing raw SQL in tinker.
|
||||
- Use `database-schema` to inspect table structure before writing migrations or models.
|
||||
- Use `get-absolute-url` to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user.
|
||||
- Use `browser-logs` to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
|
||||
|
||||
## Searching Documentation (IMPORTANT)
|
||||
|
||||
- Always use `search-docs` before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.
|
||||
- Pass a `packages` array to scope results when you know which packages are relevant.
|
||||
- Use multiple broad, topic-based queries: `['rate limiting', 'routing rate limiting', 'routing']`. Expect the most relevant results first.
|
||||
- Do not add package names to queries because package info is already shared. Use `test resource table`, not `filament 4 test resource table`.
|
||||
|
||||
### Search Syntax
|
||||
|
||||
1. Use words for auto-stemmed AND logic: `rate limit` matches both "rate" AND "limit".
|
||||
2. Use `"quoted phrases"` for exact position matching: `"infinite scroll"` requires adjacent words in order.
|
||||
3. Combine words and phrases for mixed queries: `middleware "rate limit"`.
|
||||
4. Use multiple queries for OR logic: `queries=["authentication", "middleware"]`.
|
||||
|
||||
## Artisan
|
||||
|
||||
- Run Artisan commands directly via the command line (e.g., `php artisan route:list`). Use `php artisan list` to discover available commands and `php artisan [command] --help` to check parameters.
|
||||
- Inspect routes with `php artisan route:list`. Filter with: `--method=GET`, `--name=users`, `--path=api`, `--except-vendor`, `--only-vendor`.
|
||||
- Read configuration values using dot notation: `php artisan config:show app.name`, `php artisan config:show database.default`. Or read config files directly from the `config/` directory.
|
||||
|
||||
## Tinker
|
||||
|
||||
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
|
||||
- Always use single quotes to prevent shell expansion: `php artisan tinker --execute 'Your::code();'`
|
||||
- Double quotes for PHP strings inside: `php artisan tinker --execute 'User::where("active", true)->count();'`
|
||||
|
||||
=== php rules ===
|
||||
|
||||
# PHP
|
||||
|
||||
- Always use curly braces for control structures, even for single-line bodies.
|
||||
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
|
||||
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
|
||||
- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`.
|
||||
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
|
||||
- Use array shape type definitions in PHPDoc blocks.
|
||||
|
||||
=== deployments rules ===
|
||||
|
||||
# Deployment
|
||||
|
||||
- Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications.
|
||||
|
||||
=== laravel/core rules ===
|
||||
|
||||
# Do Things the Laravel Way
|
||||
|
||||
- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using `php artisan list` and check their parameters with `php artisan [command] --help`.
|
||||
- If you're creating a generic PHP class, use `php artisan make:class`.
|
||||
- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior.
|
||||
|
||||
### Model Creation
|
||||
|
||||
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `php artisan make:model --help` to check the available options.
|
||||
|
||||
## APIs & Eloquent Resources
|
||||
|
||||
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
|
||||
|
||||
## URL Generation
|
||||
|
||||
- When generating links to other pages, prefer named routes and the `route()` function.
|
||||
|
||||
## Testing
|
||||
|
||||
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
|
||||
- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`.
|
||||
- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests.
|
||||
|
||||
## Vite Error
|
||||
|
||||
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `pnpm run build` or ask the user to run `pnpm run dev` or `composer run dev`.
|
||||
|
||||
=== phpunit/core rules ===
|
||||
|
||||
# PHPUnit
|
||||
|
||||
- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test.
|
||||
- If you see a test using "Pest", convert it to PHPUnit.
|
||||
- Every time a test has been updated, run that singular test.
|
||||
- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing.
|
||||
- Tests should cover all happy paths, failure paths, and edge cases.
|
||||
- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application.
|
||||
|
||||
## Running Tests
|
||||
|
||||
- Run the minimal number of tests, using an appropriate filter, before finalizing.
|
||||
- To run all tests: `php artisan test --compact`.
|
||||
- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`.
|
||||
- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file).
|
||||
|
||||
</laravel-boost-guidelines>
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 XinAdmin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Laravel\Sanctum\Exceptions\MissingAbilityException;
|
||||
use Modules\Common\Enum\ShowType;
|
||||
use Modules\Common\Trait\RequestJson;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Throwable;
|
||||
|
||||
class ExceptionsHandler extends ExceptionHandler
|
||||
{
|
||||
use RequestJson;
|
||||
|
||||
/**
|
||||
* A list of the exception types that are not reported.
|
||||
* 未报告的异常类型列表
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dontReport = [];
|
||||
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
public function render($request, Throwable $e): JsonResponse|Response
|
||||
{
|
||||
$exceptionHandlers = [
|
||||
HttpResponseException::class => function (HttpResponseException $e) {
|
||||
return response()->json($e->toArray(), $e->getCode());
|
||||
},
|
||||
MissingAbilityException::class => function ($e) {
|
||||
return $this->notification(
|
||||
'No Permission',
|
||||
__('system.error.no_permission'),
|
||||
ShowType::WARN_NOTIFICATION
|
||||
);
|
||||
},
|
||||
AuthenticationException::class => function ($e) {
|
||||
return response()->json([
|
||||
'msg' => __('user.not_login'),
|
||||
'success' => false
|
||||
], 401);
|
||||
},
|
||||
NotFoundHttpException::class => function ($e) {
|
||||
return $this->notification(
|
||||
'Route Not Exist',
|
||||
__('system.error.route_not_exist'),
|
||||
ShowType::WARN_NOTIFICATION
|
||||
);
|
||||
},
|
||||
ValidationException::class => function (ValidationException $e) {
|
||||
return response()->json([
|
||||
'msg' => $e->validator->errors()->first(),
|
||||
'showType' => ShowType::WARN_MESSAGE->value,
|
||||
'success' => false,
|
||||
]);
|
||||
},
|
||||
];
|
||||
|
||||
foreach ($exceptionHandlers as $exceptionType => $handler) {
|
||||
if ($e instanceof $exceptionType) {
|
||||
$response = $handler($e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($response)) {
|
||||
$debug = config('app.debug');
|
||||
$data = [
|
||||
'msg' => $e->getMessage(),
|
||||
'showType' => ShowType::ERROR_MESSAGE->value,
|
||||
'success' => false,
|
||||
];
|
||||
|
||||
if ($debug) {
|
||||
$data += [
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
'trace' => $e->getTrace(),
|
||||
'code' => $e->getCode(),
|
||||
];
|
||||
}
|
||||
|
||||
$response = response()->json($data);
|
||||
}
|
||||
|
||||
$response->headers->set('Access-Control-Allow-Origin', '*');
|
||||
$response->headers->set('Access-Control-Allow-Credentials', 'true');
|
||||
$response->headers->set('Access-Control-Max-Age', 1800);
|
||||
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, User-Language');
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Modules\Common\Enum\ShowType;
|
||||
use RuntimeException;
|
||||
|
||||
class HttpResponseException extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private string $msg;
|
||||
|
||||
/**
|
||||
* 响应类型
|
||||
*/
|
||||
private ShowType $showType;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
private array $data;
|
||||
|
||||
/**
|
||||
* 响应状态
|
||||
*/
|
||||
private bool $success;
|
||||
|
||||
public function __construct(array $data, int $code = 200)
|
||||
{
|
||||
$this->msg = $data['msg'] ?? '';
|
||||
$this->success = $data['success'] ?? true;
|
||||
if (empty($data['showType']) && $this->success) {
|
||||
$this->showType = ShowType::SUCCESS_MESSAGE;
|
||||
} elseif (empty($data['showType']) && ! $this->success) {
|
||||
$this->showType = ShowType::ERROR_MESSAGE;
|
||||
} else {
|
||||
$this->showType = ShowType::from($data['showType']);
|
||||
}
|
||||
$this->data = $data['data'] ?? [];
|
||||
parent::__construct($data['msg'] ?? '', $code);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->data,
|
||||
'success' => $this->success,
|
||||
'msg' => $this->msg,
|
||||
'showType' => $this->showType->value,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class RepositoryException extends RuntimeException
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UserRegisterRequest;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Trait\RequestJson;
|
||||
|
||||
#[RequestAttribute('/api', authGuard: 'users')]
|
||||
class IndexController
|
||||
{
|
||||
use RequestJson;
|
||||
// 权限验证白名单
|
||||
protected array $noPermission = ['index', 'login', 'register', 'mail'];
|
||||
|
||||
/** 获取首页信息 */
|
||||
#[GetRoute('/index')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$web_setting = site_config('web');
|
||||
|
||||
return $this->success(compact('web_setting'));
|
||||
}
|
||||
|
||||
/** 用户登录 */
|
||||
#[PostRoute('/login')]
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'username' => 'required|min:4|alphaDash',
|
||||
'password' => 'required|min:4|alphaDash',
|
||||
]);
|
||||
if (Auth::guard('users')->attempt($credentials, true)) {
|
||||
$data = $request->user('users')
|
||||
->createToken($credentials['username'])
|
||||
->toArray();
|
||||
return $this->success($data, __('user.login_success'));
|
||||
}
|
||||
return $this->error(__('user.login_error'));
|
||||
}
|
||||
|
||||
/** 用户注册 */
|
||||
#[PostRoute('/register')]
|
||||
public function register(UserRegisterRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$model = new UserModel;
|
||||
$model->username = $data['username'];
|
||||
$model->password = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
$model->email = $data['email'];
|
||||
if ($model->save()) {
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
return $this->error('创建用户失败');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\UserUpdateInfoRequest;
|
||||
use App\Models\UserModel;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\Common\Trait\RequestJson;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
|
||||
#[RequestAttribute('/api/user', authGuard: 'users')]
|
||||
class UserController
|
||||
{
|
||||
use RequestJson;
|
||||
protected array $noPermission = ['refreshToken'];
|
||||
|
||||
#[GetRoute]
|
||||
public function getUserInfo(): JsonResponse
|
||||
{
|
||||
$info = auth()->user();
|
||||
return $this->success(compact('info'));
|
||||
}
|
||||
|
||||
#[PostRoute('/logout')]
|
||||
public function logout(): JsonResponse
|
||||
{
|
||||
$user_id = auth('users')->id();
|
||||
$model = new UserModel;
|
||||
if ($model->logout($user_id)) {
|
||||
return $this->success('退出登录成功');
|
||||
} else {
|
||||
return $this->error($model->getErrorMsg());
|
||||
}
|
||||
}
|
||||
|
||||
#[PutRoute]
|
||||
public function setUserInfo(UserUpdateInfoRequest $request): JsonResponse
|
||||
{
|
||||
UserModel::where('user_id', auth('user')->id())->update($request->validated());
|
||||
|
||||
return $this->error('更新成功');
|
||||
}
|
||||
|
||||
#[PostRoute('/setPwd')]
|
||||
public function setPassword(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'oldPassword' => 'required|string|max:20',
|
||||
'newPassword' => 'required|string|min:6|max:20',
|
||||
'rePassword' => 'required|same:newPassword',
|
||||
]);
|
||||
$user_id = auth('user')->id();
|
||||
$user = UserModel::query()->find($user_id);
|
||||
if (! password_verify($data['oldPassword'], $user['password'])) {
|
||||
return $this->error('旧密码不正确!');
|
||||
}
|
||||
$user->password = password_hash($data['newPassword'], PASSWORD_DEFAULT);
|
||||
if ($user->save()) {
|
||||
return $this->success('更新成功');
|
||||
}
|
||||
|
||||
return $this->error('更新失败');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UserRegisterRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => 'required|min:4|alphaDash',
|
||||
'password' => 'required|min:4|alphaDash',
|
||||
'rePassword' => 'required|min:4|same:password',
|
||||
'email' => 'required|email',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UserUpdateInfoRequest extends FormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'username' => 'required|min:4|max:20',
|
||||
'nickname' => 'required|min:4|max:20',
|
||||
'gender' => 'required',
|
||||
'email' => 'required|email',
|
||||
'avatar_id' => 'required|integer',
|
||||
'mobile' => 'required|regex:/^1[34578]\d{9}$/',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
/**
|
||||
* APP 用户模型
|
||||
*/
|
||||
class UserModel extends Authenticatable
|
||||
{
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
protected $table = 'user';
|
||||
|
||||
protected $primaryKey = 'id';
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'mobile',
|
||||
'username',
|
||||
'email',
|
||||
'password',
|
||||
'nickname',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionsHandler;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\AnnoRoute\AnnoRoute;
|
||||
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
|
||||
$this->app->bind(ExceptionsHandler::class, \App\Exceptions\ExceptionsHandler::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(AnnoRoute $annoRoute): void
|
||||
{
|
||||
// 注册路由
|
||||
$annoRoute->register(app_path('Http/Controllers'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
$status = (require_once __DIR__.'/bootstrap/app.php')
|
||||
->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"agents": [
|
||||
"claude_code"
|
||||
],
|
||||
"cloud": false,
|
||||
"guidelines": true,
|
||||
"mcp": true,
|
||||
"nightwatch": false,
|
||||
"sail": false,
|
||||
"skills": [
|
||||
"ai-sdk-development",
|
||||
"laravel-best-practices",
|
||||
"tailwindcss-development",
|
||||
"xinadmin-development"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
|
||||
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
|
||||
use Modules\Common\Console\Commands\GenerateRouteHelperCommand;
|
||||
use Modules\Common\Middlewares\AllowCrossDomainMiddleware;
|
||||
use Modules\Common\Middlewares\LanguageMiddleware;
|
||||
use Modules\SystemTool\Http\Middleware\LoadAppSettingsMiddleware;
|
||||
use Modules\SystemUser\Http\Middleware\AuthGuardMiddleware;
|
||||
use Modules\SystemUser\Http\Middleware\LoginLogMiddleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
commands: __DIR__.'/../routes/console.php'
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
// 全局跨域中间件
|
||||
$middleware->append(AllowCrossDomainMiddleware::class);
|
||||
$middleware->append(LanguageMiddleware::class);
|
||||
// 全局中间件 — 从缓存加载 DB 应用设置到 config() 运行时
|
||||
$middleware->append(LoadAppSettingsMiddleware::class);
|
||||
$middleware->alias([
|
||||
'login_log' => LoginLogMiddleware::class,
|
||||
'abilities' => CheckAbilities::class,
|
||||
'ability' => CheckForAnyAbility::class,
|
||||
'authGuard' => AuthGuardMiddleware::class,
|
||||
]);
|
||||
// 未登录响应
|
||||
$middleware->redirectGuestsTo(function (Request $request) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'msg' => __('user.not_login')
|
||||
], 401);
|
||||
});
|
||||
})
|
||||
->withCommands([
|
||||
GenerateRouteHelperCommand::class,
|
||||
])
|
||||
->withExceptions(function (Exceptions $exceptions) {})->create();
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
use Modules\AnnoRoute\RouteServiceProvider;
|
||||
use Modules\Common\Providers\PaginationProvider;
|
||||
use Modules\SystemAgent\Providers\SystemAgentServiceProvider;
|
||||
use Modules\SystemTool\Providers\SystemToolServiceProvider;
|
||||
use Modules\SystemUser\Providers\SystemUserServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
RouteServiceProvider::class,
|
||||
PaginationProvider::class,
|
||||
SystemAgentServiceProvider::class,
|
||||
SystemUserServiceProvider::class,
|
||||
SystemToolServiceProvider::class,
|
||||
];
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.3",
|
||||
"ext-bcmath": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-pdo": "*",
|
||||
"ext-redis": "*",
|
||||
"laravel/ai": "^0.7.0",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^3.0",
|
||||
"predis/predis": "2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/boost": "^2.0",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^12.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Modules\\": "modules",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
},
|
||||
"files": [
|
||||
"modules/Common/helpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"npm run dev\" --names=server,queue,vite --kill-others"
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php artisan migrate --graceful --ansi",
|
||||
"@php artisan db:seed --force"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"replace": {
|
||||
"symfony/polyfill-php80": "*"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
Generated
+8874
File diff suppressed because it is too large
Load Diff
+143
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default AI Provider Names
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the AI providers below should be the
|
||||
| default for AI operations when no explicit provider is provided
|
||||
| for the operation. This should be any provider defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 'openai',
|
||||
'default_for_images' => 'gemini',
|
||||
'default_for_audio' => 'openai',
|
||||
'default_for_transcription' => 'openai',
|
||||
'default_for_embeddings' => 'openai',
|
||||
'default_for_reranking' => 'cohere',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Caching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure caching strategies for AI related operations
|
||||
| such as embedding generation. You are free to adjust these values
|
||||
| based on your application's available caching stores and needs.
|
||||
|
|
||||
*/
|
||||
|
||||
'caching' => [
|
||||
'embeddings' => [
|
||||
'cache' => false,
|
||||
'store' => env('CACHE_STORE', 'redis'),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| AI Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are each of your AI providers defined for this application. Each
|
||||
| represents an AI provider and API key combination which can be used
|
||||
| to perform tasks like text, image, and audio creation via agents.
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'anthropic' => [
|
||||
'driver' => 'anthropic',
|
||||
'key' => env('ANTHROPIC_API_KEY'),
|
||||
'url' => env('ANTHROPIC_URL', 'https://api.anthropic.com/v1'),
|
||||
],
|
||||
|
||||
'azure' => [
|
||||
'driver' => 'azure',
|
||||
'key' => env('AZURE_OPENAI_API_KEY'),
|
||||
'url' => env('AZURE_OPENAI_URL'),
|
||||
'api_version' => env('AZURE_OPENAI_API_VERSION', '2025-04-01-preview'),
|
||||
'deployment' => env('AZURE_OPENAI_DEPLOYMENT', 'gpt-4o'),
|
||||
'embedding_deployment' => env('AZURE_OPENAI_EMBEDDING_DEPLOYMENT', 'text-embedding-3-small'),
|
||||
'image_deployment' => env('AZURE_OPENAI_IMAGE_DEPLOYMENT', 'gpt-image-1'),
|
||||
],
|
||||
|
||||
'bedrock' => [
|
||||
'driver' => 'bedrock',
|
||||
'region' => env('AWS_BEDROCK_REGION', 'us-east-1'),
|
||||
'key' => env('AWS_BEARER_TOKEN_BEDROCK'),
|
||||
'access_key_id' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret_access_key' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'session_token' => env('AWS_SESSION_TOKEN'),
|
||||
'use_default_credential_provider' => env('AWS_USE_DEFAULT_CREDENTIALS', true),
|
||||
],
|
||||
|
||||
'cohere' => [
|
||||
'driver' => 'cohere',
|
||||
'key' => env('COHERE_API_KEY'),
|
||||
],
|
||||
|
||||
'deepseek' => [
|
||||
'driver' => 'deepseek',
|
||||
'key' => env('DEEPSEEK_API_KEY'),
|
||||
],
|
||||
|
||||
'eleven' => [
|
||||
'driver' => 'eleven',
|
||||
'key' => env('ELEVENLABS_API_KEY'),
|
||||
],
|
||||
|
||||
'gemini' => [
|
||||
'driver' => 'gemini',
|
||||
'key' => env('GEMINI_API_KEY'),
|
||||
'url' => env('GEMINI_URL', 'https://generativelanguage.googleapis.com/v1beta/'),
|
||||
],
|
||||
|
||||
'groq' => [
|
||||
'driver' => 'groq',
|
||||
'key' => env('GROQ_API_KEY'),
|
||||
],
|
||||
|
||||
'jina' => [
|
||||
'driver' => 'jina',
|
||||
'key' => env('JINA_API_KEY'),
|
||||
],
|
||||
|
||||
'mistral' => [
|
||||
'driver' => 'mistral',
|
||||
'key' => env('MISTRAL_API_KEY'),
|
||||
],
|
||||
|
||||
'ollama' => [
|
||||
'driver' => 'ollama',
|
||||
'key' => env('OLLAMA_API_KEY', ''),
|
||||
'url' => env('OLLAMA_URL', 'http://localhost:11434'),
|
||||
],
|
||||
|
||||
'openai' => [
|
||||
'driver' => 'openai',
|
||||
'key' => env('OPENAI_API_KEY'),
|
||||
'url' => env('OPENAI_URL', 'https://api.openai.com/v1'),
|
||||
],
|
||||
|
||||
'openrouter' => [
|
||||
'driver' => 'openrouter',
|
||||
'key' => env('OPENROUTER_API_KEY'),
|
||||
],
|
||||
|
||||
'voyageai' => [
|
||||
'driver' => 'voyageai',
|
||||
'key' => env('VOYAGEAI_API_KEY'),
|
||||
],
|
||||
|
||||
'xai' => [
|
||||
'driver' => 'xai',
|
||||
'key' => env('XAI_API_KEY'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => 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' => env('APP_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(',', 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'),
|
||||
],
|
||||
|
||||
];
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => 'sys_users',
|
||||
'passwords' => 'sys_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' => [
|
||||
'sys_users' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'sys_users',
|
||||
],
|
||||
'users' => [
|
||||
'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' => [
|
||||
'sys_users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => \Modules\SystemUser\Models\SysUserModel::class
|
||||
],
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => \App\Models\UserModel::class
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
'sys_users' => [
|
||||
'driver' => 'cache',
|
||||
'provider' => 'sys_users',
|
||||
'store' => 'sys_passwords',
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
'users' => [
|
||||
'driver' => 'cache',
|
||||
'provider' => 'users',
|
||||
'store' => 'passwords',
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the amount 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),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'redis'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'sys_cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE', 'sys_cache_locks'),
|
||||
],
|
||||
|
||||
'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',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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,
|
||||
],
|
||||
|
||||
'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([
|
||||
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([
|
||||
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' => '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' => 'sys_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(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||
],
|
||||
|
||||
'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'),
|
||||
],
|
||||
|
||||
'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'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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' => public_path('storage'),
|
||||
'url' => env('FILESYSTEM_LOCAL_URL', env('APP_URL').'/storage'),
|
||||
'visibility' => 'public',
|
||||
'throw' => 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,
|
||||
],
|
||||
'ftp' => [
|
||||
'driver' => 'ftp',
|
||||
'host' => env('FTP_HOST'),
|
||||
'username' => env('FTP_USERNAME'),
|
||||
'password' => env('FTP_PASSWORD'),
|
||||
'port' => env('FTP_PORT', 21),
|
||||
'root' => env('FTP_ROOT', ''),
|
||||
'passive' => env('FTP_PASSIVE', true),
|
||||
'ssl' => env('FTP_SSL', false),
|
||||
'timeout' => env('FTP_TIMEOUT', 30),
|
||||
'throw' => false,
|
||||
],
|
||||
'sftp' => [
|
||||
'driver' => 'sftp',
|
||||
'host' => env('SFTP_HOST'),
|
||||
'username' => env('SFTP_USERNAME'),
|
||||
'password' => env('SFTP_PASSWORD'),
|
||||
'port' => env('SFTP_PORT', 22),
|
||||
'root' => env('SFTP_ROOT', ''),
|
||||
'timeout' => env('SFTP_TIMEOUT', 30),
|
||||
'privateKey' => env('SFTP_PRIVATE_KEY'),
|
||||
'passphrase' => env('SFTP_PASSPHRASE'),
|
||||
'throw' => 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' => [],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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(',', 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,
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'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'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/log.log'),
|
||||
'level' => 'debug',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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' => [
|
||||
'mailgun' => [
|
||||
'transport' => 'mailgun',
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'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(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', 'stack'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => explode(",", env('MAIL_FAILOVER_MAILERS', 'smtp,log')),
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => explode(",", env('MAIL_ROUNDROBIN_MAILERS', 'ses,postmark')),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| In this section you may define the default configuration for each model
|
||||
| that will be generated from any database.
|
||||
|
|
||||
*/
|
||||
|
||||
'*' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Model Files Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| We need a location to store your new generated files. All files will be
|
||||
| placed within this directory. When you turn on base files, they will
|
||||
| be placed within a Base directory inside this location.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => app_path('Models'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Model Namespace
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Every generated model will belong to this namespace. It is suggested
|
||||
| that this namespace should follow PSR-4 convention and be very
|
||||
| similar to the path of your models defined above.
|
||||
|
|
||||
*/
|
||||
|
||||
'namespace' => 'App\Models',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Parent Class
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All Eloquent models should inherit from Eloquent Model class. However,
|
||||
| you can define a custom Eloquent model that suits your needs.
|
||||
| As an example one custom model has been added for you which
|
||||
| will allow you to create custom database castings.
|
||||
|
|
||||
*/
|
||||
|
||||
'parent' => Illuminate\Database\Eloquent\Model::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Traits
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sometimes you may want to append certain traits to all your models.
|
||||
| If that is what you need, you may list them bellow.
|
||||
| As an example we have a BitBooleans trait which will treat MySQL bit
|
||||
| data type as booleans. You might probably not need it, but it is
|
||||
| an example of how you can customize your models.
|
||||
|
|
||||
*/
|
||||
|
||||
'use' => [
|
||||
// Reliese\Database\Eloquent\BitBooleans::class,
|
||||
// Reliese\Database\Eloquent\BlamableBehavior::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Model Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you wish your models had appended the connection from which they
|
||||
| were generated, you should set this value to true and your
|
||||
| models will have the connection property filled.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Timestamps
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If your tables have CREATED_AT and UPDATED_AT timestamps you may
|
||||
| enable them and your models will fill their values as needed.
|
||||
| You can also specify which fields should be treated as timestamps
|
||||
| in case you don't follow the naming convention Eloquent uses.
|
||||
| If your table doesn't have these fields, timestamps will be
|
||||
| disabled for your model.
|
||||
|
|
||||
*/
|
||||
|
||||
'timestamps' => true,
|
||||
|
||||
// 'timestamps' => [
|
||||
// 'enabled' => true,
|
||||
// 'fields' => [
|
||||
// 'CREATED_AT' => 'created_at',
|
||||
// 'UPDATED_AT' => 'updated_at',
|
||||
// ]
|
||||
// ],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Soft Deletes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If your tables support soft deletes with a DELETED_AT attribute,
|
||||
| you can enable them here. You can also specify which field
|
||||
| should be treated as a soft delete attribute in case you
|
||||
| don't follow the naming convention Eloquent uses.
|
||||
| If your table doesn't have this field, soft deletes will be
|
||||
| disabled for your model.
|
||||
|
|
||||
*/
|
||||
|
||||
'soft_deletes' => true,
|
||||
|
||||
// 'soft_deletes' => [
|
||||
// 'enabled' => true,
|
||||
// 'field' => 'deleted_at',
|
||||
// ],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Date Format
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define your models' date format. The following format
|
||||
| is the default format Eloquent uses. You won't see it in your
|
||||
| models unless you change it to a more convenient value.
|
||||
|
|
||||
*/
|
||||
|
||||
'date_format' => 'Y-m-d H:i:s',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define how many models Eloquent should display when
|
||||
| paginating them. The default number is 15, so you might not
|
||||
| see this number in your models unless you change it.
|
||||
|
|
||||
*/
|
||||
|
||||
'per_page' => 15,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Base Files
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default, your models will be generated in your models path, but
|
||||
| when you generate them again they will be replaced by new ones.
|
||||
| You may want to customize your models and, at the same time, be
|
||||
| able to generate them as your tables change. For that, you
|
||||
| can enable base files. These files will be replaced whenever
|
||||
| you generate them, but your customized files will not be touched.
|
||||
|
|
||||
*/
|
||||
|
||||
'base_files' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Snake Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Eloquent treats your model attributes as snake cased attributes, but
|
||||
| if you have camel-cased fields in your database you can disable
|
||||
| that behaviour and use camel case attributes in your models.
|
||||
|
|
||||
*/
|
||||
|
||||
'snake_attributes' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Indent options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| As default indention is done with tabs, but you can change it by setting
|
||||
| this to the amount of spaces you that you want to use for indentation.
|
||||
| Usually you will use 4 spaces instead of tabs.
|
||||
|
|
||||
*/
|
||||
|
||||
'indent_with_space' => 0,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Qualified Table Names
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If some of your tables have cross-database relationships (probably in
|
||||
| MySQL), you can make sure your models take into account their
|
||||
| respective database schema.
|
||||
|
|
||||
| Can Either be NULL, FALSE or TRUE
|
||||
| TRUE: Schema name will be prepended on the table
|
||||
| FALSE:Table name will be set without schema name.
|
||||
| NULL: Table name will follow laravel pattern,
|
||||
| i.e. if class name(plural) matches table name, then table name will not be added
|
||||
*/
|
||||
|
||||
'qualified_tables' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Hidden Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When casting your models into arrays or json, the need to hide some
|
||||
| attributes sometimes arise. If your tables have some fields you
|
||||
| want to hide, you can define them bellow.
|
||||
| Some fields were defined for you.
|
||||
|
|
||||
*/
|
||||
|
||||
'hidden' => [
|
||||
'*secret*', '*password', '*token',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mass Assignment Guarded Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may want to protect some fields from mass assignment. You can
|
||||
| define them bellow. Some fields were defined for you.
|
||||
| Your fillable attributes will be those which are not in the list
|
||||
| excluding your models' primary keys.
|
||||
|
|
||||
*/
|
||||
|
||||
'guarded' => [
|
||||
// 'created_by', 'updated_by'
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Casts
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may want to specify which of your table fields should be cast as
|
||||
| something other than a string. For instance, you may want a
|
||||
| text field be cast as an array or and object.
|
||||
|
|
||||
| You may define column patterns which will be cast using the value
|
||||
| assigned. We have defined some fields for you. Feel free to
|
||||
| modify them to fit your needs.
|
||||
|
|
||||
*/
|
||||
|
||||
'casts' => [
|
||||
'*_json' => 'json',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Excluded Tables
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When performing the generation of models you may want to skip some of
|
||||
| them, because you don't want a model for them or any other reason.
|
||||
| You can define those tables bellow. The migrations table was
|
||||
| filled for you, since you may not want a model for it.
|
||||
|
|
||||
*/
|
||||
|
||||
'except' => [
|
||||
'sys_cache',
|
||||
'sys_cache_locks',
|
||||
'sys_token',
|
||||
'migrations',
|
||||
'sys_failed_jobs',
|
||||
'password_resets',
|
||||
'personal_access_tokens',
|
||||
'password_reset_tokens',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Specified Tables
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can specify specific tables. This will generate the models only
|
||||
| for selected tables, ignoring the rest.
|
||||
|
|
||||
*/
|
||||
|
||||
'only' => [
|
||||
// 'users',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Table Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If you have a prefix on your table names but don't want it in the model
|
||||
| and relation names, specify it here.
|
||||
|
|
||||
*/
|
||||
|
||||
'table_prefix' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Lower table name before doing studly
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| If tables names are capitalised using studly produces incorrect name
|
||||
| this can help fix it ie TABLE_NAME now becomes TableName
|
||||
|
|
||||
*/
|
||||
|
||||
'lower_table_name_first' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Model Names
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default the generator will create models with names that match your tables.
|
||||
| However, if you wish to manually override the naming, you can specify a mapping
|
||||
| here between table and model names.
|
||||
|
|
||||
| Example:
|
||||
| A table called 'billing_invoices' will generate a model called `BillingInvoice`,
|
||||
| but you'd prefer it to generate a model called 'Invoice'. Therefore, you'd add
|
||||
| the following array key and value:
|
||||
| 'billing_invoices' => 'Invoice',
|
||||
*/
|
||||
|
||||
'model_names' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Relation Name Strategy
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| How the relations should be named in your models.
|
||||
|
|
||||
| 'related' Use the related table as the relation name.
|
||||
| (post.author --> user.id)
|
||||
generates Post::user() and User::posts()
|
||||
|
|
||||
| 'foreign_key' Use the foreign key as the relation name.
|
||||
| This can help to provide more meaningful relationship names, and avoids naming conflicts
|
||||
| if you have more than one relationship between two tables.
|
||||
| (post.author_id --> user.id)
|
||||
| generates Post::author() and User::posts_where_author()
|
||||
| (post.editor_id --> user.id)
|
||||
| generates Post::editor() and User::posts_where_editor()
|
||||
| ID suffixes can be omitted from foreign keys.
|
||||
| (post.author --> user.id)
|
||||
| (post.editor --> user.id)
|
||||
| generates the same as above.
|
||||
| Where the foreign key matches the related table name, it behaves as per the 'related' strategy.
|
||||
| (post.user_id --> user.id)
|
||||
| generates Post::user() and User::posts()
|
||||
*/
|
||||
|
||||
'relation_name_strategy' => 'related',
|
||||
// 'relation_name_strategy' => 'foreign_key',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Determines need or not to generate constants with properties names like
|
||||
|
|
||||
| ...
|
||||
| const AGE = 'age';
|
||||
| const USER_NAME = 'user_name';
|
||||
| ...
|
||||
|
|
||||
| that later can be used in QueryBuilder like
|
||||
|
|
||||
| ...
|
||||
| $builder->select([User::USER_NAME])->where(User::AGE, '<=', 18);
|
||||
| ...
|
||||
|
|
||||
| that helps to avoid typos in strings when typing field names and allows to use
|
||||
| code competition with available model's field names.
|
||||
*/
|
||||
'with_property_constants' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Optionally includes a full list of columns in the base generated models,
|
||||
| which can be used to avoid making calls like
|
||||
|
|
||||
| ...
|
||||
| \Illuminate\Support\Facades\Schema::getColumnListing
|
||||
| ...
|
||||
|
|
||||
| which can be slow, especially for large tables.
|
||||
*/
|
||||
'with_column_list' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Disable Pluralization Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can disable pluralization tables and relations
|
||||
|
|
||||
*/
|
||||
'pluralize' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Disable Pluralization Except For Certain Tables
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can enable pluralization for certain tables
|
||||
|
|
||||
*/
|
||||
'override_pluralize_for' => [
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Move $hidden property to base files
|
||||
|--------------------------------------------------------------------------
|
||||
| When base_files is true you can set hidden_in_base_files to true
|
||||
| if you want the $hidden to be generated in base files
|
||||
|
|
||||
*/
|
||||
'hidden_in_base_files' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Move $fillable property to base files
|
||||
|--------------------------------------------------------------------------
|
||||
| When base_files is true you can set fillable_in_base_files to true
|
||||
| if you want the $fillable to be generated in base files
|
||||
|
|
||||
*/
|
||||
'fillable_in_base_files' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Generate return types for relation methods.
|
||||
|--------------------------------------------------------------------------
|
||||
| When enable_return_types is set to true, return type declarations are added
|
||||
| to all generated relation methods for your models.
|
||||
|
|
||||
| NOTE: This requires PHP 7.0 or later.
|
||||
|
|
||||
*/
|
||||
'enable_return_types' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Specifics
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| In this section you may define the default configuration for each model
|
||||
| that will be generated from a specific database. You can also nest
|
||||
| table specific configurations.
|
||||
| These values will override those defined in the section above.
|
||||
|
|
||||
*/
|
||||
|
||||
// 'shop' => [
|
||||
// 'path' => app_path(),
|
||||
// 'namespace' => 'App',
|
||||
// 'snake_attributes' => false,
|
||||
// 'qualified_tables' => true,
|
||||
// 'use' => [
|
||||
// Reliese\Database\Eloquent\BitBooleans::class,
|
||||
// ],
|
||||
// 'except' => ['migrations'],
|
||||
// 'only' => ['users'],
|
||||
// // Table Specifics Bellow:
|
||||
// 'user' => [
|
||||
// // Don't use any default trait
|
||||
// 'use' => [],
|
||||
// ]
|
||||
// ],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Connection Specifics
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| In this section you may define the default configuration for each model
|
||||
| that will be generated from a specific connection. You can also nest
|
||||
| database and table specific configurations.
|
||||
|
|
||||
| You may wish to use connection specific config for setting a parent
|
||||
| model with a read only setup, or enforcing a different set of rules
|
||||
| for a connection, e.g. using snake_case naming over CamelCase naming.
|
||||
|
|
||||
| This supports nesting with the following key configuration values, in
|
||||
| reverse precedence order (i.e. the last one found becomes the value).
|
||||
|
|
||||
| connections.{connection_name}.property
|
||||
| connections.{connection_name}.{database_name}.property
|
||||
| connections.{connection_name}.{table_name}.property
|
||||
| connections.{connection_name}.{database_name}.{table_name}.property
|
||||
|
|
||||
| These values will override those defined in the section above.
|
||||
|
|
||||
*/
|
||||
|
||||
// 'connections' => [
|
||||
// 'read_only_external' => [
|
||||
// 'parent' => \App\Models\ReadOnlyModel::class,
|
||||
// 'connection' => true,
|
||||
// 'users' => [
|
||||
// 'connection' => false,
|
||||
// ],
|
||||
// 'my_other_database' => [
|
||||
// 'password_resets' => [
|
||||
// 'connection' => false,
|
||||
// ]
|
||||
// ]
|
||||
// ],
|
||||
// ],
|
||||
];
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => 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", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'sys_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,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => 'sys_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' => 'sys_failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
|
||||
'%s%s',
|
||||
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
|
||||
Sanctum::currentApplicationUrlWithPort()
|
||||
))),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This array contains the authentication guards that will be checked when
|
||||
| Sanctum is trying to authenticate a request. If none of these guards
|
||||
| are able to authenticate the request, Sanctum will use the bearer
|
||||
| token that's present on an incoming request for authentication.
|
||||
|
|
||||
*/
|
||||
|
||||
'guard' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expiration Minutes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value controls the number of minutes until an issued token will be
|
||||
| considered expired. This will override any values set in the token's
|
||||
| "expires_at" attribute, but first-party sessions are not affected.
|
||||
|
|
||||
*/
|
||||
|
||||
'expiration' => null,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Token Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sanctum can prefix new tokens in order to take advantage of numerous
|
||||
| security scanning initiatives maintained by open source platforms
|
||||
| that notify developers if they commit tokens into repositories.
|
||||
|
|
||||
| See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
|
||||
|
|
||||
*/
|
||||
|
||||
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sanctum Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When authenticating your first-party SPA with Sanctum you may need to
|
||||
| customize some of the middleware Sanctum uses while processing the
|
||||
| request. You may change the middleware listed below as required.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => [
|
||||
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
|
||||
'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class,
|
||||
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing extends to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'token' => env('AWS_SESSION_TOKEN'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_KEY'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
'mailgun' => [
|
||||
'domain' => env('MAILGUN_DOMAIN'),
|
||||
'secret' => env('MAILGUN_SECRET'),
|
||||
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
|
||||
'scheme' => 'https',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "apc",
|
||||
| "memcached", "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'redis'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => 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: "apc", "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(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 and all 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),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* 系统用户相关数据表
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// 系统用户表
|
||||
if (! Schema::hasTable('sys_user')) {
|
||||
Schema::create('sys_user', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('系统用户ID');
|
||||
$table->string('username', 20)->unique()->comment('用户名');
|
||||
$table->string('password', 100)->comment('密码');
|
||||
$table->string('nickname', 20)->default('')->comment('昵称');
|
||||
$table->integer('avatar_id')->nullable()->comment('头像');
|
||||
$table->integer('sex')->default(0)->comment('性别(男、女)');
|
||||
$table->string('bio', 255)->default('')->nullable()->comment('个人简介');
|
||||
$table->string('mobile', 20)->default('')->comment('手机号');
|
||||
$table->string('email', 50)->unique()->comment('邮箱');
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->integer('dept_id')->default(0)->comment('部门ID');
|
||||
$table->string('login_ip', 60)->default('')->comment('最后登录IP');
|
||||
$table->timestamp('login_time')->nullable()->comment('最后登录时间');
|
||||
$table->integer('status')->default(1)->comment('状态(1正常 0停用)');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
$table->comment('系统用户表');
|
||||
});
|
||||
}
|
||||
|
||||
// 系统用户角色表
|
||||
if (! Schema::hasTable('sys_role')) {
|
||||
Schema::create('sys_role', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('角色ID');
|
||||
$table->string('name', 20)->default('')->comment('角色名称');
|
||||
$table->integer('sort')->default(0)->comment('排序');
|
||||
$table->string('description', 100)->default('')->comment('角色描述');
|
||||
$table->integer('status')->default(1)->comment('状态(1正常 0停用)');
|
||||
$table->timestamps();
|
||||
$table->comment('系统用户角色表');
|
||||
});
|
||||
}
|
||||
|
||||
// 系统用户角色中间表
|
||||
if (! Schema::hasTable('sys_user_role')) {
|
||||
Schema::create('sys_user_role', function (Blueprint $table) {
|
||||
$table->integer('user_id')->comment('用户ID');
|
||||
$table->integer('role_id')->comment('角色ID');
|
||||
$table->unique(['user_id', 'role_id'], 'user_role_unique');
|
||||
$table->comment('系统用户角色关联表');
|
||||
});
|
||||
}
|
||||
|
||||
// 系统用户部门表
|
||||
if (! Schema::hasTable('sys_dept')) {
|
||||
Schema::create('sys_dept', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('部门ID');
|
||||
$table->integer('parent_id')->default(0)->comment('父级ID');
|
||||
$table->string('name', 100)->default('')->comment('部门名称');
|
||||
$table->string('code', 100)->unique()->default('')->comment('部门编码');
|
||||
$table->tinyInteger('type')->default(0)->comment('部门类型 0:公司 1:部门 2:岗位');
|
||||
$table->integer('sort')->default(0)->comment('排序');
|
||||
$table->string('phone', '20')->default('')->nullable()->comment('部门电话');
|
||||
$table->string('email', '50')->default('')->nullable()->comment('部门邮箱');
|
||||
$table->string('address', '255')->default('')->nullable()->comment('部门地址');
|
||||
$table->string('remark', '255')->default('')->nullable()->comment('备注');
|
||||
$table->integer('status')->default(0)->comment('部门状态(0正常 1停用)');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
$table->comment('系统用户部门表');
|
||||
});
|
||||
}
|
||||
|
||||
// 系统用户权限表
|
||||
if (! Schema::hasTable('sys_rule')) {
|
||||
Schema::create('sys_rule', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('权限ID');
|
||||
$table->integer('parent_id')->default(0)->comment('父级ID');
|
||||
$table->string('type', 20)->comment("类型:'menu' | 'route' | 'rule'");
|
||||
$table->string('key', 100)->unique()->comment('唯一标识');
|
||||
$table->string('name', 100)->comment('名称');
|
||||
$table->string('path', 100)->nullable()->comment('路径');
|
||||
$table->string('icon', 100)->nullable()->comment('图标');
|
||||
$table->integer('order')->default(0)->comment('排序');
|
||||
$table->string('local', 100)->nullable()->comment('语言包');
|
||||
$table->integer('status')->default(1)->comment('状态:1、正常,0、禁用');
|
||||
$table->integer('hidden')->default(1)->comment('显示:1、显示,0、隐藏');
|
||||
$table->integer('link')->default(0)->comment('是否外链:1、是,0、否');
|
||||
$table->timestamps();
|
||||
$table->comment('系统用户权限表');
|
||||
});
|
||||
}
|
||||
|
||||
// 角色权限关联表
|
||||
if (! Schema::hasTable('sys_role_rule')) {
|
||||
Schema::create('sys_role_rule', function (Blueprint $table) {
|
||||
$table->integer('role_id')->comment('角色ID');
|
||||
$table->integer('rule_id')->comment('权限ID');
|
||||
$table->primary(['role_id', 'rule_id'], 'role_rule_primary');
|
||||
$table->comment('角色权限关联表');
|
||||
});
|
||||
}
|
||||
|
||||
// 登录日志表
|
||||
if (! Schema::hasTable('sys_login_record')) {
|
||||
Schema::create('sys_login_record', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('记录ID');
|
||||
$table->string('username', 20)->default('')->comment('用户名');
|
||||
$table->integer('user_id')->comment('用户ID');
|
||||
$table->string('ipaddr', 60)->default('')->comment('登录IP');
|
||||
$table->string('login_location', 255)->default('')->comment('登录地点');
|
||||
$table->string('browser', 255)->default('')->comment('浏览器');
|
||||
$table->string('os', 255)->default('')->comment('操作系统');
|
||||
$table->string('status', 1)->default('0')->comment('登录状态(0成功 1失败)');
|
||||
$table->string('msg', 255)->default('')->comment('提示消息');
|
||||
$table->timestamp('login_time')->comment('登录时间');
|
||||
$table->comment('系统用户登录日志表');
|
||||
});
|
||||
}
|
||||
|
||||
// token 表
|
||||
if (! Schema::hasTable('sys_access_token')) {
|
||||
Schema::create('sys_access_token', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->morphs('tokenable');
|
||||
$table->string('name');
|
||||
$table->string('token', 64)->unique();
|
||||
$table->text('abilities')->nullable();
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->comment('token table');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sys_user');
|
||||
Schema::dropIfExists('sys_rule');
|
||||
Schema::dropIfExists('sys_dept');
|
||||
Schema::dropIfExists('sys_role');
|
||||
Schema::dropIfExists('sys_user_role');
|
||||
Schema::dropIfExists('sys_role_rule');
|
||||
Schema::dropIfExists('sys_login_record');
|
||||
Schema::dropIfExists('sys_access_token');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* 字典数据表
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('sys_dict')) {
|
||||
Schema::create('sys_dict', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('name', 100)->comment('字典名称');
|
||||
$table->string('code', 100)->unique()->comment('字典编码');
|
||||
$table->string('describe', 500)->nullable()->comment('字典描述');
|
||||
$table->unsignedTinyInteger('status')->default(0)->comment('状态:0正常 1停用');
|
||||
$table->unsignedInteger('sort')->default(0)->comment('排序');
|
||||
$table->timestamps();
|
||||
$table->comment('字典类型表');
|
||||
});
|
||||
}
|
||||
if (! Schema::hasTable('sys_dict_item')) {
|
||||
Schema::create('sys_dict_item', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedBigInteger('dict_id')->comment('字典ID');
|
||||
$table->string('label', 100)->comment('字典标签');
|
||||
$table->string('value', 100)->comment('字典键值');
|
||||
$table->string('color', 50)->default('default')->comment('颜色');
|
||||
$table->unsignedTinyInteger('status')->default(0)->comment('状态:0正常 1停用');
|
||||
$table->unsignedInteger('sort')->default(0)->comment('排序');
|
||||
$table->timestamps();
|
||||
$table->index('dict_id');
|
||||
$table->comment('字典数据表');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sys_dict');
|
||||
Schema::dropIfExists('sys_dict_item');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
* 文件数据表
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('sys_file')) {
|
||||
Schema::create('sys_file', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('文件ID');
|
||||
$table->integer('group_id')->default(0)->comment('文件分组ID');
|
||||
$table->integer('channel')->default(10)->comment('上传来源(10:系统用户 20:App用户端)');
|
||||
$table->string('disk', 10)->comment('存储方式');
|
||||
$table->integer('file_type')->comment('文件类型');
|
||||
$table->string('file_name', 255)->comment('文件名称');
|
||||
$table->string('file_path', 255)->comment('文件路径');
|
||||
$table->integer('file_size')->comment('文件大小(字节)');
|
||||
$table->string('file_ext', 20)->comment('文件扩展名');
|
||||
$table->integer('uploader_id')->comment('上传者用户ID');
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
$table->comment('文件表');
|
||||
});
|
||||
}
|
||||
if (! Schema::hasTable('sys_file_group')) {
|
||||
Schema::create('sys_file_group', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('文件分组ID');
|
||||
$table->integer('parent_id')->default(0)->comment('上级ID');
|
||||
$table->string('name', 50)->comment('文件名称');
|
||||
$table->integer('sort')->comment('分组排序');
|
||||
$table->string('describe', 500)->nullable()->comment('分组描述');
|
||||
$table->timestamps();
|
||||
$table->comment('文件分组表');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sys_file');
|
||||
Schema::dropIfExists('sys_file_group');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('sys_config_items')) {
|
||||
Schema::create('sys_config_items', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('key', 50)->comment('设置项标示');
|
||||
$table->string('title', 50)->comment('设置标题');
|
||||
$table->string('describe', 500)->nullable()->default('')->comment('设置项描述');
|
||||
$table->string('values', 255)->nullable()->default('')->comment('设置值');
|
||||
$table->string('type', 50)->comment('设置类型');
|
||||
$table->string('options', 500)->nullable()->comment('options配置');
|
||||
$table->string('props', 500)->nullable()->comment('props配置');
|
||||
$table->integer('group_id')->comment('分组ID');
|
||||
$table->integer('sort')->comment('排序');
|
||||
$table->timestamps();
|
||||
$table->comment('系统设置表');
|
||||
$table->unique(['key', 'group_id']);
|
||||
});
|
||||
}
|
||||
if (! Schema::hasTable('sys_config_group')) {
|
||||
Schema::create('sys_config_group', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('title', 50)->comment('分组标题');
|
||||
$table->string('key', 50)->comment('分组KEY');
|
||||
$table->string('remark', 255)->nullable()->comment('备注描述');
|
||||
$table->timestamps();
|
||||
$table->comment('设置分组表');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sys_config_items');
|
||||
Schema::dropIfExists('sys_config_group');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('user')) {
|
||||
Schema::create('user', function (Blueprint $table) {
|
||||
$table->increments('id')->comment('用户ID');
|
||||
$table->string('username', 20)->unique()->comment('用户名');
|
||||
$table->string('password', 100)->comment('密码');
|
||||
$table->string('nickname', 20)->default('')->comment('昵称');
|
||||
$table->string('email', 50)->default('')->comment('邮箱');
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
$table->comment('APP用户表');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sys_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('sys_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('sys_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('sys_jobs');
|
||||
Schema::dropIfExists('sys_job_batches');
|
||||
Schema::dropIfExists('sys_failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('sys_app_settings')) {
|
||||
Schema::create('sys_app_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique()->index()->comment('配置键(点号表示法)');
|
||||
$table->tinyInteger('type')->index()->comment('类型枚举:10=String 15=Bool 20=Number 30=Array 40=Object 50=EncryptedString');
|
||||
$table->integer('n')->nullable()->comment('数字/布尔值');
|
||||
$table->string('s')->nullable()->comment('字符串值');
|
||||
$table->text('e')->nullable()->comment('扩展值:JSON/序列化/加密');
|
||||
$table->text('description')->nullable()->comment('描述');
|
||||
$table->timestamps();
|
||||
$table->comment('应用配置表(el-settings 风格列式类型存储)');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('sys_app_settings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
|
||||
if(! Schema::hasTable('agents')){
|
||||
Schema::create('agents', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('namespace', 500)->unique()->comment('完整类命名空间');
|
||||
$table->string('icon', 100)->nullable()->comment('图标');
|
||||
$table->string('name', 100)->comment('显示名称');
|
||||
$table->text('description')->nullable()->comment('描述');
|
||||
$table->json('tags')->nullable()->comment('标签');
|
||||
$table->boolean('enabled')->default(true)->comment('是否启用');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('agent_conversations')) {
|
||||
Schema::create('agent_conversations', function (Blueprint $table) {
|
||||
$table->string('id', 36)->primary();
|
||||
$table->foreignId('user_id')->nullable();
|
||||
$table->string('title');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'updated_at']);
|
||||
});
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('agent_conversation_messages')) {
|
||||
Schema::create('agent_conversation_messages', function (Blueprint $table) {
|
||||
$table->string('id', 36)->primary();
|
||||
$table->string('conversation_id', 36)->index();
|
||||
$table->foreignId('user_id')->nullable();
|
||||
$table->string('agent');
|
||||
$table->string('role', 25);
|
||||
$table->text('content');
|
||||
$table->text('attachments');
|
||||
$table->text('tool_calls');
|
||||
$table->text('tool_results');
|
||||
$table->text('usage');
|
||||
$table->text('meta');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['conversation_id', 'user_id', 'updated_at'], 'conversation_index');
|
||||
$table->index(['user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('agents');
|
||||
Schema::dropIfExists('agent_conversations');
|
||||
Schema::dropIfExists('agent_conversation_messages');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
SysUserSeeder::class,
|
||||
SysDataSeeder::class,
|
||||
SysAgentSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Modules\SystemAgent\Models\AgentModel;
|
||||
|
||||
class SysAgentSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$agents = [
|
||||
[
|
||||
'namespace' => 'Modules\SystemAgent\Ai\Agents\XinChatAgent',
|
||||
'name' => 'Xin Chat',
|
||||
'icon' => 'https://file.xinadmin.cn/file/favicons.ico',
|
||||
'description' => 'XinAdmin 默认 AI 助手,支持多轮对话和上下文记忆。',
|
||||
'tags' => ['XinChat', '智能对话'],
|
||||
'enabled' => true,
|
||||
],
|
||||
[
|
||||
'namespace' => 'Modules\SystemTool\Ai\Agents\TestAgent',
|
||||
'name' => 'Test Chat',
|
||||
'icon' => 'https://file.xinadmin.cn/file/favicons.ico',
|
||||
'description' => '测试智能体,用于开发调试。',
|
||||
'enabled' => false,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($agents as $agent) {
|
||||
AgentModel::firstOrCreate(
|
||||
['namespace' => $agent['namespace']],
|
||||
$agent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class SysDataSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$date = date('Y-m-d H:i:s');
|
||||
// 系统设置初始数据
|
||||
DB::table('sys_config_group')->insert([
|
||||
['id' => 1, 'title' => '网站设置', 'key' => 'web', 'remark' => '网站基础设置', 'created_at' => $date, 'updated_at' => $date]
|
||||
]);
|
||||
DB::table('sys_config_items')->insert([
|
||||
['id' => 1, 'group_id' => 1, 'key' => 'title', 'title' => '网站标题', 'describe' => '网站标题,用于展示在网站logo旁边和登录页面以及网页title中', 'values' => 'Xin Admin', 'type' => 'Input','options' => "", 'sort' => 0, 'created_at' => $date, 'updated_at' => $date,],
|
||||
['id' => 2, 'group_id' => 1, 'key' => 'logo', 'title' => '网站LOGO', 'describe' => '网站的LOGO,用于标识网站', 'values' => 'https://file.xinadmin.cn/file/favicons.ico', 'type' => 'Input','options' => "", 'sort' => 1, 'created_at' => $date, 'updated_at' => $date,],
|
||||
['id' => 3, 'group_id' => 1, 'key' => 'subtitle', 'title' => '网站副标题', 'describe' => '网站副标题,展示在登录页面标题的下面', 'values' => 'Xin Admin 快速开发框架', 'type' => 'Input','options' => "", 'sort' => 2, 'created_at' => $date, 'updated_at' => $date,],
|
||||
['id' => 4, 'group_id' => 1, 'key' => 'describe', 'title' => '网站描述', 'describe' => '网站的基本描述', 'values' => '没有描述', 'type' => 'TextArea','options' => "", 'sort' => 2, 'created_at' => $date, 'updated_at' => $date,],
|
||||
]);
|
||||
// 字典类型初始数据
|
||||
DB::table('sys_dict')->insert([
|
||||
['id' => 1, 'name' => '用户性别', 'code' => 'sys_user_sex', 'describe' => '用户性别字典', 'status' => 0, 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 2, 'name' => '菜单状态', 'code' => 'sys_show_hide', 'describe' => '菜单状态字典', 'status' => 0, 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 3, 'name' => '系统开关', 'code' => 'sys_normal_disable', 'describe' => '系统开关字典', 'status' => 0, 'sort' => 3, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 4, 'name' => '权限类型', 'code' => 'sys_rule_type', 'describe' => '系统权限类型字典', 'status' => 0, 'sort' => 4, 'created_at' => $date, 'updated_at' => $date],
|
||||
]);
|
||||
// 字典数据初始数据
|
||||
DB::table('sys_dict_item')->insert([
|
||||
// 用户性别
|
||||
['id' => 1, 'dict_id' => 1, 'label' => '男', 'value' => '0', 'color' => 'blue', 'status' => 0, 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 2, 'dict_id' => 1, 'label' => '女', 'value' => '1', 'color' => 'magenta', 'status' => 0, 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 3, 'dict_id' => 1, 'label' => '未知', 'value' => '2', 'color' => 'default', 'status' => 0, 'sort' => 3, 'created_at' => $date, 'updated_at' => $date],
|
||||
// 菜单状态
|
||||
['id' => 4, 'dict_id' => 2, 'label' => '显示', 'value' => '0', 'color' => 'green', 'status' => 0, 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 5, 'dict_id' => 2, 'label' => '隐藏', 'value' => '1', 'color' => 'red', 'status' => 0, 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
// 系统开关
|
||||
['id' => 6, 'dict_id' => 3, 'label' => '正常', 'value' => '0', 'color' => 'green', 'status' => 0, 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 7, 'dict_id' => 3, 'label' => '停用', 'value' => '1', 'color' => 'red', 'status' => 0, 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
// 权限类型
|
||||
['id' => 8, 'dict_id' => 4, 'label' => '路由', 'value' => 'route', 'color' => 'blue', 'status' => 0, 'sort' => 1, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 9, 'dict_id' => 4, 'label' => '菜单项', 'value' => 'menu', 'color' => 'green', 'status' => 0, 'sort' => 2, 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 10, 'dict_id' => 4, 'label' => '权限', 'value' => 'rule', 'color' => 'orange', 'status' => 0, 'sort' => 3, 'created_at' => $date, 'updated_at' => $date],
|
||||
]);
|
||||
// 文件初始化数据
|
||||
DB::table('sys_file_group')->insert([
|
||||
['id' => 1, 'name' => '默认分组', 'sort' => 0, 'describe' => '默认分组', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 2, 'name' => '用户头像', 'sort' => 1, 'describe' => '用户头像分组', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 3, 'name' => '系统图片', 'sort' => 2, 'describe' => '系统图片分组', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 4, 'name' => '用户上传', 'sort' => 3, 'describe' => '用户上传分组', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 5, 'name' => '系统附件', 'sort' => 4, 'describe' => '系统附件分组', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 6, 'name' => '其他文件', 'sort' => 5, 'describe' => '其他文件分组', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 7, 'name' => '临时文件', 'sort' => 6, 'describe' => '临时文件分组,用于存放临时上传的文件', 'created_at' => $date, 'updated_at' => $date],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SysUserSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$date = date('Y-m-d H:i:s');
|
||||
DB::table('sys_user')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'username' => 'admin',
|
||||
'nickname' => '管理员',
|
||||
'email' => Str::random(10).'@example.com',
|
||||
'password' => Hash::make('123456'),
|
||||
'dept_id' => 1,
|
||||
'avatar_id' => 1,
|
||||
'email_verified_at' => now(),
|
||||
'remember_token' => Str::random(10),
|
||||
'created_at' => $date,
|
||||
'updated_at' => $date,
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'username' => 'user',
|
||||
'nickname' => '财务',
|
||||
'email' => Str::random(10).'@example.com',
|
||||
'password' => Hash::make('123456'),
|
||||
'dept_id' => 2,
|
||||
'avatar_id' => 1,
|
||||
'email_verified_at' => now(),
|
||||
'remember_token' => Str::random(10),
|
||||
'created_at' => $date,
|
||||
'updated_at' => $date,
|
||||
]
|
||||
]);
|
||||
DB::table('sys_role')->insert([
|
||||
['id' => 1, 'name' => '超级管理员', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 2, 'name' => '财务', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 3, 'name' => '电商总监', 'created_at' => $date, 'updated_at' => $date],
|
||||
['id' => 4, 'name' => '市场运营', 'created_at' => $date, 'updated_at' => $date],
|
||||
]);
|
||||
DB::table('sys_dept')->insert([
|
||||
[
|
||||
'id' => 1,
|
||||
'name' => '新时代股份有限公司',
|
||||
'code' => 'A01',
|
||||
'type' => 0,
|
||||
'parent_id' => 0,
|
||||
'sort' => 0,
|
||||
'phone' => '19999999999',
|
||||
'email' => Str::random(10).'@example.com',
|
||||
'address' => '北京市海淀区某某街道103号',
|
||||
'remark' => '总公司',
|
||||
'created_at' => $date,
|
||||
'updated_at' => $date
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'name' => '新时代软件技术(洛阳)有限公司',
|
||||
'code' => 'A01-B01',
|
||||
'type' => 0,
|
||||
'parent_id' => 1,
|
||||
'sort' => 0,
|
||||
'phone' => '19999999999',
|
||||
'email' => Str::random(10).'@example.com',
|
||||
'address' => '河南省洛阳市龙门区某某街道99号',
|
||||
'remark' => '洛阳市分公司',
|
||||
'created_at' => $date,
|
||||
'updated_at' => $date
|
||||
],
|
||||
[
|
||||
'id' => 3,
|
||||
'name' => '新时代智能科技(郑州)有限公司',
|
||||
'code' => 'A01-B02',
|
||||
'type' => 0,
|
||||
'parent_id' => 1,
|
||||
'sort' => 0,
|
||||
'phone' => '19999999999',
|
||||
'email' => Str::random(10).'@example.com',
|
||||
'address' => '河南省郑州市二七区某某街道69号',
|
||||
'remark' => '郑州市分公司',
|
||||
'created_at' => $date,
|
||||
'updated_at' => $date
|
||||
],
|
||||
[
|
||||
'id' => 4,
|
||||
'name' => '新征程科技(南阳)有限公司',
|
||||
'code' => 'A01-B03',
|
||||
'type' => 0,
|
||||
'parent_id' => 1,
|
||||
'sort' => 2,
|
||||
'phone' => '19999999999',
|
||||
'email' => Str::random(10).'@example.com',
|
||||
'address' => '河南省南阳市卧龙区某某街道77号',
|
||||
'remark' => '南阳市分公司',
|
||||
'created_at' => $date,
|
||||
'updated_at' => $date
|
||||
],
|
||||
[
|
||||
'id' => 5,
|
||||
'name' => '新时代投资发展有限公司',
|
||||
'code' => 'B01',
|
||||
'type' => 0,
|
||||
'parent_id' => 0,
|
||||
'sort' => 2,
|
||||
'phone' => '19999999999',
|
||||
'email' => Str::random(10).'@example.com',
|
||||
'address' => '北京市海淀区人民路666号',
|
||||
'remark' => '我们坚信,卓越的投资在于发现价值,而卓越的投资管理在于创造价值。我们立志成为科技创业者身边最懂业务、最能赋能、最长情的资本伙伴,共同将创新的火种,转化为引领行业的参天大树。',
|
||||
'created_at' => $date,
|
||||
'updated_at' => $date
|
||||
],
|
||||
]);
|
||||
|
||||
$rules = [
|
||||
[
|
||||
'type' => 'menu',
|
||||
'name' => '仪表盘',
|
||||
'key' => 'dashboard',
|
||||
'icon' => 'PieChartOutlined',
|
||||
'local' => 'menu.dashboard',
|
||||
'children' => [
|
||||
[
|
||||
'type' => 'route',
|
||||
'name' => '分析页',
|
||||
'local' => "menu.analysis",
|
||||
'key' => 'dashboard.analysis',
|
||||
'path' => '/dashboard/analysis',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'menu',
|
||||
'name' => 'AI',
|
||||
'local' => "menu.ai",
|
||||
'icon' => "OpenAIOutlined",
|
||||
'key' => "ai",
|
||||
'children' => [
|
||||
[
|
||||
'type' => "route",
|
||||
'key' => "ai.chat",
|
||||
'name' => "AI对话",
|
||||
"path" => "/ai/chat",
|
||||
'local' => "menu.ai.chat",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '发送消息', 'key' => 'ai.chat.send'],
|
||||
['type' => 'rule', 'name' => '会话列表', 'key' => 'ai.chat.conversations'],
|
||||
['type' => 'rule', 'name' => '消息列表', 'key' => 'ai.chat.messages'],
|
||||
['type' => 'rule', 'name' => '删除会话', 'key' => 'ai.chat.delete'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'key' => "ai.conversation",
|
||||
'name' => "会话管理",
|
||||
"path" => "/ai/conversation",
|
||||
'local' => "menu.ai.conversation",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '查询会话列表', 'key' => 'ai.conversation.query'],
|
||||
['type' => 'rule', 'name' => '删除会话', 'key' => 'ai.conversation.delete'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'key' => "ai.agent",
|
||||
'name' => "Agent 管理",
|
||||
"path" => "/ai/agent",
|
||||
'local' => "menu.ai.agent",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '查询列表', 'key' => 'ai.agent.query'],
|
||||
['type' => 'rule', 'name' => '更新 Agent', 'key' => 'ai.agent.update'],
|
||||
]
|
||||
],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "menu",
|
||||
'name' => "系统管理",
|
||||
'local' => "menu.system",
|
||||
'icon' => "SettingOutlined",
|
||||
'key' => "system",
|
||||
'children' => [
|
||||
[
|
||||
'type' => "route",
|
||||
'key' => "system.user",
|
||||
'name' => "用户管理",
|
||||
'path' => "/system/user",
|
||||
'local' => "menu.system.user",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '查询列表', 'key' => 'system.user.query'],
|
||||
['type' => 'rule', 'name' => '新增用户', 'key' => 'system.user.create'],
|
||||
['type' => 'rule', 'name' => '修改用户', 'key' => 'system.user.update'],
|
||||
['type' => 'rule', 'name' => '删除用户', 'key' => 'system.user.delete'],
|
||||
['type' => 'rule', 'name' => '重置用户密码', 'key' => 'system.user.resetPassword'],
|
||||
['type' => 'rule', 'name' => '获取角色选项', 'key' => 'system.user.role'],
|
||||
['type' => 'rule', 'name' => '获取部门选项', 'key' => 'system.user.dept'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'key' => "system.dept",
|
||||
'name' => "部门管理",
|
||||
'path' => "/system/dept",
|
||||
'local' => "menu.system.dept",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '获取部门列表', 'key' => 'system.dept.query'],
|
||||
['type' => 'rule', 'name' => '新建部门', 'key' => 'system.dept.create'],
|
||||
['type' => 'rule', 'name' => '更新部门信息', 'key' => 'system.dept.update'],
|
||||
['type' => 'rule', 'name' => '删除部门', 'key' => 'system.dept.delete'],
|
||||
['type' => 'rule', 'name' => '获取部门用户', 'key' => 'system.dept.users'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'key' => "system.role",
|
||||
'name' => "角色管理",
|
||||
'path' => "/system/role",
|
||||
'local' => "menu.system.role",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '新增角色', 'key' => 'system.role.create'],
|
||||
['type' => 'rule', 'name' => '查询角色列表', 'key' => 'system.role.query'],
|
||||
['type' => 'rule', 'name' => '更新角色信息', 'key' => 'system.role.update'],
|
||||
['type' => 'rule', 'name' => '删除角色', 'key' => 'system.role.delete'],
|
||||
['type' => 'rule', 'name' => '设置启用状态', 'key' => 'system.role.status'],
|
||||
['type' => 'rule', 'name' => '获取角色用户', 'key' => 'system.role.users'],
|
||||
['type' => 'rule', 'name' => '设置角色权限', 'key' => 'system.role.setRule'],
|
||||
['type' => 'rule', 'name' => '获取权限选项', 'key' => 'system.role.ruleList'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'key' => "system.rule",
|
||||
'name' => "菜单管理",
|
||||
'path' => "/system/rule",
|
||||
'local' => "menu.system.rule",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '获取权限列表', 'key' => 'system.rule.query'],
|
||||
['type' => 'rule', 'name' => '创建权限规则', 'key' => 'system.rule.create'],
|
||||
['type' => 'rule', 'name' => '更新权限规则', 'key' => 'system.rule.update'],
|
||||
['type' => 'rule', 'name' => '删除权限规则', 'key' => 'system.rule.delete'],
|
||||
['type' => 'rule', 'name' => '获取父级权限', 'key' => 'system.rule.parentQuery'],
|
||||
['type' => 'rule', 'name' => '设置显示状态', 'key' => 'system.rule.show'],
|
||||
['type' => 'rule', 'name' => '设置启用状态', 'key' => 'system.rule.status'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'name' => "文件管理",
|
||||
'local' => "menu.system.file",
|
||||
'key' => "system.file",
|
||||
'path' => "/system/file",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '获取文件夹', 'key' => 'system.file.group.query'],
|
||||
['type' => 'rule', 'name' => '新增文件夹', 'key' => 'system.file.group.create'],
|
||||
['type' => 'rule', 'name' => '编辑文件夹', 'key' => 'system.file.group.update'],
|
||||
['type' => 'rule', 'name' => '删除文件夹', 'key' => 'system.file.group.delete'],
|
||||
['type' => 'rule', 'name' => '查询文件列表', 'key' => 'system.file.list.query'],
|
||||
['type' => 'rule', 'name' => '上传文件', 'key' => 'system.file.list.upload'],
|
||||
['type' => 'rule', 'name' => '下载文件', 'key' => 'system.file.list.download'],
|
||||
['type' => 'rule', 'name' => '删除文件', 'key' => 'system.file.list.delete'],
|
||||
['type' => 'rule', 'name' => '永久删除文件', 'key' => 'system.file.list.force-delete'],
|
||||
['type' => 'rule', 'name' => '恢复文件', 'key' => 'system.file.list.restore'],
|
||||
['type' => 'rule', 'name' => '查看回收站', 'key' => 'system.file.list.trashed'],
|
||||
['type' => 'rule', 'name' => '清空回收站', 'key' => 'system.file.list.clean-trashed'],
|
||||
['type' => 'rule', 'name' => '复制文件', 'key' => 'system.file.list.copy'],
|
||||
['type' => 'rule', 'name' => '移动文件', 'key' => 'system.file.list.move'],
|
||||
['type' => 'rule', 'name' => '重命名文件', 'key' => 'system.file.list.rename']
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'name' => "系统字典",
|
||||
'local' => "menu.system.dict",
|
||||
'key' => "system.dict",
|
||||
'path' => "/system/dict",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '字典列表', 'key' => 'system.dict.list.query'],
|
||||
['type' => 'rule', 'name' => '新增字典', 'key' => 'system.dict.list.create'],
|
||||
['type' => 'rule', 'name' => '删除字典', 'key' => 'system.dict.list.delete'],
|
||||
['type' => 'rule', 'name' => '更新字典', 'key' => 'system.dict.list.update'],
|
||||
['type' => 'rule', 'name' => '字典项列表', 'key' => 'system.dict.item.query'],
|
||||
['type' => 'rule', 'name' => '字典项新增', 'key' => 'system.dict.item.create'],
|
||||
['type' => 'rule', 'name' => '字典项编辑', 'key' => 'system.dict.item.update'],
|
||||
['type' => 'rule', 'name' => '字典项删除', 'key' => 'system.dict.item.delete'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'name' => "系统配置",
|
||||
'local' => "menu.system.config",
|
||||
'key' => "system.config",
|
||||
'path' => "/system/config",
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '配置列表', 'key' => 'system.config.items.query'],
|
||||
['type' => 'rule', 'name' => '新增配置', 'key' => 'system.config.items.create'],
|
||||
['type' => 'rule', 'name' => '编辑配置', 'key' => 'system.config.items.update'],
|
||||
['type' => 'rule', 'name' => '删除配置', 'key' => 'system.config.items.delete'],
|
||||
['type' => 'rule', 'name' => '保存配置', 'key' => 'system.config.items.save'],
|
||||
['type' => 'rule', 'name' => '刷新配置', 'key' => 'system.config.items.refresh'],
|
||||
['type' => 'rule', 'name' => '配置组编辑', 'key' => 'system.config.group.update'],
|
||||
['type' => 'rule', 'name' => '配置组删除', 'key' => 'system.config.items.item.delete'],
|
||||
['type' => 'rule', 'name' => '配置组列表', 'key' => 'system.config.group.query'],
|
||||
['type' => 'rule', 'name' => '配置组新增', 'key' => 'system.config.group.create'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'route',
|
||||
'key' => 'system.mail',
|
||||
'name' => '邮件配置',
|
||||
'path' => '/system/mail',
|
||||
'local' => 'menu.system.mail',
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '获取配置', 'key' => 'system.mail.config'],
|
||||
['type' => 'rule', 'name' => '保存配置', 'key' => 'system.mail.save'],
|
||||
['type' => 'rule', 'name' => '发送测试', 'key' => 'system.mail.test'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'route',
|
||||
'key' => 'system.storage',
|
||||
'name' => '存储配置',
|
||||
'path' => '/system/storage',
|
||||
'local' => 'menu.system.storage',
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '获取配置', 'key' => 'system.storage.config'],
|
||||
['type' => 'rule', 'name' => '保存配置', 'key' => 'system.storage.save'],
|
||||
['type' => 'rule', 'name' => '测试连接', 'key' => 'system.storage.test'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'route',
|
||||
'key' => 'system.ai',
|
||||
'name' => 'AI 配置',
|
||||
'path' => '/system/ai',
|
||||
'local' => 'menu.system.ai',
|
||||
'children' => [
|
||||
['type' => 'rule', 'name' => '获取可用AI列表', 'key' => 'system.ai.list'],
|
||||
['type' => 'rule', 'name' => '获取AI配置', 'key' => 'system.ai.config'],
|
||||
['type' => 'rule', 'name' => '保存AI配置', 'key' => 'system.ai.save'],
|
||||
['type' => 'rule', 'name' => '测试连接', 'key' => 'system.ai.test'],
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => "route",
|
||||
'name' => "系统信息",
|
||||
'local' => "menu.system.info",
|
||||
'key' => "system.info",
|
||||
'path' => "/system/info",
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'route',
|
||||
'name' => 'XinAdmin',
|
||||
'local' => "menu.xin-admin",
|
||||
'key' => "xin-admin",
|
||||
'icon' => "LinkOutlined",
|
||||
'link' => 1,
|
||||
'path' => 'https://xinadmin.cn',
|
||||
]
|
||||
];
|
||||
|
||||
$this->insertRules($rules);
|
||||
|
||||
DB::table('sys_role_rule')->insertUsing(
|
||||
['role_id', 'rule_id'],
|
||||
DB::table('sys_rule')
|
||||
->where('status', 1)
|
||||
->select(DB::raw('1 as role_id'), 'id')
|
||||
);
|
||||
|
||||
DB::table('sys_user_role')->insert([
|
||||
[
|
||||
'user_id' => 1,
|
||||
'role_id' => 1,
|
||||
],
|
||||
[
|
||||
'user_id' => 2,
|
||||
'role_id' => 2,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 递归插入权限规则数据
|
||||
*
|
||||
* @param array $rules 规则数据
|
||||
* @param int $pid 父级ID,默认为0(顶级)
|
||||
* @return void
|
||||
*/
|
||||
function insertRules(array $rules, int $pid = 0): void
|
||||
{
|
||||
$order = 0;
|
||||
foreach ($rules as $rule) {
|
||||
// 准备插入数据
|
||||
$insertData = [
|
||||
'parent_id' => $pid,
|
||||
'type' => $rule['type'],
|
||||
'key' => $rule['key'],
|
||||
'name' => $rule['name'],
|
||||
'path' => $rule['path'] ?? '',
|
||||
'icon' => $rule['icon'] ?? '',
|
||||
'order' => $order++,
|
||||
'local' => $rule['local'] ?? '',
|
||||
'status' => 1,
|
||||
'hidden' => 1,
|
||||
'link' => $rule['link'] ?? 0,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
// 插入数据并获取插入的ID
|
||||
$currentId = DB::table('sys_rule')->insertGetId($insertData);
|
||||
|
||||
// 如果有子菜单,递归插入
|
||||
if (!empty($rule['children']) && is_array($rule['children'])) {
|
||||
$this->insertRules($rule['children'], $currentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist', 'node_modules'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
"@typescript-eslint/no-explicit-any": ["off"],
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off"
|
||||
},
|
||||
},
|
||||
)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicons.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>XinAdmin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/web/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => 'These credentials do not match our records.',
|
||||
'password' => 'The provided password is incorrect.',
|
||||
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Previous',
|
||||
'next' => 'Next »',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| outcome such as failure due to an invalid password / reset token.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => 'Your password has been reset.',
|
||||
'sent' => 'We have emailed your password reset link.',
|
||||
'throttled' => 'Please wait before retrying.',
|
||||
'token' => 'This password reset token is invalid.',
|
||||
'user' => "We can't find a user with that email address.",
|
||||
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| System Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'file' => [
|
||||
'image' => 'image',
|
||||
'audio' => 'audio',
|
||||
'video' => 'video',
|
||||
'zip' => 'zip',
|
||||
'document' => 'document',
|
||||
'annex' => 'annex',
|
||||
'size_limit' => 'The file size exceeds the limit',
|
||||
'ext_limit' => 'The file extension is not allowed :ext',
|
||||
'upload_failed' => 'Upload failed',
|
||||
'not_found' => 'File not found',
|
||||
'delete_failed' => 'Delete failed',
|
||||
'download_failed' => 'Download failed',
|
||||
'invalid_visibility' => 'Invalid visibility setting',
|
||||
],
|
||||
'error' => [
|
||||
'no_permission' => 'Sorry, you do not have this permission at the moment, please contact the administrator',
|
||||
'route_not_exist' => 'Route does not exist',
|
||||
],
|
||||
'data_not_exist' => 'Data does not exist',
|
||||
];
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'not_login' => 'Please log in first',
|
||||
'user_not_exist' => 'User does not exist, please register first',
|
||||
'password_error' => 'Password error',
|
||||
'admin_login' => 'Admin Login',
|
||||
'admin_logout' => 'Admin Logout',
|
||||
'login_success' => 'Login Success',
|
||||
'login_error' => 'Login Error, Please check your username and password',
|
||||
'logout_success' => 'Logout Success',
|
||||
'old_password_error' => 'Old password error',
|
||||
'user_is_disabled' => 'User is disabled',
|
||||
'invalid_token' => 'Invalid Token',
|
||||
'refresh_token_expired' => 'Refresh Token Expired, Please login again',
|
||||
|
||||
'recharge_success' => 'Recharge Success',
|
||||
'reset_password' => 'Reset Password Success',
|
||||
];
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines contain the default error messages used by
|
||||
| the validator class. Some of these rules have multiple versions such
|
||||
| as the size rules. Feel free to tweak each of these messages here.
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => 'The :attribute field must be accepted.',
|
||||
'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
|
||||
'active_url' => 'The :attribute field must be a valid URL.',
|
||||
'after' => 'The :attribute field must be a date after :date.',
|
||||
'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
|
||||
'alpha' => 'The :attribute field must only contain letters.',
|
||||
'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
|
||||
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
|
||||
'array' => 'The :attribute field must be an array.',
|
||||
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
|
||||
'before' => 'The :attribute field must be a date before :date.',
|
||||
'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
|
||||
'between' => [
|
||||
'array' => 'The :attribute field must have between :min and :max items.',
|
||||
'file' => 'The :attribute field must be between :min and :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must be between :min and :max.',
|
||||
'string' => 'The :attribute field must be between :min and :max characters.',
|
||||
],
|
||||
'boolean' => 'The :attribute field must be true or false.',
|
||||
'can' => 'The :attribute field contains an unauthorized value.',
|
||||
'confirmed' => 'The :attribute field confirmation does not match.',
|
||||
'contains' => 'The :attribute field is missing a required value.',
|
||||
'current_password' => 'The password is incorrect.',
|
||||
'date' => 'The :attribute field must be a valid date.',
|
||||
'date_equals' => 'The :attribute field must be a date equal to :date.',
|
||||
'date_format' => 'The :attribute field must match the format :format.',
|
||||
'decimal' => 'The :attribute field must have :decimal decimal places.',
|
||||
'declined' => 'The :attribute field must be declined.',
|
||||
'declined_if' => 'The :attribute field must be declined when :other is :value.',
|
||||
'different' => 'The :attribute field and :other must be different.',
|
||||
'digits' => 'The :attribute field must be :digits digits.',
|
||||
'digits_between' => 'The :attribute field must be between :min and :max digits.',
|
||||
'dimensions' => 'The :attribute field has invalid image dimensions.',
|
||||
'distinct' => 'The :attribute field has a duplicate value.',
|
||||
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
|
||||
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
|
||||
'email' => 'The :attribute field must be a valid email address.',
|
||||
'ends_with' => 'The :attribute field must end with one of the following: :values.',
|
||||
'enum' => 'The selected :attribute is invalid.',
|
||||
'exists' => 'The selected :attribute is invalid.',
|
||||
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
|
||||
'file' => 'The :attribute field must be a file.',
|
||||
'filled' => 'The :attribute field must have a value.',
|
||||
'gt' => [
|
||||
'array' => 'The :attribute field must have more than :value items.',
|
||||
'file' => 'The :attribute field must be greater than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than :value.',
|
||||
'string' => 'The :attribute field must be greater than :value characters.',
|
||||
],
|
||||
'gte' => [
|
||||
'array' => 'The :attribute field must have :value items or more.',
|
||||
'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be greater than or equal to :value.',
|
||||
'string' => 'The :attribute field must be greater than or equal to :value characters.',
|
||||
],
|
||||
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
|
||||
'image' => 'The :attribute field must be an image.',
|
||||
'in' => 'The selected :attribute is invalid.',
|
||||
'in_array' => 'The :attribute field must exist in :other.',
|
||||
'integer' => 'The :attribute field must be an integer.',
|
||||
'ip' => 'The :attribute field must be a valid IP address.',
|
||||
'ipv4' => 'The :attribute field must be a valid IPv4 address.',
|
||||
'ipv6' => 'The :attribute field must be a valid IPv6 address.',
|
||||
'json' => 'The :attribute field must be a valid JSON string.',
|
||||
'list' => 'The :attribute field must be a list.',
|
||||
'lowercase' => 'The :attribute field must be lowercase.',
|
||||
'lt' => [
|
||||
'array' => 'The :attribute field must have less than :value items.',
|
||||
'file' => 'The :attribute field must be less than :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than :value.',
|
||||
'string' => 'The :attribute field must be less than :value characters.',
|
||||
],
|
||||
'lte' => [
|
||||
'array' => 'The :attribute field must not have more than :value items.',
|
||||
'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
|
||||
'numeric' => 'The :attribute field must be less than or equal to :value.',
|
||||
'string' => 'The :attribute field must be less than or equal to :value characters.',
|
||||
],
|
||||
'mac_address' => 'The :attribute field must be a valid MAC address.',
|
||||
'max' => [
|
||||
'array' => 'The :attribute field must not have more than :max items.',
|
||||
'file' => 'The :attribute field must not be greater than :max kilobytes.',
|
||||
'numeric' => 'The :attribute field must not be greater than :max.',
|
||||
'string' => 'The :attribute field must not be greater than :max characters.',
|
||||
],
|
||||
'max_digits' => 'The :attribute field must not have more than :max digits.',
|
||||
'mimes' => 'The :attribute field must be a file of type: :values.',
|
||||
'mimetypes' => 'The :attribute field must be a file of type: :values.',
|
||||
'min' => [
|
||||
'array' => 'The :attribute field must have at least :min items.',
|
||||
'file' => 'The :attribute field must be at least :min kilobytes.',
|
||||
'numeric' => 'The :attribute field must be at least :min.',
|
||||
'string' => 'The :attribute field must be at least :min characters.',
|
||||
],
|
||||
'min_digits' => 'The :attribute field must have at least :min digits.',
|
||||
'missing' => 'The :attribute field must be missing.',
|
||||
'missing_if' => 'The :attribute field must be missing when :other is :value.',
|
||||
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
|
||||
'missing_with' => 'The :attribute field must be missing when :values is present.',
|
||||
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
|
||||
'multiple_of' => 'The :attribute field must be a multiple of :value.',
|
||||
'not_in' => 'The selected :attribute is invalid.',
|
||||
'not_regex' => 'The :attribute field format is invalid.',
|
||||
'numeric' => 'The :attribute field must be a number.',
|
||||
'password' => [
|
||||
'letters' => 'The :attribute field must contain at least one letter.',
|
||||
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
|
||||
'numbers' => 'The :attribute field must contain at least one number.',
|
||||
'symbols' => 'The :attribute field must contain at least one symbol.',
|
||||
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
|
||||
],
|
||||
'present' => 'The :attribute field must be present.',
|
||||
'present_if' => 'The :attribute field must be present when :other is :value.',
|
||||
'present_unless' => 'The :attribute field must be present unless :other is :value.',
|
||||
'present_with' => 'The :attribute field must be present when :values is present.',
|
||||
'present_with_all' => 'The :attribute field must be present when :values are present.',
|
||||
'prohibited' => 'The :attribute field is prohibited.',
|
||||
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
|
||||
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
|
||||
'prohibits' => 'The :attribute field prohibits :other from being present.',
|
||||
'regex' => 'The :attribute field format is invalid.',
|
||||
'required' => 'The :attribute field is required.',
|
||||
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
|
||||
'required_if' => 'The :attribute field is required when :other is :value.',
|
||||
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
|
||||
'required_if_declined' => 'The :attribute field is required when :other is declined.',
|
||||
'required_unless' => 'The :attribute field is required unless :other is in :values.',
|
||||
'required_with' => 'The :attribute field is required when :values is present.',
|
||||
'required_with_all' => 'The :attribute field is required when :values are present.',
|
||||
'required_without' => 'The :attribute field is required when :values is not present.',
|
||||
'required_without_all' => 'The :attribute field is required when none of :values are present.',
|
||||
'same' => 'The :attribute field must match :other.',
|
||||
'size' => [
|
||||
'array' => 'The :attribute field must contain :size items.',
|
||||
'file' => 'The :attribute field must be :size kilobytes.',
|
||||
'numeric' => 'The :attribute field must be :size.',
|
||||
'string' => 'The :attribute field must be :size characters.',
|
||||
],
|
||||
'starts_with' => 'The :attribute field must start with one of the following: :values.',
|
||||
'string' => 'The :attribute field must be a string.',
|
||||
'timezone' => 'The :attribute field must be a valid timezone.',
|
||||
'unique' => 'The :attribute has already been taken.',
|
||||
'uploaded' => 'The :attribute failed to upload.',
|
||||
'uppercase' => 'The :attribute field must be uppercase.',
|
||||
'url' => 'The :attribute field must be a valid URL.',
|
||||
'ulid' => 'The :attribute field must be a valid ULID.',
|
||||
'uuid' => 'The :attribute field must be a valid UUID.',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify custom validation messages for attributes using the
|
||||
| convention "attribute.rule" to name the lines. This makes it quick to
|
||||
| specify a specific custom language line for a given attribute rule.
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => 'custom-message',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Validation Attributes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used to swap our attribute placeholder
|
||||
| with something more reader friendly such as "E-Mail Address" instead
|
||||
| of "email". This simply helps us make our message more expressive.
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used during authentication for various
|
||||
| messages that we need to display to the user. You are free to modify
|
||||
| these language lines according to your application's requirements.
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => '锁提供的凭据不匹配我们的记录。',
|
||||
'password' => '输入的密码不正确。',
|
||||
'throttle' => '登录尝试次数过多。请在:秒后重试。',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Pagination Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are used by the paginator library to build
|
||||
| the simple pagination links. You are free to change them to anything
|
||||
| you want to customize your views to better match your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'previous' => '« Previous',
|
||||
'next' => 'Next »',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Reset Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following language lines are the default lines which match reasons
|
||||
| that are given by the password broker for a password update attempt
|
||||
| outcome such as failure due to an invalid password / reset token.
|
||||
|
|
||||
*/
|
||||
|
||||
'reset' => '您的密码已重置。',
|
||||
'sent' => '我们已经通过电子邮件发送了您的密码重置链接。',
|
||||
'throttled' => '请稍候再试。',
|
||||
'token' => '此密码重置令牌无效。',
|
||||
'user' => "我们找不到有那个邮箱地址的用户。",
|
||||
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| System Language Lines
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
'file' => [
|
||||
'image' => '图片',
|
||||
'audio' => '音频',
|
||||
'video' => '视频',
|
||||
'zip' => '压缩包',
|
||||
'document' => '文档',
|
||||
'annex' => '附件',
|
||||
'size_limit' => '文件大小超出限制',
|
||||
'ext_limit' => '文件扩展名不允许 :ext',
|
||||
'upload_failed' => '上传失败',
|
||||
'not_found' => '文件不存在',
|
||||
'delete_failed' => '删除失败',
|
||||
'download_failed' => '下载失败',
|
||||
'invalid_visibility' => '无效的可见性设置',
|
||||
],
|
||||
'error' => [
|
||||
'no_permission' => '对不起,你暂时没有该权限,请联系管理员',
|
||||
'route_not_exist' => '路由不存在',
|
||||
],
|
||||
'data_not_exist' => '数据不存在',
|
||||
];
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 认证语言行
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 以下语言行在认证过程中用于显示各种消息。您可以根据应用程序的需求
|
||||
| 自由修改这些语言行。
|
||||
|
|
||||
*/
|
||||
|
||||
'not_login' => '请先登录',
|
||||
'user_not_exist' => '用户不存在,请先注册',
|
||||
'password_error' => '密码错误',
|
||||
'admin_login' => '管理员登录',
|
||||
'admin_logout' => '管理员退出',
|
||||
'login_success' => '登录成功',
|
||||
'login_error' => '登录失败,用户名或者密码错误!',
|
||||
'logout_success' => '退出成功',
|
||||
'old_password_error' => '旧密码错误',
|
||||
'user_is_disabled' => '用户已被禁用',
|
||||
'invalid_token' => '无效的令牌',
|
||||
'refresh_token_expired' => '刷新令牌已过期,请重新登录',
|
||||
|
||||
'recharge_success' => '充值成功',
|
||||
'reset_password' => '重置密码成功',
|
||||
|
||||
];
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 验证语言行
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 以下语言行包含验证器类使用的默认错误消息。其中一些规则有多个版本,
|
||||
| 例如大小规则。请随意调整这些消息。
|
||||
|
|
||||
*/
|
||||
|
||||
'accepted' => ':attribute 必须被接受。',
|
||||
'accepted_if' => '当 :other 为 :value 时,:attribute 必须被接受。',
|
||||
'active_url' => ':attribute 必须是一个有效的 URL。',
|
||||
'after' => ':attribute 必须是一个在 :date 之后的日期。',
|
||||
'after_or_equal' => ':attribute 必须是一个在 :date 之后或相等的日期。',
|
||||
'alpha' => ':attribute 只能包含字母。',
|
||||
'alpha_dash' => ':attribute 只能包含字母、数字、破折号和下划线。',
|
||||
'alpha_num' => ':attribute 只能包含字母和数字。',
|
||||
'array' => ':attribute 必须是一个数组。',
|
||||
'ascii' => ':attribute 只能包含单字节字母数字字符和符号。',
|
||||
'before' => ':attribute 必须是一个在 :date 之前的日期。',
|
||||
'before_or_equal' => ':attribute 必须是一个在 :date 之前或相等的日期。',
|
||||
'between' => [
|
||||
'array' => ':attribute 必须包含 :min 到 :max 个项目。',
|
||||
'file' => ':attribute 必须介于 :min 到 :max KB 之间。',
|
||||
'numeric' => ':attribute 必须介于 :min 到 :max 之间。',
|
||||
'string' => ':attribute 必须介于 :min 到 :max 个字符之间。',
|
||||
],
|
||||
'boolean' => ':attribute 必须为 true 或 false。',
|
||||
'can' => ':attribute 包含未授权的值。',
|
||||
'confirmed' => ':attribute 确认不匹配。',
|
||||
'contains' => ':attribute 缺少必需的值。',
|
||||
'current_password' => '密码不正确。',
|
||||
'date' => ':attribute 必须是一个有效的日期。',
|
||||
'date_equals' => ':attribute 必须是一个等于 :date 的日期。',
|
||||
'date_format' => ':attribute 必须符合格式 :format。',
|
||||
'decimal' => ':attribute 必须有 :decimal 位小数。',
|
||||
'declined' => ':attribute 必须被拒绝。',
|
||||
'declined_if' => '当 :other 为 :value 时,:attribute 必须被拒绝。',
|
||||
'different' => ':attribute 和 :other 必须不同。',
|
||||
'digits' => ':attribute 必须是 :digits 位数字。',
|
||||
'digits_between' => ':attribute 必须介于 :min 到 :max 位数字之间。',
|
||||
'dimensions' => ':attribute 具有无效的图片尺寸。',
|
||||
'distinct' => ':attribute 具有重复的值。',
|
||||
'doesnt_end_with' => ':attribute 不能以以下之一结尾::values。',
|
||||
'doesnt_start_with' => ':attribute 不能以以下之一开头::values。',
|
||||
'email' => ':attribute 必须是一个有效的电子邮件地址。',
|
||||
'ends_with' => ':attribute 必须以以下之一结尾::values。',
|
||||
'enum' => '所选的 :attribute 无效。',
|
||||
'exists' => '所选的 :attribute 无效。',
|
||||
'extensions' => ':attribute 必须具有以下扩展名之一::values。',
|
||||
'file' => ':attribute 必须是一个文件。',
|
||||
'filled' => ':attribute 必须有一个值。',
|
||||
'gt' => [
|
||||
'array' => ':attribute 必须包含超过 :value 个项目。',
|
||||
'file' => ':attribute 必须大于 :value KB。',
|
||||
'numeric' => ':attribute 必须大于 :value。',
|
||||
'string' => ':attribute 必须大于 :value 个字符。',
|
||||
],
|
||||
'gte' => [
|
||||
'array' => ':attribute 必须包含 :value 个项目或更多。',
|
||||
'file' => ':attribute 必须大于或等于 :value KB。',
|
||||
'numeric' => ':attribute 必须大于或等于 :value。',
|
||||
'string' => ':attribute 必须大于或等于 :value 个字符。',
|
||||
],
|
||||
'hex_color' => ':attribute 必须是一个有效的十六进制颜色。',
|
||||
'image' => ':attribute 必须是一张图片。',
|
||||
'in' => '所选的 :attribute 无效。',
|
||||
'in_array' => ':attribute 必须在 :other 中存在。',
|
||||
'integer' => ':attribute 必须是一个整数。',
|
||||
'ip' => ':attribute 必须是一个有效的 IP 地址。',
|
||||
'ipv4' => ':attribute 必须是一个有效的 IPv4 地址。',
|
||||
'ipv6' => ':attribute 必须是一个有效的 IPv6 地址。',
|
||||
'json' => ':attribute 必须是一个有效的 JSON 字符串。',
|
||||
'list' => ':attribute 必须是一个列表。',
|
||||
'lowercase' => ':attribute 必须是小写字母。',
|
||||
'lt' => [
|
||||
'array' => ':attribute 必须包含少于 :value 个项目。',
|
||||
'file' => ':attribute 必须小于 :value KB。',
|
||||
'numeric' => ':attribute 必须小于 :value。',
|
||||
'string' => ':attribute 必须小于 :value 个字符。',
|
||||
],
|
||||
'lte' => [
|
||||
'array' => ':attribute 必须包含不超过 :value 个项目。',
|
||||
'file' => ':attribute 必须小于或等于 :value KB。',
|
||||
'numeric' => ':attribute 必须小于或等于 :value。',
|
||||
'string' => ':attribute 必须小于或等于 :value 个字符。',
|
||||
],
|
||||
'mac_address' => ':attribute 必须是一个有效的 MAC 地址。',
|
||||
'max' => [
|
||||
'array' => ':attribute 不能包含超过 :max 个项目。',
|
||||
'file' => ':attribute 不能大于 :max KB。',
|
||||
'numeric' => ':attribute 不能大于 :max。',
|
||||
'string' => ':attribute 不能大于 :max 个字符。',
|
||||
],
|
||||
'max_digits' => ':attribute 不能超过 :max 位数字。',
|
||||
'mimes' => ':attribute 必须是一个类型为 :values 的文件。',
|
||||
'mimetypes' => ':attribute 必须是一个类型为 :values 的文件。',
|
||||
'min' => [
|
||||
'array' => ':attribute 必须包含至少 :min 个项目。',
|
||||
'file' => ':attribute 必须至少为 :min KB。',
|
||||
'numeric' => ':attribute 必须至少为 :min。',
|
||||
'string' => ':attribute 必须至少为 :min 个字符。',
|
||||
],
|
||||
'min_digits' => ':attribute 必须至少为 :min 位数字。',
|
||||
'missing' => ':attribute 必须缺失。',
|
||||
'missing_if' => '当 :other 为 :value 时,:attribute 必须缺失。',
|
||||
'missing_unless' => '除非 :other 为 :value,否则 :attribute 必须缺失。',
|
||||
'missing_with' => '当 :values 存在时,:attribute 必须缺失。',
|
||||
'missing_with_all' => '当 :values 存在时,:attribute 必须缺失。',
|
||||
'multiple_of' => ':attribute 必须是 :value 的倍数。',
|
||||
'not_in' => '所选的 :attribute 无效。',
|
||||
'not_regex' => ':attribute 格式无效。',
|
||||
'numeric' => ':attribute 必须是一个数字。',
|
||||
'password' => [
|
||||
'letters' => ':attribute 必须包含至少一个字母。',
|
||||
'mixed' => ':attribute 必须包含至少一个大写字母和一个小写字母。',
|
||||
'numbers' => ':attribute 必须包含至少一个数字。',
|
||||
'symbols' => ':attribute 必须包含至少一个符号。',
|
||||
'uncompromised' => '给定的 :attribute 已出现在数据泄露中。请选择不同的 :attribute。',
|
||||
],
|
||||
'present' => ':attribute 必须存在。',
|
||||
'present_if' => '当 :other 为 :value 时,:attribute 必须存在。',
|
||||
'present_unless' => '除非 :other 为 :value,否则 :attribute 必须存在。',
|
||||
'present_with' => '当 :values 存在时,:attribute 必须存在。',
|
||||
'present_with_all' => '当 :values 存在时,:attribute 必须存在。',
|
||||
'prohibited' => ':attribute 被禁止。',
|
||||
'prohibited_if' => '当 :other 为 :value 时,:attribute 被禁止。',
|
||||
'prohibited_unless' => '除非 :other 在 :values 中,否则 :attribute 被禁止。',
|
||||
'prohibits' => ':attribute 禁止 :other 存在。',
|
||||
'regex' => ':attribute 格式无效。',
|
||||
'required' => ':attribute 是必填项。',
|
||||
'required_array_keys' => ':attribute 必须包含以下条目::values。',
|
||||
'required_if' => '当 :other 为 :value 时,:attribute 是必填项。',
|
||||
'required_if_accepted' => '当 :other 被接受时,:attribute 是必填项。',
|
||||
'required_if_declined' => '当 :other 被拒绝时,:attribute 是必填项。',
|
||||
'required_unless' => '除非 :other 在 :values 中,否则 :attribute 是必填项。',
|
||||
'required_with' => '当 :values 存在时,:attribute 是必填项。',
|
||||
'required_with_all' => '当 :values 存在时,:attribute 是必填项。',
|
||||
'required_without' => '当 :values 不存在时,:attribute 是必填项。',
|
||||
'required_without_all' => '当 :values 都不存在时,:attribute 是必填项。',
|
||||
'same' => ':attribute 必须与 :other 匹配。',
|
||||
'size' => [
|
||||
'array' => ':attribute 必须包含 :size 个项目。',
|
||||
'file' => ':attribute 必须为 :size KB。',
|
||||
'numeric' => ':attribute 必须为 :size。',
|
||||
'string' => ':attribute 必须为 :size 个字符。',
|
||||
],
|
||||
'starts_with' => ':attribute 必须以以下之一开头::values。',
|
||||
'string' => ':attribute 必须是一个字符串。',
|
||||
'timezone' => ':attribute 必须是一个有效的时区。',
|
||||
'unique' => ':attribute 已被占用。',
|
||||
'uploaded' => ':attribute 上传失败。',
|
||||
'uppercase' => ':attribute 必须是大写字母。',
|
||||
'url' => ':attribute 必须是一个有效的 URL。',
|
||||
'ulid' => ':attribute 必须是一个有效的 ULID。',
|
||||
'uuid' => ':attribute 必须是一个有效的 UUID。',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 自定义验证语言行
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 在这里,您可以为属性指定自定义验证消息,使用 "attribute.rule" 的命名约定。
|
||||
| 这样可以快速为给定的属性规则指定特定的自定义语言行。
|
||||
|
|
||||
*/
|
||||
|
||||
'custom' => [
|
||||
'attribute-name' => [
|
||||
'rule-name' => '自定义消息',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 自定义验证属性
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 以下语言行用于将我们的属性占位符替换为更友好的内容,例如将 "email" 替换为 "电子邮件地址"。
|
||||
| 这有助于使我们的消息更具表现力。
|
||||
|
|
||||
*/
|
||||
|
||||
'attributes' => [],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
interface AnnoRoute
|
||||
{
|
||||
|
||||
/**
|
||||
* register route
|
||||
*
|
||||
* @param string $path
|
||||
* @return void
|
||||
*/
|
||||
public function register(string $path): void;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class AnnoRouteService implements AnnoRoute
|
||||
{
|
||||
|
||||
/**
|
||||
* register route
|
||||
* @param string|array $path
|
||||
* @return void
|
||||
*/
|
||||
public function register($path): void
|
||||
{
|
||||
$paths = is_array($path) ? $path : [$path];
|
||||
|
||||
foreach ($paths as $p) {
|
||||
$this->registerFromPath($p);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定路径注册路由
|
||||
*/
|
||||
private function registerFromPath(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()
|
||||
->in($path)
|
||||
->name('*Controller.php');
|
||||
|
||||
foreach ($finder as $controller) {
|
||||
$className = $this->getClassNameFromFile(
|
||||
$controller->getRealPath(),
|
||||
$controller->getPath()
|
||||
);
|
||||
if ($className && class_exists($className)) {
|
||||
RouteRegisterService::register($className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件路径和所在目录解析类名
|
||||
*/
|
||||
private function getClassNameFromFile(string $filePath, string $fileDir): ?string
|
||||
{
|
||||
// 读取文件内容获取命名空间
|
||||
$content = file_get_contents($filePath);
|
||||
|
||||
if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatch)) {
|
||||
// 没有命名空间,尝试使用 PSR-0 规则
|
||||
return $this->guessClassNameFromPath($filePath, $fileDir);
|
||||
}
|
||||
|
||||
$namespace = trim($namespaceMatch[1]);
|
||||
$className = basename($filePath, '.php');
|
||||
|
||||
return $namespace . '\\' . $className;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当文件没有命名空间时,通过路径猜测类名
|
||||
*/
|
||||
private function guessClassNameFromPath(string $filePath, string $fileDir): ?string
|
||||
{
|
||||
// 获取相对于项目根目录的路径
|
||||
$basePath = base_path();
|
||||
$relativePath = str_replace($basePath, '', $fileDir);
|
||||
|
||||
// 将路径转换为命名空间
|
||||
$namespace = str_replace('/', '\\', ltrim($relativePath, '/'));
|
||||
|
||||
// 移除开头的反斜杠并转换路径分隔符
|
||||
$namespace = trim($namespace, '\\');
|
||||
|
||||
$className = basename($filePath, '.php');
|
||||
|
||||
// 如果命名空间为空,直接返回类名
|
||||
if (empty($namespace)) {
|
||||
return $className;
|
||||
}
|
||||
|
||||
return $namespace . '\\' . $className;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use Modules\AnnoRoute\BaseAttribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class DeleteRoute extends BaseAttribute
|
||||
{
|
||||
/** @var string 请求方法 */
|
||||
public string $httpMethod = 'DELETE';
|
||||
|
||||
/**
|
||||
* @param string $route 路由地址
|
||||
* @param string|bool $authorize 权限字段
|
||||
* @param string|array $middleware 中间件
|
||||
* @param array $where 路由参数约束
|
||||
*/
|
||||
public function __construct(
|
||||
public string $route = '',
|
||||
public string|bool $authorize = true,
|
||||
public string | array $middleware = '',
|
||||
public array $where = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use Modules\AnnoRoute\BaseAttribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class GetRoute extends BaseAttribute
|
||||
{
|
||||
/** @var string 请求方法 */
|
||||
public string $httpMethod = 'GET';
|
||||
|
||||
/**
|
||||
* @param string $route 路由地址
|
||||
* @param string|bool $authorize 权限字段
|
||||
* @param string|array $middleware 中间件
|
||||
* @param array $where 路由参数约束
|
||||
*/
|
||||
public function __construct(
|
||||
public string $route = '',
|
||||
public string|bool $authorize = true,
|
||||
public string | array $middleware = '',
|
||||
public array $where = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use Modules\AnnoRoute\BaseAttribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class PostRoute extends BaseAttribute
|
||||
{
|
||||
/** @var string 请求方法 */
|
||||
public string $httpMethod = 'POST';
|
||||
|
||||
/**
|
||||
* @param string $route 路由地址
|
||||
* @param string|bool $authorize 权限字段
|
||||
* @param string|array $middleware 中间件
|
||||
* @param array $where 路由参数约束
|
||||
*/
|
||||
public function __construct(
|
||||
public string $route = '',
|
||||
public string|bool $authorize = true,
|
||||
public string | array $middleware = '',
|
||||
public array $where = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use Modules\AnnoRoute\BaseAttribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_METHOD)]
|
||||
class PutRoute extends BaseAttribute
|
||||
{
|
||||
/** @var string 请求方法 */
|
||||
public string $httpMethod = 'PUT';
|
||||
|
||||
/**
|
||||
* @param string $route 路由地址
|
||||
* @param string|bool $authorize 权限字段
|
||||
* @param string|array $middleware 中间件
|
||||
* @param array $where 路由参数约束
|
||||
*/
|
||||
public function __construct(
|
||||
public string $route = '',
|
||||
public string|bool $authorize = true,
|
||||
public string | array $middleware = '',
|
||||
public array $where = [],
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute\Attribute;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class RequestAttribute
|
||||
{
|
||||
/**
|
||||
* @param string $routePrefix 路由前缀
|
||||
* @param string $abilitiesPrefix 权限前缀
|
||||
* @param string | array $middleware 控制器中间件
|
||||
* @param string | null $authGuard 用户提供程序
|
||||
*/
|
||||
public function __construct(
|
||||
public string $routePrefix = '',
|
||||
public string $abilitiesPrefix = '',
|
||||
public string | array $middleware = '',
|
||||
public ?string $authGuard = null
|
||||
)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\Common\Enum\ShowType as ShopTypeEnum;
|
||||
|
||||
abstract class BaseAttribute
|
||||
{
|
||||
/**
|
||||
* 路由地址,使用该属性来指定路由地址,完整的路由地址由 RequestAttribute 的 routePrefix 和 route 拼接而成,例如:
|
||||
* RequestAttribute 的 routePrefix 为 /admin/user,Mapping 的 route 为 /create,那么完整的路由地址为 /admin/user/create
|
||||
*
|
||||
* Routing address, use this attribute to specify the routing address. The complete routing address is composed
|
||||
* of the routePrefix of RequestAttribute and the route concatenated, for example: If the routePrefix of RequestAttribute
|
||||
* is /admin/user and the route of Mapping is /create, then the complete routing address is /admin/user/create
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $route = '';
|
||||
|
||||
/**
|
||||
* 路由中间件,该属性用于指定路由中间件,多个中间件以数组形式传递,例如:['auth', 'admin']
|
||||
*
|
||||
* Route middleware, this attribute is used to specify the route middleware, multiple middleware are passed in
|
||||
* as an array, for example: ['auth', 'admin']
|
||||
*
|
||||
* @var string|array
|
||||
*/
|
||||
public string | array $middleware = '';
|
||||
|
||||
/**
|
||||
* HTTP 的请求方法
|
||||
*
|
||||
* HTTP request method
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $httpMethod = 'POST';
|
||||
|
||||
/**
|
||||
* 权限字符串,该属性用于指定权限【能力】字符串,用于权限【能力】认证,完整的能力字符串由 RequestAttribute 的 abilitiesPrefix 和 authorize
|
||||
* 拼接而成,例如:RequestAttribute 的 abilitiesPrefix 为 admin,Mapping 的 authorize 为 user,那么完整的权限【能力】字符串为 admin.user
|
||||
*
|
||||
* Permission string, this attribute is used to specify the permission string, used for permission authentication,
|
||||
* the complete permission string is composed of the abilitiesPrefix of RequestAttribute and the authorize concatenated,
|
||||
* for example: If the abilitiesPrefix of RequestAttribute is admin and the authorize of Mapping is user, then the complete
|
||||
* permission string is admin.user
|
||||
*
|
||||
* @var string | bool
|
||||
*/
|
||||
public string | bool $authorize = true;
|
||||
|
||||
/**
|
||||
* 路由参数约束,该属性用于指定路由参数的正则约束,例如:['id' => '[0-9]+']
|
||||
*
|
||||
* Route parameter constraints, this attribute is used to specify regex constraints for route parameters,
|
||||
* for example: ['id' => '[0-9]+']
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public array $where = [];
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
* @param $msg array|string
|
||||
* @return JsonResponse
|
||||
*/
|
||||
protected static function success(array|string $msg = ''): JsonResponse
|
||||
{
|
||||
if (is_array($msg)) {
|
||||
$data = $msg;
|
||||
$msg = '';
|
||||
} else {
|
||||
$data = [];
|
||||
}
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
'showType' => ShopTypeEnum::SUCCESS_MESSAGE->value,
|
||||
'msg' => $msg,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
|
||||
class RouteRegisterService
|
||||
{
|
||||
/**
|
||||
* @var array|string[]
|
||||
*/
|
||||
private static array $mapping = [
|
||||
GetRoute::class,
|
||||
PostRoute::class,
|
||||
PutRoute::class,
|
||||
DeleteRoute::class
|
||||
];
|
||||
|
||||
/**
|
||||
* register 注册路由
|
||||
*/
|
||||
public static function register(string $className): void
|
||||
{
|
||||
try {
|
||||
$classRef = new ReflectionClass($className);
|
||||
|
||||
$classAttr = collect($classRef->getAttributes());
|
||||
if($classAttr->isEmpty()) return;
|
||||
|
||||
$classAttrName = $classAttr->map->getName();
|
||||
if(! $classAttrName->contains(RequestAttribute::class)) {
|
||||
return;
|
||||
}
|
||||
$requestMapping = $classAttr->first(fn ($item) => $item->getName() == RequestAttribute::class);
|
||||
$routeInstance = $requestMapping->newInstance();
|
||||
// 默认参数
|
||||
$routePrefix = $routeInstance->routePrefix ?? '';
|
||||
$authGuard = $routeInstance->authGuard ?? null;
|
||||
$abilitiesPrefix = $routeInstance->abilitiesPrefix ?? '';
|
||||
$middleware = self::registerMiddleware($routeInstance->middleware);
|
||||
|
||||
$methods = $classRef->getMethods();
|
||||
|
||||
foreach ($methods as $method) {
|
||||
// 方法注解
|
||||
$attributes = $method->getAttributes();
|
||||
if(empty($attributes)) {
|
||||
continue;
|
||||
}
|
||||
$methodName = $method->getName();
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
if (in_array($attribute->getName(), self::$mapping)) {
|
||||
$instance = $attribute->newInstance();
|
||||
self::registerRoute(
|
||||
$instance,
|
||||
$methodName,
|
||||
$className,
|
||||
$authGuard,
|
||||
$middleware,
|
||||
$routePrefix,
|
||||
$abilitiesPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ReflectionException $e) {
|
||||
echo $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册路由
|
||||
* @param BaseAttribute $instance
|
||||
* @param string $method
|
||||
* @param string $className
|
||||
* @param string|null $authGuard
|
||||
* @param array $middleware
|
||||
* @param string $routePrefix
|
||||
* @param string $abilitiesPrefix
|
||||
* @return void
|
||||
*/
|
||||
private static function registerRoute(
|
||||
BaseAttribute $instance,
|
||||
string $method,
|
||||
string $className,
|
||||
string $authGuard = null,
|
||||
array $middleware = [],
|
||||
string $routePrefix = '',
|
||||
string $abilitiesPrefix = ''
|
||||
): void
|
||||
{
|
||||
$authorize = $instance->authorize;
|
||||
|
||||
$authMiddleware = [];
|
||||
if (!empty($authorize)) {
|
||||
$authMiddleware[] = 'auth:sanctum';
|
||||
if(! empty($authGuard) ) {
|
||||
$authMiddleware[] = 'authGuard:' . $authGuard;
|
||||
} else {
|
||||
$authMiddleware[] = 'authGuard';
|
||||
}
|
||||
if (is_string($authorize) && !empty($abilitiesPrefix)) {
|
||||
$authMiddleware[] = 'abilities:' . $abilitiesPrefix . '.' . $authorize;
|
||||
} else {
|
||||
$authMiddleware[] = 'abilities:' . $authorize;
|
||||
}
|
||||
}
|
||||
|
||||
$middleware = array_merge($authMiddleware, self::registerMiddleware($instance->middleware), $middleware);
|
||||
$route = Route::{Str::lower($instance->httpMethod)}(
|
||||
$routePrefix . $instance->route,
|
||||
[$className, $method]
|
||||
)->middleware(array_unique($middleware));
|
||||
|
||||
if (!empty($instance->where)) {
|
||||
$route->where($instance->where);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取中间件
|
||||
* @param $middleware
|
||||
* @return string[]
|
||||
*/
|
||||
private static function registerMiddleware($middleware): array
|
||||
{
|
||||
if(empty($middleware)) {
|
||||
return [];
|
||||
}
|
||||
if(is_array($middleware)) {
|
||||
return $middleware;
|
||||
}
|
||||
if (is_string($middleware)) {
|
||||
return [$middleware];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Modules\AnnoRoute;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind(AnnoRoute::class, AnnoRouteService::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class GenerateRouteHelperCommand extends Command
|
||||
{
|
||||
protected $signature = 'route:helper';
|
||||
|
||||
protected $description = 'Scan annotation routes and generate IDE route helper files for Laravel Idea';
|
||||
|
||||
private static array $mapping = [
|
||||
GetRoute::class,
|
||||
PostRoute::class,
|
||||
PutRoute::class,
|
||||
DeleteRoute::class,
|
||||
];
|
||||
|
||||
private static array $httpMethodMap = [
|
||||
GetRoute::class => 'get',
|
||||
PostRoute::class => 'post',
|
||||
PutRoute::class => 'put',
|
||||
DeleteRoute::class => 'delete',
|
||||
];
|
||||
|
||||
/** @var array<string, list<array{method: string, uri: string, controller: string, action: string, middleware: array}>> */
|
||||
private array $controllerRoutes = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$paths = [
|
||||
app_path('Http/Controllers'),
|
||||
base_path('modules'),
|
||||
];
|
||||
|
||||
$totalRoutes = 0;
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if (!is_dir($path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$finder = new Finder();
|
||||
$finder->files()->in($path)->name('*Controller.php');
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$className = $this->getClassNameFromFile(
|
||||
$file->getRealPath(),
|
||||
$file->getPath()
|
||||
);
|
||||
|
||||
if ($className && class_exists($className)) {
|
||||
$routes = $this->scanController($className);
|
||||
if (!empty($routes)) {
|
||||
$this->controllerRoutes[$className] = $routes;
|
||||
$totalRoutes += count($routes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->controllerRoutes)) {
|
||||
$this->warn('No annotation routes found.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->writeHelperFile();
|
||||
$this->info("Routes generated to: " . base_path('routes/api.php'));
|
||||
$this->info("Found {$totalRoutes} routes.");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function scanController(string $className): array
|
||||
{
|
||||
$routes = [];
|
||||
|
||||
try {
|
||||
$classRef = new ReflectionClass($className);
|
||||
} catch (ReflectionException) {
|
||||
return $routes;
|
||||
}
|
||||
|
||||
$classAttrs = collect($classRef->getAttributes());
|
||||
if ($classAttrs->isEmpty()) {
|
||||
return $routes;
|
||||
}
|
||||
|
||||
$hasRequestAttr = $classAttrs->some(
|
||||
fn($attr) => $attr->getName() === RequestAttribute::class
|
||||
);
|
||||
if (!$hasRequestAttr) {
|
||||
return $routes;
|
||||
}
|
||||
|
||||
$requestAttr = $classAttrs->first(
|
||||
fn($attr) => $attr->getName() === RequestAttribute::class
|
||||
);
|
||||
$requestInstance = $requestAttr->newInstance();
|
||||
$routePrefix = $requestInstance->routePrefix ?? '';
|
||||
$authGuard = $requestInstance->authGuard ?? null;
|
||||
$abilitiesPrefix = $requestInstance->abilitiesPrefix ?? '';
|
||||
$classMiddleware = $this->normalizeMiddleware($requestInstance->middleware);
|
||||
|
||||
foreach ($classRef->getMethods() as $method) {
|
||||
$attrs = $method->getAttributes();
|
||||
if (empty($attrs)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$methodName = $method->getName();
|
||||
|
||||
foreach ($attrs as $attr) {
|
||||
if (in_array($attr->getName(), self::$mapping)) {
|
||||
$instance = $attr->newInstance();
|
||||
|
||||
$authMiddleware = $this->buildAuthMiddleware(
|
||||
$instance->authorize,
|
||||
$authGuard,
|
||||
$abilitiesPrefix,
|
||||
);
|
||||
|
||||
$allMiddleware = array_values(array_unique(array_merge(
|
||||
$authMiddleware,
|
||||
$this->normalizeMiddleware($instance->middleware),
|
||||
$classMiddleware,
|
||||
)));
|
||||
|
||||
$routes[] = [
|
||||
'method' => self::$httpMethodMap[$attr->getName()],
|
||||
'uri' => $instance->route,
|
||||
'prefix' => trim($routePrefix, '/'),
|
||||
'controller' => $className,
|
||||
'action' => $methodName,
|
||||
'middleware' => $allMiddleware,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
private function buildAuthMiddleware(string|bool $authorize, ?string $authGuard, string $abilitiesPrefix): array
|
||||
{
|
||||
if (empty($authorize) || $authorize === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$authMiddleware = ['auth:sanctum'];
|
||||
|
||||
if (!empty($authGuard)) {
|
||||
$authMiddleware[] = 'authGuard:' . $authGuard;
|
||||
} else {
|
||||
$authMiddleware[] = 'authGuard';
|
||||
}
|
||||
|
||||
if (is_string($authorize) && !empty($abilitiesPrefix)) {
|
||||
$authMiddleware[] = 'abilities:' . $abilitiesPrefix . '.' . $authorize;
|
||||
} else {
|
||||
$authMiddleware[] = 'abilities:' . (is_string($authorize) ? $authorize : '');
|
||||
}
|
||||
|
||||
return $authMiddleware;
|
||||
}
|
||||
|
||||
private function normalizeMiddleware(string|array $middleware): array
|
||||
{
|
||||
if (empty($middleware)) {
|
||||
return [];
|
||||
}
|
||||
if (is_array($middleware)) {
|
||||
return $middleware;
|
||||
}
|
||||
return [$middleware];
|
||||
}
|
||||
|
||||
private function getClassNameFromFile(string $filePath, string $fileDir): ?string
|
||||
{
|
||||
$content = file_get_contents($filePath);
|
||||
|
||||
if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatch)) {
|
||||
return $this->guessClassNameFromPath($filePath, $fileDir);
|
||||
}
|
||||
|
||||
$namespace = trim($namespaceMatch[1]);
|
||||
$className = basename($filePath, '.php');
|
||||
|
||||
return $namespace . '\\' . $className;
|
||||
}
|
||||
|
||||
private function guessClassNameFromPath(string $filePath, string $fileDir): ?string
|
||||
{
|
||||
$basePath = base_path();
|
||||
$relativePath = str_replace($basePath, '', $fileDir);
|
||||
$namespace = str_replace('/', '\\', ltrim($relativePath, '/'));
|
||||
$namespace = trim($namespace, '\\');
|
||||
$className = basename($filePath, '.php');
|
||||
|
||||
return empty($namespace) ? $className : $namespace . '\\' . $className;
|
||||
}
|
||||
|
||||
private function writeHelperFile(): void
|
||||
{
|
||||
$outputPath = base_path('routes/api.php');
|
||||
|
||||
$lines = [
|
||||
'<?php',
|
||||
'',
|
||||
'use Illuminate\Support\Facades\Route;',
|
||||
'',
|
||||
'Route::get(\'/\', function () {',
|
||||
' return "Hello, thank you for using XinAdmin. ";',
|
||||
'});',
|
||||
'',
|
||||
];
|
||||
|
||||
// Each controller gets its own Route::controller group
|
||||
foreach ($this->controllerRoutes as $className => $routes) {
|
||||
$prefix = $routes[0]['prefix'];
|
||||
|
||||
$shortName = substr(strrchr($className, '\\'), 1);
|
||||
$lines[] = "// {$shortName}";
|
||||
|
||||
$chain = "Route::controller({$className}::class)";
|
||||
if ($prefix !== '') {
|
||||
$chain .= "->prefix('{$prefix}')";
|
||||
}
|
||||
$chain .= '->group(function () {';
|
||||
$lines[] = $chain;
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$uri = $route['uri'];
|
||||
if ($uri === '') {
|
||||
$uri = '/';
|
||||
}
|
||||
|
||||
$middlewareStr = '';
|
||||
if (!empty($route['middleware'])) {
|
||||
$middlewareStr = "->middleware(['" . implode("', '", $route['middleware']) . "'])";
|
||||
}
|
||||
|
||||
$lines[] = " Route::{$route['method']}('{$uri}', '{$route['action']}'){$middlewareStr};";
|
||||
}
|
||||
|
||||
$lines[] = '});';
|
||||
$lines[] = '';
|
||||
}
|
||||
|
||||
File::put($outputPath, implode("\n", $lines));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Modules\Common\Enum;
|
||||
|
||||
/**
|
||||
* 响应状态枚举类
|
||||
*/
|
||||
enum ShowType: int
|
||||
{
|
||||
// 成功响应
|
||||
case SUCCESS_MESSAGE = 0;
|
||||
|
||||
// 警告响应
|
||||
case WARN_MESSAGE = 1;
|
||||
|
||||
// 错误响应
|
||||
case ERROR_MESSAGE = 2;
|
||||
|
||||
// 成功通知
|
||||
case SUCCESS_NOTIFICATION = 3;
|
||||
|
||||
// 警告通知
|
||||
case WARN_NOTIFICATION = 4;
|
||||
|
||||
// 错误通知
|
||||
case ERROR_NOTIFICATION = 5;
|
||||
|
||||
// 静默响应
|
||||
case SILENT = 99;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Http\Controllers;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\Common\Trait\RequestJson;
|
||||
|
||||
abstract class BaseController extends Controller
|
||||
{
|
||||
use RequestJson;
|
||||
|
||||
/**
|
||||
* 查询字符串
|
||||
* The fields queried by the current model
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $searchField = [];
|
||||
|
||||
/**
|
||||
* 快速搜索字段
|
||||
* Quick search field
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $quickSearchField = [];
|
||||
|
||||
|
||||
/** 构建查询方法 */
|
||||
protected function buildSearch(array $params, Builder $model): Builder
|
||||
{
|
||||
// 构建筛选
|
||||
if (isset($params['filter']) && $params['filter'] != '') {
|
||||
if(is_array($params['filter'])) {
|
||||
$filter = $params['filter'];
|
||||
} else {
|
||||
$filter = json_decode($params['filter'], true);
|
||||
}
|
||||
foreach ($filter as $k => $v) {
|
||||
if (! $v) {
|
||||
continue;
|
||||
}
|
||||
$model->whereIn($k, $v);
|
||||
}
|
||||
unset($params['filter']);
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
foreach ($this->searchField ?? [] as $key => $op) {
|
||||
if (isset($params[$key]) && $params[$key] != '') {
|
||||
if (in_array($op, ['=', '>', '<>', '<', '>=', '<='])) {
|
||||
$model->where($key, $op, $params[$key]);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'like') {
|
||||
$model->where($key, $op, '%'.$params[$key].'%');
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'afterLike') {
|
||||
$model->where($key, $op, $params[$key].'%');
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'beforeLike') {
|
||||
$model->where($key, $op, '%'.$params[$key]);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'date') {
|
||||
$date = date('Y-m-d', strtotime($params[$key]));
|
||||
$model->whereDate($key, $date);
|
||||
|
||||
continue;
|
||||
}
|
||||
if ($op == 'betweenDate') {
|
||||
if (is_array($params[$key])) {
|
||||
$start = $params[$key][0];
|
||||
$end = $params[$key][1];
|
||||
$model->whereDate($key, '>=', $start);
|
||||
$model->whereDate($key, '<=', $end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 快速搜索
|
||||
if (isset($params['keywordSearch']) && $params['keywordSearch'] != '') {
|
||||
$quickSearchArr = $this->quickSearchField ?? [];
|
||||
if (count($quickSearchArr) > 0) {
|
||||
$model->whereAny(
|
||||
$quickSearchArr,
|
||||
'like',
|
||||
'%'.str_replace('%', '\%', $params['keywordSearch']).'%'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建排序
|
||||
if (isset($params['sorter']) && $params['sorter']) {
|
||||
if(is_array($params['sorter'])) {
|
||||
$sorter = $params['sorter'];
|
||||
} else {
|
||||
$sorter = json_decode($params['sorter'], true);
|
||||
}
|
||||
if (count($sorter) > 0) {
|
||||
$column = array_keys($sorter)[0];
|
||||
$direction = $sorter[$column] == 'ascend' ? 'asc' : 'desc';
|
||||
$model->orderBy($column, $direction);
|
||||
}
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BaseFormRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* 是否为更新请求
|
||||
* @return bool
|
||||
*/
|
||||
protected function isUpdate(): bool
|
||||
{
|
||||
if ($this->isMethod('PUT') || $this->isMethod('PATCH')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Middlewares;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AllowCrossDomainMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
// 添加跨域头
|
||||
$response->headers->set('Access-Control-Allow-Origin', '*');
|
||||
$response->headers->set('Access-Control-Allow-Credentials', 'true');
|
||||
$response->headers->set('Access-Control-Max-Age', 1800);
|
||||
$response->headers->set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
$response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, User-Language');
|
||||
|
||||
// 如果是预检请求, 返回 204
|
||||
if ($request->isMethod('OPTIONS')) {
|
||||
return response()->json([], 204, $response->headers->all());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Middlewares;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class LanguageMiddleware
|
||||
{
|
||||
/**
|
||||
* 支持的语言列表
|
||||
*/
|
||||
protected array $supportedLanguages = [
|
||||
'en' => 'en', // 英语
|
||||
'zh' => 'zh', // 简体中文
|
||||
'jp' => 'ja', // 日语
|
||||
];
|
||||
|
||||
/**
|
||||
* 默认语言
|
||||
*/
|
||||
protected string $defaultLanguage = 'zh';
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
// 获取当前语言
|
||||
$locale = $this->getLocale($request);
|
||||
// 设置应用语言
|
||||
App::setLocale($locale);
|
||||
// 让请求继续处理
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言设置
|
||||
*/
|
||||
protected function getLocale(Request $request): string
|
||||
{
|
||||
// 优先级 1: URL 参数 (例如 ?lang=en)
|
||||
if ($request->has('lang')) {
|
||||
$lang = $request->get('lang');
|
||||
if ($this->isSupported($lang)) {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先级 2: User-Language 头
|
||||
$browserLocale = $this->getBrowserLocale($request);
|
||||
if ($browserLocale && $this->isSupported($browserLocale)) {
|
||||
return $browserLocale;
|
||||
}
|
||||
|
||||
// 优先级 3: Session 中存储的语言
|
||||
if (Session::has('locale')) {
|
||||
$lang = Session::get('locale');
|
||||
if ($this->isSupported($lang)) {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先级 4: 配置文件中的默认语言
|
||||
return config('app.locale', $this->defaultLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 User-Language 头中获取浏览器偏好语言
|
||||
*/
|
||||
protected function getBrowserLocale(Request $request): ?string
|
||||
{
|
||||
$acceptLanguage = $request->header('User-Language');
|
||||
|
||||
if (!$acceptLanguage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $acceptLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查语言是否被支持
|
||||
*/
|
||||
protected function isSupported(string $locale): bool
|
||||
{
|
||||
return array_key_exists($locale, $this->supportedLanguages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Common\Providers;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class PaginationProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->bind('Illuminate\Pagination\LengthAwarePaginator', function ($app, $options) {
|
||||
return new class(
|
||||
$options['items'],
|
||||
$options['total'],
|
||||
$options['perPage'],
|
||||
$options['currentPage'],
|
||||
$options['options']
|
||||
) extends LengthAwarePaginator {
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'data' => $this->items(),
|
||||
'total' => $this->total(),
|
||||
'pageSize' => $this->perPage(),
|
||||
'current' => $this->currentPage(),
|
||||
];
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
namespace Modules\Common\Trait;
|
||||
|
||||
use App\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Modules\Common\Enum\ShowType as ShopTypeEnum;
|
||||
|
||||
/**
|
||||
* 响应 trait
|
||||
* 支持 throw 响应
|
||||
*/
|
||||
trait RequestJson
|
||||
{
|
||||
/**
|
||||
* 成功响应
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function success(string|array $data = [], string $message = 'ok'): JsonResponse
|
||||
{
|
||||
if (is_array($data)) {
|
||||
return self::renderJson(true, $data, $message);
|
||||
}
|
||||
|
||||
return self::renderJson(true, [], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出成功响应,中断程序运行
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function throwSuccess(string|array $data = [], string $message = 'ok'): void
|
||||
{
|
||||
if (is_array($data)) {
|
||||
self::renderThrow(true, $data, $message);
|
||||
}
|
||||
self::renderThrow(true, [], $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回失败响应
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function error(string|array $data = [], string $message = ''): JsonResponse
|
||||
{
|
||||
if (is_array($data)) {
|
||||
return self::renderJson(false, $data, $message, ShopTypeEnum::ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
return self::renderJson(false, [], $data, ShopTypeEnum::ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出失败响应,中断程序运行
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function throwError(string|array $data = [], string $message = ''): void
|
||||
{
|
||||
if (is_array($data)) {
|
||||
self::renderThrow(false, $data, $message, ShopTypeEnum::ERROR_MESSAGE);
|
||||
}
|
||||
self::renderThrow(false, [], $data, ShopTypeEnum::ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回警告响应
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function warn(string|array $data = [], string $message = ''): JsonResponse
|
||||
{
|
||||
if (is_array($data)) {
|
||||
return self::renderJson(false, $data, $message, ShopTypeEnum::WARN_MESSAGE);
|
||||
}
|
||||
|
||||
return self::renderJson(false, [], $data, ShopTypeEnum::WARN_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出失败警告,中断程序运行
|
||||
*
|
||||
* @param string|array $data 响应数据
|
||||
* @param string $message 响应内容
|
||||
*/
|
||||
protected function throwWarn(string|array $data = [], string $message = ''): void
|
||||
{
|
||||
if (is_array($data)) {
|
||||
self::renderThrow(false, $data, $message, ShopTypeEnum::WARN_MESSAGE);
|
||||
}
|
||||
self::renderThrow(false, [], $data, ShopTypeEnum::WARN_MESSAGE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知响应
|
||||
*
|
||||
* @param string $msg 通知标题
|
||||
* @param string $description 通知描述
|
||||
* @param string $placement 通知位置 top topLeft topRight bottom bottomLeft bottomRight
|
||||
* @param ShopTypeEnum $showTypeEnum 通知类型
|
||||
*/
|
||||
protected function notification(
|
||||
string $msg,
|
||||
string $description,
|
||||
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_NOTIFICATION,
|
||||
string $placement = 'topRight'
|
||||
): JsonResponse {
|
||||
$showType = $showTypeEnum->value;
|
||||
$success = false;
|
||||
return response()->json(compact('description', 'success', 'msg', 'showType', 'placement'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回 Json 响应
|
||||
*
|
||||
* @param bool $success 响应状态
|
||||
* @param array $data 响应数据
|
||||
* @param string $msg 响应内容
|
||||
*/
|
||||
protected static function renderJson(
|
||||
bool $success = true,
|
||||
array $data = [],
|
||||
string $msg = '',
|
||||
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_MESSAGE
|
||||
): JsonResponse {
|
||||
$showType = $showTypeEnum->value;
|
||||
|
||||
return response()->json(compact('data', 'success', 'msg', 'showType'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 抛出 API 数据
|
||||
*
|
||||
* @param bool $success 响应状态
|
||||
* @param mixed $data 返回数据
|
||||
* @param string $msg 响应内容
|
||||
* @param ShopTypeEnum $showTypeEnum
|
||||
*/
|
||||
public static function renderThrow(
|
||||
bool $success = true,
|
||||
array $data = [],
|
||||
string $msg = '',
|
||||
ShopTypeEnum $showTypeEnum = ShopTypeEnum::SUCCESS_MESSAGE
|
||||
) {
|
||||
$showType = $showTypeEnum->value;
|
||||
throw new HttpResponseException(compact('data', 'success', 'msg', 'showType'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
use Modules\SystemTool\Services\SysConfigService;
|
||||
|
||||
if (! function_exists('site_config')) {
|
||||
/**
|
||||
* 获取或设置系统配置
|
||||
*
|
||||
* @param string|null $name 配置名称,格式:'group.key' 或 'group',为null时返回所有配置
|
||||
* @param mixed|null $default 默认值(仅在获取时使用)
|
||||
* @return mixed
|
||||
*
|
||||
* @example
|
||||
* site_config('site.name') // 获取配置
|
||||
* site_config('site.name', 'Default') // 带默认值获取
|
||||
* site_config('site') // 获取整个组
|
||||
* site_config() // 获取所有配置
|
||||
*/
|
||||
function site_config(?string $name = null, mixed $default = null): mixed
|
||||
{
|
||||
return SysConfigService::getConfig($name, $default);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('web_path')) {
|
||||
/**
|
||||
* Get the path to the web of the install.
|
||||
*
|
||||
* @param string $path
|
||||
* @return string
|
||||
*/
|
||||
function web_path(string $path = ''): string
|
||||
{
|
||||
return base_path('web'. DIRECTORY_SEPARATOR . $path);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('getTreeData')) {
|
||||
/**
|
||||
* 获取树形数据
|
||||
*
|
||||
* @param array $list
|
||||
* @param int $parentId
|
||||
* @param string[] $params
|
||||
* @return array
|
||||
*/
|
||||
function getTreeData(
|
||||
array &$list,
|
||||
int $parentId = 0,
|
||||
array $params = []
|
||||
): array
|
||||
{
|
||||
$params = array_merge($params, [
|
||||
'id' => 'id',
|
||||
'parent_id' => 'parent_id',
|
||||
'children' => 'children'
|
||||
]);
|
||||
$data = [];
|
||||
foreach ($list as $k => $item) {
|
||||
if ($item[$params['parent_id']] == $parentId) {
|
||||
$children = getTreeData($list, $item[$params['id']]);
|
||||
!empty($children) && $item[$params['children']] = $children;
|
||||
$data[] = $item;
|
||||
unset($list[$k]);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Ai\Agents;
|
||||
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class XinChatAgent implements Agent, Conversational
|
||||
{
|
||||
use Promptable, RemembersConversations;
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return 'You are a helpful, friendly AI assistant. Answer questions clearly and concisely. '
|
||||
. 'If you don\'t know something, be honest about it. '
|
||||
. 'Use markdown formatting when it helps readability.';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PutRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemAgent\Models\AgentModel;
|
||||
|
||||
#[RequestAttribute('/ai/agent', 'ai.agent')]
|
||||
class AgentController extends BaseController
|
||||
{
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(): JsonResponse
|
||||
{
|
||||
$agents = AgentModel::orderBy('id')->get();
|
||||
return $this->success($agents->toArray());
|
||||
}
|
||||
|
||||
#[GetRoute('/{id}', authorize: 'query', where: ['id' => '[0-9]+'])]
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$agent = AgentModel::find($id);
|
||||
if (! $agent) {
|
||||
return $this->error('Agent not found');
|
||||
}
|
||||
return $this->success($agent->toArray());
|
||||
}
|
||||
|
||||
#[PutRoute('/{id}', authorize: 'update', where: ['id' => '[0-9]+'])]
|
||||
public function update(int $id, Request $request): JsonResponse
|
||||
{
|
||||
$enabled = $request->boolean('enabled', true);
|
||||
$model = AgentModel::find($id);
|
||||
if (! $model) {
|
||||
return $this->error('Agent not found');
|
||||
}
|
||||
$model->enabled = $enabled;
|
||||
$model->save();
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Ai\Responses\StreamableAgentResponse;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\PostRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemAgent\Ai\Agents\XinChatAgent;
|
||||
use Modules\SystemAgent\Models\AgentModel;
|
||||
|
||||
#[RequestAttribute('/ai/chat', 'ai.chat')]
|
||||
class ChatController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 发送消息并返回 SSE 流式响应
|
||||
*/
|
||||
#[PostRoute('/send', 'send')]
|
||||
public function send(Request $request): StreamableAgentResponse|JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'message' => 'required|string|max:10000',
|
||||
'conversation_id' => 'nullable|string|max:36',
|
||||
'agent_id' => 'nullable|integer|exists:agents,id',
|
||||
]);
|
||||
|
||||
$message = $request->input('message');
|
||||
$conversationId = $request->input('conversation_id');
|
||||
$agentId = $request->input('agent_id');
|
||||
$user = $request->user();
|
||||
|
||||
try {
|
||||
if ($agentId) {
|
||||
$agentModel = AgentModel::findOrFail($agentId);
|
||||
$agentClass = $agentModel->namespace;
|
||||
$agent = $agentClass::make();
|
||||
} else {
|
||||
$agent = XinChatAgent::make();
|
||||
}
|
||||
|
||||
if ($conversationId) {
|
||||
// 继续已有会话
|
||||
$response = $agent
|
||||
->continue($conversationId, as: $user)
|
||||
->stream($message);
|
||||
} else {
|
||||
// 新建会话
|
||||
$response = $agent
|
||||
->forUser($user)
|
||||
->stream($message);
|
||||
}
|
||||
|
||||
return $response;
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('AI 响应失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的会话列表
|
||||
*/
|
||||
#[GetRoute('/conversations', 'conversations')]
|
||||
public function conversations(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$conversations = $user->conversations()
|
||||
->latest('updated_at')
|
||||
->get()
|
||||
->map(fn ($conversation) => [
|
||||
'key' => $conversation->id,
|
||||
'label' => $conversation->title,
|
||||
'updated_at' => $conversation->updated_at->toISOString(),
|
||||
]);
|
||||
|
||||
return $this->success($conversations->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定会话的消息列表
|
||||
*/
|
||||
#[GetRoute('/messages/{conversationId}', 'messages')]
|
||||
public function messages(Request $request, string $conversationId): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$conversation = $user->conversations()->find($conversationId);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$messages = $conversation->messages()
|
||||
->oldest()
|
||||
->get()
|
||||
->map(fn ($msg) => [
|
||||
'key' => $msg->id,
|
||||
'role' => $msg->role,
|
||||
'content' => $msg->content,
|
||||
'created_at' => $msg->created_at->toISOString(),
|
||||
]);
|
||||
|
||||
return $this->success($messages->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定会话
|
||||
*/
|
||||
#[DeleteRoute('/messages/{conversationId}', 'delete')]
|
||||
public function deleteConversation(Request $request, string $conversationId): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$conversation = $user->conversations()->find($conversationId);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$conversation->delete();
|
||||
|
||||
return $this->success('会话已删除');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Laravel\Ai\Models\Conversation;
|
||||
use Modules\AnnoRoute\Attribute\DeleteRoute;
|
||||
use Modules\AnnoRoute\Attribute\GetRoute;
|
||||
use Modules\AnnoRoute\Attribute\RequestAttribute;
|
||||
use Modules\Common\Http\Controllers\BaseController;
|
||||
use Modules\SystemUser\Models\SysUserModel;
|
||||
|
||||
#[RequestAttribute('/ai/conversation', 'ai.conversation')]
|
||||
class ConversationController extends BaseController
|
||||
{
|
||||
protected array $searchField = [
|
||||
'title' => 'like',
|
||||
];
|
||||
|
||||
protected array $quickSearchField = ['title'];
|
||||
|
||||
/**
|
||||
* 会话列表
|
||||
*/
|
||||
#[GetRoute(authorize: 'query')]
|
||||
public function query(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
$perPage = (int) ($params['pageSize'] ?? 10);
|
||||
$query = Conversation::query()->withCount('messages');
|
||||
|
||||
$data = $this->buildSearch($params, $query)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->paginate($perPage);
|
||||
|
||||
$userIds = $data->pluck('user_id')->filter()->unique();
|
||||
$users = SysUserModel::whereIn('id', $userIds)->pluck('username', 'id');
|
||||
|
||||
$data = $data->through(function ($conversation) use ($users) {
|
||||
return [
|
||||
'id' => $conversation->id,
|
||||
'user_id' => $conversation->user_id,
|
||||
'username' => $users[$conversation->user_id] ?? '',
|
||||
'title' => $conversation->title,
|
||||
'message_count' => $conversation->messages_count,
|
||||
'created_at' => $conversation->created_at?->toISOString(),
|
||||
'updated_at' => $conversation->updated_at?->toISOString(),
|
||||
];
|
||||
});
|
||||
|
||||
return $this->success($data->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*/
|
||||
#[DeleteRoute(route: '/{id}', authorize: 'delete', where: ['id' => '[a-zA-Z0-9\-]+'])]
|
||||
public function delete(string $id): JsonResponse
|
||||
{
|
||||
$conversation = Conversation::find($id);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$conversation->delete();
|
||||
|
||||
return $this->success('会话已删除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息列表
|
||||
*/
|
||||
#[GetRoute('/{id}/messages', authorize: 'query', where: ['id' => '[a-zA-Z0-9\-]+'])]
|
||||
public function messages(string $id, Request $request): JsonResponse
|
||||
{
|
||||
$conversation = Conversation::find($id);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$perPage = (int) $request->input('pageSize', 20);
|
||||
$data = $conversation->messages()
|
||||
->orderBy('created_at')
|
||||
->paginate($perPage)
|
||||
->toArray();
|
||||
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话详情
|
||||
*/
|
||||
#[GetRoute('/{id}', authorize: 'query', where: ['id' => '[a-zA-Z0-9\-]+'])]
|
||||
public function show(string $id): JsonResponse
|
||||
{
|
||||
$conversation = Conversation::withCount('messages')->find($id);
|
||||
|
||||
if (! $conversation) {
|
||||
return $this->error('会话不存在');
|
||||
}
|
||||
|
||||
$user = $conversation->user_id
|
||||
? SysUserModel::find($conversation->user_id)
|
||||
: null;
|
||||
|
||||
return $this->success([
|
||||
'id' => $conversation->id,
|
||||
'user_id' => $conversation->user_id,
|
||||
'username' => $user?->username ?? '',
|
||||
'title' => $conversation->title,
|
||||
'message_count' => $conversation->messages_count,
|
||||
'created_at' => $conversation->created_at?->toISOString(),
|
||||
'updated_at' => $conversation->updated_at?->toISOString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Http\Requests;
|
||||
|
||||
use Modules\Common\Http\Requests\BaseFormRequest;
|
||||
|
||||
class AgentFormRequest extends BaseFormRequest
|
||||
{
|
||||
protected $stopOnFirstFailure = true;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'enabled' => 'required|boolean',
|
||||
'name' => 'nullable|max:100',
|
||||
'description' => 'nullable|max:1000',
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'enabled.required' => '启用状态不能为空',
|
||||
'enabled.boolean' => '启用状态格式错误',
|
||||
'name.max' => '名称不能超过100个字符',
|
||||
'description.max' => '描述不能超过1000个字符',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AgentModel extends Model
|
||||
{
|
||||
protected $table = 'agents';
|
||||
|
||||
protected $fillable = [
|
||||
'namespace',
|
||||
'name',
|
||||
'icon',
|
||||
'description',
|
||||
'tags',
|
||||
'enabled',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tags' => 'array',
|
||||
'enabled' => 'boolean',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemAgent\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Modules\AnnoRoute\AnnoRoute;
|
||||
|
||||
class SystemAgentServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(AnnoRoute $annoRoute): void
|
||||
{
|
||||
$annoRoute->register(base_path('modules/SystemAgent/Http/Controllers'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Ai\Agents;
|
||||
|
||||
use Laravel\Ai\Concerns\RemembersConversations;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Messages\Message;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class TestAgent implements Agent, Conversational, HasTools
|
||||
{
|
||||
use Promptable, RemembersConversations;
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of messages comprising the conversation so far.
|
||||
*
|
||||
* @return Message[]
|
||||
*/
|
||||
public function messages(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools available to the agent.
|
||||
*
|
||||
* @return Tool[]
|
||||
*/
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\SystemTool\Ai\Boots;
|
||||
|
||||
use Laravel\Boost\Contracts\SupportsGuidelines;
|
||||
use Laravel\Boost\Contracts\SupportsMcp;
|
||||
use Laravel\Boost\Contracts\SupportsSkills;
|
||||
use Laravel\Boost\Install\Agents\Agent;
|
||||
use Laravel\Boost\Install\Enums\McpInstallationStrategy;
|
||||
use Laravel\Boost\Install\Enums\Platform;
|
||||
|
||||
class Reasonix extends Agent implements SupportsGuidelines, SupportsMcp, SupportsSkills
|
||||
{
|
||||
public function name(): string
|
||||
{
|
||||
return 'reasonix';
|
||||
}
|
||||
|
||||
public function displayName(): string
|
||||
{
|
||||
return 'Reasonix';
|
||||
}
|
||||
|
||||
public function systemDetectionConfig(Platform $platform): array
|
||||
{
|
||||
return match ($platform) {
|
||||
Platform::Darwin, Platform::Linux => [
|
||||
'command' => 'command -v reasonix',
|
||||
],
|
||||
Platform::Windows => [
|
||||
'command' => 'cmd /c where reasonix 2>nul',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public function projectDetectionConfig(): array
|
||||
{
|
||||
return [
|
||||
'paths' => ['.reasonix'],
|
||||
'files' => ['REASONIX.md'],
|
||||
];
|
||||
}
|
||||
|
||||
public function mcpInstallationStrategy(): McpInstallationStrategy
|
||||
{
|
||||
return McpInstallationStrategy::FILE;
|
||||
}
|
||||
|
||||
public function mcpConfigPath(): string
|
||||
{
|
||||
return config('boost.agents.reasonix.mcp_config_path', '.mcp.json');
|
||||
}
|
||||
|
||||
public function guidelinesPath(): string
|
||||
{
|
||||
return config('boost.agents.reasonix.guidelines_path', 'REASONIX.md');
|
||||
}
|
||||
|
||||
public function skillsPath(): string
|
||||
{
|
||||
return config('boost.agents.reasonix.skills_path', '.reasonix/skills');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Attributes;
|
||||
|
||||
use Attribute;
|
||||
use Modules\SystemTool\Enum\ESettingType;
|
||||
|
||||
/**
|
||||
* 在 SettingsDefinition 子类上声明配置项
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
|
||||
class Setting
|
||||
{
|
||||
public function __construct(
|
||||
public string $config,
|
||||
public ESettingType $type = ESettingType::String,
|
||||
public ?string $description = null,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Base;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use InvalidArgumentException;
|
||||
use Modules\SystemTool\Attributes\Setting;
|
||||
use Modules\SystemTool\Enum\ESettingType;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* 设置定义基类
|
||||
*/
|
||||
abstract class SettingsDefinition
|
||||
{
|
||||
/**
|
||||
* 聚合缓存键名 — 全局中间件 LoadAppSettingsMiddleware 使用
|
||||
* 所有设置一次性缓存在此键中,set() 写入时自动清除。
|
||||
*/
|
||||
const AGGREGATE_CACHE_KEY = 'app-settings-all';
|
||||
|
||||
/**
|
||||
* 解析后的定义缓存(类名 → 定义数组)
|
||||
*
|
||||
* @var array<string, array<string, array{config: string, type: string, description: string|null}>>
|
||||
*/
|
||||
protected static array $definitions = [];
|
||||
|
||||
/**
|
||||
* 从 #[Setting] Attribute 解析设置定义
|
||||
*
|
||||
* @return array<string, array{config: string, type: string, description: string|null}>
|
||||
*/
|
||||
public static function getDefinition(): array
|
||||
{
|
||||
$class = static::class;
|
||||
|
||||
if (! isset(self::$definitions[$class])) {
|
||||
$reflection = new ReflectionClass($class);
|
||||
$attributes = $reflection->getAttributes(Setting::class);
|
||||
$definition = [];
|
||||
|
||||
foreach ($attributes as $attribute) {
|
||||
/** @var Setting $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$definition[$instance->config] = [
|
||||
'config' => $instance->config,
|
||||
'type' => $instance->type->value,
|
||||
'description' => $instance->description,
|
||||
];
|
||||
}
|
||||
|
||||
self::$definitions[$class] = $definition;
|
||||
}
|
||||
|
||||
return self::$definitions[$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某个 key 是否在本类的定义中
|
||||
*/
|
||||
public static function hasDefinitionKey(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, static::getDefinition());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库表名
|
||||
*/
|
||||
public static function getTableName(): string
|
||||
{
|
||||
return config('app_settings.table', 'sys_app_settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本类的所有配置项定义同步到 应用配置 表
|
||||
*
|
||||
* - 新 key 插入,值来自当前 config() 的默认值
|
||||
* - 已有 key 只更新 description,不覆盖值
|
||||
* - 部署时在 migration 中调用
|
||||
*/
|
||||
public static function init(): void
|
||||
{
|
||||
$definition = static::getDefinition();
|
||||
$table = static::getTableName();
|
||||
|
||||
foreach ($definition as $key => $setting) {
|
||||
$exists = DB::table($table)->where('key', $key)->exists();
|
||||
|
||||
if (! $exists) {
|
||||
$value = static::getConfigValue($key);
|
||||
static::set($key, $value);
|
||||
} else {
|
||||
DB::table($table)
|
||||
->where('key', $key)
|
||||
->update([
|
||||
'description' => $setting['description'] ?? null,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取配置值
|
||||
*
|
||||
* @throws InvalidArgumentException 当 key 未定义时
|
||||
*/
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (! static::hasDefinitionKey($key)) {
|
||||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||||
}
|
||||
|
||||
return static::getCacheValue($key, function () use ($key, $default) {
|
||||
return static::getDBValue($key, function () use ($key, $default) {
|
||||
return static::setCacheValue(
|
||||
key: $key,
|
||||
value: static::getConfigValue($key, $default),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入配置值 → DB + Cache
|
||||
*
|
||||
* @throws InvalidArgumentException 当 key 未定义或类型不匹配时
|
||||
*/
|
||||
public static function set(string $key, mixed $value): void
|
||||
{
|
||||
if (! static::hasDefinitionKey($key)) {
|
||||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||||
}
|
||||
|
||||
static::setDBValue($key, $value);
|
||||
static::setCacheValue($key, $value);
|
||||
|
||||
// 当前请求立即生效(中间件在下一个请求才会重新加载)
|
||||
config([$key => $value]);
|
||||
|
||||
// 清除聚合缓存,使下次请求通过中间件重新从 DB 加载
|
||||
Cache::forget(self::AGGREGATE_CACHE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Laravel config 读取(最终 fallback)
|
||||
*/
|
||||
public static function getConfigValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return config($key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从缓存读取
|
||||
*
|
||||
* @param mixed $default 默认值或闭包 (fn() => mixed)
|
||||
*/
|
||||
public static function getCacheValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if ($default instanceof \Closure) {
|
||||
return Cache::rememberForever('app-setting-' . $key, $default);
|
||||
}
|
||||
|
||||
return Cache::get('app-setting-' . $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入缓存
|
||||
*/
|
||||
public static function setCacheValue(string $key, mixed $value, ?int $ttl = null): mixed
|
||||
{
|
||||
if ($ttl !== null) {
|
||||
Cache::put('app-setting-' . $key, $value, $ttl);
|
||||
} else {
|
||||
Cache::forever('app-setting-' . $key, $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本类所有配置项从 DB 加载到 Laravel config() 运行时
|
||||
*
|
||||
* 调用后 config('filesystems.default') 等可直接返回 DB 值。
|
||||
* 在 getConfig() 批量读取场景下避免逐个 get() 的开销。
|
||||
*/
|
||||
public static function reloadIntoConfig(): void
|
||||
{
|
||||
$table = static::getTableName();
|
||||
$rows = DB::table($table)->whereIn('key', array_keys(static::getDefinition()))->get();
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$value = match ((int) $row->type) {
|
||||
ESettingType::String->value => $row->s,
|
||||
ESettingType::Bool->value => is_null($row->n) ? null : (bool) $row->n,
|
||||
ESettingType::Number->value => is_null($row->n) ? null : (int) $row->n,
|
||||
ESettingType::Array->value => is_null($row->e) ? null : json_decode($row->e, true),
|
||||
ESettingType::Object->value => is_null($row->e) ? null : unserialize(base64_decode($row->e)),
|
||||
ESettingType::EncryptedString->value => is_null($row->e) ? null : \Illuminate\Support\Facades\Crypt::decrypt(base64_decode($row->e)),
|
||||
default => $row->s ?? null,
|
||||
};
|
||||
|
||||
config([$row->key => $value]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除某个 key 的缓存
|
||||
*/
|
||||
public static function forgetCache(string $key): void
|
||||
{
|
||||
Cache::forget('app-setting-' . $key);
|
||||
|
||||
// 同时清除聚合缓存,保持一致性
|
||||
Cache::forget(self::AGGREGATE_CACHE_KEY);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从数据库读取(按类型自动转换)
|
||||
*
|
||||
* @param mixed $default 默认值或闭包
|
||||
*/
|
||||
public static function getDBValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$rec = DB::table(static::getTableName())->where('key', $key)->first();
|
||||
|
||||
if (! $rec) {
|
||||
return $default instanceof \Closure ? $default() : value($default);
|
||||
}
|
||||
|
||||
return match ((int) $rec->type) {
|
||||
ESettingType::String->value => $rec->s,
|
||||
ESettingType::Bool->value => is_null($rec->n) ? null : (bool) $rec->n,
|
||||
ESettingType::Number->value => is_null($rec->n) ? null : (int) $rec->n,
|
||||
ESettingType::Array->value => is_null($rec->e) ? null : json_decode($rec->e, true),
|
||||
ESettingType::Object->value => is_null($rec->e) ? null : unserialize(base64_decode($rec->e)),
|
||||
ESettingType::EncryptedString->value => is_null($rec->e) ? null : Crypt::decrypt(base64_decode($rec->e)),
|
||||
default => $rec->s ?? $rec->value ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入数据库(按类型选择列)
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public static function setDBValue(string $key, mixed $value): void
|
||||
{
|
||||
$definition = static::getDefinition();
|
||||
|
||||
if (! isset($definition[$key])) {
|
||||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||||
}
|
||||
|
||||
$type = $definition[$key]['type'];
|
||||
$description = $definition[$key]['description'] ?? null;
|
||||
$table = static::getTableName();
|
||||
|
||||
$data = [
|
||||
'key' => $key,
|
||||
'type' => $type,
|
||||
'description' => $description,
|
||||
'updated_at' => now(),
|
||||
];
|
||||
|
||||
switch ((int) $type) {
|
||||
case ESettingType::String->value:
|
||||
if (! is_string($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be a string.");
|
||||
}
|
||||
$data['s'] = $value;
|
||||
$data['n'] = null;
|
||||
$data['e'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::Bool->value:
|
||||
if (! is_bool($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be a boolean.");
|
||||
}
|
||||
$data['n'] = is_null($value) ? null : (int) $value;
|
||||
$data['s'] = null;
|
||||
$data['e'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::Number->value:
|
||||
if (! is_numeric($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be a number.");
|
||||
}
|
||||
$data['n'] = $value;
|
||||
$data['s'] = null;
|
||||
$data['e'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::Array->value:
|
||||
if (! is_array($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be an array.");
|
||||
}
|
||||
$data['e'] = is_null($value) ? null : json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
$data['s'] = null;
|
||||
$data['n'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::Object->value:
|
||||
if (! is_object($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be an object.");
|
||||
}
|
||||
$data['e'] = is_null($value) ? null : base64_encode(serialize($value));
|
||||
$data['s'] = null;
|
||||
$data['n'] = null;
|
||||
break;
|
||||
|
||||
case ESettingType::EncryptedString->value:
|
||||
if (! is_string($value) && ! is_null($value)) {
|
||||
throw new InvalidArgumentException("Value for '{$key}' must be a string.");
|
||||
}
|
||||
$data['e'] = is_null($value) ? null : base64_encode(Crypt::encrypt($value));
|
||||
$data['s'] = null;
|
||||
$data['n'] = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new InvalidArgumentException("Unknown setting type '{$type}' for key '{$key}'.");
|
||||
}
|
||||
|
||||
$exists = DB::table($table)->where('key', $key)->exists();
|
||||
|
||||
if ($exists) {
|
||||
DB::table($table)->where('key', $key)->update($data);
|
||||
} else {
|
||||
$data['created_at'] = now();
|
||||
DB::table($table)->insert($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库删除某个配置项
|
||||
*/
|
||||
public static function deleteDBValue(string $key): void
|
||||
{
|
||||
DB::table(static::getTableName())->where('key', $key)->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Enum;
|
||||
|
||||
/**
|
||||
* 设置类型枚举
|
||||
*
|
||||
* 对应 应用设置 表的 type 列,决定值存储在 s / n / e 哪个字段。
|
||||
*/
|
||||
enum ESettingType: int
|
||||
{
|
||||
case String = 10;
|
||||
case Bool = 15;
|
||||
case Number = 20;
|
||||
case Array = 30;
|
||||
case Object = 40;
|
||||
case EncryptedString = 50;
|
||||
|
||||
/**
|
||||
* 是否为字符串类型
|
||||
*/
|
||||
public function isString(): bool
|
||||
{
|
||||
return $this === self::String;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为数字/布尔类型(存储在 n 列)
|
||||
*/
|
||||
public function isNumeric(): bool
|
||||
{
|
||||
return in_array($this, [self::Bool, self::Number], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为扩展类型(存储在 e 列)
|
||||
*/
|
||||
public function isExtended(): bool
|
||||
{
|
||||
return in_array($this, [self::Array, self::Object, self::EncryptedString], true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\SystemTool\Enum;
|
||||
|
||||
/**
|
||||
* 枚举类:文件类型
|
||||
* Class FileType
|
||||
*/
|
||||
enum FileType: int
|
||||
{
|
||||
// 图片
|
||||
case IMAGE = 10;
|
||||
|
||||
// 音频
|
||||
case AUDIO = 20;
|
||||
|
||||
// 视频
|
||||
case VIDEO = 30;
|
||||
|
||||
// 压缩包
|
||||
case ZIP = 40;
|
||||
|
||||
// 文档
|
||||
case DOCUMENT = 50;
|
||||
|
||||
// 未知文件
|
||||
case ANNEX = 99;
|
||||
|
||||
/**
|
||||
* 获取类型名称
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::IMAGE => __('system.file.image'),
|
||||
self::AUDIO => __('system.file.audio'),
|
||||
self::VIDEO => __('system.file.video'),
|
||||
self::ZIP => __('system.file.zip'),
|
||||
self::DOCUMENT => __('system.file.document'),
|
||||
self::ANNEX => __('system.file.annex'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览地址
|
||||
*/
|
||||
public function previewPath(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::IMAGE => 'static/image.svg',
|
||||
self::AUDIO => 'static/audio.svg',
|
||||
self::VIDEO => 'static/video.svg',
|
||||
self::ZIP => 'static/zip.svg',
|
||||
self::DOCUMENT => 'static/document.svg',
|
||||
self::ANNEX => 'static/annex.svg',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据扩展名推断文件类型
|
||||
*/
|
||||
public static function guessFromExtension(string $extension): self
|
||||
{
|
||||
$extension = strtolower($extension);
|
||||
|
||||
foreach (self::cases() as $case) {
|
||||
if ($case === self::ANNEX) continue; // 跳过 OTHER
|
||||
|
||||
if (in_array($extension, $case->fileExt())) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
|
||||
return self::ANNEX;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件扩展名
|
||||
*/
|
||||
public function fileExt(): array|string
|
||||
{
|
||||
return match ($this) {
|
||||
self::IMAGE => ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'avif', 'webp', 'svg', 'ico'],
|
||||
self::AUDIO => ['mp3', 'wma', 'wav', 'ape', 'flac', 'ogg', 'aac'],
|
||||
self::VIDEO => ['mp4', 'mov', 'wmv', 'flv', 'avl', 'webm', 'mkv'],
|
||||
self::DOCUMENT => ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'md', 'csv'],
|
||||
self::ZIP => ['zip', 'rar', '7z', 'tar', 'gz'],
|
||||
self::ANNEX => '*',
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user