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