初始化项目

This commit is contained in:
2026-02-08 22:38:13 +08:00
commit 334d2c6312
201 changed files with 32724 additions and 0 deletions
+869
View File
@@ -0,0 +1,869 @@
# Vue3 后台管理项目开发规范
## 项目概述
本项目是一个基于 Vue3 的后台管理系统,采用现代化技术栈构建,提供高效、美观的管理界面。
## 技术栈
- **Vue 3**: 渐进式 JavaScript 框架
- **Vite**: 下一代前端构建工具
- **Ant Design Vue**: 基于 Vue 3 的 UI 组件库
- **Vue Router**: Vue.js 官方路由管理器
- **Pinia**: Vue 3 官方状态管理库
- **Axios**: HTTP 客户端
- **JavaScript**: 主要开发语言(非 TypeScript
- **Composition API**: 组合式 API 开发模式
## 图标系统
项目采用 Ant Design Vue 图标库,已全局引入:
- **Ant Design Vue Icons**: Ant Design Vue 官方图标库
**重要提示**: 图标已全局默认引入,开发时请勿重复引入或按需引入,直接使用即可。
## 开发规范
### 1. 项目结构
```
resources/admin/
├── src/
│ ├── api/ # API 接口层
│ │ ├── auth.js # 认证相关接口
│ │ ├── menu.js # 菜单接口
│ │ └── system.js # 系统相关接口
│ ├── assets/ # 静态资源
│ │ ├── images/ # 图片资源
│ │ └── style/ # 全局样式
│ ├── components/ # 公共组件
│ │ ├── scEditor/ # 富文本编辑器
│ │ ├── scForm/ # 表单组件
│ │ ├── scIconPicker/ # 图标选择器
│ │ ├── scTable/ # 表格组件
│ │ └── scUpload/ # 上传组件
│ ├── config/ # 配置文件
│ │ ├── index.js # 主配置
│ │ ├── routes.js # 路由配置
│ │ └── upload.js # 上传配置
│ ├── hooks/ # 组合式 API Hooks
│ │ ├── useI18n.js # 国际化 Hook
│ │ └── useTable.js # 表格 Hook
│ ├── i18n/ # 国际化配置
│ │ ├── index.js # i18n 配置
│ │ └── locales/ # 语言包
│ ├── layouts/ # 布局组件
│ │ ├── components/ # 布局子组件
│ │ ├── other/ # 其他布局
│ │ └── index.vue # 主布局
│ ├── pages/ # 页面组件
│ │ ├── auth/ # 认证页面
│ │ ├── home/ # 首页
│ │ ├── login/ # 登录页
│ │ ├── system/ # 系统管理
│ │ └── ucenter/ # 个人中心
│ ├── router/ # 路由配置
│ │ ├── index.js # 主路由
│ │ └── systemRoutes.js # 系统路由
│ ├── stores/ # 状态管理
│ │ ├── index.js # Store 入口
│ │ ├── persist.js # 持久化配置
│ │ └── modules/ # Store 模块
│ ├── utils/ # 工具函数
│ │ ├── request.js # Axios 封装
│ │ └── tool.js # 工具函数
│ ├── App.vue # 根组件
│ ├── boot.js # 引导文件
│ ├── main.js # 入口文件
│ └── style.css # 全局样式
├── public/ # 公共资源
├── index.html # HTML 模板
├── package.json # 依赖配置
├── vite.config.js # Vite 配置
└── README.md # 项目说明
```
### 2. 组件开发规范
#### 组件命名
- **单文件组件**: 使用 PascalCase 命名,如 `UserList.vue`
- **公共组件**: 以 `sc` 开头,如 `scTable.vue`
- **页面组件**: 使用语义化命名,如 `UserManagement.vue`
#### 组件结构
```vue
<template>
<!-- 模板内容 -->
</template>
<script setup>
// 导入
import { ref, computed, onMounted } from 'vue'
// 响应式数据
const state = ref({})
// 计算属性
const computedValue = computed(() => {})
// 方法
const handleAction = () => {}
// 生命周期
onMounted(() => {})
</script>
<style scoped>
/* 样式 */
</style>
```
### 3. API 接口开发规范
#### API 文件组织
每个业务模块对应一个 API 文件,统一放在 `src/api/` 目录下。
```javascript
// src/api/auth.js
import request from '@/utils/request'
export default {
// 认证相关
login: {
post: async function (params) {
return await request.post('auth/login', params)
},
},
logout: {
post: async function () {
return await request.post('auth/logout')
},
},
me: {
get: async function () {
return await request.get('auth/me')
},
},
// 权限和菜单
permissions: {
menu: {
get: async function () {
return await request.get('permissions/menu')
},
},
tree: {
get: async function () {
return await request.get('permissions/tree')
},
},
},
}
```
#### 使用示例
```javascript
import authApi from '@/api/auth'
const login = async () => {
try {
const res = await authApi.login.post({
username: 'admin',
password: '123456'
})
// 处理响应
} catch (error) {
// 处理错误
}
}
// 获取用户菜单
const getMenu = async () => {
const res = await authApi.permissions.menu.get()
return res.data
}
```
### 4. 路由开发规范
#### 路由类型
项目包含两类路由:
1. **静态路由**: 定义在 `src/router/systemRoutes.js` 中的基础路由,如登录页、404 页面等
2. **动态路由**: 用户登录后,通过 API 获取菜单数据,动态添加到路由中
#### 静态路由定义
```javascript
// src/router/systemRoutes.js
export default [
{
path: '/login',
name: 'Login',
component: () => import('@/pages/login/index.vue'),
meta: {
title: '登录',
hidden: true
}
},
{
path: '/',
name: 'Layout',
component: () => import('@/layouts/index.vue'),
redirect: '/dashboard',
children: [
// 动态路由将被添加到这里
]
}
]
```
#### 动态路由加载
用户登录后,系统会自动执行以下流程:
1. 调用后端 API 获取用户菜单和权限信息
2. 将菜单数据转换为路由格式
3. 将动态路由添加到路由器中
4. 生成菜单树用于侧边栏展示
**路由元信息 (meta)**:
- `title`: 页面标题
- `icon`: 菜单图标(使用 Ant Design Vue 图标名称)
- `hidden`: 是否隐藏菜单
- `noAuth`: 是否不需要认证
- `keepAlive`: 是否缓存页面
- `affix`: 是否固定标签页
**后端菜单数据格式**:
```javascript
{
path: '/system',
name: 'System',
title: '系统管理',
icon: 'Setting',
component: 'views/system', // 组件路径,相对于 pages 目录
redirect: '/system/user',
children: [
{
path: 'user',
name: 'SystemUser',
title: '用户管理',
icon: 'User',
component: 'views/system/user'
}
]
}
```
**路由转换逻辑**:
```javascript
// 将后端菜单转换为路由格式
function transformMenusToRoutes(menus) {
return menus.map(menu => {
const route = {
path: menu.path,
name: menu.name,
meta: {
title: menu.title,
icon: menu.icon,
hidden: menu.hidden,
keepAlive: menu.keepAlive || false
}
}
if (menu.component) {
route.component = loadComponent(menu.component)
}
if (menu.children) {
route.children = transformMenusToRoutes(menu.children)
}
return route
})
}
```
#### 路由守卫
系统通过路由守卫实现权限控制和动态路由加载:
```javascript
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore()
const isLoggedIn = userStore.isLoggedIn()
// 白名单直接放行
if (whiteList.includes(to.path)) {
return next()
}
// 未登录跳转登录页
if (!isLoggedIn) {
return next({ path: '/login', query: { redirect: to.fullPath } })
}
// 动态路由加载
if (!isDynamicRouteLoaded) {
const menus = userStore.getMenu()
const dynamicRoutes = transformMenusToRoutes(menus)
// 添加动态路由
dynamicRoutes.forEach(route => {
router.addRoute('Layout', route)
})
isDynamicRouteLoaded = true
next({ ...to, replace: true })
} else {
next()
}
})
```
### 5. 状态管理规范
#### Pinia Store 定义
使用组合式 API 定义 Store。
#### User Store(用户认证与权限)
`src/stores/modules/user.js` 负责管理用户认证信息和权限数据:
```javascript
// src/stores/modules/user.js
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { resetRouter } from '../../router'
export const useUserStore = defineStore('user', () => {
// State
const token = ref('') // 访问令牌
const refreshToken = ref('') // 刷新令牌
const userInfo = ref(null) // 用户信息
const menu = ref([]) // 用户菜单
const permissions = ref([]) // 用户权限节点
// Getters
const isLoggedIn = () => !!token.value
// Actions
function setToken(newToken) {
token.value = newToken
}
function setUserInfo(info) {
userInfo.value = info
}
// 设置菜单(合并静态菜单和后端菜单)
function setMenu(newMenu) {
const staticMenus = userRoutes || []
let mergedMenus = [...staticMenus]
if (newMenu && newMenu.length > 0) {
const menuMap = new Map()
// 添加静态菜单
staticMenus.forEach(m => {
if (m.path) menuMap.set(m.path, m)
})
// 添加后端菜单(覆盖重复路径)
newMenu.forEach(m => {
if (m.path) menuMap.set(m.path, m)
})
mergedMenus = Array.from(menuMap.values())
}
menu.value = mergedMenus
}
function getMenu() {
return menu.value
}
function setPermissions(data) {
permissions.value = data
}
function hasPermission(permission) {
if (!permissions.value || permissions.value.length === 0) {
return false
}
return permissions.value.includes(permission)
}
function logout() {
token.value = ''
refreshToken.value = ''
userInfo.value = null
menu.value = []
resetRouter()
}
return {
token,
refreshToken,
userInfo,
menu,
permissions,
setToken,
setUserInfo,
setMenu,
getMenu,
setPermissions,
hasPermission,
logout,
isLoggedIn
}
})
```
#### Store 使用
```javascript
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
// 使用 state
console.log(userStore.token)
console.log(userStore.menu)
console.log(userStore.permissions)
// 调用 action
userStore.setToken('xxx')
userStore.setMenu(menus)
userStore.setPermissions(permissions)
// 检查权限
if (userStore.hasPermission('user.create')) {
// 有权限,执行操作
}
// 登出
userStore.logout()
```
#### Store 持久化
使用 `pinia-plugin-persistedstate` 实现数据持久化:
```javascript
{
persist: {
key: 'user-store',
storage: customStorage,
pick: ['token', 'refreshToken', 'userInfo', 'menu']
}
}
```
#### 权限指令
项目提供权限指令,用于在模板中控制元素显示:
```vue
<template>
<!-- 只有拥有 user.create 权限时显示 -->
<a-button v-permission="'user.create'">新增</a-button>
<!-- 拥有多个权限之一时显示 -->
<a-button v-permission="['user.create', 'user.update']">编辑</a-button>
</template>
<script setup>
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
</script>
```
### 6. 表格开发规范
#### 使用 useTable Hook
项目提供了 `useTable` Hook 简化表格开发:
```javascript
import { useTable } from '@/hooks/useTable'
import { ref } from 'vue'
const {
loading,
dataSource,
pagination,
handleSearch,
handleReset,
handlePageChange
} = useTable({
api: userApi.getList, // API 方法
immediate: true // 是否立即加载
})
// 搜索参数
const searchParams = ref({
keyword: '',
status: ''
})
```
#### scTable 组件使用
```vue
<template>
<sc-table
:columns="columns"
:data-source="dataSource"
:loading="loading"
:pagination="pagination"
@page-change="handlePageChange"
>
<template #action="{ record }">
<a-button @click="handleEdit(record)">编辑</a-button>
</template>
</sc-table>
</template>
```
### 7. 表单开发规范
#### scForm 组件使用
```vue
<template>
<sc-form
ref="formRef"
:model="formData"
:rules="formRules"
:items="formItems"
@submit="handleSubmit"
/>
</template>
<script setup>
import { ref } from 'vue'
const formRef = ref(null)
const formData = ref({
username: '',
email: ''
})
const formRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }
]
}
const formItems = [
{
type: 'input',
prop: 'username',
label: '用户名',
placeholder: '请输入用户名'
},
{
type: 'input',
prop: 'email',
label: '邮箱',
placeholder: '请输入邮箱'
}
]
const handleSubmit = async () => {
await formRef.value.validate()
// 提交逻辑
}
</script>
```
### 8. 图标使用规范
#### Ant Design Vue Icons
```vue
<!-- 直接使用,无需导入 -->
<template>
<a-icon type="user" />
<a-icon type="setting" />
</template>
```
#### 常用图标
- `user`: 用户
- `setting`: 设置
- `delete`: 删除
- `edit`: 编辑
- `plus`: 添加
- `search`: 搜索
- `reload`: 刷新
- `download`: 下载
- `upload`: 上传
- `eye`: 查看
- `eye-invisible`: 隐藏
- `check-circle`: 成功
- `close-circle`: 失败
- `info-circle`: 信息
- `warning`: 警告
### 9. 国际化 (i18n) 规范
#### 使用 i18n
```javascript
import { useI18n } from '@/hooks/useI18n'
const { t } = useI18n()
// 使用
console.log(t('common.save'))
console.log(t('user.deleteConfirm'))
```
#### 语言文件组织
```javascript
// src/i18n/locales/zh.js
export default {
common: {
save: '保存',
cancel: '取消',
confirm: '确认',
delete: '删除'
},
user: {
deleteConfirm: '确定要删除该用户吗?'
}
}
```
### 10. 文件上传规范
#### scUpload 组件使用
```vue
<template>
<sc-upload
v-model="imageUrl"
:limit="1"
accept="image/*"
list-type="picture-card"
/>
</template>
<script setup>
import { ref } from 'vue'
const imageUrl = ref('')
</script>
```
### 11. 富文本编辑器规范
#### scEditor 组件使用
```vue
<template>
<sc-editor
v-model="content"
:height="400"
:toolbar="toolbarConfig"
/>
</template>
<script setup>
import { ref } from 'vue'
const content = ref('')
const toolbarConfig = [
'bold', 'italic', 'underline', 'strike',
'list', 'orderedList', 'quote', 'codeBlock',
'image', 'link'
]
</script>
```
### 12. 样式规范
#### 全局样式
`src/style.css` 中定义全局样式。
#### 组件样式
使用 `scoped` 避免样式污染:
```vue
<style scoped>
.user-list {
padding: 20px;
}
.user-list .table {
margin-top: 20px;
}
</style>
```
#### 命名规范
- 使用 BEM 命名法
- 类名使用 kebab-case
### 13. 工具函数使用
#### request.js (HTTP 请求)
```javascript
import request from '@/utils/request'
// GET 请求
request.get('/api/users', { params: { page: 1 } })
// POST 请求
request.post('/api/users', { name: 'test' })
// PUT 请求
request.put('/api/users/1', { name: 'updated' })
// DELETE 请求
request.delete('/api/users/1')
```
#### tool.js (工具函数)
```javascript
import { formatDate, deepClone } from '@/utils/tool'
// 格式化日期
formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')
// 深拷贝
deepClone(originalObject)
```
### 14. 开发流程
#### 登录流程
1. 用户输入用户名和密码
2. 调用 `authApi.login.post()` 发送登录请求
3. 后端返回 `token``refreshToken``userInfo``menu``permissions`
4. 前端保存数据到 Store(持久化)
5. 路由守卫检测到登录状态,加载动态路由
6. 跳转到首页或重定向页
```javascript
import { useUserStore } from '@/stores/modules/user'
import authApi from '@/api/auth'
const userStore = useUserStore()
const handleLogin = async () => {
try {
const res = await authApi.login.post({
username: 'admin',
password: '123456'
})
// 保存 token
userStore.setToken(res.data.token)
userStore.setRefreshToken(res.data.refreshToken)
// 保存用户信息
userStore.setUserInfo(res.data.user)
// 保存菜单(合并静态菜单)
userStore.setMenu(res.data.menu)
// 保存权限节点
userStore.setPermissions(res.data.permissions)
// 跳转首页
router.push('/dashboard')
} catch (error) {
message.error('登录失败')
}
}
```
#### 添加新页面
1. 在 `src/pages/` 对应模块下创建页面组件
2. 在 `src/api/` 中创建对应的 API 文件
3. 在后端添加对应的菜单和权限配置
4. 前端会自动加载动态路由(无需手动配置路由)
#### 添加新组件
1. 在 `src/components/` 或对应子目录下创建组件
2. 遵循组件结构规范
3. 添加必要的 Props 和 Emits
4. 编写组件文档
### 15. 常用命令
```bash
# 安装依赖
npm install
# 开发环境启动
npm run dev
# 生产环境构建
npm run build
# 预览生产构建
npm run preview
# 代码格式化
npm run format
# 代码检查
npm run lint
```
### 16. 注意事项
1. **禁止重复引入图标**: Ant Design Vue 图标已全局引入,直接使用即可
2. **使用组合式 API**: 新代码统一使用 `<script setup>` 语法
3. **组件复用**: 优先使用项目提供的公共组件(scTable、scForm 等)
4. **API 统一管理**: 所有接口统一在 `src/api/` 目录下管理
5. **路由懒加载**: 路由组件必须使用动态导入
6. **环境变量**: 通过 `import.meta.env` 访问环境变量
7. **不要编写 demo**: 开发过程中不编写示例代码
8. **测试提示**: 如需测试,提示用户是否运行测试,不主动运行
### 17. 代码质量
- 遵循 Vue 3 官方风格指南
- 保持代码简洁、可读
- 适当添加注释说明复杂逻辑
- 使用语义化的变量和函数命名
- 避免过多的嵌套层级
### 18. 性能优化
- 合理使用 `v-if``v-show`
- 列表渲染必须设置 `key`
- 大列表使用虚拟滚动
- 图片懒加载
- 路由懒加载
- 组件按需引入(除图标外)
+996
View File
@@ -0,0 +1,996 @@
# Laravel + Laravel-S 后端 API 项目开发规范
## 项目概述
本项目是一个基于 Laravel 框架开发的纯后端 API 系统,使用 `hhxsv5/laravel-s` 作为粘合剂,将 Swoole 与 Laravel 框架完美结合,提供高性能的异步服务能力。项目采用模块化开发架构,使用 `nwidart/laravel-modules` 扩展实现业务模块化管理。
## 技术栈
- **PHP**: 主要开发语言
- **Laravel**: Web 应用框架
- **Laravel-S (hhxsv5/laravel-s)**: Swoole 集成扩展
- **Swoole**: 高性能 PHP 协程框架
- **JWT (tymon/jwt-auth)**: 用户认证
- **nwidart/laravel-modules**: 模块化开发扩展
- **MySQL**: 关系型数据库
## 模块化架构
### 模块分类
项目分为两类模块:
1. **基础模块 (Base Modules)**
- **Auth**: 认证授权模块(用户、角色、权限)
- **System**: 系统配置模块(配置、日志、字典)
- 特点:不使用 `nwidart/laravel-modules` 扩展,直接在 Laravel 应用结构中开发
- 位置:`app/` 目录下
2. **业务模块 (Business Modules)**
- 使用 `nwidart/laravel-modules` 扩展创建
- 位置:`Modules/` 目录下
- 每个业务模块独立拥有完整的 MVC 结构
### 项目结构
```
├── app/ # 基础模块目录
│ ├── Http/
│ │ ├── Controllers/ # 基础模块控制器
│ │ │ ├── Auth/ # 认证相关控制器
│ │ │ │ └── Admin
│ │ │ │ │ ├── Auth.php
│ │ │ │ │ ├── User.php
│ │ │ │ │ ├── Role.php
│ │ │ │ │ └── Permission.php
│ │ │ └── System/ # 系统相关控制器
│ │ │ └── Admin
│ │ │ └── Api
│ │ └── Middleware/ # 中间件
│ ├── Models/ # 基础模块模型
│ │ └── Auth/
│ │ ├── User.php
│ │ ├── Role.php
│ │ └── Permission.php
│ └── Services/ # 基础模块服务层
│ └── Auth/
│ ├── AuthService.php
│ ├── UserService.php
│ ├── RoleService.php
│ └── PermissionService.php
├── Modules/ # 业务模块目录
│ ├── ModuleName/ # 业务模块示例
│ │ ├── App/
│ │ │ ├── Http/
│ │ │ │ ├── Controllers/ # 模块控制器
│ │ │ │ │ ├── Admin/ # 后台管理控制器
│ │ │ │ │ └── Api/ # 用户端API控制器
│ │ │ │ ├── Middleware/ # 模块中间件
│ │ │ │ └── Requests/ # 表单验证
│ │ │ ├── Models/ # 模块模型
│ │ │ ├── Services/ # 模块服务层
│ │ │ └── Providers/ # 模块服务提供者
│ │ ├── Database/
│ │ │ ├── migrations/ # 模块迁移文件
│ │ │ └── seeders/ # 模块数据填充
│ │ ├── Routes/
│ │ │ ├── admin.php # 后台管理路由(/admin前缀)
│ │ │ └── api.php # 用户端API路由(/api前缀)
│ │ ├── Resources/ # API 资源
│ │ ├── config/ # 模块配置文件
│ │ ├── module.json # 模块配置
│ │ └── README.md # 模块说明文档
├── config/
│ ├── modules.php # 模块配置
│ └── laravels.php # Laravel-S 配置文件
├── database/
│ ├── migrations/ # 基础模块迁移文件
│ └── seeders/ # 基础模块数据填充
├── routes/
│ ├── admin.php # 后台管理路由(基础模块)
│ └── api.php # 公共 API 路由(基础模块)
└── storage/
├── laravels.conf # Laravel-S 运行时配置
└── laravels.pid # Laravel-S 进程 ID
```
## 开发规范
### 1. 控制器层 (Controllers)
#### 基础模块控制器
**命名规范:**
- 文件路径: `app/Http/Controllers/{Module}/{Name}.php`
- 类名: `{Name}` (不加 Controller 后缀)
- 命名空间: `App\Http\Controllers\{Module}`
**示例:**
```php
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use App\Services\Auth\UserService;
class User extends Controller
{
protected $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function index(Request $request)
{
$result = $this->userService->getList($request->all());
return response()->json($result);
}
}
```
#### 业务模块控制器
业务模块控制器分为两类:
**1. 后台管理控制器**
**命名规范:**
- 文件路径: `Modules/{ModuleName}/App/Http/Controllers/Admin/{Name}.php`
- 类名: `{Name}` (不加 Controller 后缀)
- 命名空间: `Modules\{ModuleName}\App\Http\Controllers\Admin`
- 访问路径: `/admin/{module}/{resource}`
**示例:**
```php
<?php
namespace Modules\Blog\App\Http\Controllers\Admin;
use Illuminate\Http\Request;
use Modules\Blog\App\Services\PostService;
class Post extends Controller
{
protected $postService;
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
public function index(Request $request)
{
$result = $this->postService->getList($request->all());
return response()->json($result);
}
}
```
**2. 用户端 API 控制器**
**命名规范:**
- 文件路径: `Modules/{ModuleName}/App/Http/Controllers/Api/{Name}.php`
- 类名: `{Name}` (不加 Controller 后缀)
- 命名空间: `Modules\{ModuleName}\App\Http\Controllers\Api`
- 访问路径: `/api/{module}/{resource}`
**示例:**
```php
<?php
namespace Modules\Blog\App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Modules\Blog\App\Services\PostService;
class Post extends Controller
{
protected $postService;
public function __construct(PostService $postService)
{
$this->postService = $postService;
}
public function index(Request $request)
{
$result = $this->postService->getList($request->all());
return response()->json($result);
}
}
```
### 2. 服务层 (Services)
#### 基础模块服务
**命名规范:**
- 文件路径: `app/Services/{Module}/{Name}Service.php`
- 类名: `{Name}Service`
- 命名空间: `App\Services\{Module}`
**示例:**
```php
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use Illuminate\Support\Facades\Hash;
class UserService
{
public function create(array $data): User
{
$data['password'] = Hash::make($data['password']);
return User::create($data);
}
public function getById(int $id): ?User
{
return User::find($id);
}
}
```
#### 业务模块服务
**命名规范:**
- 文件路径: `Modules/{ModuleName}/App/Services/{Name}Service.php`
- 类名: `{Name}Service`
- 命名空间: `Modules\{ModuleName}\App\Services`
**示例:**
```php
<?php
namespace Modules\Blog\App\Services;
use Modules\Blog\App\Models\Post;
class PostService
{
public function create(array $data): Post
{
return Post::create($data);
}
public function getById(int $id): ?Post
{
return Post::find($id);
}
}
```
### 3. 模型层 (Models)
#### 基础模块模型
**命名规范:**
- 文件路径: `app/Models/{Module}/{Name}.php`
- 类名: `{Name}` (单数形式)
- 命名空间: `App\Models\{Module}`
- 数据表: `{module}_{names}` (复数形式,带模块前缀)
**示例:**
```php
<?php
namespace App\Models\Auth;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
protected $table = 'auth_roles';
protected $fillable = ['name', 'description'];
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class, 'auth_role_permission');
}
}
```
#### 业务模块模型
**命名规范:**
- 文件路径: `Modules/{ModuleName}/App/Models/{Name}.php`
- 类名: `{Name}` (单数形式)
- 命名空间: `Modules\{ModuleName}\App\Models`
- 数据表: `{module}_{names}` (复数形式,带模块前缀)
**示例:**
```php
<?php
namespace Modules\Blog\App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $table = 'blog_posts';
protected $fillable = ['title', 'content', 'status'];
}
```
### 4. 中间件 (Middleware)
**常用中间件:**
- `AdminAuth`: 后台管理认证
- `api.throttle`: API 请求频率限制
### 5. API 路由
#### 基础模块路由
基础模块路由定义在 `routes/` 目录下,通过 `bootstrap/app.php` 进行配置:
- `routes/admin.php`: 后台管理路由,自动添加 `/admin` 前缀和 `admin.` 路由名称前缀
- `routes/api.php`: 公共 API 路由,自动添加 `/api` 前缀
- `routes/web.php`: Web 路由
- `routes/console.php`: 命令行路由
**路由配置 (bootstrap/app.php):**
```php
<?php
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
then: function() {
Route::middleware(['api'])
->prefix('admin')
->name('admin.')
->group(base_path('routes/admin.php'));
},
health: '/up',
)
->create();
```
**中间件配置:**
```php
<?php
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'auth.check' => \App\Http\Middleware\AuthCheckMiddleware::class,
]);
})
```
**异常处理配置:**
```php
<?php
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(function(Request $request, Throwable $e){
return $request->expectsJson();
});
})
```
**事件监听器配置:**
```php
<?php
->withEvents(discover: [
__DIR__ . '/../app/Listeners'
])
```
**路由示例:**
```php
<?php
// routes/admin.php - 后台管理路由
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\User;
Route::middleware(['admin.auth'])->group(function () {
Route::apiResource('users', User::class);
// 路由名称: admin.users.index, admin.users.store, etc.
});
// routes/api.php - 公共 API 路由
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Auth\User;
Route::middleware(['auth:api'])->group(function () {
Route::apiResource('profile', User::class);
});
```
#### 业务模块路由
业务模块路由包含两个文件,分别定义后台管理和用户端 API 路由:
**1. 后台管理路由 (Routes/admin.php)**
文件路径:`Modules/{ModuleName}/Routes/admin.php`
```php
<?php
use Illuminate\Support\Facades\Route;
use Modules\Blog\App\Http\Controllers\Admin\Post;
Route::middleware(['admin.auth'])->group(function () {
Route::apiResource('blog/posts', Post::class);
});
```
路由访问路径:`/admin/{module}/{resource}`
**2. 用户端 API 路由 (Routes/api.php)**
文件路径:`Modules/{ModuleName}/Routes/api.php`
```php
<?php
use Illuminate\Support\Facades\Route;
use Modules\Blog\App\Http\Controllers\Api\Post;
Route::middleware(['auth:api'])->group(function () {
Route::apiResource('blog/posts', Post::class);
});
```
路由访问路径:`/api/{module}/{resource}`
### 6. 模块创建命令
使用 `nwidart/laravel-modules` 创建新业务模块:
```bash
# 创建模块
php artisan module:make ModuleName
# 创建带有资源的模块
php artisan module:make ModuleName --resource
# 创建多个模块
php artisan module:make Blog Shop Order
```
### 7. 模块组件创建命令
```bash
# 在指定模块中创建后台管理控制器
php artisan module:make-controller Admin/Post Blog
# 在指定模块中创建用户端API控制器
php artisan module:make-controller Api/Post Blog
# 在指定模块中创建模型
php artisan module:make-model Post Blog
# 在指定模块中创建迁移
php artisan module:make-migration create_posts_table Blog
# 在指定模块中创建服务类(需要自定义)
# 手动在 Modules/Blog/App/Services/ 目录创建
```
**注意:** 使用 `module:make-controller` 命令创建的控制器会自动添加 `Controller` 后缀,需要手动重命名文件和类名以移除后缀。
## API 响应规范
### 统一响应格式
**成功响应:**
```json
{
"code": 200,
"message": "success",
"data": {}
}
```
**错误响应:**
```json
{
"code": 400,
"message": "错误信息",
"data": null
}
```
**分页响应:**
```json
{
"code": 200,
"message": "success",
"data": {
"list": [],
"total": 100,
"page": 1,
"page_size": 20
}
}
```
### HTTP 状态码规范
- `200 OK`: 请求成功
- `201 Created`: 创建成功
- `204 No Content`: 删除成功
- `400 Bad Request`: 请求参数错误
- `401 Unauthorized`: 未认证
- `403 Forbidden`: 无权限
- `404 Not Found`: 资源不存在
- `422 Unprocessable Entity`: 表单验证失败
- `500 Internal Server Error`: 服务器错误
## Laravel-S / Swoole 开发注意事项
### 1. 长生命周期注意事项
由于 Swoole 是长生命周期运行,需注意以下几点:
**避免使用静态变量:**
```php
// ❌ 错误
public function handle()
{
static $counter = 0; // 会累积,不会重置
}
// ✅ 正确
public function handle()
{
$counter = 0; // 每次请求重新初始化
}
```
**避免使用全局变量:**
- 不要依赖 `$_GET`, `$_POST` 等超全局变量
- 使用 Laravel 的 `Request` 对象获取请求数据
### 2. 连接池管理
数据库、Redis 等连接需要正确管理,避免连接泄漏。
**配置连接池:** 在 `config/laravels.php` 中配置:
```php
'swoole' => [
'enable_coroutine' => true,
'worker_num' => 4,
'max_request' => 5000,
'max_request_grace' => 500,
]
```
### 3. 热重载机制
开发环境可以使用文件监控实现热重载:
```bash
# 使用 bin/laravels reload
php bin/laravels reload
# 或使用文件监控
bin/fswatch # Linux/Mac
bin/inotify # Linux
```
### 4. 定时器
使用 Swoole 定时器时,要确保定时器在合适的时机清除:
```php
\Swoole\Timer::after(5000, function() {
// 5秒后执行
});
$timerId = \Swoole\Timer::tick(1000, function() {
// 每秒执行
return false; // 返回 false 停止定时器
});
```
## 数据库规范
### 迁移文件
#### 基础模块迁移
- 文件路径: `database/migrations/`
- 迁移文件命名: `{YYYY_MM_DD_HHMMSS}_{description}.php`
- 表命名: `{module}_{table_names}` (模块名小写 + 下划线 + 表名复数,所有模块表都必须带模块名前缀)
**表命名示例:**
- Auth 模块: `auth_users`, `auth_roles`, `auth_permissions`, `auth_role_permission`
- System 模块: `system_configs`, `system_logs`, `system_dictionaries`
**迁移文件示例:**
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('auth_users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamps();
$table->softDeletes();
});
}
public function down()
{
Schema::dropIfExists('auth_users');
}
};
```
#### 业务模块迁移
- 文件路径: `Modules/{ModuleName}/Database/migrations/`
- 迁移文件命名: `{YYYY_MM_DD_HHMMSS}_{description}.php`
- 表命名: `{module}_{table_names}` (模块名小写 + 下划线 + 表名复数,所有模块表都必须带模块名前缀)
**表命名示例:**
- Blog 模块: `blog_posts`, `blog_categories`, `blog_tags`, `blog_post_tag`
- Shop 模块: `shop_products`, `shop_orders`, `shop_order_items`
- Order 模块: `order_payments`, `order_shippings`
### 字段命名规范
- 使用蛇形命名: `user_id`, `created_at`
- 布尔类型使用 `is_` 前缀: `is_active`, `is_deleted`
- 主键统一使用 `id`
### 数据填充
#### 基础模块数据填充
- 文件路径: `database/seeders/`
#### 业务模块数据填充
- 文件路径: `Modules/{ModuleName}/Database/seeders/`
## 认证与授权
### JWT 认证
使用 `tymon/jwt-auth` 实现 JWT 认证:
```php
// 登录获取 token
$token = auth('api')->attempt($credentials);
// 使用 token 认证
$user = auth('api')->user();
// 刷新 token
$newToken = auth('api')->refresh();
// 登出
auth('api')->logout();
```
### RBAC 权限控制
基于角色的访问控制 (Role-Based Access Control):
- User (用户) - Role (角色) - Permission (权限)
- 多对多关系设计
## 常用命令
### Laravel 命令
```bash
# 创建基础模块控制器
php artisan make:controller Auth/UserController
# 创建基础模块模型
php artisan make:model Auth/User
# 创建基础模块迁移
php artisan make:migration create_auth_users_table
# 执行迁移(包括所有模块)
php artisan migrate
# 回滚迁移
php artisan migrate:rollback
# 清除缓存
php artisan cache:clear
php artisan config:clear
php artisan route:clear
```
### Laravel-S 命令
```bash
# 启动服务
php bin/laravels start
# 停止服务
php bin/laravels stop
# 重启服务
php bin/laravels restart
# 重载服务(平滑重启)
php bin/laravels reload
# 查看状态
php bin/laravels status
# 查看帮助
php bin/laravels help
```
### Laravel Modules 命令
```bash
# 创建模块
php artisan module:make ModuleName
# 列出所有模块
php artisan module:list
# 启用模块
php artisan module:enable ModuleName
# 禁用模块
php artisan module:disable ModuleName
# 删除模块
php artisan module:delete ModuleName
# 模块迁移
php artisan module:migrate ModuleName
# 回滚模块迁移
php artisan module:migrate-rollback ModuleName
# 刷新模块迁移
php artisan module:migrate-refresh ModuleName
# 模块数据填充
php artisan module:seed ModuleName
# 重新发布模块配置
php artisan vendor:publish --tag=modules-config
```
## 代码质量
### PSR 规范
遵循 PSR-4 自动加载规范和 PSR-12 编码规范。
### 注释规范
- 类和方法添加 DocBlock 注释
- 复杂逻辑添加行内注释
**示例:**
```php
/**
* 用户服务类
*
* @package App\Services\Auth
*/
class UserService
{
/**
* 创建用户
*
* @param array $data 用户数据
* @return User
*/
public function create(array $data): User
{
// 业务逻辑
}
}
```
## 模块开发规范
### 1. 模块独立性
每个业务模块应保持独立,避免直接依赖其他业务模块:
- 模块间通信通过接口或事件实现
- 避免跨模块直接调用服务类
- 使用 Laravel 事件系统实现模块间解耦
### 2. 模块配置
每个业务模块应提供配置文件:
```php
// Modules/Blog/config/blog.php
return [
'per_page' => 20,
'status' => [
'draft' => 0,
'published' => 1,
],
];
```
### 3. 模块 README
每个业务模块应包含 README.md 文档,说明:
- 模块功能描述
- API 接口列表
- 数据库表结构
- 依赖说明
- 使用示例
### 4. 模块版本控制
使用 `module.json` 文件管理模块版本:
```json
{
"name": "Blog",
"alias": "blog",
"description": "Blog module",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Blog\\App\\Providers\\BlogServiceProvider"
],
"files": []
}
```
## 异常处理
### 自定义异常
创建自定义异常类处理业务异常:
```php
<?php
namespace App\Exceptions;
use Exception;
class BusinessException extends Exception
{
protected $code;
public function __construct(string $message, int $code = 400)
{
parent::__construct($message, $code);
$this->code = $code;
}
public function render()
{
return response()->json([
'code' => $this->code,
'message' => $this->message,
'data' => null
], $this->code);
}
}
```
## 测试规范
### 单元测试
```bash
# 创建测试
php artisan make:test UserTest
# 运行测试
php artisan test
```
### 模块测试
```bash
# 创建模块测试
php artisan module:test PostTest Blog
# 运行所有测试
php artisan test
```
## 部署注意事项
1. 生产环境关闭调试模式: `APP_ENV=production`, `APP_DEBUG=false`
2. 配置合适的 Worker 数量
3. 设置 `max_request` 防止内存泄漏
4. 配置日志轮转
5. 设置适当的超时时间
6. 确保所有模块的迁移都已执行
## 安全规范
1. 所有用户输入必须经过验证
2. 敏感数据必须加密存储
3. 使用 CSRF 防护(API 除外)
4. 使用 HTTPS 传输
5. 定期更新依赖包
6. 不要在代码中硬编码密码或密钥
7. 模块间通信需进行权限验证
## 性能优化
1. 使用 Redis 缓存热点数据
2. 使用队列处理耗时任务
3. 优化数据库查询,避免 N+1 问题
4. 使用 Eager Loading
5. 合理使用索引
6. 使用 Swoole 协程提高并发性能
7. 模块按需加载,禁用不使用的模块
## Git 规范
### 提交信息格式
```
<type>(<scope>): <subject>
<body>
<footer>
```
**Type 类型:**
- `feat`: 新功能
- `fix`: 修复
- `docs`: 文档
- `style`: 格式
- `refactor`: 重构
- `test`: 测试
- `chore`: 构建/工具
**Scope 示例:**
- `auth`: 认证模块
- `system`: 系统模块
- `blog`: 博客业务模块
- `shop`: 商城业务模块
**示例:**
```
feat(auth): 添加用户登录功能
- 实现 JWT 认证
- 添加登录接口
- 完善权限验证
Closes #123
```
## 资源链接
- [Laravel 文档](https://laravel.com/docs)
- [Laravel-S 文档](https://github.com/hhxsv5/laravel-s)
- [Swoole 文档](https://www.swoole.com/)
- [JWT-Auth 文档](https://github.com/tymondesigns/jwt-auth)
- [Laravel Modules 文档](https://nwidart.com/laravel-modules/)
+18
View File
@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 4
[compose.yaml]
indent_size = 4
+65
View File
@@ -0,0 +1,65 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
# PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1
# DB_PORT=3306
# DB_DATABASE=laravel
# DB_USERNAME=root
# DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
+11
View File
@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
+25
View File
@@ -0,0 +1,25 @@
*.log
.DS_Store
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
/.fleet
/.idea
/.nova
/.phpunit.cache
/.vscode
/.zed
/auth.json
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
Homestead.json
Homestead.yaml
Thumbs.db
*lock*
+59
View File
@@ -0,0 +1,59 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Redberry](https://redberry.international/laravel-development)**
- **[Active Logic](https://activelogic.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
+67
View File
@@ -0,0 +1,67 @@
<?php
namespace App\Exports;
use App\Models\Auth\Department;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
class DepartmentExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize
{
protected $departmentIds;
public function __construct(array $departmentIds = [])
{
$this->departmentIds = $departmentIds;
}
/**
* 获取数据集合
*/
public function collection()
{
$query = Department::query();
if (!empty($this->departmentIds)) {
$query->whereIn('id', $this->departmentIds);
}
return $query->get();
}
/**
* 设置表头
*/
public function headings(): array
{
return [
'ID',
'部门名称',
'上级部门',
'负责人',
'联系电话',
'排序',
'状态',
'创建时间',
];
}
/**
* 映射数据
*/
public function map($department): array
{
return [
$department->id,
$department->name,
$department->parent_id ? Department::find($department->parent_id)?->name : '',
$department->leader,
$department->phone,
$department->sort,
$department->status == 1 ? '启用' : '禁用',
$department->created_at ? $department->created_at->toDateTimeString() : '',
];
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Exports;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
class GenericExport implements FromCollection, WithHeadings
{
protected $data;
public function __construct(array $data)
{
$this->data = $data;
}
/**
* 获取数据集合
*/
public function collection()
{
return collect($this->data);
}
/**
* 设置表头
*/
public function headings(): array
{
// 第一行作为表头
return $this->data[0] ?? [];
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Exports;
use App\Models\Auth\User;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithMapping;
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
class UserExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize
{
protected $userIds;
public function __construct(array $userIds = [])
{
$this->userIds = $userIds;
}
/**
* 获取数据集合
*/
public function collection()
{
$query = User::with(['department', 'roles']);
if (!empty($this->userIds)) {
$query->whereIn('id', $this->userIds);
}
return $query->get();
}
/**
* 设置表头
*/
public function headings(): array
{
return [
'ID',
'用户名',
'真实姓名',
'邮箱',
'手机号',
'部门',
'角色',
'状态',
'最后登录时间',
'创建时间',
];
}
/**
* 映射数据
*/
public function map($user): array
{
return [
$user->id,
$user->username,
$user->real_name,
$user->email,
$user->phone,
$user->department ? $user->department->name : '',
$user->roles->pluck('name')->implode(','),
$user->status == 1 ? '启用' : '禁用',
$user->last_login_at ? $user->last_login_at->toDateTimeString() : '',
$user->created_at ? $user->created_at->toDateTimeString() : '',
];
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
namespace App\Http\Controllers\Auth\Admin;
use App\Http\Controllers\Controller;
use App\Services\Auth\AuthService;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Exception;
class Auth extends Controller
{
protected $authService;
public function __construct(AuthService $authService)
{
$this->authService = $authService;
}
/**
* 管理员登录
*/
public function login(Request $request)
{
try {
$validated = $request->validate([
'username' => 'required|string',
'password' => 'required|string',
]);
$result = $this->authService->login($validated);
return response()->json([
'code' => 200,
'message' => '登录成功',
'data' => $result,
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => $e->errors(),
], 422);
} catch (Exception $e) {
return response()->json([
'code' => 500,
'message' => '登录失败:' . $e->getMessage(),
'data' => null,
], 500);
}
}
/**
* 管理员登出
*/
public function logout(Request $request)
{
try {
$this->authService->logout();
return response()->json([
'code' => 200,
'message' => '登出成功',
'data' => null,
]);
} catch (Exception $e) {
return response()->json([
'code' => 500,
'message' => '登出失败:' . $e->getMessage(),
'data' => null,
], 500);
}
}
/**
* 刷新token
*/
public function refresh(Request $request)
{
try {
$result = $this->authService->refresh();
return response()->json([
'code' => 200,
'message' => '刷新成功',
'data' => $result,
]);
} catch (Exception $e) {
return response()->json([
'code' => 401,
'message' => 'Token无效或已过期',
'data' => null,
], 401);
}
}
/**
* 获取当前用户信息
*/
public function me(Request $request)
{
try {
$result = $this->authService->me();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
} catch (Exception $e) {
return response()->json([
'code' => 401,
'message' => '未登录或token已过期',
'data' => null,
], 401);
}
}
/**
* 找回密码
*/
public function resetPassword(Request $request)
{
try {
$validated = $request->validate([
'username' => 'required|string',
'password' => 'required|string|min:6|confirmed',
]);
$this->authService->resetPassword($validated);
return response()->json([
'code' => 200,
'message' => '密码重置成功',
'data' => null,
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => $e->errors(),
], 422);
} catch (Exception $e) {
return response()->json([
'code' => 500,
'message' => '密码重置失败:' . $e->getMessage(),
'data' => null,
], 500);
}
}
/**
* 修改密码
*/
public function changePassword(Request $request)
{
try {
$validated = $request->validate([
'old_password' => 'required|string',
'password' => 'required|string|min:6|confirmed',
]);
$this->authService->changePassword($validated);
return response()->json([
'code' => 200,
'message' => '密码修改成功',
'data' => null,
]);
} catch (ValidationException $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => $e->errors(),
], 422);
} catch (Exception $e) {
return response()->json([
'code' => 500,
'message' => '密码修改失败:' . $e->getMessage(),
'data' => null,
], 500);
}
}
}
@@ -0,0 +1,228 @@
<?php
namespace App\Http\Controllers\Auth\Admin;
use App\Http\Controllers\Controller;
use App\Services\Auth\DepartmentService;
use App\Services\Auth\ImportExportService;
use Illuminate\Http\Request;
class Department extends Controller
{
protected $departmentService;
protected $importExportService;
public function __construct(
DepartmentService $departmentService,
ImportExportService $importExportService
) {
$this->departmentService = $departmentService;
$this->importExportService = $importExportService;
}
/**
* 获取部门列表
*/
public function index(Request $request)
{
$params = $request->all();
$result = $this->departmentService->getList($params);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
}
/**
* 获取部门树
*/
public function tree()
{
$result = $this->departmentService->getTree();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['tree' => $result],
]);
}
/**
* 获取所有部门(不分页)
*/
public function getAll()
{
$result = $this->departmentService->getAll();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['list' => $result],
]);
}
/**
* 获取部门详情
*/
public function show($id)
{
$result = $this->departmentService->getById($id);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
}
/**
* 创建部门
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'parent_id' => 'nullable|integer|exists:auth_departments,id',
'leader' => 'nullable|string|max:50',
'phone' => 'nullable|string|max:20',
'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1',
]);
$result = $this->departmentService->create($validated);
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => ['id' => $result->id],
], 201);
}
/**
* 更新部门
*/
public function update(Request $request, $id)
{
$validated = $request->validate([
'name' => 'nullable|string|max:50',
'parent_id' => 'nullable|integer|exists:auth_departments,id',
'leader' => 'nullable|string|max:50',
'phone' => 'nullable|string|max:20',
'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1',
]);
$result = $this->departmentService->update($id, $validated);
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => ['id' => $result->id],
]);
}
/**
* 删除部门
*/
public function destroy($id)
{
$this->departmentService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null,
]);
}
/**
* 批量删除部门
*/
public function batchDelete(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
]);
$count = $this->departmentService->batchDelete($validated['ids']);
return response()->json([
'code' => 200,
'message' => "成功删除 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 批量更新部门状态
*/
public function batchUpdateStatus(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
'status' => 'required|integer|in:0,1',
]);
$count = $this->departmentService->batchUpdateStatus($validated['ids'], $validated['status']);
return response()->json([
'code' => 200,
'message' => "成功更新 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 导出部门
*/
public function export(Request $request)
{
$validated = $request->validate([
'ids' => 'nullable|array',
'ids.*' => 'integer',
]);
$filename = $this->importExportService->exportDepartments($validated['ids'] ?? []);
$filePath = $this->importExportService->getExportFilePath($filename);
return response()->download($filePath, $filename)->deleteFileAfterSend();
}
/**
* 导入部门
*/
public function import(Request $request)
{
$validated = $request->validate([
'file' => 'required|file|mimes:xlsx,xls',
]);
$file = $request->file('file');
$realPath = $file->getRealPath();
$filename = $file->getClientOriginalName();
$result = $this->importExportService->importDepartments($filename, $realPath);
return response()->json([
'code' => 200,
'message' => "导入完成,成功 {$result['success_count']} 条,失败 {$result['error_count']}",
'data' => $result,
]);
}
/**
* 下载部门导入模板
*/
public function downloadTemplate()
{
$filename = $this->importExportService->downloadDepartmentTemplate();
$filePath = $this->importExportService->getExportFilePath($filename);
return response()->download($filePath, $filename)->deleteFileAfterSend();
}
}
@@ -0,0 +1,182 @@
<?php
namespace App\Http\Controllers\Auth\Admin;
use App\Http\Controllers\Controller;
use App\Services\Auth\PermissionService;
use Illuminate\Http\Request;
class Permission extends Controller
{
protected $permissionService;
public function __construct(PermissionService $permissionService)
{
$this->permissionService = $permissionService;
}
/**
* 获取权限列表
*/
public function index(Request $request)
{
$params = $request->all();
$result = $this->permissionService->getList($params);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
}
/**
* 获取权限树
*/
public function tree(Request $request)
{
$params = $request->all();
$result = $this->permissionService->getTree($params);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['tree' => $result],
]);
}
/**
* 获取菜单树
*/
public function menu(Request $request)
{
$result = $this->permissionService->getMenuTree();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['tree' => $result],
]);
}
/**
* 获取权限详情
*/
public function show($id)
{
$result = $this->permissionService->getById($id);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
}
/**
* 创建权限
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'code' => 'required|string|max:100|unique:auth_permissions,code',
'type' => 'required|in:menu,api,button',
'route' => 'nullable|string|max:200',
'component' => 'nullable|string|max:200',
'icon' => 'nullable|string|max:50',
'parent_id' => 'nullable|integer|exists:auth_permissions,id',
'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1',
'meta' => 'nullable|array',
]);
$result = $this->permissionService->create($validated);
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => ['id' => $result->id],
], 201);
}
/**
* 更新权限
*/
public function update(Request $request, $id)
{
$validated = $request->validate([
'name' => 'nullable|string|max:50',
'code' => 'nullable|string|max:100|unique:auth_permissions,code,' . $id,
'type' => 'nullable|in:menu,api,button',
'route' => 'nullable|string|max:200',
'component' => 'nullable|string|max:200',
'icon' => 'nullable|string|max:50',
'parent_id' => 'nullable|integer|exists:auth_permissions,id',
'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1',
'meta' => 'nullable|array',
]);
$result = $this->permissionService->update($id, $validated);
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => ['id' => $result->id],
]);
}
/**
* 删除权限
*/
public function destroy($id)
{
$this->permissionService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null,
]);
}
/**
* 批量删除权限
*/
public function batchDelete(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
]);
$count = $this->permissionService->batchDelete($validated['ids']);
return response()->json([
'code' => 200,
'message' => "成功删除 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 批量更新权限状态
*/
public function batchUpdateStatus(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
'status' => 'required|integer|in:0,1',
]);
$count = $this->permissionService->batchUpdateStatus($validated['ids'], $validated['status']);
return response()->json([
'code' => 200,
'message' => "成功更新 {$count} 条数据",
'data' => ['count' => $count],
]);
}
}
+240
View File
@@ -0,0 +1,240 @@
<?php
namespace App\Http\Controllers\Auth\Admin;
use App\Http\Controllers\Controller;
use App\Services\Auth\RoleService;
use Illuminate\Http\Request;
class Role extends Controller
{
protected $roleService;
public function __construct(RoleService $roleService)
{
$this->roleService = $roleService;
}
/**
* 获取角色列表
*/
public function index(Request $request)
{
$params = $request->all();
$result = $this->roleService->getList($params);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
}
/**
* 获取所有角色(不分页)
*/
public function getAll()
{
$result = $this->roleService->getAll();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['list' => $result],
]);
}
/**
* 获取角色详情
*/
public function show($id)
{
$result = $this->roleService->getById($id);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
}
/**
* 创建角色
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'code' => 'required|string|max:50|unique:auth_roles,code',
'description' => 'nullable|string|max:200',
'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1',
'permission_ids' => 'nullable|array',
'permission_ids.*' => 'integer|exists:auth_permissions,id',
]);
$result = $this->roleService->create($validated);
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => ['id' => $result->id],
], 201);
}
/**
* 更新角色
*/
public function update(Request $request, $id)
{
$validated = $request->validate([
'name' => 'nullable|string|max:50',
'code' => 'nullable|string|max:50|unique:auth_roles,code,' . $id,
'description' => 'nullable|string|max:200',
'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1',
'permission_ids' => 'nullable|array',
'permission_ids.*' => 'integer|exists:auth_permissions,id',
]);
$result = $this->roleService->update($id, $validated);
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => ['id' => $result->id],
]);
}
/**
* 删除角色
*/
public function destroy($id)
{
$this->roleService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null,
]);
}
/**
* 批量删除角色
*/
public function batchDelete(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
]);
$count = $this->roleService->batchDelete($validated['ids']);
return response()->json([
'code' => 200,
'message' => "成功删除 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 批量更新角色状态
*/
public function batchUpdateStatus(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
'status' => 'required|integer|in:0,1',
]);
$count = $this->roleService->batchUpdateStatus($validated['ids'], $validated['status']);
return response()->json([
'code' => 200,
'message' => "成功更新 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 分配权限
*/
public function assignPermissions(Request $request, $id)
{
$validated = $request->validate([
'permission_ids' => 'required|array',
'permission_ids.*' => 'integer|exists:auth_permissions,id',
]);
$this->roleService->assignPermissions($id, $validated['permission_ids']);
return response()->json([
'code' => 200,
'message' => '权限分配成功',
'data' => null,
]);
}
/**
* 获取角色的权限列表
*/
public function getPermissions($id)
{
$result = $this->roleService->getPermissions($id);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['tree' => $result],
]);
}
/**
* 复制角色
*/
public function copy(Request $request, $id)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'code' => 'required|string|max:50|unique:auth_roles,code',
'description' => 'nullable|string|max:200',
'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1',
]);
$result = $this->roleService->copy($id, $validated);
return response()->json([
'code' => 200,
'message' => '复制成功',
'data' => ['id' => $result->id],
], 201);
}
/**
* 批量复制角色
*/
public function batchCopy(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
'name' => 'nullable|string|max:50',
'code' => 'nullable|string|max:50',
'description' => 'nullable|string|max:200',
'sort' => 'nullable|integer|min:0',
'status' => 'nullable|integer|in:0,1',
]);
$result = $this->roleService->batchCopy($validated['ids'], $validated);
return response()->json([
'code' => 200,
'message' => "复制完成,成功 {$result['success_count']} 个,失败 {$result['error_count']}",
'data' => $result,
]);
}
}
+329
View File
@@ -0,0 +1,329 @@
<?php
namespace App\Http\Controllers\Auth\Admin;
use App\Http\Controllers\Controller;
use App\Services\Auth\UserService;
use App\Services\Auth\UserOnlineService;
use App\Services\Auth\ImportExportService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class User extends Controller
{
protected $userService;
protected $userOnlineService;
protected $importExportService;
public function __construct(
UserService $userService,
UserOnlineService $userOnlineService,
ImportExportService $importExportService
) {
$this->userService = $userService;
$this->userOnlineService = $userOnlineService;
$this->importExportService = $importExportService;
}
/**
* 获取用户列表
*/
public function index(Request $request)
{
$params = $request->all();
$result = $this->userService->getList($params);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
}
/**
* 获取用户详情
*/
public function show(Request $request, $id)
{
$result = $this->userService->getById($id);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result,
]);
}
/**
* 创建用户
*/
public function store(Request $request)
{
$validated = $request->validate([
'username' => 'required|string|max:50|unique:auth_users,username',
'password' => 'required|string|min:6',
'real_name' => 'required|string|max:50',
'email' => 'nullable|email|unique:auth_users,email',
'phone' => 'nullable|string|max:20',
'department_id' => 'nullable|integer|exists:auth_departments,id',
'role_ids' => 'nullable|array',
'role_ids.*' => 'integer|exists:auth_roles,id',
'status' => 'nullable|integer|in:0,1',
]);
$result = $this->userService->create($validated);
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => ['id' => $result->id],
], 201);
}
/**
* 更新用户
*/
public function update(Request $request, $id)
{
$validated = $request->validate([
'username' => 'nullable|string|max:50|unique:auth_users,username,' . $id,
'password' => 'nullable|string|min:6',
'real_name' => 'nullable|string|max:50',
'email' => 'nullable|email|unique:auth_users,email,' . $id,
'phone' => 'nullable|string|max:20',
'department_id' => 'nullable|integer|exists:auth_departments,id',
'role_ids' => 'nullable|array',
'role_ids.*' => 'integer|exists:auth_roles,id',
'status' => 'nullable|integer|in:0,1',
]);
$result = $this->userService->update($id, $validated);
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => ['id' => $result->id],
]);
}
/**
* 删除用户
*/
public function destroy(Request $request, $id)
{
$this->userService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null,
]);
}
/**
* 批量删除用户
*/
public function batchDelete(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
]);
$count = $this->userService->batchDelete($validated['ids']);
return response()->json([
'code' => 200,
'message' => "成功删除 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 批量更新用户状态
*/
public function batchUpdateStatus(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
'status' => 'required|integer|in:0,1',
]);
$count = $this->userService->batchUpdateStatus($validated['ids'], $validated['status']);
return response()->json([
'code' => 200,
'message' => "成功更新 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 批量分配部门
*/
public function batchAssignDepartment(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
'department_id' => 'nullable|integer|exists:auth_departments,id',
]);
$count = $this->userService->batchAssignDepartment($validated['ids'], $validated['department_id']);
return response()->json([
'code' => 200,
'message' => "成功分配 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 批量分配角色
*/
public function batchAssignRoles(Request $request)
{
$validated = $request->validate([
'ids' => 'required|array',
'ids.*' => 'integer',
'role_ids' => 'nullable|array',
'role_ids.*' => 'integer|exists:auth_roles,id',
]);
$count = $this->userService->batchAssignRoles($validated['ids'], $validated['role_ids'] ?? []);
return response()->json([
'code' => 200,
'message' => "成功分配 {$count} 条数据",
'data' => ['count' => $count],
]);
}
/**
* 导出用户
*/
public function export(Request $request)
{
$validated = $request->validate([
'ids' => 'nullable|array',
'ids.*' => 'integer',
]);
$filename = $this->importExportService->exportUsers($validated['ids'] ?? []);
$filePath = $this->importExportService->getExportFilePath($filename);
return response()->download($filePath, $filename)->deleteFileAfterSend();
}
/**
* 导入用户
*/
public function import(Request $request)
{
$validated = $request->validate([
'file' => 'required|file|mimes:xlsx,xls',
]);
$file = $request->file('file');
$realPath = $file->getRealPath();
$filename = $file->getClientOriginalName();
$result = $this->importExportService->importUsers($filename, $realPath);
return response()->json([
'code' => 200,
'message' => "导入完成,成功 {$result['success_count']} 条,失败 {$result['error_count']}",
'data' => $result,
]);
}
/**
* 下载用户导入模板
*/
public function downloadTemplate()
{
$filename = $this->importExportService->downloadUserTemplate();
$filePath = $this->importExportService->getExportFilePath($filename);
return response()->download($filePath, $filename)->deleteFileAfterSend();
}
/**
* 获取在线用户数量
*/
public function getOnlineCount()
{
$count = $this->userOnlineService->getOnlineCount();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['count' => $count],
]);
}
/**
* 获取在线用户列表
*/
public function getOnlineUsers(Request $request)
{
$limit = $request->get('limit', 100);
$users = $this->userOnlineService->getOnlineUsers($limit);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['list' => $users],
]);
}
/**
* 获取用户的所有会话
*/
public function getUserSessions($userId)
{
$sessions = $this->userOnlineService->getUserSessions($userId);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => ['sessions' => $sessions],
]);
}
/**
* 强制用户下线(单个会话)
*/
public function setUserOffline($userId, Request $request)
{
$validated = $request->validate([
'token' => 'nullable|string',
]);
if (!empty($validated['token'])) {
$this->userOnlineService->setOffline($userId, $validated['token']);
}
return response()->json([
'code' => 200,
'message' => '操作成功',
'data' => null,
]);
}
/**
* 强制用户所有设备下线
*/
public function setUserAllOffline($userId)
{
$this->userOnlineService->setAllOffline($userId);
return response()->json([
'code' => 200,
'message' => '操作成功',
'data' => null,
]);
}
}
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
+172
View File
@@ -0,0 +1,172 @@
<?php
namespace App\Http\Controllers\System\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\CityService;
class City extends Controller
{
protected $cityService;
public function __construct(CityService $cityService)
{
$this->cityService = $cityService;
}
public function index(Request $request)
{
$result = $this->cityService->getList($request->all());
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result
]);
}
public function tree()
{
$tree = $this->cityService->getCachedTree();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $tree
]);
}
public function show(int $id)
{
$city = $this->cityService->getById($id);
if (!$city) {
return response()->json([
'code' => 404,
'message' => '城市不存在',
'data' => null
], 404);
}
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $city
]);
}
public function store(Request $request)
{
try {
$city = $this->cityService->create($request->all());
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => $city
], 201);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function update(Request $request, int $id)
{
try {
$city = $this->cityService->update($id, $request->all());
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => $city
]);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function destroy(int $id)
{
try {
$this->cityService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function batchDelete(Request $request)
{
$this->cityService->batchDelete($request->input('ids', []));
return response()->json([
'code' => 200,
'message' => '批量删除成功',
'data' => null
]);
}
public function batchUpdateStatus(Request $request)
{
$this->cityService->batchUpdateStatus(
$request->input('ids', []),
$request->input('status', true)
);
return response()->json([
'code' => 200,
'message' => '批量更新状态成功',
'data' => null
]);
}
public function children(int $parentId)
{
$children = $this->cityService->getChildren($parentId);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $children
]);
}
public function provinces()
{
$provinces = $this->cityService->getProvinces();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $provinces
]);
}
public function cities(int $provinceId)
{
$cities = $this->cityService->getCities($provinceId);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $cities
]);
}
public function districts(int $cityId)
{
$districts = $this->cityService->getDistricts($cityId);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $districts
]);
}
}
@@ -0,0 +1,142 @@
<?php
namespace App\Http\Controllers\System\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\ConfigService;
class Config extends Controller
{
protected $configService;
public function __construct(ConfigService $configService)
{
$this->configService = $configService;
}
public function index(Request $request)
{
$result = $this->configService->getList($request->all());
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result
]);
}
public function show(int $id)
{
$config = $this->configService->getById($id);
if (!$config) {
return response()->json([
'code' => 404,
'message' => '配置不存在',
'data' => null
], 404);
}
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $config
]);
}
public function store(Request $request)
{
try {
$config = $this->configService->create($request->all());
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => $config
], 201);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function update(Request $request, int $id)
{
try {
$config = $this->configService->update($id, $request->all());
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => $config
]);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function destroy(int $id)
{
try {
$this->configService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function batchDelete(Request $request)
{
$this->configService->batchDelete($request->input('ids', []));
return response()->json([
'code' => 200,
'message' => '批量删除成功',
'data' => null
]);
}
public function batchUpdateStatus(Request $request)
{
$this->configService->batchUpdateStatus(
$request->input('ids', []),
$request->input('status', true)
);
return response()->json([
'code' => 200,
'message' => '批量更新状态成功',
'data' => null
]);
}
public function getGroups()
{
$groups = $this->configService->getGroups();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $groups
]);
}
public function getByGroup(Request $request)
{
$configs = $this->configService->getByGroup($request->input('group'));
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $configs
]);
}
}
@@ -0,0 +1,211 @@
<?php
namespace App\Http\Controllers\System\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\DictionaryService;
class Dictionary extends Controller
{
protected $dictionaryService;
public function __construct(DictionaryService $dictionaryService)
{
$this->dictionaryService = $dictionaryService;
}
public function index(Request $request)
{
$result = $this->dictionaryService->getList($request->all());
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result
]);
}
public function all()
{
$dictionaries = $this->dictionaryService->getAll();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $dictionaries
]);
}
public function show(int $id)
{
$dictionary = $this->dictionaryService->getById($id);
if (!$dictionary) {
return response()->json([
'code' => 404,
'message' => '字典不存在',
'data' => null
], 404);
}
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $dictionary
]);
}
public function store(Request $request)
{
try {
$dictionary = $this->dictionaryService->create($request->all());
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => $dictionary
], 201);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function update(Request $request, int $id)
{
try {
$dictionary = $this->dictionaryService->update($id, $request->all());
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => $dictionary
]);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function destroy(int $id)
{
try {
$this->dictionaryService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function batchDelete(Request $request)
{
$this->dictionaryService->batchDelete($request->input('ids', []));
return response()->json([
'code' => 200,
'message' => '批量删除成功',
'data' => null
]);
}
public function batchUpdateStatus(Request $request)
{
$this->dictionaryService->batchUpdateStatus(
$request->input('ids', []),
$request->input('status', true)
);
return response()->json([
'code' => 200,
'message' => '批量更新状态成功',
'data' => null
]);
}
public function getItemsList(Request $request)
{
$result = $this->dictionaryService->getItemsList($request->all());
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result
]);
}
public function storeItem(Request $request)
{
try {
$item = $this->dictionaryService->createItem($request->all());
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => $item
], 201);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function updateItem(Request $request, int $id)
{
try {
$item = $this->dictionaryService->updateItem($id, $request->all());
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => $item
]);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function destroyItem(int $id)
{
$this->dictionaryService->deleteItem($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null
]);
}
public function batchDeleteItems(Request $request)
{
$this->dictionaryService->batchDeleteItems($request->input('ids', []));
return response()->json([
'code' => 200,
'message' => '批量删除成功',
'data' => null
]);
}
public function batchUpdateItemsStatus(Request $request)
{
$this->dictionaryService->batchUpdateItemsStatus(
$request->input('ids', []),
$request->input('status', true)
);
return response()->json([
'code' => 200,
'message' => '批量更新状态成功',
'data' => null
]);
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
namespace App\Http\Controllers\System\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\LogRequest;
use App\Services\System\LogService;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\GenericExport;
class Log extends Controller
{
protected $logService;
public function __construct(LogService $logService)
{
$this->logService = $logService;
}
public function index(LogRequest $request)
{
$result = $this->logService->getList($request->validated());
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result
]);
}
public function export(LogRequest $request)
{
$params = $request->validated();
$pageSize = $params['page_size'] ?? 10000; // 导出时默认获取更多数据
// 获取所有符合条件的日志(不分页)
$query = $this->logService->getListQuery($params);
$logs = $query->limit($pageSize)->get();
// 准备导出数据
$headers = [
'ID', '用户名', '模块', '操作', '请求方法', 'URL', 'IP地址',
'状态码', '状态', '错误信息', '执行时间(ms)', '创建时间'
];
$data = [];
foreach ($logs as $log) {
$data[] = [
$log->id,
$log->username,
$log->module,
$log->action,
$log->method,
$log->url,
$log->ip,
$log->status_code,
$log->status === 'success' ? '成功' : '失败',
$log->error_message ?? '-',
$log->execution_time,
$log->created_at->format('Y-m-d H:i:s'),
];
}
$filename = '系统操作日志_' . date('YmdHis') . '.xlsx';
return Excel::download(new GenericExport($headers, $data), $filename);
}
public function show(int $id)
{
$log = $this->logService->getById($id);
if (!$log) {
return response()->json([
'code' => 404,
'message' => '日志不存在',
'data' => null
], 404);
}
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $log
]);
}
public function destroy(int $id)
{
$this->logService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null
]);
}
public function batchDelete(Request $request)
{
$this->logService->batchDelete($request->input('ids', []));
return response()->json([
'code' => 200,
'message' => '批量删除成功',
'data' => null
]);
}
public function clearLogs(Request $request)
{
$days = $request->input('days', 30);
$this->logService->clearLogs($days);
return response()->json([
'code' => 200,
'message' => '清理成功',
'data' => null
]);
}
public function getStatistics(LogRequest $request)
{
$statistics = $this->logService->getStatistics($request->validated());
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $statistics
]);
}
}
+152
View File
@@ -0,0 +1,152 @@
<?php
namespace App\Http\Controllers\System\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\TaskService;
class Task extends Controller
{
protected $taskService;
public function __construct(TaskService $taskService)
{
$this->taskService = $taskService;
}
public function index(Request $request)
{
$result = $this->taskService->getList($request->all());
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $result
]);
}
public function all()
{
$tasks = $this->taskService->getAll();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $tasks
]);
}
public function show(int $id)
{
$task = $this->taskService->getById($id);
if (!$task) {
return response()->json([
'code' => 404,
'message' => '任务不存在',
'data' => null
], 404);
}
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $task
]);
}
public function store(Request $request)
{
try {
$task = $this->taskService->create($request->all());
return response()->json([
'code' => 200,
'message' => '创建成功',
'data' => $task
], 201);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function update(Request $request, int $id)
{
try {
$task = $this->taskService->update($id, $request->all());
return response()->json([
'code' => 200,
'message' => '更新成功',
'data' => $task
]);
} catch (\Exception $e) {
return response()->json([
'code' => 422,
'message' => $e->getMessage(),
'data' => null
], 422);
}
}
public function destroy(int $id)
{
$this->taskService->delete($id);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null
]);
}
public function batchDelete(Request $request)
{
$this->taskService->batchDelete($request->input('ids', []));
return response()->json([
'code' => 200,
'message' => '批量删除成功',
'data' => null
]);
}
public function batchUpdateStatus(Request $request)
{
$this->taskService->batchUpdateStatus(
$request->input('ids', []),
$request->input('status', true)
);
return response()->json([
'code' => 200,
'message' => '批量更新状态成功',
'data' => null
]);
}
public function run(int $id)
{
try {
$result = $this->taskService->run($id);
return response()->json([
'code' => 200,
'message' => '执行成功',
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function getStatistics()
{
$statistics = $this->taskService->getStatistics();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $statistics
]);
}
}
@@ -0,0 +1,152 @@
<?php
namespace App\Http\Controllers\System\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\UploadService;
class Upload extends Controller
{
protected $uploadService;
public function __construct(UploadService $uploadService)
{
$this->uploadService = $uploadService;
}
public function upload(Request $request)
{
try {
$request->validate([
'file' => 'required|file|max:10240',
]);
$file = $request->file('file');
$directory = $request->input('directory', 'uploads');
$options = [
'compress' => $request->input('compress', false),
'quality' => $request->input('quality', 80),
'width' => $request->input('width'),
'height' => $request->input('height'),
];
$result = $this->uploadService->upload($file, $directory, $options);
return response()->json([
'code' => 200,
'message' => '上传成功',
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function uploadMultiple(Request $request)
{
try {
$request->validate([
'files' => 'required|array',
'files.*' => 'file|max:10240',
]);
$files = $request->file('files');
$directory = $request->input('directory', 'uploads');
$options = [
'compress' => $request->input('compress', false),
'quality' => $request->input('quality', 80),
'width' => $request->input('width'),
'height' => $request->input('height'),
];
$results = $this->uploadService->uploadMultiple($files, $directory, $options);
return response()->json([
'code' => 200,
'message' => '上传成功',
'data' => $results
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function uploadBase64(Request $request)
{
try {
$request->validate([
'base64' => 'required|string',
]);
$base64 = $request->input('base64');
$directory = $request->input('directory', 'uploads');
$fileName = $request->input('file_name');
$result = $this->uploadService->uploadBase64($base64, $directory, $fileName);
return response()->json([
'code' => 200,
'message' => '上传成功',
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function delete(Request $request)
{
try {
$request->validate([
'path' => 'required|string',
]);
$path = $request->input('path');
$this->uploadService->delete($path);
return response()->json([
'code' => 200,
'message' => '删除成功',
'data' => null
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function batchDelete(Request $request)
{
try {
$request->validate([
'paths' => 'required|array',
]);
$paths = $request->input('paths', []);
$this->uploadService->deleteMultiple($paths);
return response()->json([
'code' => 200,
'message' => '批量删除成功',
'data' => null
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace App\Http\Controllers\System\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\CityService;
class City extends Controller
{
protected $cityService;
public function __construct(CityService $cityService)
{
$this->cityService = $cityService;
}
public function tree()
{
$tree = $this->cityService->getCachedTree();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $tree
]);
}
public function provinces()
{
$provinces = $this->cityService->getProvinces();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $provinces
]);
}
public function cities(int $provinceId)
{
$cities = $this->cityService->getCities($provinceId);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $cities
]);
}
public function districts(int $cityId)
{
$districts = $this->cityService->getDistricts($cityId);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $districts
]);
}
public function show(int $id)
{
$city = $this->cityService->getById($id);
if (!$city) {
return response()->json([
'code' => 404,
'message' => '城市不存在',
'data' => null
], 404);
}
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $city
]);
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\System\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\ConfigService;
class Config extends Controller
{
protected $configService;
public function __construct(ConfigService $configService)
{
$this->configService = $configService;
}
public function index()
{
$configs = $this->configService->getAllConfig();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $configs
]);
}
public function getByGroup(Request $request)
{
$configs = $this->configService->getByGroup($request->input('group'));
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $configs
]);
}
public function getByKey(Request $request)
{
$key = $request->input('key');
$value = $this->configService->getConfigValue($key);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => [
'key' => $key,
'value' => $value,
]
]);
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\System\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\DictionaryService;
class Dictionary extends Controller
{
protected $dictionaryService;
public function __construct(DictionaryService $dictionaryService)
{
$this->dictionaryService = $dictionaryService;
}
public function index()
{
$dictionaries = $this->dictionaryService->getAll();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $dictionaries
]);
}
public function getByCode(Request $request)
{
$code = $request->input('code');
$items = $this->dictionaryService->getItemsByCode($code);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => [
'code' => $code,
'items' => $items,
]
]);
}
public function show(int $id)
{
$dictionary = $this->dictionaryService->getById($id);
if (!$dictionary) {
return response()->json([
'code' => 404,
'message' => '字典不存在',
'data' => null
], 404);
}
return response()->json([
'code' => 200,
'message' => 'success',
'data' => $dictionary
]);
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers\System\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\System\UploadService;
class Upload extends Controller
{
protected $uploadService;
public function __construct(UploadService $uploadService)
{
$this->uploadService = $uploadService;
}
public function upload(Request $request)
{
try {
$request->validate([
'file' => 'required|file|max:10240',
]);
$file = $request->file('file');
$directory = $request->input('directory', 'uploads');
$options = [
'compress' => $request->input('compress', false),
'quality' => $request->input('quality', 80),
'width' => $request->input('width'),
'height' => $request->input('height'),
];
$result = $this->uploadService->upload($file, $directory, $options);
return response()->json([
'code' => 200,
'message' => '上传成功',
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function uploadMultiple(Request $request)
{
try {
$request->validate([
'files' => 'required|array',
'files.*' => 'file|max:10240',
]);
$files = $request->file('files');
$directory = $request->input('directory', 'uploads');
$options = [
'compress' => $request->input('compress', false),
'quality' => $request->input('quality', 80),
'width' => $request->input('width'),
'height' => $request->input('height'),
];
$results = $this->uploadService->uploadMultiple($files, $directory, $options);
return response()->json([
'code' => 200,
'message' => '上传成功',
'data' => $results
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
public function uploadBase64(Request $request)
{
try {
$request->validate([
'base64' => 'required|string',
]);
$base64 = $request->input('base64');
$directory = $request->input('directory', 'uploads');
$fileName = $request->input('file_name');
$result = $this->uploadService->uploadBase64($base64, $directory, $fileName);
return response()->json([
'code' => 200,
'message' => '上传成功',
'data' => $result
]);
} catch (\Exception $e) {
return response()->json([
'code' => 400,
'message' => $e->getMessage(),
'data' => null
], 400);
}
}
}
+395
View File
@@ -0,0 +1,395 @@
<?php
namespace App\Http\Controllers\System;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Services\WebSocket\WebSocketService;
/**
* WebSocket Controller
*
* Provides API endpoints for WebSocket operations
*/
class WebSocket extends Controller
{
/**
* @var WebSocketService
*/
protected $webSocketService;
/**
* WebSocket constructor
*/
public function __construct()
{
$this->webSocketService = app(WebSocketService::class);
}
/**
* Get online user count
*
* @return JsonResponse
*/
public function getOnlineCount(): JsonResponse
{
$count = $this->webSocketService->getOnlineUserCount();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => [
'online_count' => $count
]
]);
}
/**
* Get online user IDs
*
* @return JsonResponse
*/
public function getOnlineUsers(): JsonResponse
{
$userIds = $this->webSocketService->getOnlineUserIds();
return response()->json([
'code' => 200,
'message' => 'success',
'data' => [
'user_ids' => $userIds,
'count' => count($userIds)
]
]);
}
/**
* Check if a user is online
*
* @param Request $request
* @return JsonResponse
*/
public function checkOnline(Request $request): JsonResponse
{
$request->validate([
'user_id' => 'required|integer'
]);
$userId = $request->input('user_id');
$isOnline = $this->webSocketService->isUserOnline($userId);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => [
'user_id' => $userId,
'is_online' => $isOnline
]
]);
}
/**
* Send message to a specific user
*
* @param Request $request
* @return JsonResponse
*/
public function sendToUser(Request $request): JsonResponse
{
$request->validate([
'user_id' => 'required|integer',
'type' => 'required|string',
'data' => 'required|array'
]);
$userId = $request->input('user_id');
$type = $request->input('type');
$data = $request->input('data');
$message = [
'type' => $type,
'data' => $data
];
$sent = $this->webSocketService->sendToUser($userId, $message);
return response()->json([
'code' => $sent ? 200 : 404,
'message' => $sent ? 'Message sent successfully' : 'User is not online',
'data' => [
'user_id' => $userId,
'sent' => $sent
]
], $sent ? 200 : 404);
}
/**
* Send message to multiple users
*
* @param Request $request
* @return JsonResponse
*/
public function sendToUsers(Request $request): JsonResponse
{
$request->validate([
'user_ids' => 'required|array',
'user_ids.*' => 'integer',
'type' => 'required|string',
'data' => 'required|array'
]);
$userIds = $request->input('user_ids');
$type = $request->input('type');
$data = $request->input('data');
$message = [
'type' => $type,
'data' => $data
];
$sentTo = $this->webSocketService->sendToUsers($userIds, $message);
return response()->json([
'code' => 200,
'message' => 'success',
'data' => [
'total_users' => count($userIds),
'sent_to' => $sentTo,
'failed' => count($userIds) - count($sentTo)
]
]);
}
/**
* Broadcast message to all users
*
* @param Request $request
* @return JsonResponse
*/
public function broadcast(Request $request): JsonResponse
{
$request->validate([
'type' => 'required|string',
'data' => 'required|array',
'exclude_user_id' => 'nullable|integer'
]);
$type = $request->input('type');
$data = $request->input('data');
$excludeUserId = $request->input('exclude_user_id');
$message = [
'type' => $type,
'data' => $data
];
$count = $this->webSocketService->broadcast($message, $excludeUserId);
return response()->json([
'code' => 200,
'message' => 'Broadcast sent successfully',
'data' => [
'sent_to' => $count,
'exclude_user_id' => $excludeUserId
]
]);
}
/**
* Send message to a channel
*
* @param Request $request
* @return JsonResponse
*/
public function sendToChannel(Request $request): JsonResponse
{
$request->validate([
'channel' => 'required|string',
'type' => 'required|string',
'data' => 'required|array'
]);
$channel = $request->input('channel');
$type = $request->input('type');
$data = $request->input('data');
$message = [
'type' => $type,
'data' => $data
];
$count = $this->webSocketService->sendToChannel($channel, $message);
return response()->json([
'code' => 200,
'message' => 'Message sent to channel successfully',
'data' => [
'channel' => $channel,
'sent_to' => $count
]
]);
}
/**
* Send system notification
*
* @param Request $request
* @return JsonResponse
*/
public function sendNotification(Request $request): JsonResponse
{
$request->validate([
'title' => 'required|string|max:255',
'message' => 'required|string|max:1000',
'type' => 'nullable|string|in:info,success,warning,error',
'extra_data' => 'nullable|array'
]);
$title = $request->input('title');
$message = $request->input('message');
$type = $request->input('type', 'info');
$extraData = $request->input('extra_data', []);
$count = $this->webSocketService->sendSystemNotification($title, $message, $type, $extraData);
return response()->json([
'code' => 200,
'message' => 'Notification sent successfully',
'data' => [
'sent_to' => $count
]
]);
}
/**
* Send notification to specific users
*
* @param Request $request
* @return JsonResponse
*/
public function sendNotificationToUsers(Request $request): JsonResponse
{
$request->validate([
'user_ids' => 'required|array',
'user_ids.*' => 'integer',
'title' => 'required|string|max:255',
'message' => 'required|string|max:1000',
'type' => 'nullable|string|in:info,success,warning,error',
'extra_data' => 'nullable|array'
]);
$userIds = $request->input('user_ids');
$title = $request->input('title');
$message = $request->input('message');
$type = $request->input('type', 'info');
$extraData = $request->input('extra_data', []);
$sentTo = $this->webSocketService->sendNotificationToUsers($userIds, $title, $message, $type, $extraData);
return response()->json([
'code' => 200,
'message' => 'Notification sent successfully',
'data' => [
'total_users' => count($userIds),
'sent_to' => $sentTo,
'failed' => count($userIds) - count($sentTo)
]
]);
}
/**
* Push data update
*
* @param Request $request
* @return JsonResponse
*/
public function pushDataUpdate(Request $request): JsonResponse
{
$request->validate([
'user_ids' => 'required|array',
'user_ids.*' => 'integer',
'resource_type' => 'required|string',
'action' => 'required|string|in:create,update,delete',
'data' => 'required|array'
]);
$userIds = $request->input('user_ids');
$resourceType = $request->input('resource_type');
$action = $request->input('action');
$data = $request->input('data');
$sentTo = $this->webSocketService->pushDataUpdate($userIds, $resourceType, $action, $data);
return response()->json([
'code' => 200,
'message' => 'Data update pushed successfully',
'data' => [
'resource_type' => $resourceType,
'action' => $action,
'total_users' => count($userIds),
'sent_to' => $sentTo,
'failed' => count($userIds) - count($sentTo)
]
]);
}
/**
* Push data update to channel
*
* @param Request $request
* @return JsonResponse
*/
public function pushDataUpdateToChannel(Request $request): JsonResponse
{
$request->validate([
'channel' => 'required|string',
'resource_type' => 'required|string',
'action' => 'required|string|in:create,update,delete',
'data' => 'required|array'
]);
$channel = $request->input('channel');
$resourceType = $request->input('resource_type');
$action = $request->input('action');
$data = $request->input('data');
$count = $this->webSocketService->pushDataUpdateToChannel($channel, $resourceType, $action, $data);
return response()->json([
'code' => 200,
'message' => 'Data update pushed to channel successfully',
'data' => [
'channel' => $channel,
'resource_type' => $resourceType,
'action' => $action,
'sent_to' => $count
]
]);
}
/**
* Disconnect a user from WebSocket
*
* @param Request $request
* @return JsonResponse
*/
public function disconnectUser(Request $request): JsonResponse
{
$request->validate([
'user_id' => 'required|integer'
]);
$userId = $request->input('user_id');
$disconnected = $this->webSocketService->disconnectUser($userId);
return response()->json([
'code' => $disconnected ? 200 : 404,
'message' => $disconnected ? 'User disconnected successfully' : 'User is not online',
'data' => [
'user_id' => $userId,
'disconnected' => $disconnected
]
], $disconnected ? 200 : 404);
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class AuthCheckMiddleware
{
/**
* 处理传入请求
*
* @param Request $request
* @param Closure $next
* @param string|null $guard 认证守卫名称(默认为 api
* @param string|null $permission 需要检查的权限编码(可选)
* @return Response
*/
public function handle(Request $request, Closure $next, ?string $guard = 'api', ?string $permission = null): Response
{
// 检查是否已认证
if (!Auth::guard($guard)->check()) {
return response()->json([
'code' => 401,
'message' => '未登录或token已过期',
'data' => null,
], 401);
}
// 获取当前用户
$user = Auth::guard($guard)->user();
// 检查用户状态
if (isset($user->status) && $user->status !== 1) {
return response()->json([
'code' => 403,
'message' => '账号已被禁用',
'data' => null,
], 403);
}
// 如果需要检查权限
if ($permission !== null) {
if (!$this->checkPermission($user, $permission, $guard)) {
return response()->json([
'code' => 403,
'message' => '无权限访问',
'data' => null,
], 403);
}
}
// 将用户信息添加到请求中
$request->merge(['auth_user' => $user]);
// 更新用户最后活跃时间
if (method_exists($user, 'updateLastActiveAt')) {
$user->updateLastActiveAt();
}
return $next($request);
}
/**
* 检查用户权限
*
* @param mixed $user
* @param string $permission 权限编码
* @param string $guard 认证守卫
* @return bool
*/
protected function checkPermission($user, string $permission, string $guard): bool
{
// 如果用户有所有权限标识
if (method_exists($user, 'hasAllPermissions') && $user->hasAllPermissions()) {
return true;
}
// 检查用户是否有指定权限
if (method_exists($user, 'hasPermission')) {
return $user->hasPermission($permission);
}
return false;
}
/**
* 检查多个权限(满足任意一个即可)
*
* @param array $permissions 权限编码数组
* @return bool
*/
protected function checkAnyPermission($user, array $permissions): bool
{
foreach ($permissions as $permission) {
if ($this->checkPermission($user, $permission, 'api')) {
return true;
}
}
return false;
}
/**
* 检查多个权限(必须全部满足)
*
* @param array $permissions 权限编码数组
* @return bool
*/
protected function checkAllPermissions($user, array $permissions): bool
{
foreach ($permissions as $permission) {
if (!$this->checkPermission($user, $permission, 'api')) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,242 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log as LaravelLog;
use App\Services\System\LogService;
use Throwable;
class LogRequestMiddleware
{
protected $logService;
public function __construct(LogService $logService)
{
$this->logService = $logService;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$startTime = microtime(true);
$response = $next($request);
$endTime = microtime(true);
$executionTime = round(($endTime - $startTime) * 1000, 2); // 转换为毫秒
// 异步记录日志,不影响响应速度
$this->logRequest($request, $response, $executionTime);
return $response;
}
/**
* 记录请求日志
*
* @param \Illuminate\Http\Request $request
* @param \Illuminate\Http\Response $response
* @param float $executionTime
* @return void
*/
protected function logRequest(Request $request, $response, float $executionTime): void
{
try {
// 获取当前用户信息
$user = Auth::guard('admin')->user();
$userId = $user ? $user->id : null;
$username = $user ? $user->username : 'guest';
// 解析模块和操作
$module = $this->parseModule($request->path());
$action = $this->parseAction($request->method(), $request->path());
// 获取请求参数(排除敏感信息)
$params = $this->sanitizeParams($request->all());
// 获取响应数据
$result = null;
if ($response->getStatusCode() >= 400) {
$result = $response->getContent();
}
// 确定日志状态
$status = $response->getStatusCode() < 400 ? 'success' : 'error';
$errorMessage = null;
if ($status === 'error') {
$errorMessage = $this->extractErrorMessage($result);
}
// 构建日志数据
$logData = [
'user_id' => $userId,
'username' => $username,
'module' => $module,
'action' => $action,
'method' => $request->method(),
'url' => $request->fullUrl(),
'ip' => $this->getClientIp($request),
'user_agent' => $request->userAgent(),
'params' => $params,
'result' => $result,
'status_code' => $response->getStatusCode(),
'status' => $status,
'error_message' => $errorMessage,
'execution_time' => $executionTime,
];
// 记录到数据库
$this->logService->create($logData);
// 同时记录到 Laravel 日志(用于错误)
if ($status === 'error') {
LaravelLog::error('API Request Error', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'user_id' => $userId,
'error' => $errorMessage,
]);
}
} catch (Throwable $e) {
// 记录日志失败不影响业务流程
LaravelLog::error('Log request failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
}
}
/**
* 解析模块名称
*
* @param string $path
* @return string
*/
protected function parseModule(string $path): string
{
$segments = explode('/', trim($path, '/'));
// 移除前缀(如 admin, api
$prefixes = ['admin', 'api'];
if (in_array($segments[0], $prefixes)) {
array_shift($segments);
}
return $segments[0] ?? 'unknown';
}
/**
* 解析操作名称
*
* @param string $method
* @param string $path
* @return string
*/
protected function parseAction(string $method, string $path): string
{
$segments = explode('/', trim($path, '/'));
// 获取资源名称
$resource = end($segments);
// 如果资源是 ID,则取前一个作为资源名
if (is_numeric($resource)) {
$resource = prev($segments);
}
// 根据方法映射操作
$actionMap = [
'GET' => '查询',
'POST' => '创建',
'PUT' => '更新',
'PATCH' => '更新',
'DELETE' => '删除',
];
$action = $actionMap[$method] ?? '操作';
return $action . ' ' . $resource;
}
/**
* 清理敏感参数
*
* @param array $params
* @return array
*/
protected function sanitizeParams(array $params): array
{
$sensitiveKeys = ['password', 'password_confirmation', 'token', 'secret', 'key'];
array_walk_recursive($params, function (&$value, $key) use ($sensitiveKeys) {
if (in_array(strtolower($key), $sensitiveKeys)) {
$value = '******';
}
});
return $params;
}
/**
* 提取错误信息
*
* @param string|null $content
* @return string|null
*/
protected function extractErrorMessage(?string $content): ?string
{
if (empty($content)) {
return null;
}
try {
$data = json_decode($content, true);
if (isset($data['message'])) {
return $data['message'];
}
} catch (\Exception $e) {
// JSON 解析失败,返回原始内容
}
return $content;
}
/**
* 获取客户端 IP
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function getClientIp(Request $request): string
{
$ip = $request->ip();
// 检查代理头
$headers = [
'HTTP_X_FORWARDED_FOR',
'HTTP_CLIENT_IP',
'HTTP_X_REAL_IP',
'HTTP_CF_CONNECTING_IP',
];
foreach ($headers as $header) {
if ($request->hasHeader($header)) {
$forwardedIps = explode(',', $request->header($header));
$ip = trim($forwardedIps[0]);
break;
}
}
return $ip;
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
class LogRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
$rules = [];
if ($this->isMethod('GET')) {
// 列表查询参数验证
$rules = [
'user_id' => 'nullable|integer|exists:auth_users,id',
'username' => 'nullable|string|max:50',
'module' => 'nullable|string|max:50',
'action' => 'nullable|string|max:100',
'status' => 'nullable|in:success,error',
'start_date' => 'nullable|date',
'end_date' => 'nullable|date|after_or_equal:start_date',
'ip' => 'nullable|ip',
'page' => 'nullable|integer|min:1',
'page_size' => 'nullable|integer|min:1|max:100',
];
} elseif ($this->isMethod('POST')) {
// 批量删除参数验证
if ($this->routeIs('*.batch-delete')) {
$rules = [
'ids' => 'required|array',
'ids.*' => 'required|integer|exists:system_logs,id',
];
}
// 清理日志参数验证
if ($this->routeIs('*.clear')) {
$rules = [
'days' => 'nullable|integer|min:1|max:365',
];
}
} elseif ($this->isMethod('DELETE')) {
// 单个删除参数验证
$rules = [
'id' => 'required|integer|exists:system_logs,id',
];
}
return $rules;
}
/**
* Get custom messages for validator errors.
*
* @return array
*/
public function messages(): array
{
return [
'user_id.exists' => '用户不存在',
'username.max' => '用户名最多50个字符',
'module.max' => '模块名最多50个字符',
'action.max' => '操作名最多100个字符',
'status.in' => '状态值必须是 success 或 error',
'start_date.date' => '开始日期格式不正确',
'end_date.date' => '结束日期格式不正确',
'end_date.after_or_equal' => '结束日期必须大于或等于开始日期',
'ip.ip' => 'IP地址格式不正确',
'page.integer' => '页码必须是整数',
'page.min' => '页码必须大于0',
'page_size.integer' => '每页数量必须是整数',
'page_size.min' => '每页数量必须大于0',
'page_size.max' => '每页数量不能超过100',
'ids.required' => '请选择要删除的日志',
'ids.array' => '日志ID必须是数组',
'ids.*.required' => '日志ID不能为空',
'ids.*.integer' => '日志ID必须是整数',
'ids.*.exists' => '日志不存在',
'days.integer' => '天数必须是整数',
'days.min' => '天数必须大于0',
'days.max' => '天数不能超过365',
'id.required' => '日志ID不能为空',
'id.integer' => '日志ID必须是整数',
'id.exists' => '日志不存在',
];
}
/**
* Handle a failed validation attempt.
*
* @param \Illuminate\Contracts\Validation\Validator $validator
* @return void
*
* @throws \Illuminate\Http\Exceptions\HttpResponseException
*/
protected function failedValidation(Validator $validator): void
{
throw new HttpResponseException(
response()->json([
'code' => 422,
'message' => $validator->errors()->first(),
'data' => null,
], 422)
);
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
// 设置默认值
if ($this->isMethod('GET')) {
$this->merge([
'page' => $this->input('page', 1),
'page_size' => $this->input('page_size', 20),
'days' => $this->input('days', 30),
]);
}
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
namespace App\Imports;
use App\Models\Auth\Department;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation;
class DepartmentImport implements ToCollection, WithHeadingRow, WithValidation
{
protected $successCount = 0;
protected $errorCount = 0;
protected $errors = [];
/**
* 处理导入数据
*/
public function collection(Collection $rows)
{
foreach ($rows as $index => $row) {
try {
// 跳过空行
if (empty($row['部门名称'])) {
continue;
}
// 检查部门名称是否已存在
$exists = Department::where('name', $row['部门名称'])->exists();
if ($exists) {
$this->addError($index + 2, '部门名称已存在: ' . $row['部门名称']);
continue;
}
// 查找父级部门
$parentId = null;
if (!empty($row['上级部门名称'])) {
$parent = Department::where('name', $row['上级部门名称'])->first();
if (!$parent) {
$this->addError($index + 2, '上级部门不存在: ' . $row['上级部门名称']);
continue;
}
$parentId = $parent->id;
}
// 创建部门
Department::create([
'name' => $row['部门名称'],
'parent_id' => $parentId,
'leader' => $row['负责人'] ?? null,
'phone' => $row['联系电话'] ?? null,
'sort' => $row['排序'] ?? 0,
'status' => 1,
]);
$this->successCount++;
} catch (\Exception $e) {
$this->addError($index + 2, $e->getMessage());
}
}
}
/**
* 验证规则
*/
public function rules(): array
{
return [
'部门名称' => 'required|string|max:50',
'上级部门名称' => 'nullable|string|max:50',
'负责人' => 'nullable|string|max:50',
'联系电话' => 'nullable|string|max:20',
'排序' => 'nullable|integer|min:0',
];
}
/**
* 自定义验证消息
*/
public function customValidationMessages(): array
{
return [
'部门名称.required' => '部门名称不能为空',
];
}
/**
* 添加错误
*/
protected function addError(int $row, string $message): void
{
$this->errorCount++;
$this->errors[] = "{$row} 行: {$message}";
}
/**
* 获取成功数量
*/
public function getSuccessCount(): int
{
return $this->successCount;
}
/**
* 获取错误数量
*/
public function getErrorCount(): int
{
return $this->errorCount;
}
/**
* 获取错误信息
*/
public function getErrors(): array
{
return $this->errors;
}
}
+147
View File
@@ -0,0 +1,147 @@
<?php
namespace App\Imports;
use App\Models\Auth\User;
use App\Models\Auth\Department;
use App\Models\Auth\Role;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Hash;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Maatwebsite\Excel\Concerns\WithValidation;
class UserImport implements ToCollection, WithHeadingRow, WithValidation
{
protected $successCount = 0;
protected $errorCount = 0;
protected $errors = [];
/**
* 处理导入数据
*/
public function collection(Collection $rows)
{
foreach ($rows as $index => $row) {
try {
// 跳过空行
if (empty($row['用户名'])) {
continue;
}
// 检查用户名是否已存在
$exists = User::where('username', $row['用户名'])->exists();
if ($exists) {
$this->addError($index + 2, '用户名已存在');
continue;
}
// 查找部门
$departmentId = null;
if (!empty($row['部门名称'])) {
$department = Department::where('name', $row['部门名称'])->first();
if (!$department) {
$this->addError($index + 2, '部门不存在: ' . $row['部门名称']);
continue;
}
$departmentId = $department->id;
}
// 查找角色
$roleIds = [];
if (!empty($row['角色名称(多个用逗号分隔)'])) {
$roleNames = array_map('trim', explode(',', $row['角色名称(多个用逗号分隔)']));
$roles = Role::whereIn('name', $roleNames)->get();
if ($roles->count() != count($roleNames)) {
$existingNames = $roles->pluck('name')->toArray();
$notFound = array_diff($roleNames, $existingNames);
$this->addError($index + 2, '角色不存在: ' . implode(', ', $notFound));
continue;
}
$roleIds = $roles->pluck('id')->toArray();
}
// 创建用户
$user = User::create([
'username' => $row['用户名'],
'password' => Hash::make($row['密码']),
'real_name' => $row['真实姓名'],
'email' => $row['邮箱'] ?? null,
'phone' => $row['手机号'] ?? null,
'department_id' => $departmentId,
'status' => 1,
]);
// 分配角色
if (!empty($roleIds)) {
$user->roles()->attach($roleIds);
}
$this->successCount++;
} catch (\Exception $e) {
$this->addError($index + 2, $e->getMessage());
}
}
}
/**
* 验证规则
*/
public function rules(): array
{
return [
'用户名' => 'required|string|max:50',
'密码' => 'required|string|min:6',
'真实姓名' => 'required|string|max:50',
'邮箱' => 'nullable|email',
'手机号' => 'nullable|string|max:20',
];
}
/**
* 自定义验证消息
*/
public function customValidationMessages(): array
{
return [
'用户名.required' => '用户名不能为空',
'密码.required' => '密码不能为空',
'真实姓名.required' => '真实姓名不能为空',
'密码.min' => '密码至少6位',
];
}
/**
* 添加错误
*/
protected function addError(int $row, string $message): void
{
$this->errorCount++;
$this->errors[] = "{$row} 行: {$message}";
}
/**
* 获取成功数量
*/
public function getSuccessCount(): int
{
return $this->successCount;
}
/**
* 获取错误数量
*/
public function getErrorCount(): int
{
return $this->errorCount;
}
/**
* 获取错误信息
*/
public function getErrors(): array
{
return $this->errors;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Models\Auth;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Department extends Model
{
use SoftDeletes;
protected $table = 'auth_departments';
protected $fillable = [
'name',
'parent_id',
'leader',
'phone',
'sort',
'status',
];
protected $casts = [
'parent_id' => 'integer',
'sort' => 'integer',
'status' => 'integer',
];
/**
* 获取子部门
*/
public function children(): HasMany
{
return $this->hasMany(Department::class, 'parent_id');
}
/**
* 获取父部门
*/
public function parent()
{
return $this->belongsTo(Department::class, 'parent_id');
}
/**
* 获取部门下的用户
*/
public function users(): HasMany
{
return $this->hasMany(User::class, 'department_id');
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Models\Auth;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Permission extends Model
{
use SoftDeletes;
protected $table = 'auth_permissions';
protected $fillable = [
'name',
'code',
'type',
'parent_id',
'route',
'component',
'meta',
'sort',
'status',
];
protected $casts = [
'parent_id' => 'integer',
'meta' => 'array',
'sort' => 'integer',
'status' => 'integer',
];
/**
* 关联角色
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'auth_role_permission', 'permission_id', 'role_id')
->withTimestamps();
}
/**
* 获取子权限
*/
public function children()
{
return $this->hasMany(Permission::class, 'parent_id');
}
/**
* 获取父权限
*/
public function parent()
{
return $this->belongsTo(Permission::class, 'parent_id');
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Models\Auth;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Role extends Model
{
use SoftDeletes;
protected $table = 'auth_roles';
protected $fillable = [
'name',
'code',
'description',
'sort',
'status',
];
protected $casts = [
'sort' => 'integer',
'status' => 'integer',
];
/**
* 关联权限
*/
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class, 'auth_role_permission', 'role_id', 'permission_id')
->withTimestamps();
}
/**
* 关联用户
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'auth_user_role', 'role_id', 'user_id')
->withTimestamps();
}
}
+109
View File
@@ -0,0 +1,109 @@
<?php
namespace App\Models\Auth;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
use SoftDeletes;
protected $table = 'auth_users';
protected $fillable = [
'username',
'password',
'real_name',
'email',
'phone',
'department_id',
'avatar',
'status',
'last_login_at',
'last_login_ip',
];
protected $hidden = [
'password',
'deleted_at',
];
protected $casts = [
'status' => 'integer',
'department_id' => 'integer',
'last_login_at' => 'datetime',
];
/**
* 关联部门
*/
public function department(): BelongsTo
{
return $this->belongsTo(Department::class, 'department_id');
}
/**
* 关联角色
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'auth_user_role', 'user_id', 'role_id')
->withTimestamps();
}
/**
* 获取用户的所有权限
*/
public function permissions()
{
return $this->roles()->with('permissions');
}
/**
* 检查用户是否有指定权限
*/
public function hasPermission(string $permissionCode): bool
{
foreach ($this->roles as $role) {
foreach ($role->permissions as $permission) {
if ($permission->code === $permissionCode) {
return true;
}
}
}
return false;
}
/**
* 检查用户是否有指定角色
*/
public function hasRole(string $roleCode): bool
{
return $this->roles()->where('code', $roleCode)->exists();
}
/**
* 获取 JWT 标识符
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* 获取 JWT 自定义声明
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Models\System;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class City extends Model
{
protected $table = 'system_cities';
protected $fillable = [
'parent_id',
'name',
'code',
'pinyin',
'pinyin_short',
'level',
'sort',
'status',
];
protected $casts = [
'parent_id' => 'integer',
'level' => 'integer',
'sort' => 'integer',
'status' => 'boolean',
];
public function children(): HasMany
{
return $this->hasMany(City::class, 'parent_id')->orderBy('sort');
}
public function activeChildren(): HasMany
{
return $this->hasMany(City::class, 'parent_id')
->where('status', true)
->orderBy('sort');
}
public function parent()
{
return $this->belongsTo(City::class, 'parent_id');
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Models\System;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Config extends Model
{
use SoftDeletes;
protected $table = 'system_configs';
protected $fillable = [
'group',
'key',
'name',
'type',
'options',
'value',
'default_value',
'description',
'validation',
'sort',
'is_system',
'status',
];
protected $casts = [
'options' => 'array',
'is_system' => 'boolean',
'status' => 'boolean',
];
public function getOptionsAttribute($value)
{
if (is_string($value)) {
return json_decode($value, true) ?? [];
}
return $value;
}
public function setOptionsAttribute($value)
{
$this->attributes['options'] = is_array($value) ? json_encode($value) : $value;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Models\System;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Dictionary extends Model
{
use SoftDeletes;
protected $table = 'system_dictionaries';
protected $fillable = [
'name',
'code',
'description',
'status',
'sort',
];
protected $casts = [
'status' => 'boolean',
];
public function items(): HasMany
{
return $this->hasMany(DictionaryItem::class, 'dictionary_id')->orderBy('sort');
}
public function activeItems(): HasMany
{
return $this->hasMany(DictionaryItem::class, 'dictionary_id')
->where('status', true)
->orderBy('sort');
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Models\System;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DictionaryItem extends Model
{
protected $table = 'system_dictionary_items';
protected $fillable = [
'dictionary_id',
'label',
'value',
'color',
'description',
'is_default',
'status',
'sort',
];
protected $casts = [
'is_default' => 'boolean',
'status' => 'boolean',
];
public function dictionary(): BelongsTo
{
return $this->belongsTo(Dictionary::class, 'dictionary_id');
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace App\Models\System;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Log extends Model
{
protected $table = 'system_logs';
protected $fillable = [
'user_id',
'username',
'module',
'action',
'method',
'url',
'ip',
'user_agent',
'params',
'result',
'status_code',
'status',
'error_message',
'execution_time',
];
protected $casts = [
'params' => 'array',
'execution_time' => 'integer',
'status_code' => 'integer',
];
public function user(): BelongsTo
{
return $this->belongsTo(\App\Models\Auth\User::class, 'user_id');
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Models\System;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Task extends Model
{
use SoftDeletes;
protected $table = 'system_tasks';
protected $fillable = [
'name',
'command',
'description',
'type',
'expression',
'timezone',
'is_active',
'run_in_background',
'without_overlapping',
'only_one',
'last_run_at',
'next_run_at',
'last_output',
'run_count',
'failed_count',
];
protected $casts = [
'is_active' => 'boolean',
'run_in_background' => 'boolean',
'without_overlapping' => 'boolean',
'only_one' => 'boolean',
'last_run_at' => 'datetime',
'next_run_at' => 'datetime',
'run_count' => 'integer',
'failed_count' => 'integer',
];
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}
+248
View File
@@ -0,0 +1,248 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use App\Services\Auth\PermissionService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthService
{
protected $permissionService;
public function __construct(PermissionService $permissionService)
{
$this->permissionService = $permissionService;
}
/**
* 管理员登录
*/
public function login(array $credentials): array
{
$user = User::where('username', $credentials['username'])->first();
if (!$user || !Hash::check($credentials['password'], $user->password)) {
throw ValidationException::withMessages([
'username' => ['用户名或密码错误'],
]);
}
if ($user->status !== 1) {
throw ValidationException::withMessages([
'username' => ['账号已被禁用'],
]);
}
// 更新登录信息
$user->update([
'last_login_at' => now(),
'last_login_ip' => request()->ip(),
]);
// 生成token
$token = Auth::guard('admin')->login($user);
// 生成refresh token
$refreshToken = Auth::guard('admin')->refresh();
// 获取用户菜单
$menu = $this->getUserMenu($user);
// 获取用户权限列表
$permissions = $this->getUserPermissions($user);
return [
'token' => $token,
'refreshToken' => $refreshToken,
'user' => $this->getUserInfo($user),
'menu' => $menu,
'permissions' => $permissions,
];
}
/**
* 管理员登出
*/
public function logout(): void
{
Auth::guard('admin')->logout();
}
/**
* 刷新token
*/
public function refresh(): array
{
$newToken = Auth::guard('admin')->refresh();
$user = Auth::guard('admin')->user();
// 生成新的refresh token
$newRefreshToken = Auth::guard('admin')->refresh();
// 获取用户菜单
$menu = $this->getUserMenu($user);
// 获取用户权限列表
$permissions = $this->getUserPermissions($user);
return [
'token' => $newToken,
'refreshToken' => $newRefreshToken,
'user' => $this->getUserInfo($user),
'menu' => $menu,
'permissions' => $permissions,
];
}
/**
* 获取当前用户信息
*/
public function me(): array
{
$user = Auth::guard('admin')->user();
return $this->getUserInfo($user);
}
/**
* 找回密码
*/
public function resetPassword(array $data): void
{
$user = User::where('username', $data['username'])
->orWhere('email', $data['username'])
->orWhere('phone', $data['username'])
->first();
if (!$user) {
throw ValidationException::withMessages([
'username' => ['用户不存在'],
]);
}
$user->update([
'password' => Hash::make($data['password']),
]);
}
/**
* 修改密码
*/
public function changePassword(array $data): void
{
$user = Auth::guard('admin')->user();
if (!Hash::check($data['old_password'], $user->password)) {
throw ValidationException::withMessages([
'old_password' => ['原密码错误'],
]);
}
$user->update([
'password' => Hash::make($data['password']),
]);
}
/**
* 获取用户信息详情
*/
private function getUserInfo(User $user): array
{
$user->load(['department', 'roles.permissions']);
return [
'id' => $user->id,
'username' => $user->username,
'real_name' => $user->real_name,
'email' => $user->email,
'phone' => $user->phone,
'avatar' => $user->avatar,
'department' => $user->department ? [
'id' => $user->department->id,
'name' => $user->department->name,
] : null,
'roles' => $user->roles->pluck('name')->toArray(),
'permissions' => $this->getUserPermissions($user),
'status' => $user->status,
'last_login_at' => $user->last_login_at ? $user->last_login_at->toDateTimeString() : null,
];
}
/**
* 获取用户菜单
*/
private function getUserMenu(User $user): array
{
// 获取用户的所有权限
$permissionIds = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
$permissionIds[] = $permission->id;
}
}
// 查询菜单类型的权限
$menuPermissions = \App\Models\Auth\Permission::whereIn('id', $permissionIds)
->where('type', 'menu')
->where('status', 1)
->orderBy('sort', 'asc')
->get();
// 构建菜单树
return $this->buildMenuTree($menuPermissions);
}
/**
* 构建菜单树
*/
private function buildMenuTree($permissions, $parentId = 0): array
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission->parent_id == $parentId) {
$node = [
'path' => $permission->route,
'name' => $permission->code,
'meta' => $permission->meta ? json_decode($permission->meta, true) : [],
];
// 添加组件路径
if ($permission->component) {
$node['component'] = $permission->component;
}
// 添加重定向
if (!empty($node['meta']['redirect'])) {
$node['redirect'] = $node['meta']['redirect'];
}
// 递归构建子菜单
$children = $this->buildMenuTree($permissions, $permission->id);
if (!empty($children)) {
$node['children'] = $children;
}
$tree[] = $node;
}
}
return $tree;
}
/**
* 获取用户权限列表
*/
private function getUserPermissions(User $user): array
{
$permissions = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
if (!in_array($permission->code, $permissions)) {
$permissions[] = $permission->code;
}
}
}
return $permissions;
}
}
+319
View File
@@ -0,0 +1,319 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\Department;
use Illuminate\Validation\ValidationException;
class DepartmentService
{
/**
* 获取部门列表
*/
public function getList(array $params): array
{
$query = Department::query();
// 搜索条件
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('leader', 'like', '%' . $params['keyword'] . '%')
->orWhere('phone', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
if (isset($params['parent_id']) && $params['parent_id'] !== '') {
$query->where('parent_id', $params['parent_id']);
}
// 排序
$orderBy = $params['order_by'] ?? 'sort';
$orderDirection = $params['order_direction'] ?? 'asc';
$query->orderBy($orderBy, $orderDirection);
// 分页
$page = $params['page'] ?? 1;
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize, ['*'], 'page', $page);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取部门树
*/
public function getTree(array $params = []): array
{
$query = Department::query();
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$departments = $query->orderBy('sort', 'asc')->get();
return $this->buildTree($departments);
}
/**
* 获取所有部门(不分页)
*/
public function getAll(): array
{
$departments = Department::where('status', 1)->orderBy('sort', 'asc')->get();
return $departments->map(function ($department) {
return [
'id' => $department->id,
'name' => $department->name,
'parent_id' => $department->parent_id,
];
})->toArray();
}
/**
* 获取部门详情
*/
public function getById(int $id): array
{
$department = Department::with(['parent', 'children'])->find($id);
if (!$department) {
throw ValidationException::withMessages([
'id' => ['部门不存在'],
]);
}
return [
'id' => $department->id,
'name' => $department->name,
'parent_id' => $department->parent_id,
'parent' => $department->parent ? [
'id' => $department->parent->id,
'name' => $department->parent->name,
] : null,
'leader' => $department->leader,
'phone' => $department->phone,
'sort' => $department->sort,
'status' => $department->status,
'children_count' => $department->children()->count(),
'users_count' => $department->users()->count(),
'created_at' => $department->created_at->toDateTimeString(),
'updated_at' => $department->updated_at->toDateTimeString(),
];
}
/**
* 创建部门
*/
public function create(array $data): Department
{
// 检查部门名称是否已存在
$query = Department::where('name', $data['name']);
if (!empty($data['parent_id'])) {
$query->where('parent_id', $data['parent_id']);
} else {
$query->where('parent_id', 0);
}
if ($query->exists()) {
throw ValidationException::withMessages([
'name' => ['同级部门名称已存在'],
]);
}
// 如果有父级ID,检查父级是否存在
if (!empty($data['parent_id'])) {
$parent = Department::find($data['parent_id']);
if (!$parent) {
throw ValidationException::withMessages([
'parent_id' => ['父级部门不存在'],
]);
}
}
return Department::create([
'name' => $data['name'],
'parent_id' => $data['parent_id'] ?? 0,
'leader' => $data['leader'] ?? null,
'phone' => $data['phone'] ?? null,
'sort' => $data['sort'] ?? 0,
'status' => $data['status'] ?? 1,
]);
}
/**
* 更新部门
*/
public function update(int $id, array $data): Department
{
$department = Department::find($id);
if (!$department) {
throw ValidationException::withMessages([
'id' => ['部门不存在'],
]);
}
// 检查部门名称是否已被其他部门使用
if (isset($data['name']) && $data['name'] !== $department->name) {
$query = Department::where('name', $data['name'])
->where('id', '!=', $id);
$parentId = isset($data['parent_id']) ? $data['parent_id'] : $department->parent_id;
if ($parentId) {
$query->where('parent_id', $parentId);
} else {
$query->where('parent_id', 0);
}
if ($query->exists()) {
throw ValidationException::withMessages([
'name' => ['同级部门名称已存在'],
]);
}
}
// 如果有父级ID,检查父级是否存在
if (isset($data['parent_id']) && !empty($data['parent_id'])) {
$parent = Department::find($data['parent_id']);
if (!$parent) {
throw ValidationException::withMessages([
'parent_id' => ['父级部门不存在'],
]);
}
// 不能将部门设置为自己的子级
if ($data['parent_id'] == $id) {
throw ValidationException::withMessages([
'parent_id' => ['不能将部门设置为自己的子级'],
]);
}
// 不能将部门设置为自己的子孙级
if ($this->isDescendant($id, $data['parent_id'])) {
throw ValidationException::withMessages([
'parent_id' => ['不能将部门设置为自己的子孙级'],
]);
}
}
$updateData = [
'name' => $data['name'] ?? $department->name,
'parent_id' => $data['parent_id'] ?? $department->parent_id,
'leader' => $data['leader'] ?? $department->leader,
'phone' => $data['phone'] ?? $department->phone,
'sort' => $data['sort'] ?? $department->sort,
'status' => $data['status'] ?? $department->status,
];
$department->update($updateData);
return $department;
}
/**
* 删除部门
*/
public function delete(int $id): void
{
$department = Department::find($id);
if (!$department) {
throw ValidationException::withMessages([
'id' => ['部门不存在'],
]);
}
// 检查是否有子部门
if ($department->children()->exists()) {
throw ValidationException::withMessages([
'id' => ['该部门下还有子部门,无法删除'],
]);
}
// 检查部门下是否有用户
if ($department->users()->exists()) {
throw ValidationException::withMessages([
'id' => ['该部门下还有用户,无法删除'],
]);
}
$department->delete();
}
/**
* 批量删除部门
*/
public function batchDelete(array $ids): int
{
// 检查是否有子部门
$hasChildren = Department::whereIn('id', $ids)->whereHas('children')->exists();
if ($hasChildren) {
throw ValidationException::withMessages([
'ids' => ['选中的部门中还有子部门,无法删除'],
]);
}
// 检查部门下是否有用户
$hasUsers = Department::whereIn('id', $ids)->whereHas('users')->exists();
if ($hasUsers) {
throw ValidationException::withMessages([
'ids' => ['选中的部门中还有用户,无法删除'],
]);
}
return Department::whereIn('id', $ids)->delete();
}
/**
* 批量更新部门状态
*/
public function batchUpdateStatus(array $ids, int $status): int
{
return Department::whereIn('id', $ids)->update(['status' => $status]);
}
/**
* 构建部门树
*/
private function buildTree($departments, $parentId = 0): array
{
$tree = [];
foreach ($departments as $department) {
if ($department->parent_id == $parentId) {
$node = [
'id' => $department->id,
'name' => $department->name,
'parent_id' => $department->parent_id,
'leader' => $department->leader,
'phone' => $department->phone,
'sort' => $department->sort,
'status' => $department->status,
'children' => $this->buildTree($departments, $department->id),
];
$tree[] = $node;
}
}
return $tree;
}
/**
* 检查是否为子孙部门
*/
private function isDescendant($id, $childId): bool
{
if ($id == $childId) {
return true;
}
$child = Department::find($childId);
if (!$child || $child->parent_id == 0) {
return false;
}
return $this->isDescendant($id, $child->parent_id);
}
}
+201
View File
@@ -0,0 +1,201 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use App\Models\Auth\Department;
use App\Models\Auth\Role;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\UserExport;
use App\Exports\DepartmentExport;
use App\Imports\UserImport;
use App\Imports\DepartmentImport;
class ImportExportService
{
/**
* 下载用户导入模板
*/
public function downloadUserTemplate(): string
{
$filename = 'user_import_template_' . date('YmdHis') . '.xlsx';
$path = storage_path('app/exports/' . $filename);
// 确保目录存在
if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0755, true);
}
// 使用模板数据创建Excel
$templateData = [
[
'用户名*',
'密码*',
'真实姓名*',
'邮箱',
'手机号',
'部门名称',
'角色名称(多个用逗号分隔)',
'备注',
],
[
'test001',
'123456',
'测试用户001',
'test001@example.com',
'13800138001',
'技术部',
'管理员',
'示例数据',
],
];
Excel::store(new \App\Exports\GenericExport($templateData), 'exports/' . $filename);
return $filename;
}
/**
* 下载部门导入模板
*/
public function downloadDepartmentTemplate(): string
{
$filename = 'department_import_template_' . date('YmdHis') . '.xlsx';
// 确保目录存在
if (!is_dir(storage_path('app/exports'))) {
mkdir(storage_path('app/exports'), 0755, true);
}
$templateData = [
[
'部门名称*',
'上级部门名称',
'负责人',
'联系电话',
'排序',
'备注',
],
[
'前端开发组',
'技术部',
'张三',
'13800138001',
'1',
'示例数据',
],
];
Excel::store(new \App\Exports\GenericExport($templateData), 'exports/' . $filename);
return $filename;
}
/**
* 导出用户数据
*/
public function exportUsers(array $userIds = []): string
{
$filename = 'users_export_' . date('YmdHis') . '.xlsx';
// 确保目录存在
if (!is_dir(storage_path('app/exports'))) {
mkdir(storage_path('app/exports'), 0755, true);
}
Excel::store(new UserExport($userIds), 'exports/' . $filename);
return $filename;
}
/**
* 导出部门数据
*/
public function exportDepartments(array $departmentIds = []): string
{
$filename = 'departments_export_' . date('YmdHis') . '.xlsx';
// 确保目录存在
if (!is_dir(storage_path('app/exports'))) {
mkdir(storage_path('app/exports'), 0755, true);
}
Excel::store(new DepartmentExport($departmentIds), 'exports/' . $filename);
return $filename;
}
/**
* 导入用户数据
*/
public function importUsers(string $filePath, string $realPath): array
{
if (!file_exists($realPath)) {
throw ValidationException::withMessages([
'file' => ['文件不存在'],
]);
}
$import = new UserImport();
Excel::import($import, $realPath);
// 删除临时文件
if (file_exists($realPath)) {
unlink($realPath);
}
return [
'success_count' => $import->getSuccessCount(),
'error_count' => $import->getErrorCount(),
'errors' => $import->getErrors(),
];
}
/**
* 导入部门数据
*/
public function importDepartments(string $filePath, string $realPath): array
{
if (!file_exists($realPath)) {
throw ValidationException::withMessages([
'file' => ['文件不存在'],
]);
}
$import = new DepartmentImport();
Excel::import($import, $realPath);
// 删除临时文件
if (file_exists($realPath)) {
unlink($realPath);
}
return [
'success_count' => $import->getSuccessCount(),
'error_count' => $import->getErrorCount(),
'errors' => $import->getErrors(),
];
}
/**
* 获取导出文件路径
*/
public function getExportFilePath(string $filename): string
{
return storage_path('app/exports/' . $filename);
}
/**
* 删除导出文件
*/
public function deleteExportFile(string $filename): bool
{
$path = $this->getExportFilePath($filename);
if (file_exists($path)) {
return unlink($path);
}
return true;
}
}
@@ -0,0 +1,256 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use App\Models\Auth\Role;
use App\Models\Auth\Permission;
use Illuminate\Support\Facades\Cache;
class PermissionCacheService
{
protected $cachePrefix = 'permission:';
protected $cacheMinutes = 60; // 缓存60分钟
/**
* 获取用户的权限列表(带缓存)
*/
public function getUserPermissions(int $userId): array
{
$cacheKey = $this->getUserPermissionsCacheKey($userId);
return Cache::remember($cacheKey, now()->addMinutes($this->cacheMinutes), function() use ($userId) {
$user = User::find($userId);
if (!$user) {
return [];
}
$permissions = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
$permissions[$permission->id] = [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
'route' => $permission->route,
];
}
}
return array_values($permissions);
});
}
/**
* 获取用户的权限编码列表(带缓存)
*/
public function getUserPermissionCodes(int $userId): array
{
$cacheKey = $this->getUserPermissionCodesCacheKey($userId);
return Cache::remember($cacheKey, now()->addMinutes($this->cacheMinutes), function() use ($userId) {
$permissions = $this->getUserPermissions($userId);
return array_column($permissions, 'code');
});
}
/**
* 获取用户的菜单树(带缓存)
*/
public function getUserMenuTree(int $userId): array
{
$cacheKey = $this->getUserMenuTreeCacheKey($userId);
return Cache::remember($cacheKey, now()->addMinutes($this->cacheMinutes), function() use ($userId) {
$user = User::find($userId);
if (!$user) {
return [];
}
// 获取用户的所有权限ID
$permissionIds = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
$permissionIds[] = $permission->id;
}
}
// 获取菜单类型的权限
$permissions = Permission::whereIn('id', $permissionIds)
->whereIn('type', ['menu', 'api'])
->where('status', 1)
->orderBy('sort', 'asc')
->get();
return $this->buildMenuTree($permissions->toArray());
});
}
/**
* 检查用户是否有某个权限(带缓存)
*/
public function userHasPermission(int $userId, string $permissionCode): bool
{
$codes = $this->getUserPermissionCodes($userId);
return in_array($permissionCode, $codes);
}
/**
* 获取角色的权限列表(带缓存)
*/
public function getRolePermissions(int $roleId): array
{
$cacheKey = $this->getRolePermissionsCacheKey($roleId);
return Cache::remember($cacheKey, now()->addMinutes($this->cacheMinutes), function() use ($roleId) {
$role = Role::find($roleId);
if (!$role) {
return [];
}
return $role->permissions->map(function($permission) {
return [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
];
})->toArray();
});
}
/**
* 清除用户权限缓存
*/
public function clearUserPermissionCache(int $userId): void
{
Cache::forget($this->getUserPermissionsCacheKey($userId));
Cache::forget($this->getUserPermissionCodesCacheKey($userId));
Cache::forget($this->getUserMenuTreeCacheKey($userId));
}
/**
* 清除角色权限缓存
*/
public function clearRolePermissionCache(int $roleId): void
{
Cache::forget($this->getRolePermissionsCacheKey($roleId));
// 清除所有拥有该角色的用户权限缓存
$role = Role::find($roleId);
if ($role) {
foreach ($role->users as $user) {
$this->clearUserPermissionCache($user->id);
}
}
}
/**
* 清除所有权限缓存
*/
public function clearAllPermissionCache(): void
{
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . '*');
if (!empty($keys)) {
$redis->del($keys);
}
}
}
/**
* 清除指定用户的权限缓存(当用户角色变化时调用)
*/
public function onUserRolesChanged(int $userId): void
{
$this->clearUserPermissionCache($userId);
}
/**
* 清除角色的用户缓存(当角色权限变化时调用)
*/
public function onRolePermissionsChanged(int $roleId): void
{
$this->clearRolePermissionCache($roleId);
}
/**
* 清除权限的所有缓存(当权限本身变化时调用)
*/
public function onPermissionChanged(int $permissionId): void
{
// 清除所有缓存,因为权限变化可能影响所有用户
$this->clearAllPermissionCache();
}
/**
* 构建菜单树
*/
protected function buildMenuTree(array $permissions, int $parentId = 0): array
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission['parent_id'] == $parentId) {
$node = [
'id' => $permission['id'],
'name' => $permission['name'],
'code' => $permission['code'],
'type' => $permission['type'],
'route' => $permission['route'],
'component' => $permission['component'],
'meta' => json_decode($permission['meta'] ?? '{}', true),
'sort' => $permission['sort'],
'children' => $this->buildMenuTree($permissions, $permission['id']),
];
// 如果没有子节点,移除children字段
if (empty($node['children'])) {
unset($node['children']);
}
$tree[] = $node;
}
}
// 按sort排序
usort($tree, function($a, $b) {
return $a['sort'] <=> $b['sort'];
});
return $tree;
}
/**
* 生成用户权限缓存键
*/
protected function getUserPermissionsCacheKey(int $userId): string
{
return $this->cachePrefix . 'user:' . $userId . ':permissions';
}
/**
* 生成用户权限编码缓存键
*/
protected function getUserPermissionCodesCacheKey(int $userId): string
{
return $this->cachePrefix . 'user:' . $userId . ':permission_codes';
}
/**
* 生成用户菜单树缓存键
*/
protected function getUserMenuTreeCacheKey(int $userId): string
{
return $this->cachePrefix . 'user:' . $userId . ':menu_tree';
}
/**
* 生成角色权限缓存键
*/
protected function getRolePermissionsCacheKey(int $roleId): string
{
return $this->cachePrefix . 'role:' . $roleId . ':permissions';
}
}
+323
View File
@@ -0,0 +1,323 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\Permission;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class PermissionService
{
/**
* 获取权限列表
*/
public function getList(array $params): array
{
$query = Permission::query();
// 搜索条件
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('code', 'like', '%' . $params['keyword'] . '%');
});
}
if (!empty($params['type'])) {
$query->where('type', $params['type']);
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
// 排序
$orderBy = $params['order_by'] ?? 'sort';
$orderDirection = $params['order_direction'] ?? 'asc';
$query->orderBy($orderBy, $orderDirection);
// 分页
$page = $params['page'] ?? 1;
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize, ['*'], 'page', $page);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取权限树
*/
public function getTree(array $params = []): array
{
$query = Permission::query();
if (!empty($params['type'])) {
$query->where('type', $params['type']);
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$permissions = $query->orderBy('sort', 'asc')->get();
return $this->buildTree($permissions);
}
/**
* 获取菜单树(前端使用)
*/
public function getMenuTree(int $userId = null): array
{
$query = Permission::whereIn('type', ['menu', 'api'])
->where('status', 1);
if ($userId) {
// 获取用户的权限
$user = \App\Models\Auth\User::find($userId);
if ($user) {
$permissionIds = [];
foreach ($user->roles as $role) {
foreach ($role->permissions as $permission) {
$permissionIds[] = $permission->id;
}
}
$query->whereIn('id', $permissionIds);
}
}
$permissions = $query->orderBy('sort', 'asc')->get();
return $this->buildTree($permissions);
}
/**
* 获取权限详情
*/
public function getById(int $id): array
{
$permission = Permission::with(['parent'])->find($id);
if (!$permission) {
throw ValidationException::withMessages([
'id' => ['权限不存在'],
]);
}
return [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
'parent_id' => $permission->parent_id,
'parent' => $permission->parent ? [
'id' => $permission->parent->id,
'name' => $permission->parent->name,
] : null,
'route' => $permission->route,
'component' => $permission->component,
'meta' => $permission->meta,
'sort' => $permission->sort,
'status' => $permission->status,
'created_at' => $permission->created_at->toDateTimeString(),
'updated_at' => $permission->updated_at->toDateTimeString(),
];
}
/**
* 创建权限
*/
public function create(array $data): Permission
{
// 检查权限名称是否已存在
if (Permission::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['权限名称已存在'],
]);
}
// 检查权限编码是否已存在
if (Permission::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['权限编码已存在'],
]);
}
// 如果有父级ID,检查父级是否存在
if (!empty($data['parent_id'])) {
$parent = Permission::find($data['parent_id']);
if (!$parent) {
throw ValidationException::withMessages([
'parent_id' => ['父级权限不存在'],
]);
}
}
return Permission::create([
'name' => $data['name'],
'code' => $data['code'],
'type' => $data['type'] ?? 'api',
'parent_id' => $data['parent_id'] ?? 0,
'route' => $data['route'] ?? null,
'component' => $data['component'] ?? null,
'meta' => $data['meta'] ?? null,
'sort' => $data['sort'] ?? 0,
'status' => $data['status'] ?? 1,
]);
}
/**
* 更新权限
*/
public function update(int $id, array $data): Permission
{
$permission = Permission::find($id);
if (!$permission) {
throw ValidationException::withMessages([
'id' => ['权限不存在'],
]);
}
// 检查权限名称是否已被其他权限使用
if (isset($data['name']) && $data['name'] !== $permission->name) {
if (Permission::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['权限名称已存在'],
]);
}
}
// 检查权限编码是否已被其他权限使用
if (isset($data['code']) && $data['code'] !== $permission->code) {
if (Permission::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['权限编码已存在'],
]);
}
}
// 如果有父级ID,检查父级是否存在
if (isset($data['parent_id']) && !empty($data['parent_id'])) {
$parent = Permission::find($data['parent_id']);
if (!$parent) {
throw ValidationException::withMessages([
'parent_id' => ['父级权限不存在'],
]);
}
// 不能将权限设置为自己的子级
if ($data['parent_id'] == $id) {
throw ValidationException::withMessages([
'parent_id' => ['不能将权限设置为自己的子级'],
]);
}
}
$updateData = [
'name' => $data['name'] ?? $permission->name,
'code' => $data['code'] ?? $permission->code,
'type' => $data['type'] ?? $permission->type,
'parent_id' => $data['parent_id'] ?? $permission->parent_id,
'route' => $data['route'] ?? $permission->route,
'component' => $data['component'] ?? $permission->component,
'meta' => isset($data['meta']) ? $data['meta'] : $permission->meta,
'sort' => $data['sort'] ?? $permission->sort,
'status' => $data['status'] ?? $permission->status,
];
$permission->update($updateData);
return $permission;
}
/**
* 删除权限
*/
public function delete(int $id): void
{
$permission = Permission::find($id);
if (!$permission) {
throw ValidationException::withMessages([
'id' => ['权限不存在'],
]);
}
// 检查是否有子权限
if ($permission->children()->exists()) {
throw ValidationException::withMessages([
'id' => ['该权限下还有子权限,无法删除'],
]);
}
// 检查是否被角色使用
if ($permission->roles()->exists()) {
throw ValidationException::withMessages([
'id' => ['该权限已被角色使用,无法删除'],
]);
}
$permission->delete();
}
/**
* 批量删除权限
*/
public function batchDelete(array $ids): int
{
// 检查是否有子权限
$hasChildren = Permission::whereIn('id', $ids)->whereHas('children')->exists();
if ($hasChildren) {
throw ValidationException::withMessages([
'ids' => ['选中的权限中还有子权限,无法删除'],
]);
}
// 检查是否被角色使用
$hasRoles = Permission::whereIn('id', $ids)->whereHas('roles')->exists();
if ($hasRoles) {
throw ValidationException::withMessages([
'ids' => ['选中的权限中已被角色使用,无法删除'],
]);
}
return Permission::whereIn('id', $ids)->delete();
}
/**
* 批量更新权限状态
*/
public function batchUpdateStatus(array $ids, int $status): int
{
return Permission::whereIn('id', $ids)->update(['status' => $status]);
}
/**
* 构建权限树
*/
private function buildTree($permissions, $parentId = 0): array
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission->parent_id == $parentId) {
$node = [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
'route' => $permission->route,
'component' => $permission->component,
'meta' => $permission->meta,
'sort' => $permission->sort,
'status' => $permission->status,
'children' => $this->buildTree($permissions, $permission->id),
];
$tree[] = $node;
}
}
return $tree;
}
}
+430
View File
@@ -0,0 +1,430 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\Role;
use App\Models\Auth\Permission;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class RoleService
{
/**
* 获取角色列表
*/
public function getList(array $params): array
{
$query = Role::query();
// 搜索条件
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('code', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
// 排序
$orderBy = $params['order_by'] ?? 'sort';
$orderDirection = $params['order_direction'] ?? 'asc';
$query->orderBy($orderBy, $orderDirection);
// 分页
$page = $params['page'] ?? 1;
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize, ['*'], 'page', $page);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取所有角色(不分页)
*/
public function getAll(): array
{
$roles = Role::where('status', 1)->orderBy('sort', 'asc')->get();
return $roles->map(function ($role) {
return [
'id' => $role->id,
'name' => $role->name,
'code' => $role->code,
];
})->toArray();
}
/**
* 获取角色详情
*/
public function getById(int $id): array
{
$role = Role::with(['permissions'])->find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
return [
'id' => $role->id,
'name' => $role->name,
'code' => $role->code,
'description' => $role->description,
'sort' => $role->sort,
'status' => $role->status,
'permissions' => $role->permissions->pluck('id')->toArray(),
'created_at' => $role->created_at->toDateTimeString(),
'updated_at' => $role->updated_at->toDateTimeString(),
];
}
/**
* 创建角色
*/
public function create(array $data): Role
{
// 检查角色名称是否已存在
if (Role::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['角色名称已存在'],
]);
}
// 检查角色编码是否已存在
if (Role::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['角色编码已存在'],
]);
}
DB::beginTransaction();
try {
$role = Role::create([
'name' => $data['name'],
'code' => $data['code'],
'description' => $data['description'] ?? null,
'sort' => $data['sort'] ?? 0,
'status' => $data['status'] ?? 1,
]);
// 关联权限
if (!empty($data['permission_ids'])) {
$role->permissions()->attach($data['permission_ids']);
}
DB::commit();
return $role;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 更新角色
*/
public function update(int $id, array $data): Role
{
$role = Role::find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
// 检查角色名称是否已被其他角色使用
if (isset($data['name']) && $data['name'] !== $role->name) {
if (Role::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['角色名称已存在'],
]);
}
}
// 检查角色编码是否已被其他角色使用
if (isset($data['code']) && $data['code'] !== $role->code) {
if (Role::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['角色编码已存在'],
]);
}
}
DB::beginTransaction();
try {
$updateData = [
'name' => $data['name'] ?? $role->name,
'code' => $data['code'] ?? $role->code,
'description' => $data['description'] ?? $role->description,
'sort' => $data['sort'] ?? $role->sort,
'status' => $data['status'] ?? $role->status,
];
$role->update($updateData);
// 更新权限关联
if (isset($data['permission_ids'])) {
$role->permissions()->sync($data['permission_ids']);
}
DB::commit();
return $role;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 删除角色
*/
public function delete(int $id): void
{
$role = Role::find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
// 检查角色下是否有用户
if ($role->users()->exists()) {
throw ValidationException::withMessages([
'id' => ['该角色下还有用户,无法删除'],
]);
}
$role->delete();
}
/**
* 批量删除角色
*/
public function batchDelete(array $ids): int
{
// 检查角色下是否有用户
$hasUsers = Role::whereIn('id', $ids)->whereHas('users')->exists();
if ($hasUsers) {
throw ValidationException::withMessages([
'ids' => ['选中的角色中还有用户,无法删除'],
]);
}
return Role::whereIn('id', $ids)->delete();
}
/**
* 批量更新角色状态
*/
public function batchUpdateStatus(array $ids, int $status): int
{
return Role::whereIn('id', $ids)->update(['status' => $status]);
}
/**
* 分配权限
*/
public function assignPermissions(int $id, array $permissionIds): void
{
$role = Role::find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
$role->permissions()->sync($permissionIds);
}
/**
* 获取角色的权限列表
*/
public function getPermissions(int $id): array
{
$role = Role::with(['permissions' => function ($query) {
$query->orderBy('sort', 'asc');
}])->find($id);
if (!$role) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
return $this->buildPermissionTree($role->permissions);
}
/**
* 复制角色
*/
public function copy(int $id, array $data): Role
{
$sourceRole = Role::with(['permissions'])->find($id);
if (!$sourceRole) {
throw ValidationException::withMessages([
'id' => ['角色不存在'],
]);
}
// 检查新角色名称是否已存在
if (Role::where('name', $data['name'])->exists()) {
throw ValidationException::withMessages([
'name' => ['角色名称已存在'],
]);
}
// 检查新角色编码是否已存在
if (Role::where('code', $data['code'])->exists()) {
throw ValidationException::withMessages([
'code' => ['角色编码已存在'],
]);
}
DB::beginTransaction();
try {
// 创建新角色
$newRole = Role::create([
'name' => $data['name'],
'code' => $data['code'],
'description' => $data['description'] ?? $sourceRole->description,
'sort' => $data['sort'] ?? $sourceRole->sort,
'status' => $data['status'] ?? 1,
]);
// 复制权限
$permissionIds = $sourceRole->permissions->pluck('id')->toArray();
if (!empty($permissionIds)) {
$newRole->permissions()->attach($permissionIds);
}
DB::commit();
return $newRole;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 批量复制角色
*/
public function batchCopy(array $roleIds, array $data): array
{
$successCount = 0;
$errorCount = 0;
$errors = [];
$newRoles = [];
DB::beginTransaction();
try {
foreach ($roleIds as $index => $roleId) {
try {
$sourceRole = Role::with(['permissions'])->find($roleId);
if (!$sourceRole) {
$errors[] = "角色ID {$roleId} 不存在";
$errorCount++;
continue;
}
// 生成新的名称和编码
$newName = $data['name'] ?? ($sourceRole->name . ' (副本)');
$newCode = $data['code'] ?? ($sourceRole->code . '_copy_' . time());
// 检查名称和编码是否已存在
$nameSuffix = '';
$codeSuffix = '';
$counter = 1;
while (Role::where('name', $newName . $nameSuffix)->exists()) {
$nameSuffix = ' (' . $counter . ')';
$counter++;
}
$counter = 1;
while (Role::where('code', $newCode . $codeSuffix)->exists()) {
$codeSuffix = '_' . $counter;
$counter++;
}
// 创建新角色
$newRole = Role::create([
'name' => $newName . $nameSuffix,
'code' => $newCode . $codeSuffix,
'description' => $data['description'] ?? $sourceRole->description,
'sort' => $data['sort'] ?? $sourceRole->sort,
'status' => $data['status'] ?? 1,
]);
// 复制权限
$permissionIds = $sourceRole->permissions->pluck('id')->toArray();
if (!empty($permissionIds)) {
$newRole->permissions()->attach($permissionIds);
}
$newRoles[] = [
'id' => $newRole->id,
'name' => $newRole->name,
'code' => $newRole->code,
];
$successCount++;
} catch (\Exception $e) {
$errors[] = "复制角色ID {$roleId} 失败:" . $e->getMessage();
$errorCount++;
}
}
DB::commit();
return [
'success_count' => $successCount,
'error_count' => $errorCount,
'errors' => $errors,
'new_roles' => $newRoles,
];
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 构建权限树
*/
private function buildPermissionTree($permissions, $parentId = 0): array
{
$tree = [];
foreach ($permissions as $permission) {
if ($permission->parent_id == $parentId) {
$node = [
'id' => $permission->id,
'name' => $permission->name,
'code' => $permission->code,
'type' => $permission->type,
'route' => $permission->route,
'component' => $permission->component,
'meta' => $permission->meta,
'sort' => $permission->sort,
'status' => $permission->status,
'children' => $this->buildPermissionTree($permissions, $permission->id),
];
$tree[] = $node;
}
}
return $tree;
}
}
+208
View File
@@ -0,0 +1,208 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use Illuminate\Support\Facades\Cache;
class UserOnlineService
{
protected $cachePrefix = 'user_online:';
protected $expireMinutes = 5;
/**
* 设置用户在线
*/
public function setOnline(int $userId, string $token): void
{
$key = $this->getCacheKey($userId, $token);
Cache::put($key, [
'user_id' => $userId,
'token' => $token,
'last_active_at' => now()->toDateTimeString(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
], now()->addMinutes($this->expireMinutes));
// 更新用户的最后在线时间
User::where('id', $userId)->update([
'last_active_at' => now(),
]);
}
/**
* 更新用户在线状态
*/
public function updateOnline(int $userId, string $token): void
{
$key = $this->getCacheKey($userId, $token);
if (Cache::has($key)) {
Cache::put($key, [
'user_id' => $userId,
'token' => $token,
'last_active_at' => now()->toDateTimeString(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
], now()->addMinutes($this->expireMinutes));
}
}
/**
* 设置用户离线
*/
public function setOffline(int $userId, string $token): void
{
$key = $this->getCacheKey($userId, $token);
Cache::forget($key);
}
/**
* 设置用户所有设备离线
*/
public function setAllOffline(int $userId): void
{
$pattern = $this->cachePrefix . $userId . ':*';
$keys = Cache::store('redis')->getPrefix() . $pattern;
// Redis 模式删除
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . $userId . ':*');
if (!empty($keys)) {
$redis->del($keys);
}
}
}
/**
* 检查用户是否在线
*/
public function isOnline(int $userId): bool
{
$pattern = $this->cachePrefix . $userId . ':*';
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . $userId . ':*');
return !empty($keys);
}
return false;
}
/**
* 获取用户在线信息
*/
public function getOnlineInfo(int $userId, string $token): ?array
{
$key = $this->getCacheKey($userId, $token);
return Cache::get($key);
}
/**
* 获取用户所有在线会话
*/
public function getUserSessions(int $userId): array
{
$sessions = [];
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . $userId . ':*');
foreach ($keys as $key) {
$session = $redis->get($key);
if ($session) {
$sessions[] = json_decode($session, true);
}
}
}
return $sessions;
}
/**
* 获取所有在线用户数量
*/
public function getOnlineCount(): int
{
$count = 0;
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . '*');
// 去重用户ID
$userIds = [];
foreach ($keys as $key) {
preg_match('/user_online:(\d+):/', $key, $matches);
if (isset($matches[1])) {
$userIds[$matches[1]] = true;
}
}
$count = count($userIds);
}
return $count;
}
/**
* 获取在线用户列表
*/
public function getOnlineUsers(int $limit = 100): array
{
$onlineUsers = [];
if (Cache::getStore() instanceof \Illuminate\Cache\RedisStore) {
$redis = Cache::getStore()->connection();
$keys = $redis->keys($this->cachePrefix . '*');
$userIds = [];
$userSessions = [];
foreach ($keys as $key) {
$session = $redis->get($key);
if ($session) {
$session = json_decode($session, true);
$userId = $session['user_id'];
if (!isset($userIds[$userId])) {
$userIds[$userId] = $userId;
}
$userSessions[$userId] = $session;
}
}
$userIdList = array_values(array_slice($userIds, 0, $limit));
$users = User::whereIn('id', $userIdList)->get();
foreach ($users as $user) {
$onlineUsers[] = [
'id' => $user->id,
'username' => $user->username,
'real_name' => $user->real_name,
'avatar' => $user->avatar,
'last_active_at' => $userSessions[$user->id]['last_active_at'] ?? null,
'ip' => $userSessions[$user->id]['ip'] ?? null,
];
}
}
return $onlineUsers;
}
/**
* 清理过期会话
*/
public function cleanExpiredSessions(): int
{
// Redis 的 TTL 会自动清理过期键
return 0;
}
/**
* 生成缓存键
*/
protected function getCacheKey(int $userId, string $token): string
{
return $this->cachePrefix . $userId . ':' . md5($token);
}
}
+320
View File
@@ -0,0 +1,320 @@
<?php
namespace App\Services\Auth;
use App\Models\Auth\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\Auth\UserExport;
use App\Imports\Auth\UserImport;
use App\Jobs\Auth\UserImportJob;
use App\Jobs\Auth\UserExportJob;
class UserService
{
/**
* 获取用户列表
*/
public function getList(array $params): array
{
$query = User::with(['department', 'roles']);
// 搜索条件
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('username', 'like', '%' . $params['keyword'] . '%')
->orWhere('real_name', 'like', '%' . $params['keyword'] . '%')
->orWhere('phone', 'like', '%' . $params['keyword'] . '%')
->orWhere('email', 'like', '%' . $params['keyword'] . '%');
});
}
if (!empty($params['department_id'])) {
$query->where('department_id', $params['department_id']);
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
if (!empty($params['role_id'])) {
$query->whereHas('roles', function ($q) use ($params) {
$q->where('role_id', $params['role_id']);
});
}
// 排序
$orderBy = $params['order_by'] ?? 'id';
$orderDirection = $params['order_direction'] ?? 'desc';
$query->orderBy($orderBy, $orderDirection);
// 分页
$page = $params['page'] ?? 1;
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize, ['*'], 'page', $page);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $page,
'page_size' => $pageSize,
];
}
/**
* 获取用户详情
*/
public function getById(int $id): array
{
$user = User::with(['department', 'roles'])->find($id);
if (!$user) {
throw ValidationException::withMessages([
'id' => ['用户不存在'],
]);
}
return $this->formatUserInfo($user);
}
/**
* 创建用户
*/
public function create(array $data): User
{
// 检查用户名是否已存在
if (User::where('username', $data['username'])->exists()) {
throw ValidationException::withMessages([
'username' => ['用户名已存在'],
]);
}
// 检查手机号是否已存在
if (!empty($data['phone']) && User::where('phone', $data['phone'])->exists()) {
throw ValidationException::withMessages([
'phone' => ['手机号已被使用'],
]);
}
// 检查邮箱是否已存在
if (!empty($data['email']) && User::where('email', $data['email'])->exists()) {
throw ValidationException::withMessages([
'email' => ['邮箱已被使用'],
]);
}
DB::beginTransaction();
try {
$user = User::create([
'username' => $data['username'],
'password' => Hash::make($data['password']),
'real_name' => $data['real_name'],
'email' => $data['email'] ?? null,
'phone' => $data['phone'] ?? null,
'department_id' => $data['department_id'] ?? null,
'avatar' => $data['avatar'] ?? null,
'status' => $data['status'] ?? 1,
]);
// 关联角色
if (!empty($data['role_ids'])) {
$user->roles()->attach($data['role_ids']);
}
DB::commit();
return $user;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 更新用户
*/
public function update(int $id, array $data): User
{
$user = User::find($id);
if (!$user) {
throw ValidationException::withMessages([
'id' => ['用户不存在'],
]);
}
// 检查用户名是否已被其他用户使用
if (isset($data['username']) && $data['username'] !== $user->username) {
if (User::where('username', $data['username'])->exists()) {
throw ValidationException::withMessages([
'username' => ['用户名已存在'],
]);
}
}
// 检查手机号是否已被其他用户使用
if (isset($data['phone']) && $data['phone'] !== $user->phone) {
if (User::where('phone', $data['phone'])->exists()) {
throw ValidationException::withMessages([
'phone' => ['手机号已被使用'],
]);
}
}
// 检查邮箱是否已被其他用户使用
if (isset($data['email']) && $data['email'] !== $user->email) {
if (User::where('email', $data['email'])->exists()) {
throw ValidationException::withMessages([
'email' => ['邮箱已被使用'],
]);
}
}
DB::beginTransaction();
try {
$updateData = [
'real_name' => $data['real_name'] ?? $user->real_name,
'email' => $data['email'] ?? $user->email,
'phone' => $data['phone'] ?? $user->phone,
'department_id' => $data['department_id'] ?? $user->department_id,
'avatar' => $data['avatar'] ?? $user->avatar,
'status' => $data['status'] ?? $user->status,
];
if (isset($data['username'])) {
$updateData['username'] = $data['username'];
}
if (isset($data['password'])) {
$updateData['password'] = Hash::make($data['password']);
}
$user->update($updateData);
// 更新角色关联
if (isset($data['role_ids'])) {
$user->roles()->sync($data['role_ids']);
}
DB::commit();
return $user;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 删除用户
*/
public function delete(int $id): void
{
$user = User::find($id);
if (!$user) {
throw ValidationException::withMessages([
'id' => ['用户不存在'],
]);
}
$user->delete();
}
/**
* 批量删除用户
*/
public function batchDelete(array $ids): int
{
return User::whereIn('id', $ids)->delete();
}
/**
* 批量更新用户状态
*/
public function batchUpdateStatus(array $ids, int $status): int
{
return User::whereIn('id', $ids)->update(['status' => $status]);
}
/**
* 批量分配部门
*/
public function batchAssignDepartment(array $ids, int $departmentId): int
{
return User::whereIn('id', $ids)->update(['department_id' => $departmentId]);
}
/**
* 批量分配角色
*/
public function batchAssignRoles(array $ids, array $roleIds): void
{
foreach ($ids as $userId) {
$user = User::find($userId);
if ($user) {
$user->roles()->sync($roleIds);
}
}
}
/**
* 导出用户数据
*/
public function export(array $params): string
{
// 异步导出
$job = new UserExportJob($params);
dispatch($job);
return '导出任务已提交,请稍后在导出列表中查看';
}
/**
* 导入用户数据
*/
public function import(\Illuminate\Http\UploadedFile $file): array
{
$path = $file->store('imports');
// 异步导入
$job = new UserImportJob($path);
dispatch($job);
return [
'message' => '导入任务已提交,请稍后在导入列表中查看',
'file_path' => $path,
];
}
/**
* 格式化用户信息
*/
private function formatUserInfo(User $user): array
{
return [
'id' => $user->id,
'username' => $user->username,
'real_name' => $user->real_name,
'email' => $user->email,
'phone' => $user->phone,
'avatar' => $user->avatar,
'department' => $user->department ? [
'id' => $user->department->id,
'name' => $user->department->name,
] : null,
'roles' => $user->roles->map(function ($role) {
return [
'id' => $role->id,
'name' => $role->name,
'code' => $role->code,
];
})->toArray(),
'status' => $user->status,
'last_login_at' => $user->last_login_at ? $user->last_login_at->toDateTimeString() : null,
'last_login_ip' => $user->last_login_ip,
'created_at' => $user->created_at->toDateTimeString(),
'updated_at' => $user->updated_at->toDateTimeString(),
];
}
}
+198
View File
@@ -0,0 +1,198 @@
<?php
namespace App\Services\System;
use App\Models\System\City;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class CityService
{
public function getList(array $params): array
{
$query = City::query();
if (!empty($params['parent_id'])) {
$query->where('parent_id', $params['parent_id']);
}
if (!empty($params['level'])) {
$query->where('level', $params['level']);
}
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('code', 'like', '%' . $params['keyword'] . '%')
->orWhere('pinyin', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$query->orderBy('sort')->orderBy('id');
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function getTree(): array
{
return $this->buildTree(City::where('status', true)->orderBy('sort')->get());
}
public function getChildren(int $parentId): array
{
return City::where('parent_id', $parentId)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getByCode(string $code): ?City
{
return City::where('code', $code)->first();
}
public function getById(int $id): ?City
{
return City::find($id);
}
public function getByPinyin(string $pinyin): array
{
return City::where('pinyin', 'like', '%' . $pinyin . '%')
->where('status', true)
->get()
->toArray();
}
public function create(array $data): City
{
Validator::make($data, [
'name' => 'required|string|max:100',
'code' => 'required|string|max:50|unique:system_cities,code',
'level' => 'required|integer|in:1,2,3',
'parent_id' => 'sometimes|exists:system_cities,id',
])->validate();
$city = City::create($data);
$this->clearCache();
return $city;
}
public function update(int $id, array $data): City
{
$city = City::findOrFail($id);
Validator::make($data, [
'name' => 'sometimes|required|string|max:100',
'code' => 'sometimes|required|string|max:50|unique:system_cities,code,' . $id,
'level' => 'sometimes|required|integer|in:1,2,3',
'parent_id' => 'sometimes|exists:system_cities,id',
])->validate();
$city->update($data);
$this->clearCache();
return $city;
}
public function delete(int $id): bool
{
$city = City::findOrFail($id);
if ($city->children()->exists()) {
throw new \Exception('该城市下有子级数据,不能删除');
}
$city->delete();
$this->clearCache();
return true;
}
public function batchDelete(array $ids): bool
{
City::whereIn('id', $ids)->delete();
$this->clearCache();
return true;
}
public function batchUpdateStatus(array $ids, bool $status): bool
{
City::whereIn('id', $ids)->update(['status' => $status]);
$this->clearCache();
return true;
}
private function buildTree(array $cities, int $parentId = 0): array
{
$tree = [];
foreach ($cities as $city) {
if ($city['parent_id'] == $parentId) {
$children = $this->buildTree($cities, $city['id']);
if (!empty($children)) {
$city['children'] = $children;
}
$tree[] = $city;
}
}
return $tree;
}
private function clearCache(): void
{
Cache::forget('system:cities:tree');
}
public function getCachedTree(): array
{
$cacheKey = 'system:cities:tree';
$tree = Cache::get($cacheKey);
if ($tree === null) {
$tree = $this->getTree();
Cache::put($cacheKey, $tree, 3600);
}
return $tree;
}
public function getProvinces(): array
{
return City::where('level', 1)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getCities(int $provinceId): array
{
return City::where('parent_id', $provinceId)
->where('level', 2)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getDistricts(int $cityId): array
{
return City::where('parent_id', $cityId)
->where('level', 3)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
}
+158
View File
@@ -0,0 +1,158 @@
<?php
namespace App\Services\System;
use App\Models\System\Config;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class ConfigService
{
public function getList(array $params): array
{
$query = Config::query();
if (!empty($params['group'])) {
$query->where('group', $params['group']);
}
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('key', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$pageSize = $params['page_size'] ?? 20;
$list = $query->orderBy('sort')->orderBy('id')->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function getById(int $id): ?Config
{
return Config::find($id);
}
public function getByKey(string $key): ?Config
{
return Config::where('key', $key)->first();
}
public function getByGroup(string $group): array
{
return Config::where('group', $group)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getAllConfig(): array
{
$cacheKey = 'system:configs:all';
$configs = Cache::get($cacheKey);
if ($configs === null) {
$configs = Config::where('status', true)
->orderBy('sort')
->get()
->keyBy('key')
->toArray();
Cache::put($cacheKey, $configs, 3600);
}
return $configs;
}
public function getConfigValue(string $key, $default = null)
{
$config = $this->getByKey($key);
if (!$config) {
return $default;
}
return $config->value ?? $config->default_value ?? $default;
}
public function create(array $data): Config
{
Validator::make($data, [
'group' => 'required|string|max:50',
'key' => 'required|string|max:100|unique:system_configs,key',
'name' => 'required|string|max:100',
'type' => 'required|string|in:string,text,number,boolean,select,radio,checkbox,file,json',
])->validate();
$config = Config::create($data);
$this->clearCache();
return $config;
}
public function update(int $id, array $data): Config
{
$config = Config::findOrFail($id);
Validator::make($data, [
'group' => 'sometimes|required|string|max:50',
'key' => 'sometimes|required|string|max:100|unique:system_configs,key,' . $id,
'name' => 'sometimes|required|string|max:100',
'type' => 'sometimes|required|string|in:string,text,number,boolean,select,radio,checkbox,file,json',
])->validate();
$config->update($data);
$this->clearCache();
return $config;
}
public function delete(int $id): bool
{
$config = Config::findOrFail($id);
if ($config->is_system) {
throw new \Exception('系统配置不能删除');
}
$config->delete();
$this->clearCache();
return true;
}
public function batchDelete(array $ids): bool
{
$configs = Config::whereIn('id', $ids)->where('is_system', false)->get();
Config::whereIn('id', $configs->pluck('id'))->delete();
$this->clearCache();
return true;
}
public function batchUpdateStatus(array $ids, bool $status): bool
{
Config::whereIn('id', $ids)->update(['status' => $status]);
$this->clearCache();
return true;
}
private function clearCache(): void
{
Cache::forget('system:configs:all');
}
public function getGroups(): array
{
return Config::where('status', true)
->select('group')
->distinct()
->pluck('group')
->toArray();
}
}
+210
View File
@@ -0,0 +1,210 @@
<?php
namespace App\Services\System;
use App\Models\System\Dictionary;
use App\Models\System\DictionaryItem;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class DictionaryService
{
public function getList(array $params): array
{
$query = Dictionary::query();
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('code', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$pageSize = $params['page_size'] ?? 20;
$list = $query->orderBy('sort')->orderBy('id')->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function getAll(): array
{
return Dictionary::where('status', true)
->orderBy('sort')
->get()
->toArray();
}
public function getById(int $id): ?Dictionary
{
return Dictionary::with('items')->find($id);
}
public function getByCode(string $code): ?Dictionary
{
return Dictionary::where('code', $code)->first();
}
public function getItemsByCode(string $code): array
{
$cacheKey = 'system:dictionary:' . $code;
$items = Cache::get($cacheKey);
if ($items === null) {
$dictionary = Dictionary::where('code', $code)->first();
if ($dictionary) {
$items = DictionaryItem::where('dictionary_id', $dictionary->id)
->where('status', true)
->orderBy('sort')
->get()
->toArray();
Cache::put($cacheKey, $items, 3600);
} else {
$items = [];
}
}
return $items;
}
public function create(array $data): Dictionary
{
Validator::make($data, [
'name' => 'required|string|max:100',
'code' => 'required|string|max:50|unique:system_dictionaries,code',
])->validate();
$dictionary = Dictionary::create($data);
$this->clearCache();
return $dictionary;
}
public function update(int $id, array $data): Dictionary
{
$dictionary = Dictionary::findOrFail($id);
Validator::make($data, [
'name' => 'sometimes|required|string|max:100',
'code' => 'sometimes|required|string|max:50|unique:system_dictionaries,code,' . $id,
])->validate();
$dictionary->update($data);
$this->clearCache();
return $dictionary;
}
public function delete(int $id): bool
{
$dictionary = Dictionary::findOrFail($id);
DictionaryItem::where('dictionary_id', $id)->delete();
$dictionary->delete();
$this->clearCache();
return true;
}
public function batchDelete(array $ids): bool
{
DictionaryItem::whereIn('dictionary_id', $ids)->delete();
Dictionary::whereIn('id', $ids)->delete();
$this->clearCache();
return true;
}
public function batchUpdateStatus(array $ids, bool $status): bool
{
Dictionary::whereIn('id', $ids)->update(['status' => $status]);
$this->clearCache();
return true;
}
private function clearCache(): void
{
$codes = Dictionary::pluck('code')->toArray();
foreach ($codes as $code) {
Cache::forget('system:dictionary:' . $code);
}
}
public function getItemsList(array $params): array
{
$query = DictionaryItem::query();
if (!empty($params['dictionary_id'])) {
$query->where('dictionary_id', $params['dictionary_id']);
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', $params['status']);
}
$query->orderBy('sort')->orderBy('id');
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function createItem(array $data): DictionaryItem
{
Validator::make($data, [
'dictionary_id' => 'required|exists:system_dictionaries,id',
'label' => 'required|string|max:100',
'value' => 'required|string|max:100',
])->validate();
$item = DictionaryItem::create($data);
$this->clearCache();
return $item;
}
public function updateItem(int $id, array $data): DictionaryItem
{
$item = DictionaryItem::findOrFail($id);
Validator::make($data, [
'dictionary_id' => 'sometimes|required|exists:system_dictionaries,id',
'label' => 'sometimes|required|string|max:100',
'value' => 'sometimes|required|string|max:100',
])->validate();
$item->update($data);
$this->clearCache();
return $item;
}
public function deleteItem(int $id): bool
{
$item = DictionaryItem::findOrFail($id);
$item->delete();
$this->clearCache();
return true;
}
public function batchDeleteItems(array $ids): bool
{
DictionaryItem::whereIn('id', $ids)->delete();
$this->clearCache();
return true;
}
public function batchUpdateItemsStatus(array $ids, bool $status): bool
{
DictionaryItem::whereIn('id', $ids)->update(['status' => $status]);
$this->clearCache();
return true;
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
namespace App\Services\System;
use App\Models\System\Log;
use Illuminate\Support\Facades\Auth;
class LogService
{
public function create(array $data): Log
{
return Log::create($data);
}
public function getList(array $params): array
{
$query = $this->buildQuery($params);
$query->orderBy('created_at', 'desc');
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
/**
* 构建查询(用于导出等场景)
*
* @param array $params
* @return \Illuminate\Database\Eloquent\Builder
*/
public function getListQuery(array $params)
{
return $this->buildQuery($params);
}
/**
* 构建基础查询
*
* @param array $params
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function buildQuery(array $params)
{
$query = Log::query()->with('user:id,name,username');
if (!empty($params['user_id'])) {
$query->where('user_id', $params['user_id']);
}
if (!empty($params['username'])) {
$query->where('username', 'like', '%' . $params['username'] . '%');
}
if (!empty($params['module'])) {
$query->where('module', $params['module']);
}
if (!empty($params['action'])) {
$query->where('action', $params['action']);
}
if (!empty($params['status'])) {
$query->where('status', $params['status']);
}
if (!empty($params['start_date']) && !empty($params['end_date'])) {
$query->whereBetween('created_at', [$params['start_date'], $params['end_date']]);
}
if (!empty($params['ip'])) {
$query->where('ip', 'like', '%' . $params['ip'] . '%');
}
return $query;
}
public function getById(int $id): ?Log
{
return Log::with('user')->find($id);
}
public function delete(int $id): bool
{
$log = Log::findOrFail($id);
return $log->delete();
}
public function clearLogs(string $days = '30'): bool
{
Log::where('created_at', '<', now()->subDays($days))->delete();
return true;
}
public function batchDelete(array $ids): bool
{
Log::whereIn('id', $ids)->delete();
return true;
}
public function getStatistics(array $params = []): array
{
$query = Log::query();
if (!empty($params['start_date']) && !empty($params['end_date'])) {
$query->whereBetween('created_at', [$params['start_date'], $params['end_date']]);
}
$total = $query->count();
$successCount = (clone $query)->where('status', 'success')->count();
$errorCount = (clone $query)->where('status', 'error')->count();
return [
'total' => $total,
'success' => $successCount,
'error' => $errorCount,
];
}
}
+167
View File
@@ -0,0 +1,167 @@
<?php
namespace App\Services\System;
use App\Models\System\Task;
use Illuminate\Support\Facades\Validator;
class TaskService
{
public function getList(array $params): array
{
$query = Task::query();
if (!empty($params['keyword'])) {
$query->where(function ($q) use ($params) {
$q->where('name', 'like', '%' . $params['keyword'] . '%')
->orWhere('command', 'like', '%' . $params['keyword'] . '%');
});
}
if (isset($params['is_active']) && $params['is_active'] !== '') {
$query->where('is_active', $params['is_active']);
}
if (!empty($params['type'])) {
$query->where('type', $params['type']);
}
$query->orderBy('id');
$pageSize = $params['page_size'] ?? 20;
$list = $query->paginate($pageSize);
return [
'list' => $list->items(),
'total' => $list->total(),
'page' => $list->currentPage(),
'page_size' => $list->perPage(),
];
}
public function getAll(): array
{
return Task::all()->toArray();
}
public function getById(int $id): ?Task
{
return Task::find($id);
}
public function create(array $data): Task
{
Validator::make($data, [
'name' => 'required|string|max:100',
'command' => 'required|string|max:255',
'type' => 'required|string|in:command,job,closure',
'expression' => 'required|string',
])->validate();
return Task::create($data);
}
public function update(int $id, array $data): Task
{
$task = Task::findOrFail($id);
Validator::make($data, [
'name' => 'sometimes|required|string|max:100',
'command' => 'sometimes|required|string|max:255',
'type' => 'sometimes|required|string|in:command,job,closure',
'expression' => 'sometimes|required|string',
])->validate();
$task->update($data);
return $task;
}
public function delete(int $id): bool
{
$task = Task::findOrFail($id);
return $task->delete();
}
public function batchDelete(array $ids): bool
{
Task::whereIn('id', $ids)->delete();
return true;
}
public function batchUpdateStatus(array $ids, bool $status): bool
{
Task::whereIn('id', $ids)->update(['is_active' => $status]);
return true;
}
public function run(int $id): array
{
$task = Task::findOrFail($id);
$startTime = microtime(true);
$output = '';
$status = 'success';
$errorMessage = '';
try {
switch ($task->type) {
case 'command':
$command = $task->command;
if ($task->run_in_background) {
$command .= ' > /dev/null 2>&1 &';
}
exec($command, $output, $statusCode);
$output = implode("\n", $output);
if ($statusCode !== 0) {
throw new \Exception('Command failed with status code: ' . $statusCode);
}
break;
case 'job':
$jobClass = $task->command;
if (class_exists($jobClass)) {
dispatch(new $jobClass());
} else {
throw new \Exception('Job class not found: ' . $jobClass);
}
break;
case 'closure':
throw new \Exception('Closure type tasks cannot be run directly');
}
$task->increment('run_count');
} catch (\Exception $e) {
$status = 'error';
$errorMessage = $e->getMessage();
$task->increment('failed_count');
}
$executionTime = round((microtime(true) - $startTime) * 1000);
$task->update([
'last_run_at' => now(),
'last_output' => substr($output, 0, 10000),
]);
return [
'status' => $status,
'output' => $output,
'error_message' => $errorMessage,
'execution_time' => $executionTime,
];
}
public function getStatistics(): array
{
$total = Task::count();
$active = Task::where('is_active', true)->count();
$inactive = Task::where('is_active', false)->count();
return [
'total' => $total,
'active' => $active,
'inactive' => $inactive,
];
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
namespace App\Services\System;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
class UploadService
{
protected $disk;
protected $allowedImageTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
protected $allowedFileTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'rar', 'txt'];
protected $maxFileSize = 10 * 1024 * 1024; // 10MB
public function __construct()
{
$this->disk = Storage::disk('public');
}
public function upload(UploadedFile $file, string $directory = 'uploads', array $options = []): array
{
$extension = strtolower($file->getClientOriginalExtension());
if (!$this->validateFile($file, $extension)) {
throw new \Exception('文件验证失败');
}
$fileName = $this->generateFileName($file, $extension);
$filePath = $directory . '/' . date('Ymd') . '/' . $fileName;
if (in_array($extension, $this->allowedImageTypes) && isset($options['compress'])) {
$this->compressImage($file, $filePath, $options);
} else {
$this->disk->put($filePath, file_get_contents($file));
}
$url = $this->disk->url($filePath);
$fullPath = $this->disk->path($filePath);
return [
'url' => $url,
'path' => $filePath,
'name' => $file->getClientOriginalName(),
'size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'extension' => $extension,
];
}
public function uploadMultiple(array $files, string $directory = 'uploads', array $options = []): array
{
$results = [];
foreach ($files as $file) {
if ($file instanceof UploadedFile) {
$results[] = $this->upload($file, $directory, $options);
}
}
return $results;
}
public function uploadBase64(string $base64, string $directory = 'uploads', string $fileName = null): array
{
if (preg_match('/^data:image\/(\w+);base64,/', $base64, $matches)) {
$type = $matches[1];
$extension = $type;
$data = substr($base64, strpos($base64, ',') + 1);
$data = base64_decode($data);
if (!$data) {
throw new \Exception('Base64解码失败');
}
$fileName = $fileName ?: $this->generateUniqueFileName($extension);
$filePath = $directory . '/' . date('Ymd') . '/' . $fileName;
$this->disk->put($filePath, $data);
return [
'url' => $this->disk->url($filePath),
'path' => $filePath,
'name' => $fileName,
'size' => strlen($data),
'mime_type' => 'image/' . $type,
'extension' => $extension,
];
}
throw new \Exception('无效的Base64图片数据');
}
public function delete(string $path): bool
{
if ($this->disk->exists($path)) {
return $this->disk->delete($path);
}
return false;
}
public function deleteMultiple(array $paths): bool
{
foreach ($paths as $path) {
$this->delete($path);
}
return true;
}
private function validateFile(UploadedFile $file, string $extension): bool
{
if ($file->getSize() > $this->maxFileSize) {
throw new \Exception('文件大小超过限制');
}
$allowedTypes = array_merge($this->allowedImageTypes, $this->allowedFileTypes);
if (!in_array($extension, $allowedTypes)) {
throw new \Exception('不允许的文件类型');
}
return true;
}
private function generateFileName(UploadedFile $file, string $extension): string
{
return uniqid() . '_' . Str::random(6) . '.' . $extension;
}
private function generateUniqueFileName(string $extension): string
{
return uniqid() . '_' . Str::random(6) . '.' . $extension;
}
private function compressImage(UploadedFile $file, string $filePath, array $options): void
{
$quality = $options['quality'] ?? 80;
$width = $options['width'] ?? null;
$height = $options['height'] ?? null;
$image = Image::make($file);
if ($width || $height) {
$image->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
}
$image->encode(null, $quality);
$this->disk->put($filePath, (string) $image);
}
public function getFileUrl(string $path): string
{
return $this->disk->url($path);
}
public function fileExists(string $path): bool
{
return $this->disk->exists($path);
}
}
+451
View File
@@ -0,0 +1,451 @@
<?php
namespace App\Services\WebSocket;
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
use Illuminate\Support\Facades\Log;
use App\Services\Auth\UserOnlineService;
/**
* WebSocket Handler
*
* Handles WebSocket connections, messages, and disconnections
*/
class WebSocketHandler implements WebSocketHandlerInterface
{
/**
* @var UserOnlineService
*/
protected $userOnlineService;
/**
* WebSocketHandler constructor
*/
public function __construct()
{
$this->userOnlineService = app(UserOnlineService::class);
}
/**
* Handle WebSocket connection open event
*
* @param Server $server
* @param Request $request
* @return void
*/
public function onOpen(Server $server, Request $request): void
{
try {
$fd = $request->fd;
$path = $request->server['path_info'] ?? $request->server['request_uri'] ?? '/';
Log::info('WebSocket connection opened', [
'fd' => $fd,
'path' => $path,
'ip' => $request->server['remote_addr'] ?? 'unknown'
]);
// Extract user ID from query parameters if provided
$userId = $request->get['user_id'] ?? null;
$token = $request->get['token'] ?? null;
if ($userId && $token) {
// Store user connection mapping
$server->wsTable->set('uid:' . $userId, [
'value' => $fd,
'expiry' => time() + 3600, // 1 hour expiry
]);
$server->wsTable->set('fd:' . $fd, [
'value' => $userId,
'expiry' => time() + 3600
]);
// Update user online status
$this->userOnlineService->updateUserOnlineStatus($userId, $fd, true);
Log::info('User connected to WebSocket', [
'user_id' => $userId,
'fd' => $fd
]);
// Send welcome message to client
$server->push($fd, json_encode([
'type' => 'welcome',
'data' => [
'message' => 'WebSocket connection established',
'user_id' => $userId,
'timestamp' => time()
]
]));
} else {
Log::warning('WebSocket connection without authentication', [
'fd' => $fd
]);
// Send error message
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Authentication required. Please provide user_id and token.',
'code' => 401
]
]));
}
} catch (\Exception $e) {
Log::error('WebSocket onOpen error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
/**
* Handle WebSocket message event
*
* @param Server $server
* @param Frame $frame
* @return void
*/
public function onMessage(Server $server, Frame $frame): void
{
try {
$fd = $frame->fd;
$data = $frame->data;
Log::info('WebSocket message received', [
'fd' => $fd,
'data' => $data,
'opcode' => $frame->opcode
]);
// Parse incoming message
$message = json_decode($data, true);
if (!$message) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Invalid JSON format',
'code' => 400
]
]));
return;
}
// Handle different message types
$this->handleMessage($server, $fd, $message);
} catch (\Exception $e) {
Log::error('WebSocket onMessage error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
/**
* Handle WebSocket message based on type
*
* @param Server $server
* @param int $fd
* @param array $message
* @return void
*/
protected function handleMessage(Server $server, int $fd, array $message): void
{
$type = $message['type'] ?? 'unknown';
$data = $message['data'] ?? [];
switch ($type) {
case 'ping':
// Respond to ping with pong
$server->push($fd, json_encode([
'type' => 'pong',
'data' => [
'timestamp' => time()
]
]));
break;
case 'heartbeat':
// Handle heartbeat
$server->push($fd, json_encode([
'type' => 'heartbeat_ack',
'data' => [
'timestamp' => time()
]
]));
break;
case 'chat':
// Handle chat message
$this->handleChatMessage($server, $fd, $data);
break;
case 'broadcast':
// Handle broadcast message (admin only)
$this->handleBroadcast($server, $fd, $data);
break;
case 'subscribe':
// Handle channel subscription
$this->handleSubscribe($server, $fd, $data);
break;
case 'unsubscribe':
// Handle channel unsubscription
$this->handleUnsubscribe($server, $fd, $data);
break;
default:
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Unknown message type: ' . $type,
'code' => 400
]
]));
break;
}
}
/**
* Handle chat message
*
* @param Server $server
* @param int $fd
* @param array $data
* @return void
*/
protected function handleChatMessage(Server $server, int $fd, array $data): void
{
$toUserId = $data['to_user_id'] ?? null;
$content = $data['content'] ?? '';
if (!$toUserId || !$content) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Missing required fields: to_user_id and content',
'code' => 400
]
]));
return;
}
// Get target user's connection
$targetFd = $server->wsTable->get('uid:' . $toUserId);
if ($targetFd && $targetFd['value']) {
$server->push((int)$targetFd['value'], json_encode([
'type' => 'chat',
'data' => [
'from_user_id' => $server->wsTable->get('fd:' . $fd)['value'] ?? null,
'content' => $content,
'timestamp' => time()
]
]));
// Send delivery receipt to sender
$server->push($fd, json_encode([
'type' => 'message_delivered',
'data' => [
'to_user_id' => $toUserId,
'content' => $content,
'timestamp' => time()
]
]));
} else {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Target user is not online',
'code' => 404
]
]));
}
}
/**
* Handle broadcast message
*
* @param Server $server
* @param int $fd
* @param array $data
* @return void
*/
protected function handleBroadcast(Server $server, int $fd, array $data): void
{
$message = $data['message'] ?? '';
$userId = $server->wsTable->get('fd:' . $fd)['value'] ?? null;
// TODO: Check if user has admin permission to broadcast
// For now, allow any authenticated user
if (!$message) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Message content is required',
'code' => 400
]
]));
return;
}
// Broadcast to all connected clients except sender
$broadcastData = json_encode([
'type' => 'broadcast',
'data' => [
'from_user_id' => $userId,
'message' => $message,
'timestamp' => time()
]
]);
foreach ($server->connections as $connectionFd) {
if ($server->isEstablished($connectionFd) && $connectionFd !== $fd) {
$server->push($connectionFd, $broadcastData);
}
}
// Send confirmation to sender
$server->push($fd, json_encode([
'type' => 'broadcast_sent',
'data' => [
'message' => $message,
'timestamp' => time()
]
]));
}
/**
* Handle channel subscription
*
* @param Server $server
* @param int $fd
* @param array $data
* @return void
*/
protected function handleSubscribe(Server $server, int $fd, array $data): void
{
$channel = $data['channel'] ?? '';
if (!$channel) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Channel name is required',
'code' => 400
]
]));
return;
}
// Store subscription in wsTable
$server->wsTable->set('channel:' . $channel . ':fd:' . $fd, [
'value' => 1,
'expiry' => time() + 7200 // 2 hours
]);
$server->push($fd, json_encode([
'type' => 'subscribed',
'data' => [
'channel' => $channel,
'timestamp' => time()
]
]));
Log::info('User subscribed to channel', [
'fd' => $fd,
'channel' => $channel
]);
}
/**
* Handle channel unsubscription
*
* @param Server $server
* @param int $fd
* @param array $data
* @return void
*/
protected function handleUnsubscribe(Server $server, int $fd, array $data): void
{
$channel = $data['channel'] ?? '';
if (!$channel) {
$server->push($fd, json_encode([
'type' => 'error',
'data' => [
'message' => 'Channel name is required',
'code' => 400
]
]));
return;
}
// Remove subscription from wsTable
$server->wsTable->del('channel:' . $channel . ':fd:' . $fd);
$server->push($fd, json_encode([
'type' => 'unsubscribed',
'data' => [
'channel' => $channel,
'timestamp' => time()
]
]));
Log::info('User unsubscribed from channel', [
'fd' => $fd,
'channel' => $channel
]);
}
/**
* Handle WebSocket connection close event
*
* @param Server $server
* @param $fd
* @param $reactorId
* @return void
*/
public function onClose(Server $server, $fd, $reactorId): void
{
try {
Log::info('WebSocket connection closed', [
'fd' => $fd,
'reactor_id' => $reactorId
]);
// Get user ID from wsTable
$userId = $server->wsTable->get('fd:' . $fd)['value'] ?? null;
if ($userId) {
// Remove user connection mapping
$server->wsTable->del('uid:' . $userId);
$server->wsTable->del('fd:' . $fd);
// Update user online status
$this->userOnlineService->updateUserOnlineStatus($userId, $fd, false);
Log::info('User disconnected from WebSocket', [
'user_id' => $userId,
'fd' => $fd
]);
}
// Clean up channel subscriptions
// Note: In production, you might want to iterate through all channel keys
// and remove the ones associated with this fd
} catch (\Exception $e) {
Log::error('WebSocket onClose error', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
}
+402
View File
@@ -0,0 +1,402 @@
<?php
namespace App\Services\WebSocket;
use Illuminate\Support\Facades\Log;
use Swoole\WebSocket\Server;
/**
* WebSocket Service
*
* Provides helper functions for WebSocket operations
*/
class WebSocketService
{
/**
* Get Swoole WebSocket Server instance
*
* @return Server|null
*/
public function getServer(): ?Server
{
return app('swoole.server');
}
/**
* Send message to a specific user
*
* @param int $userId
* @param array $data
* @return bool
*/
public function sendToUser(int $userId, array $data): bool
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
Log::warning('WebSocket server not available', ['user_id' => $userId]);
return false;
}
$fdInfo = $server->wsTable->get('uid:' . $userId);
if (!$fdInfo || !$fdInfo['value']) {
Log::info('User not connected to WebSocket', ['user_id' => $userId]);
return false;
}
$fd = (int)$fdInfo['value'];
if (!$server->isEstablished($fd)) {
Log::info('WebSocket connection not established', ['user_id' => $userId, 'fd' => $fd]);
// Clean up stale connection
$server->wsTable->del('uid:' . $userId);
$server->wsTable->del('fd:' . $fd);
return false;
}
$server->push($fd, json_encode($data));
Log::info('Message sent to user via WebSocket', [
'user_id' => $userId,
'fd' => $fd,
'data' => $data
]);
return true;
}
/**
* Send message to multiple users
*
* @param array $userIds
* @param array $data
* @return array Array of user IDs who received the message
*/
public function sendToUsers(array $userIds, array $data): array
{
$sentTo = [];
foreach ($userIds as $userId) {
if ($this->sendToUser($userId, $data)) {
$sentTo[] = $userId;
}
}
return $sentTo;
}
/**
* Broadcast message to all connected clients
*
* @param array $data
* @param int|null $excludeUserId User ID to exclude from broadcast
* @return int Number of clients the message was sent to
*/
public function broadcast(array $data, ?int $excludeUserId = null): int
{
$server = $this->getServer();
if (!$server) {
Log::warning('WebSocket server not available for broadcast');
return 0;
}
$message = json_encode($data);
$count = 0;
foreach ($server->connections as $fd) {
if (!$server->isEstablished($fd)) {
continue;
}
// Check if we should exclude this user
if ($excludeUserId) {
$fdInfo = $server->wsTable->get('fd:' . $fd);
if ($fdInfo && $fdInfo['value'] == $excludeUserId) {
continue;
}
}
$server->push($fd, $message);
$count++;
}
Log::info('Broadcast sent via WebSocket', [
'data' => $data,
'exclude_user_id' => $excludeUserId,
'count' => $count
]);
return $count;
}
/**
* Send message to all subscribers of a channel
*
* @param string $channel
* @param array $data
* @return int Number of subscribers who received the message
*/
public function sendToChannel(string $channel, array $data): int
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
Log::warning('WebSocket server not available for channel broadcast', ['channel' => $channel]);
return 0;
}
$count = 0;
$message = json_encode($data);
// Iterate through all connections and check if they're subscribed to the channel
foreach ($server->connections as $fd) {
if (!$server->isEstablished($fd)) {
continue;
}
$subscription = $server->wsTable->get('channel:' . $channel . ':fd:' . $fd);
if ($subscription) {
$server->push($fd, $message);
$count++;
}
}
Log::info('Channel message sent via WebSocket', [
'channel' => $channel,
'data' => $data,
'count' => $count
]);
return $count;
}
/**
* Get online user count
*
* @return int
*/
public function getOnlineUserCount(): int
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
return 0;
}
// Count established connections
$count = 0;
foreach ($server->connections as $fd) {
if ($server->isEstablished($fd)) {
$count++;
}
}
return $count;
}
/**
* Check if a user is online
*
* @param int $userId
* @return bool
*/
public function isUserOnline(int $userId): bool
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
return false;
}
$fdInfo = $server->wsTable->get('uid:' . $userId);
if (!$fdInfo || !$fdInfo['value']) {
return false;
}
$fd = (int)$fdInfo['value'];
return $server->isEstablished($fd);
}
/**
* Disconnect a user from WebSocket
*
* @param int $userId
* @return bool
*/
public function disconnectUser(int $userId): bool
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
return false;
}
$fdInfo = $server->wsTable->get('uid:' . $userId);
if (!$fdInfo || !$fdInfo['value']) {
return false;
}
$fd = (int)$fdInfo['value'];
if ($server->isEstablished($fd)) {
$server->push($fd, json_encode([
'type' => 'disconnect',
'data' => [
'message' => 'You have been disconnected',
'timestamp' => time()
]
]));
// Close the connection
$server->disconnect($fd);
// Clean up
$server->wsTable->del('uid:' . $userId);
$server->wsTable->del('fd:' . $fd);
Log::info('User disconnected from WebSocket by server', [
'user_id' => $userId,
'fd' => $fd
]);
return true;
}
return false;
}
/**
* Get all online user IDs
*
* @return array
*/
public function getOnlineUserIds(): array
{
$server = $this->getServer();
if (!$server || !isset($server->wsTable)) {
return [];
}
$userIds = [];
foreach ($server->connections as $fd) {
if (!$server->isEstablished($fd)) {
continue;
}
$fdInfo = $server->wsTable->get('fd:' . $fd);
if ($fdInfo && $fdInfo['value']) {
$userIds[] = (int)$fdInfo['value'];
}
}
return array_unique($userIds);
}
/**
* Send system notification to all online users
*
* @param string $title
* @param string $message
* @param string $type
* @param array $extraData
* @return int
*/
public function sendSystemNotification(string $title, string $message, string $type = 'info', array $extraData = []): int
{
$data = [
'type' => 'notification',
'data' => [
'title' => $title,
'message' => $message,
'type' => $type, // info, success, warning, error
'timestamp' => time(),
...$extraData
]
];
return $this->broadcast($data);
}
/**
* Send notification to specific users
*
* @param array $userIds
* @param string $title
* @param string $message
* @param string $type
* @param array $extraData
* @return array
*/
public function sendNotificationToUsers(array $userIds, string $title, string $message, string $type = 'info', array $extraData = []): array
{
$data = [
'type' => 'notification',
'data' => [
'title' => $title,
'message' => $message,
'type' => $type,
'timestamp' => time(),
...$extraData
]
];
return $this->sendToUsers($userIds, $data);
}
/**
* Push data update to specific users
*
* @param array $userIds
* @param string $resourceType
* @param string $action
* @param array $data
* @return array
*/
public function pushDataUpdate(array $userIds, string $resourceType, string $action, array $data): array
{
$message = [
'type' => 'data_update',
'data' => [
'resource_type' => $resourceType, // e.g., 'user', 'order', 'product'
'action' => $action, // create, update, delete
'data' => $data,
'timestamp' => time()
]
];
return $this->sendToUsers($userIds, $message);
}
/**
* Push data update to a channel
*
* @param string $channel
* @param string $resourceType
* @param string $action
* @param array $data
* @return int
*/
public function pushDataUpdateToChannel(string $channel, string $resourceType, string $action, array $data): int
{
$message = [
'type' => 'data_update',
'data' => [
'resource_type' => $resourceType,
'action' => $action,
'data' => $data,
'timestamp' => time()
]
];
return $this->sendToChannel($channel, $message);
}
}
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
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...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
WORK_DIR=$1
if [ ! -n "${WORK_DIR}" ] ;then
WORK_DIR="."
fi
echo "Restarting LaravelS..."
./bin/laravels restart -d -i
echo "Starting fswatch..."
LOCKING=0
fswatch -e ".*" -i "\\.php$" -r ${WORK_DIR} | while read file
do
if [[ ! ${file} =~ .php$ ]] ;then
continue
fi
if [ ${LOCKING} -eq 1 ] ;then
echo "Reloading, skipped."
continue
fi
echo "File ${file} has been modified."
LOCKING=1
./bin/laravels reload
LOCKING=0
done
exit 0
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
WORK_DIR=$1
if [ ! -n "${WORK_DIR}" ] ;then
WORK_DIR="."
fi
echo "Restarting LaravelS..."
./bin/laravels restart -d -i
echo "Starting inotifywait..."
LOCKING=0
inotifywait --event modify --event create --event move --event delete -mrq ${WORK_DIR} | while read file
do
if [[ ! ${file} =~ .php$ ]] ;then
continue
fi
if [ ${LOCKING} -eq 1 ] ;then
echo "Reloading, skipped."
continue
fi
echo "File ${file} has been modified."
LOCKING=1
./bin/laravels reload
LOCKING=0
done
exit 0
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env php
<?php
/**
* This autoloader is only used to pull laravel-s.
* Class Psr4Autoloader
*/
class Psr4Autoloader
{
/**
* An associative array where the key is a namespace prefix and the value
* is an array of base directories for classes in that namespace.
*
* @var array
*/
protected $prefixes = [];
/**
* Register loader with SPL autoloader stack.
*
* @return void
*/
public function register()
{
spl_autoload_register([$this, 'loadClass']);
}
/**
* Adds a base directory for a namespace prefix.
*
* @param string $prefix The namespace prefix.
* @param string $base_dir A base directory for class files in the
* namespace.
* @param bool $prepend If true, prepend the base directory to the stack
* instead of appending it; this causes it to be searched first rather
* than last.
* @return void
*/
public function addNamespace($prefix, $base_dir, $prepend = false)
{
// normalize namespace prefix
$prefix = trim($prefix, '\\') . '\\';
// normalize the base directory with a trailing separator
$base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/';
// initialize the namespace prefix array
if (isset($this->prefixes[$prefix]) === false) {
$this->prefixes[$prefix] = [];
}
// retain the base directory for the namespace prefix
if ($prepend) {
array_unshift($this->prefixes[$prefix], $base_dir);
} else {
$this->prefixes[$prefix][] = $base_dir;
}
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully-qualified class name.
* @return mixed The mapped file name on success, or boolean false on
* failure.
*/
public function loadClass($class)
{
// the current namespace prefix
$prefix = $class;
// work backwards through the namespace names of the fully-qualified
// class name to find a mapped file name
while (false !== $pos = strrpos($prefix, '\\')) {
// retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1);
// the rest is the relative class name
$relative_class = substr($class, $pos + 1);
// try to load a mapped file for the prefix and relative class
$mapped_file = $this->loadMappedFile($prefix, $relative_class);
if ($mapped_file) {
return $mapped_file;
}
// remove the trailing namespace separator for the next iteration
// of strrpos()
$prefix = rtrim($prefix, '\\');
}
// never found a mapped file
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix The namespace prefix.
* @param string $relative_class The relative class name.
* @return mixed Boolean false if no mapped file can be loaded, or the
* name of the mapped file that was loaded.
*/
protected function loadMappedFile($prefix, $relative_class)
{
// are there any base directories for this namespace prefix?
if (isset($this->prefixes[$prefix]) === false) {
return false;
}
// look through base directories for this namespace prefix
foreach ($this->prefixes[$prefix] as $base_dir) {
// replace the namespace prefix with the base directory,
// replace namespace separators with directory separators
// in the relative class name, append with .php
$file = $base_dir
. str_replace('\\', '/', $relative_class)
. '.php';
// if the mapped file exists, require it
if ($this->requireFile($file)) {
// yes, we're done
return $file;
}
}
// never found it
return false;
}
/**
* If a file exists, require it from the file system.
*
* @param string $file The file to require.
* @return bool True if the file exists, false if not.
*/
public function requireFile($file)
{
if (file_exists($file)) {
require $file;
return true;
}
return false;
}
}
$basePath = dirname(__DIR__);
$loader = new Psr4Autoloader();
$loader->register();
// Register laravel-s
$loader->addNamespace('Hhxsv5\LaravelS', $basePath . '/vendor/hhxsv5/laravel-s/src');
// Register laravel-s dependencies
// To fix issue #364 https://github.com/hhxsv5/laravel-s/issues/364
$loader->addNamespace('Symfony\Polyfill\Php80', $basePath . '/vendor/symfony/polyfill-php80');
$loader->requireFile($basePath . '/vendor/symfony/polyfill-php80/bootstrap.php');
$loader->addNamespace('Symfony\Component\Console', $basePath . '/vendor/symfony/console');
$loader->addNamespace('Symfony\Component\String', $basePath . '/vendor/symfony/string');
$loader->addNamespace('Symfony\Contracts\Service', $basePath . '/vendor/symfony/service-contracts');
$loader->addNamespace('Symfony\Contracts', $basePath . '/vendor/symfony/contracts');
$command = new Hhxsv5\LaravelS\Console\Portal($basePath);
$input = new Symfony\Component\Console\Input\ArgvInput();
$output = new Symfony\Component\Console\Output\ConsoleOutput();
$code = $command->run($input, $output);
exit($code);
+35
View File
@@ -0,0 +1,35 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use App\Http\Middleware\AuthCheckMiddleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
then: function() {
Route::middleware(['api'])
->prefix('admin')
->name('admin.')
->group(base_path('routes/admin.php'));
},
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'auth.check' => \App\Http\Middleware\AuthCheckMiddleware::class,
'log.request' => \App\Http\Middleware\LogRequestMiddleware::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(function(Request $request, Throwable $e){
return $request->expectsJson();
});
})
->withEvents(discover: [
__DIR__ . '/../app/Listeners'
])->create();
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
App\Providers\AppServiceProvider::class,
];
+90
View File
@@ -0,0 +1,90 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.2",
"hhxsv5/laravel-s": "^3.8",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
"nwidart/laravel-modules": "^12.0",
"tymon/jwt-auth": "^2.2"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.24",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^11.5.3"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"setup": [
"composer install",
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
"@php artisan key:generate",
"@php artisan migrate --force",
"npm install",
"npm run build"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
],
"test": [
"@php artisan config:clear --ansi",
"@php artisan test"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"pre-package-uninstall": [
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true,
"wikimedia/composer-merge-plugin": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
+126
View File
@@ -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' => 'UTC',
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
];
+124
View File
@@ -0,0 +1,124 @@
<?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' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'jwt',
'provider' => 'admin',
],
'api' => [
'driver' => 'jwt',
'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' => [
'admin' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\Auth\User::class),
],
'users' => [
'driver' => 'database',
'table' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the number of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];
+117
View File
@@ -0,0 +1,117 @@
<?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', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane",
| "failover", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
'failover' => [
'driver' => 'failover',
'stores' => [
'database',
'array',
],
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
];
+34
View File
@@ -0,0 +1,34 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Cross-Origin Resource Sharing (CORS) Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your settings for cross-origin resource sharing
| or "CORS". This determines what cross-origin operations may execute
| in web browsers. You are free to adjust these settings as needed.
|
| To learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
*/
'paths' => ['api/*', 'admin/*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => false,
];
+183
View File
@@ -0,0 +1,183 @@
<?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,
'transaction_mode' => 'DEFERRED',
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => env('DB_SSLMODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'max_retries' => env('REDIS_MAX_RETRIES', 3),
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
],
],
];
+80
View File
@@ -0,0 +1,80 @@
<?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' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'report' => false,
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
+301
View File
@@ -0,0 +1,301 @@
<?php
/*
* This file is part of jwt-auth.
*
* (c) Sean Tymon <tymon148@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
return [
/*
|--------------------------------------------------------------------------
| JWT Authentication Secret
|--------------------------------------------------------------------------
|
| Don't forget to set this in your .env file, as it will be used to sign
| your tokens. A helper command is provided for this:
| `php artisan jwt:secret`
|
| Note: This will be used for Symmetric algorithms only (HMAC),
| since RSA and ECDSA use a private/public key combo (See below).
|
*/
'secret' => env('JWT_SECRET'),
/*
|--------------------------------------------------------------------------
| JWT Authentication Keys
|--------------------------------------------------------------------------
|
| The algorithm you are using, will determine whether your tokens are
| signed with a random string (defined in `JWT_SECRET`) or using the
| following public & private keys.
|
| Symmetric Algorithms:
| HS256, HS384 & HS512 will use `JWT_SECRET`.
|
| Asymmetric Algorithms:
| RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below.
|
*/
'keys' => [
/*
|--------------------------------------------------------------------------
| Public Key
|--------------------------------------------------------------------------
|
| A path or resource to your public key.
|
| E.g. 'file://path/to/public/key'
|
*/
'public' => env('JWT_PUBLIC_KEY'),
/*
|--------------------------------------------------------------------------
| Private Key
|--------------------------------------------------------------------------
|
| A path or resource to your private key.
|
| E.g. 'file://path/to/private/key'
|
*/
'private' => env('JWT_PRIVATE_KEY'),
/*
|--------------------------------------------------------------------------
| Passphrase
|--------------------------------------------------------------------------
|
| The passphrase for your private key. Can be null if none set.
|
*/
'passphrase' => env('JWT_PASSPHRASE'),
],
/*
|--------------------------------------------------------------------------
| JWT time to live
|--------------------------------------------------------------------------
|
| Specify the length of time (in minutes) that the token will be valid for.
| Defaults to 1 hour.
|
| You can also set this to null, to yield a never expiring token.
| Some people may want this behaviour for e.g. a mobile app.
| This is not particularly recommended, so make sure you have appropriate
| systems in place to revoke the token if necessary.
| Notice: If you set this to null you should remove 'exp' element from 'required_claims' list.
|
*/
'ttl' => env('JWT_TTL', 60),
/*
|--------------------------------------------------------------------------
| Refresh time to live
|--------------------------------------------------------------------------
|
| Specify the length of time (in minutes) that the token can be refreshed
| within. I.E. The user can refresh their token within a 2 week window of
| the original token being created until they must re-authenticate.
| Defaults to 2 weeks.
|
| You can also set this to null, to yield an infinite refresh time.
| Some may want this instead of never expiring tokens for e.g. a mobile app.
| This is not particularly recommended, so make sure you have appropriate
| systems in place to revoke the token if necessary.
|
*/
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
/*
|--------------------------------------------------------------------------
| JWT hashing algorithm
|--------------------------------------------------------------------------
|
| Specify the hashing algorithm that will be used to sign the token.
|
*/
'algo' => env('JWT_ALGO', Tymon\JWTAuth\Providers\JWT\Provider::ALGO_HS256),
/*
|--------------------------------------------------------------------------
| Required Claims
|--------------------------------------------------------------------------
|
| Specify the required claims that must exist in any token.
| A TokenInvalidException will be thrown if any of these claims are not
| present in the payload.
|
*/
'required_claims' => [
'iss',
'iat',
'exp',
'nbf',
'sub',
'jti',
],
/*
|--------------------------------------------------------------------------
| Persistent Claims
|--------------------------------------------------------------------------
|
| Specify the claim keys to be persisted when refreshing a token.
| `sub` and `iat` will automatically be persisted, in
| addition to the these claims.
|
| Note: If a claim does not exist then it will be ignored.
|
*/
'persistent_claims' => [
// 'foo',
// 'bar',
],
/*
|--------------------------------------------------------------------------
| Lock Subject
|--------------------------------------------------------------------------
|
| This will determine whether a `prv` claim is automatically added to
| the token. The purpose of this is to ensure that if you have multiple
| authentication models e.g. `App\User` & `App\OtherPerson`, then we
| should prevent one authentication request from impersonating another,
| if 2 tokens happen to have the same id across the 2 different models.
|
| Under specific circumstances, you may want to disable this behaviour
| e.g. if you only have one authentication model, then you would save
| a little on token size.
|
*/
'lock_subject' => true,
/*
|--------------------------------------------------------------------------
| Leeway
|--------------------------------------------------------------------------
|
| This property gives the jwt timestamp claims some "leeway".
| Meaning that if you have any unavoidable slight clock skew on
| any of your servers then this will afford you some level of cushioning.
|
| This applies to the claims `iat`, `nbf` and `exp`.
|
| Specify in seconds - only if you know you need it.
|
*/
'leeway' => env('JWT_LEEWAY', 0),
/*
|--------------------------------------------------------------------------
| Blacklist Enabled
|--------------------------------------------------------------------------
|
| In order to invalidate tokens, you must have the blacklist enabled.
| If you do not want or need this functionality, then set this to false.
|
*/
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
/*
| -------------------------------------------------------------------------
| Blacklist Grace Period
| -------------------------------------------------------------------------
|
| When multiple concurrent requests are made with the same JWT,
| it is possible that some of them fail, due to token regeneration
| on every request.
|
| Set grace period in seconds to prevent parallel request failure.
|
*/
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
/*
|--------------------------------------------------------------------------
| Cookies encryption
|--------------------------------------------------------------------------
|
| By default Laravel encrypt cookies for security reason.
| If you decide to not decrypt cookies, you will have to configure Laravel
| to not encrypt your cookie token by adding its name into the $except
| array available in the middleware "EncryptCookies" provided by Laravel.
| see https://laravel.com/docs/master/responses#cookies-and-encryption
| for details.
|
| Set it to true if you want to decrypt cookies.
|
*/
'decrypt_cookies' => false,
/*
|--------------------------------------------------------------------------
| Providers
|--------------------------------------------------------------------------
|
| Specify the various providers used throughout the package.
|
*/
'providers' => [
/*
|--------------------------------------------------------------------------
| JWT Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to create and decode the tokens.
|
*/
'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class,
/*
|--------------------------------------------------------------------------
| Authentication Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to authenticate users.
|
*/
'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
/*
|--------------------------------------------------------------------------
| Storage Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to store tokens in the blacklist.
|
*/
'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,
],
];
+317
View File
@@ -0,0 +1,317 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| LaravelS Settings
|--------------------------------------------------------------------------
|
| English https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#laravels-settings
| Chinese https://github.com/hhxsv5/laravel-s/blob/master/Settings-CN.md#laravels-%E9%85%8D%E7%BD%AE%E9%A1%B9
|
*/
/*
|--------------------------------------------------------------------------
| The IP address of the server
|--------------------------------------------------------------------------
|
| IPv4: use "127.0.0.1" to listen local address, and "0.0.0.0" to listen all addresses.
| IPv6: use "::1" to listen local address, and "::"(equivalent to 0:0:0:0:0:0:0:0) to listen all addresses.
|
*/
'listen_ip' => env('LARAVELS_LISTEN_IP', '127.0.0.1'),
/*
|--------------------------------------------------------------------------
| The port of the server
|--------------------------------------------------------------------------
|
| Require root privilege if port is less than 1024.
|
*/
'listen_port' => env('LARAVELS_LISTEN_PORT', 8000),
/*
|--------------------------------------------------------------------------
| The socket type of the server
|--------------------------------------------------------------------------
|
| Usually, you dont need to care about it.
| Unless you want Nginx to proxy to the UnixSocket Stream file, you need
| to modify it to SWOOLE_SOCK_UNIX_STREAM, and listen_ip is the path of UnixSocket Stream file.
| List of socket types:
| SWOOLE_SOCK_TCP: TCP
| SWOOLE_SOCK_TCP6: TCP IPv6
| SWOOLE_SOCK_UDP: UDP
| SWOOLE_SOCK_UDP6: UDP IPv6
| SWOOLE_UNIX_DGRAM: Unix socket dgram
| SWOOLE_UNIX_STREAM: Unix socket stream
| Enable SSL: $sock_type | SWOOLE_SSL. To enable SSL, check the configuration about SSL.
| https://www.swoole.co.uk/docs/modules/swoole-server-doc
| https://www.swoole.co.uk/docs/modules/swoole-server/configuration
|
*/
'socket_type' => defined('SWOOLE_SOCK_TCP') ? SWOOLE_SOCK_TCP : 1,
/*
|--------------------------------------------------------------------------
| Server Name
|--------------------------------------------------------------------------
|
| This value represents the name of the server that will be
| displayed in the header of each request.
|
*/
'server' => env('LARAVELS_SERVER', ''),
/*
|--------------------------------------------------------------------------
| Handle Static Resource
|--------------------------------------------------------------------------
|
| Whether handle the static resource by LaravelS(Require Swoole >= 1.7.21, Handle by Swoole if Swoole >= 1.9.17).
| Suggest that Nginx handles the statics and LaravelS handles the dynamics.
| The default path of static resource is base_path('public'), you can modify swoole.document_root to change it.
|
*/
'handle_static' => env('LARAVELS_HANDLE_STATIC', false),
/*
|--------------------------------------------------------------------------
| Laravel Base Path
|--------------------------------------------------------------------------
|
| The basic path of Laravel, default base_path(), be used for symbolic link.
|
*/
'laravel_base_path' => env('LARAVEL_BASE_PATH', base_path()),
/*
|--------------------------------------------------------------------------
| Inotify Reload
|--------------------------------------------------------------------------
|
| This feature requires inotify extension.
| https://github.com/hhxsv5/laravel-s#automatically-reload-after-modifying-code
|
*/
'inotify_reload' => [
// Whether enable the Inotify Reload to reload all worker processes when your code is modified.
'enable' => env('LARAVELS_INOTIFY_RELOAD', false),
// The file path that Inotify watches
'watch_path' => base_path(),
// The file types that Inotify watches
'file_types' => ['.php'],
// The excluded/ignored directories that Inotify watches
'excluded_dirs' => [],
// Whether output the reload log
'log' => true,
],
/*
|--------------------------------------------------------------------------
| Swoole Event Handlers
|--------------------------------------------------------------------------
|
| Configure the event callback function of Swoole, key-value format,
| key is the event name, and value is the class that implements the event
| processing interface.
|
| https://github.com/hhxsv5/laravel-s#configuring-the-event-callback-function-of-swoole
|
*/
'event_handlers' => [],
/*
|--------------------------------------------------------------------------
| WebSockets
|--------------------------------------------------------------------------
|
| Swoole WebSocket Server settings.
|
| https://github.com/hhxsv5/laravel-s#enable-websocket-server
|
*/
'websocket' => [
'enable' => env('LARAVELS_WEBSOCKET', true),
'handler' => \App\Services\WebSocket\WebSocketHandler::class,
],
/*
|--------------------------------------------------------------------------
| Sockets - multi-port mixed protocol
|--------------------------------------------------------------------------
|
| The socket(port) list for TCP/UDP.
|
| https://github.com/hhxsv5/laravel-s#multi-port-mixed-protocol
|
*/
'sockets' => [],
/*
|--------------------------------------------------------------------------
| Custom Process
|--------------------------------------------------------------------------
|
| Support developers to create custom processes for monitoring,
| reporting, or other special tasks.
|
| https://github.com/hhxsv5/laravel-s#custom-process
|
*/
'processes' => [],
/*
|--------------------------------------------------------------------------
| Timer
|--------------------------------------------------------------------------
|
| Wrapper cron job base on Swoole's Millisecond Timer, replace Linux Crontab.
|
| https://github.com/hhxsv5/laravel-s#millisecond-cron-job
|
*/
'timer' => [
'enable' => env('LARAVELS_TIMER', false),
// The list of cron job
'jobs' => [
// Enable LaravelScheduleJob to run `php artisan schedule:run` every 1 minute, replace Linux Crontab
// Hhxsv5\LaravelS\Illuminate\LaravelScheduleJob::class,
],
// Max waiting time of reloading
'max_wait_time' => 5,
// Enable the global lock to ensure that only one instance starts the timer
// when deploying multiple instances.
// This feature depends on Redis https://laravel.com/docs/8.x/redis
'global_lock' => false,
'global_lock_key' => config('app.name', 'Laravel'),
],
/*
|--------------------------------------------------------------------------
| Swoole Tables
|--------------------------------------------------------------------------
|
| All defined tables will be created before Swoole starting.
|
| https://github.com/hhxsv5/laravel-s#use-swooletable
|
*/
'swoole_tables' => [
// WebSocket table for storing user connections
'wsTable' => [
'size' => 102400, // Maximum number of rows
'column' => [
['name' => 'value', 'type' => \Swoole\Table::TYPE_STRING, 'size' => 1024],
['name' => 'expiry', 'type' => \Swoole\Table::TYPE_INT, 'size' => 4],
],
],
],
/*
|--------------------------------------------------------------------------
| Re-register Providers
|--------------------------------------------------------------------------
|
| The Service Provider list, will be re-registered each request, and run method boot()
| if it exists. Usually, be used to clear the Service Provider
| which registers Singleton instances.
|
| https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#register_providers
|
*/
'register_providers' => [],
/*
|--------------------------------------------------------------------------
| Cleaners
|--------------------------------------------------------------------------
|
| The list of cleaners for each request is used to clean up some residual
| global variables, singleton objects, and static properties to avoid
| data pollution between requests.
|
| https://github.com/hhxsv5/laravel-s/blob/master/Settings.md#cleaners
|
*/
'cleaners' => [],
/*
|--------------------------------------------------------------------------
| Destroy Controllers
|--------------------------------------------------------------------------
|
| Automatically destroy the controllers after each request to solve
| the problem of the singleton controllers.
|
| https://github.com/hhxsv5/laravel-s/blob/master/KnownIssues.md#singleton-controller
|
*/
'destroy_controllers' => [
'enable' => false,
'excluded_list' => [],
],
/*
|--------------------------------------------------------------------------
| Swoole Settings
|--------------------------------------------------------------------------
|
| Swoole's original configuration items.
|
| More settings
| Chinese https://wiki.swoole.com/#/server/setting
| English https://www.swoole.co.uk/docs/modules/swoole-server/configuration
|
*/
'swoole' => [
'daemonize' => env('LARAVELS_DAEMONIZE', false),
'dispatch_mode' => env('LARAVELS_DISPATCH_MODE', 3),
'worker_num' => env('LARAVELS_WORKER_NUM', 30),
//'task_worker_num' => env('LARAVELS_TASK_WORKER_NUM', 10),
'task_ipc_mode' => 1,
'task_max_request' => env('LARAVELS_TASK_MAX_REQUEST', 100000),
'task_tmpdir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
'max_request' => env('LARAVELS_MAX_REQUEST', 100000),
'open_tcp_nodelay' => true,
'pid_file' => storage_path('laravels.pid'),
'log_level' => env('LARAVELS_LOG_LEVEL', 4),
'log_file' => storage_path(sprintf('logs/swoole-%s.log', date('Y-m'))),
'document_root' => base_path('public'),
'buffer_output_size' => 2 * 1024 * 1024,
'socket_buffer_size' => 8 * 1024 * 1024,
'package_max_length' => 4 * 1024 * 1024,
'reload_async' => true,
'max_wait_time' => 60,
'enable_reuse_port' => true,
'enable_coroutine' => false,
'upload_tmp_dir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
'http_compression' => env('LARAVELS_HTTP_COMPRESSION', false),
],
];
+132
View File
@@ -0,0 +1,132 @@
<?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(',', (string) env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'handler_with' => [
'stream' => 'php://stderr',
],
'formatter' => env('LOG_STDERR_FORMATTER'),
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
+118
View File
@@ -0,0 +1,118 @@
<?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' => [
'smtp' => [
'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],
'postmark' => [
'transport' => 'postmark',
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
// 'client' => [
// 'timeout' => 5,
// ],
],
'resend' => [
'transport' => 'resend',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
'retry_after' => 60,
],
'roundrobin' => [
'transport' => 'roundrobin',
'mailers' => [
'ses',
'postmark',
],
'retry_after' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all emails sent by your application to be sent from
| the same address. Here you may specify a name and address that is
| used globally for all emails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
];
+129
View File
@@ -0,0 +1,129 @@
<?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",
| "deferred", "background", "failover", "null"
|
*/
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'connection' => env('DB_QUEUE_CONNECTION'),
'table' => env('DB_QUEUE_TABLE', 'jobs'),
'queue' => env('DB_QUEUE', 'default'),
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
'queue' => env('BEANSTALKD_QUEUE', 'default'),
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
'block_for' => 0,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
'deferred' => [
'driver' => 'deferred',
],
'background' => [
'driver' => 'background',
],
'failover' => [
'driver' => 'failover',
'connections' => [
'database',
'deferred',
],
],
],
/*
|--------------------------------------------------------------------------
| Job Batching
|--------------------------------------------------------------------------
|
| The following options configure the database and table that store job
| batching information. These options can be updated to any database
| connection and table which has been defined by your application.
|
*/
'batching' => [
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'job_batches',
],
/*
|--------------------------------------------------------------------------
| Failed Queue Jobs
|--------------------------------------------------------------------------
|
| These options configure the behavior of failed queue job logging so you
| can control how and where failed jobs are stored. Laravel ships with
| support for storing failed jobs in a simple file or in a database.
|
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'sqlite'),
'table' => 'failed_jobs',
],
];
+38
View File
@@ -0,0 +1,38 @@
<?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 packages to have
| a conventional file to locate the various service credentials.
|
*/
'postmark' => [
'key' => env('POSTMARK_API_KEY'),
],
'resend' => [
'key' => env('RESEND_API_KEY'),
],
'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'slack' => [
'notifications' => [
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
],
],
];
+217
View File
@@ -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", "memcached",
| "redis", "dynamodb", "array"
|
*/
'driver' => env('SESSION_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Session Lifetime
|--------------------------------------------------------------------------
|
| Here you may specify the number of minutes that you wish the session
| to be allowed to remain idle before it expires. If you want them
| to expire immediately when the browser is closed then you may
| indicate that via the expire_on_close configuration option.
|
*/
'lifetime' => (int) env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
/*
|--------------------------------------------------------------------------
| Session Encryption
|--------------------------------------------------------------------------
|
| This option allows you to easily specify that all of your session data
| should be encrypted before it's stored. All encryption is performed
| automatically by Laravel and you may use the session like normal.
|
*/
'encrypt' => env('SESSION_ENCRYPT', false),
/*
|--------------------------------------------------------------------------
| Session File Location
|--------------------------------------------------------------------------
|
| When utilizing the "file" session driver, the session files are placed
| on disk. The default storage location is defined here; however, you
| are free to provide another location where they should be stored.
|
*/
'files' => storage_path('framework/sessions'),
/*
|--------------------------------------------------------------------------
| Session Database Connection
|--------------------------------------------------------------------------
|
| When using the "database" or "redis" session drivers, you may specify a
| connection that should be used to manage these sessions. This should
| correspond to a connection in your database configuration options.
|
*/
'connection' => env('SESSION_CONNECTION'),
/*
|--------------------------------------------------------------------------
| Session Database Table
|--------------------------------------------------------------------------
|
| When using the "database" session driver, you may specify the table to
| be used to store sessions. Of course, a sensible default is defined
| for you; however, you're welcome to change this to another table.
|
*/
'table' => env('SESSION_TABLE', 'sessions'),
/*
|--------------------------------------------------------------------------
| Session Cache Store
|--------------------------------------------------------------------------
|
| When using one of the framework's cache driven session backends, you may
| define the cache store which should be used to store the session data
| between requests. This must match one of your defined cache stores.
|
| Affects: "dynamodb", "memcached", "redis"
|
*/
'store' => env('SESSION_STORE'),
/*
|--------------------------------------------------------------------------
| Session Sweeping Lottery
|--------------------------------------------------------------------------
|
| Some session drivers must manually sweep their storage location to get
| rid of old sessions from storage. Here are the chances that it will
| happen on a given request. By default, the odds are 2 out of 100.
|
*/
'lottery' => [2, 100],
/*
|--------------------------------------------------------------------------
| Session Cookie Name
|--------------------------------------------------------------------------
|
| Here you may change the name of the session cookie that is created by
| the framework. Typically, you should not need to change this value
| since doing so does not grant a meaningful security improvement.
|
*/
'cookie' => env(
'SESSION_COOKIE',
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
),
/*
|--------------------------------------------------------------------------
| Session Cookie Path
|--------------------------------------------------------------------------
|
| The session cookie path determines the path for which the cookie will
| be regarded as available. Typically, this will be the root path of
| your application, but you're free to change this when necessary.
|
*/
'path' => env('SESSION_PATH', '/'),
/*
|--------------------------------------------------------------------------
| Session Cookie Domain
|--------------------------------------------------------------------------
|
| This value determines the domain and subdomains the session cookie is
| available to. By default, the cookie will be available to the root
| domain without subdomains. Typically, this shouldn't be changed.
|
*/
'domain' => env('SESSION_DOMAIN'),
/*
|--------------------------------------------------------------------------
| HTTPS Only Cookies
|--------------------------------------------------------------------------
|
| By setting this option to true, session cookies will only be sent back
| to the server if the browser has a HTTPS connection. This will keep
| the cookie from being sent to you when it can't be done securely.
|
*/
'secure' => env('SESSION_SECURE_COOKIE'),
/*
|--------------------------------------------------------------------------
| HTTP Access Only
|--------------------------------------------------------------------------
|
| Setting this value to true will prevent JavaScript from accessing the
| value of the cookie and the cookie will only be accessible through
| the HTTP protocol. It's unlikely you should disable this option.
|
*/
'http_only' => env('SESSION_HTTP_ONLY', true),
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| will set this value to "lax" to permit secure cross-site requests.
|
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
| Supported: "lax", "strict", "none", null
|
*/
'same_site' => env('SESSION_SAME_SITE', 'lax'),
/*
|--------------------------------------------------------------------------
| Partitioned Cookies
|--------------------------------------------------------------------------
|
| Setting this value to true will tie the cookie to the top-level site for
| a cross-site context. Partitioned cookies are accepted by the browser
| when flagged "secure" and the Same-Site attribute is set to "none".
|
*/
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
];
+1
View File
@@ -0,0 +1 @@
*.sqlite*
@@ -0,0 +1,121 @@
<?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('auth_users', function (Blueprint $table) {
$table->id();
$table->string('username', 50)->unique()->comment('用户名');
$table->string('password')->comment('密码');
$table->string('real_name', 50)->comment('真实姓名');
$table->string('email', 100)->nullable()->comment('邮箱');
$table->string('phone', 20)->nullable()->comment('手机号');
$table->unsignedBigInteger('department_id')->nullable()->comment('部门ID');
$table->string('avatar')->nullable()->comment('头像');
$table->tinyInteger('status')->default(1)->comment('状态:1启用 0禁用');
$table->timestamp('last_login_at')->nullable()->comment('最后登录时间');
$table->string('last_login_ip', 50)->nullable()->comment('最后登录IP');
$table->softDeletes();
$table->timestamps();
$table->index('department_id');
$table->index('status');
});
// 部门表
Schema::create('auth_departments', function (Blueprint $table) {
$table->id();
$table->string('name', 100)->comment('部门名称');
$table->unsignedBigInteger('parent_id')->default(0)->comment('父部门ID');
$table->string('leader', 50)->nullable()->comment('部门负责人');
$table->string('phone', 20)->nullable()->comment('联系电话');
$table->integer('sort')->default(0)->comment('排序');
$table->tinyInteger('status')->default(1)->comment('状态:1启用 0禁用');
$table->softDeletes();
$table->timestamps();
$table->index('parent_id');
$table->index('status');
});
// 角色表
Schema::create('auth_roles', function (Blueprint $table) {
$table->id();
$table->string('name', 50)->unique()->comment('角色名称');
$table->string('code', 50)->unique()->comment('角色编码');
$table->text('description')->nullable()->comment('角色描述');
$table->integer('sort')->default(0)->comment('排序');
$table->tinyInteger('status')->default(1)->comment('状态:1启用 0禁用');
$table->softDeletes();
$table->timestamps();
$table->index('status');
});
// 权限表
Schema::create('auth_permissions', function (Blueprint $table) {
$table->id();
$table->string('name', 100)->unique()->comment('权限名称');
$table->string('code', 100)->unique()->comment('权限编码');
$table->string('type', 20)->default('api')->comment('类型:api 菜单 按钮');
$table->unsignedBigInteger('parent_id')->default(0)->comment('父级ID');
$table->string('route')->nullable()->comment('路由');
$table->string('component')->nullable()->comment('前端组件路径');
$table->json('meta')->nullable()->comment('元数据(隐藏菜单、面包屑等)');
$table->integer('sort')->default(0)->comment('排序');
$table->tinyInteger('status')->default(1)->comment('状态:1启用 0禁用');
$table->softDeletes();
$table->timestamps();
$table->index('parent_id');
$table->index('type');
$table->index('status');
});
// 用户角色关联表
Schema::create('auth_user_role', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id')->comment('用户ID');
$table->unsignedBigInteger('role_id')->comment('角色ID');
$table->timestamps();
$table->unique(['user_id', 'role_id']);
$table->index('user_id');
$table->index('role_id');
});
// 角色权限关联表
Schema::create('auth_role_permission', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('role_id')->comment('角色ID');
$table->unsignedBigInteger('permission_id')->comment('权限ID');
$table->timestamps();
$table->unique(['role_id', 'permission_id']);
$table->index('role_id');
$table->index('permission_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('auth_role_permission');
Schema::dropIfExists('auth_user_role');
Schema::dropIfExists('auth_permissions');
Schema::dropIfExists('auth_roles');
Schema::dropIfExists('auth_departments');
Schema::dropIfExists('auth_users');
}
};
@@ -0,0 +1,145 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
// 系统配置表
Schema::create('system_configs', function (Blueprint $table) {
$table->comment('系统配置表');
$table->id();
$table->string('group')->comment('配置分组');
$table->string('key')->unique()->comment('配置键');
$table->string('name')->comment('配置名称');
$table->string('type')->default('string')->comment('字段类型:string, text, number, boolean, select, radio, checkbox, file, json');
$table->text('options')->nullable()->comment('选项配置(JSON格式)');
$table->text('value')->nullable()->comment('配置值');
$table->string('default_value')->nullable()->comment('默认值');
$table->text('description')->nullable()->comment('配置说明');
$table->string('validation')->nullable()->comment('验证规则');
$table->integer('sort')->default(0)->comment('排序');
$table->boolean('is_system')->default(false)->comment('是否系统配置');
$table->boolean('status')->default(true)->comment('状态');
$table->timestamps();
$table->softDeletes();
$table->index('group');
$table->index('key');
});
// 系统日志表
Schema::create('system_logs', function (Blueprint $table) {
$table->comment('系统日志表');
$table->id();
$table->unsignedBigInteger('user_id')->nullable()->comment('用户ID');
$table->string('username')->nullable()->comment('用户名');
$table->string('module')->comment('模块名称');
$table->string('action')->comment('操作类型');
$table->string('method')->comment('请求方法');
$table->string('url')->comment('请求URL');
$table->string('ip')->nullable()->comment('IP地址');
$table->text('user_agent')->nullable()->comment('用户代理');
$table->json('params')->nullable()->comment('请求参数');
$table->text('result')->nullable()->comment('操作结果');
$table->integer('status_code')->default(200)->comment('状态码');
$table->string('status')->default('success')->comment('状态');
$table->text('error_message')->nullable()->comment('错误信息');
$table->integer('execution_time')->nullable()->comment('执行时间(毫秒)');
$table->timestamps();
$table->index('user_id');
$table->index('module');
$table->index('action');
$table->index('created_at');
});
// 系统字典表
Schema::create('system_dictionaries', function (Blueprint $table) {
$table->comment('系统字典表');
$table->id();
$table->string('name')->comment('字典名称');
$table->string('code')->unique()->comment('字典编码');
$table->text('description')->nullable()->comment('字典描述');
$table->boolean('status')->default(true)->comment('状态');
$table->integer('sort')->default(0)->comment('排序');
$table->timestamps();
$table->softDeletes();
});
// 系统字典项表
Schema::create('system_dictionary_items', function (Blueprint $table) {
$table->comment('系统字典项表');
$table->id();
$table->unsignedBigInteger('dictionary_id')->comment('字典ID');
$table->string('label')->comment('标签名称');
$table->string('value')->comment('标签值');
$table->string('color')->nullable()->comment('颜色标识');
$table->text('description')->nullable()->comment('描述');
$table->boolean('is_default')->default(false)->comment('是否默认');
$table->boolean('status')->default(true)->comment('状态');
$table->integer('sort')->default(0)->comment('排序');
$table->timestamps();
$table->foreign('dictionary_id')->references('id')->on('system_dictionaries')->onDelete('cascade');
$table->index('dictionary_id');
});
// 系统任务表
Schema::create('system_tasks', function (Blueprint $table) {
$table->comment('系统任务表');
$table->id();
$table->string('name')->comment('任务名称');
$table->string('command')->comment('任务命令');
$table->string('description')->nullable()->comment('任务描述');
$table->string('type')->default('command')->comment('任务类型:command, job, closure');
$table->string('expression')->comment('Cron表达式');
$table->string('timezone')->default('Asia/Shanghai')->comment('时区');
$table->boolean('is_active')->default(true)->comment('是否启用');
$table->boolean('run_in_background')->default(true)->comment('是否后台运行');
$table->boolean('without_overlapping')->default(false)->comment('是否防止重叠');
$table->boolean('only_one')->default(false)->comment('是否只运行一个实例');
$table->timestamp('last_run_at')->nullable()->comment('最后运行时间');
$table->timestamp('next_run_at')->nullable()->comment('下次运行时间');
$table->text('last_output')->nullable()->comment('最后输出');
$table->integer('run_count')->default(0)->comment('运行次数');
$table->integer('failed_count')->default(0)->comment('失败次数');
$table->timestamps();
$table->softDeletes();
$table->index('is_active');
});
// 系统城市表
Schema::create('system_cities', function (Blueprint $table) {
$table->comment('系统城市表');
$table->id();
$table->unsignedBigInteger('parent_id')->default(0)->comment('父级ID');
$table->string('name')->comment('城市名称');
$table->string('code')->unique()->comment('城市编码');
$table->string('pinyin')->nullable()->comment('拼音');
$table->string('pinyin_short')->nullable()->comment('拼音首字母');
$table->integer('level')->default(1)->comment('级别:1=省,2=市,3=区/县');
$table->integer('sort')->default(0)->comment('排序');
$table->boolean('status')->default(true)->comment('状态');
$table->timestamps();
$table->index('parent_id');
$table->index('code');
$table->index('level');
});
}
public function down()
{
Schema::dropIfExists('system_dictionary_items');
Schema::dropIfExists('system_dictionaries');
Schema::dropIfExists('system_configs');
Schema::dropIfExists('system_logs');
Schema::dropIfExists('system_tasks');
Schema::dropIfExists('system_cities');
}
};
+570
View File
@@ -0,0 +1,570 @@
<?php
namespace Database\Seeders;
use App\Models\Auth\User;
use App\Models\Auth\Role;
use App\Models\Auth\Permission;
use App\Models\Auth\Department;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
class AuthSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// SQLite 不支持 SET FOREIGN_KEY_CHECKS,使用 PRAGMA
if (DB::getDriverName() === 'sqlite') {
DB::statement('PRAGMA foreign_keys = OFF;');
} else {
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
}
// 清空表数据
DB::table('auth_role_permission')->truncate();
DB::table('auth_user_role')->truncate();
Permission::truncate();
Role::truncate();
User::truncate();
Department::truncate();
$this->createPermissions();
$this->createRoles();
$this->createDepartments();
$this->createUsers();
$this->assignPermissionsToRole();
$this->assignRoleToUser();
$this->command->info('Auth module data seeded successfully!');
// 恢复外键约束
if (DB::getDriverName() === 'sqlite') {
DB::statement('PRAGMA foreign_keys = ON;');
} else {
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
}
/**
* 创建权限节点
*/
private function createPermissions(): void
{
$permissions = [
// 系统管理
[
'name' => '系统管理',
'code' => 'system',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system',
'component' => 'Layout',
'meta' => json_encode([
'icon' => 'Setting',
'hidden' => false,
'hiddenBreadcrumb' => false,
'affix' => null,
]),
'sort' => 1,
'status' => 1,
],
// 用户管理
[
'name' => '用户管理',
'code' => 'system.users',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/users',
'component' => 'system/users/index',
'meta' => json_encode([
'icon' => 'User',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 2,
'status' => 1,
],
[
'name' => '查看用户',
'code' => 'system.users.view',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.users.index',
'component' => null,
'meta' => null,
'sort' => 1,
'status' => 1,
],
[
'name' => '创建用户',
'code' => 'system.users.create',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.users.store',
'component' => null,
'meta' => null,
'sort' => 2,
'status' => 1,
],
[
'name' => '编辑用户',
'code' => 'system.users.update',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.users.update',
'component' => null,
'meta' => null,
'sort' => 3,
'status' => 1,
],
[
'name' => '删除用户',
'code' => 'system.users.delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.users.destroy',
'component' => null,
'meta' => null,
'sort' => 4,
'status' => 1,
],
[
'name' => '批量删除用户',
'code' => 'system.users.batch-delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.users.batch-delete',
'component' => null,
'meta' => null,
'sort' => 5,
'status' => 1,
],
[
'name' => '导出用户',
'code' => 'system.users.export',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.users.export',
'component' => null,
'meta' => null,
'sort' => 6,
'status' => 1,
],
[
'name' => '导入用户',
'code' => 'system.users.import',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.users.import',
'component' => null,
'meta' => null,
'sort' => 7,
'status' => 1,
],
// 角色管理
[
'name' => '角色管理',
'code' => 'system.roles',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/roles',
'component' => 'system/roles/index',
'meta' => json_encode([
'icon' => 'UserFilled',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 3,
'status' => 1,
],
[
'name' => '查看角色',
'code' => 'system.roles.view',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.roles.index',
'component' => null,
'meta' => null,
'sort' => 1,
'status' => 1,
],
[
'name' => '创建角色',
'code' => 'system.roles.create',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.roles.store',
'component' => null,
'meta' => null,
'sort' => 2,
'status' => 1,
],
[
'name' => '编辑角色',
'code' => 'system.roles.update',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.roles.update',
'component' => null,
'meta' => null,
'sort' => 3,
'status' => 1,
],
[
'name' => '删除角色',
'code' => 'system.roles.delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.roles.destroy',
'component' => null,
'meta' => null,
'sort' => 4,
'status' => 1,
],
[
'name' => '批量删除角色',
'code' => 'system.roles.batch-delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.roles.batch-delete',
'component' => null,
'meta' => null,
'sort' => 5,
'status' => 1,
],
[
'name' => '分配权限',
'code' => 'system.roles.assign-permissions',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.roles.assign-permissions',
'component' => null,
'meta' => null,
'sort' => 6,
'status' => 1,
],
// 权限管理
[
'name' => '权限管理',
'code' => 'system.permissions',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/permissions',
'component' => 'system/permissions/index',
'meta' => json_encode([
'icon' => 'Lock',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 4,
'status' => 1,
],
[
'name' => '查看权限',
'code' => 'system.permissions.view',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.permissions.index',
'component' => null,
'meta' => null,
'sort' => 1,
'status' => 1,
],
[
'name' => '创建权限',
'code' => 'system.permissions.create',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.permissions.store',
'component' => null,
'meta' => null,
'sort' => 2,
'status' => 1,
],
[
'name' => '编辑权限',
'code' => 'system.permissions.update',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.permissions.update',
'component' => null,
'meta' => null,
'sort' => 3,
'status' => 1,
],
[
'name' => '删除权限',
'code' => 'system.permissions.delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.permissions.destroy',
'component' => null,
'meta' => null,
'sort' => 4,
'status' => 1,
],
[
'name' => '批量删除权限',
'code' => 'system.permissions.batch-delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.permissions.batch-delete',
'component' => null,
'meta' => null,
'sort' => 5,
'status' => 1,
],
// 部门管理
[
'name' => '部门管理',
'code' => 'system.departments',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/departments',
'component' => 'system/departments/index',
'meta' => json_encode([
'icon' => 'OfficeBuilding',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 5,
'status' => 1,
],
[
'name' => '查看部门',
'code' => 'system.departments.view',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.departments.index',
'component' => null,
'meta' => null,
'sort' => 1,
'status' => 1,
],
[
'name' => '创建部门',
'code' => 'system.departments.create',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.departments.store',
'component' => null,
'meta' => null,
'sort' => 2,
'status' => 1,
],
[
'name' => '编辑部门',
'code' => 'system.departments.update',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.departments.update',
'component' => null,
'meta' => null,
'sort' => 3,
'status' => 1,
],
[
'name' => '删除部门',
'code' => 'system.departments.delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.departments.destroy',
'component' => null,
'meta' => null,
'sort' => 4,
'status' => 1,
],
[
'name' => '批量删除部门',
'code' => 'system.departments.batch-delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.departments.batch-delete',
'component' => null,
'meta' => null,
'sort' => 5,
'status' => 1,
],
];
foreach ($permissions as $permission) {
Permission::create($permission);
}
}
/**
* 创建角色
*/
private function createRoles(): void
{
Role::insert([
[
'name' => '超级管理员',
'code' => 'super_admin',
'description' => '拥有系统所有权限',
'sort' => 1,
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => '管理员',
'code' => 'admin',
'description' => '拥有系统管理权限',
'sort' => 2,
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => '普通用户',
'code' => 'user',
'description' => '普通用户角色',
'sort' => 3,
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
/**
* 创建部门
*/
private function createDepartments(): void
{
Department::insert([
[
'name' => '总公司',
'parent_id' => 0,
'leader' => '张三',
'phone' => '13800138000',
'sort' => 1,
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => '技术部',
'parent_id' => 0,
'leader' => '李四',
'phone' => '13800138001',
'sort' => 1,
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => '运营部',
'parent_id' => 0,
'leader' => '王五',
'phone' => '13800138002',
'sort' => 2,
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
],
[
'name' => '财务部',
'parent_id' => 0,
'leader' => '赵六',
'phone' => '13800138003',
'sort' => 3,
'status' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
// 更新parent_id为实际ID
$departments = Department::all();
$rootDept = $departments->where('name', '总公司')->first();
$techDept = $departments->where('name', '技术部')->first();
$opsDept = $departments->where('name', '运营部')->first();
$financeDept = $departments->where('name', '财务部')->first();
if ($rootDept) {
$techDept->update(['parent_id' => $rootDept->id]);
$opsDept->update(['parent_id' => $rootDept->id]);
$financeDept->update(['parent_id' => $rootDept->id]);
}
}
/**
* 创建用户
*/
private function createUsers(): void
{
$departments = Department::all();
$techDept = $departments->where('name', '技术部')->first();
User::insert([
[
'username' => 'admin',
'password' => Hash::make('admin888'),
'real_name' => '超级管理员',
'email' => 'admin@example.com',
'phone' => '13800138888',
'department_id' => $techDept ? $techDept->id : null,
'avatar' => null,
'status' => 1,
'last_login_at' => null,
'last_login_ip' => null,
'created_at' => now(),
'updated_at' => now(),
],
[
'username' => 'manager',
'password' => Hash::make('123456789'),
'real_name' => '部门经理',
'email' => 'manager@example.com',
'phone' => '13800138889',
'department_id' => $techDept ? $techDept->id : null,
'avatar' => null,
'status' => 1,
'last_login_at' => null,
'last_login_ip' => null,
'created_at' => now(),
'updated_at' => now(),
],
]);
}
/**
* 分配权限给角色
*/
private function assignPermissionsToRole(): void
{
$permissions = Permission::all();
$superAdminRole = Role::where('code', 'super_admin')->first();
if ($superAdminRole) {
$permissionIds = $permissions->pluck('id')->toArray();
$superAdminRole->permissions()->attach($permissionIds);
}
}
/**
* 分配角色给用户
*/
private function assignRoleToUser(): void
{
$superAdminRole = Role::where('code', 'super_admin')->first();
$adminRole = Role::where('code', 'admin')->first();
$adminUser = User::where('username', 'admin')->first();
$managerUser = User::where('username', 'manager')->first();
if ($adminUser && $superAdminRole) {
$adminUser->roles()->attach([$superAdminRole->id]);
}
if ($managerUser && $adminRole) {
$managerUser->roles()->attach([$adminRole->id]);
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// $this->call(UserSeeder::class);
$this->call(AuthSeeder::class);
$this->call(SystemSeeder::class);
}
}
+636
View File
@@ -0,0 +1,636 @@
<?php
namespace Database\Seeders;
use App\Models\System\Config;
use App\Models\System\Dictionary;
use App\Models\System\DictionaryItem;
use App\Models\Auth\Permission;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class SystemSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// SQLite 不支持 SET FOREIGN_KEY_CHECKS,使用 PRAGMA
if (DB::getDriverName() === 'sqlite') {
DB::statement('PRAGMA foreign_keys = OFF;');
} else {
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
}
// 清空表数据
DictionaryItem::truncate();
Dictionary::truncate();
Config::truncate();
$this->createSystemPermissions();
$this->createSystemDictionaries();
$this->createSystemConfigs();
$this->command->info('System module data seeded successfully!');
// 恢复外键约束
if (DB::getDriverName() === 'sqlite') {
DB::statement('PRAGMA foreign_keys = ON;');
} else {
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
}
/**
* 创建系统管理权限节点
*/
private function createSystemPermissions(): void
{
$permissions = [
// 系统配置
[
'name' => '系统配置',
'code' => 'system.config',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/config',
'component' => 'system/config/index',
'meta' => json_encode([
'icon' => 'SettingFilled',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 6,
'status' => 1,
],
[
'name' => '查看配置',
'code' => 'system.config.view',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.config.index',
'component' => null,
'meta' => null,
'sort' => 1,
'status' => 1,
],
[
'name' => '创建配置',
'code' => 'system.config.create',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.config.store',
'component' => null,
'meta' => null,
'sort' => 2,
'status' => 1,
],
[
'name' => '编辑配置',
'code' => 'system.config.update',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.config.update',
'component' => null,
'meta' => null,
'sort' => 3,
'status' => 1,
],
[
'name' => '删除配置',
'code' => 'system.config.delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.config.destroy',
'component' => null,
'meta' => null,
'sort' => 4,
'status' => 1,
],
[
'name' => '批量删除配置',
'code' => 'system.config.batch-delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.config.batch-delete',
'component' => null,
'meta' => null,
'sort' => 5,
'status' => 1,
],
// 系统日志
[
'name' => '系统日志',
'code' => 'system.logs',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/logs',
'component' => 'system/logs/index',
'meta' => json_encode([
'icon' => 'DocumentCopy',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 7,
'status' => 1,
],
[
'name' => '查看日志',
'code' => 'system.logs.view',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.logs.index',
'component' => null,
'meta' => null,
'sort' => 1,
'status' => 1,
],
[
'name' => '删除日志',
'code' => 'system.logs.delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.logs.destroy',
'component' => null,
'meta' => null,
'sort' => 2,
'status' => 1,
],
[
'name' => '批量删除日志',
'code' => 'system.logs.batch-delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.logs.batch-delete',
'component' => null,
'meta' => null,
'sort' => 3,
'status' => 1,
],
[
'name' => '导出日志',
'code' => 'system.logs.export',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.logs.export',
'component' => null,
'meta' => null,
'sort' => 4,
'status' => 1,
],
// 数据字典
[
'name' => '数据字典',
'code' => 'system.dictionaries',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/dictionaries',
'component' => 'system/dictionaries/index',
'meta' => json_encode([
'icon' => 'Notebook',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 8,
'status' => 1,
],
[
'name' => '查看字典',
'code' => 'system.dictionaries.view',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.dictionaries.index',
'component' => null,
'meta' => null,
'sort' => 1,
'status' => 1,
],
[
'name' => '创建字典',
'code' => 'system.dictionaries.create',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.dictionaries.store',
'component' => null,
'meta' => null,
'sort' => 2,
'status' => 1,
],
[
'name' => '编辑字典',
'code' => 'system.dictionaries.update',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.dictionaries.update',
'component' => null,
'meta' => null,
'sort' => 3,
'status' => 1,
],
[
'name' => '删除字典',
'code' => 'system.dictionaries.delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.dictionaries.destroy',
'component' => null,
'meta' => null,
'sort' => 4,
'status' => 1,
],
[
'name' => '批量删除字典',
'code' => 'system.dictionaries.batch-delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.dictionaries.batch-delete',
'component' => null,
'meta' => null,
'sort' => 5,
'status' => 1,
],
// 定时任务
[
'name' => '定时任务',
'code' => 'system.tasks',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/tasks',
'component' => 'system/tasks/index',
'meta' => json_encode([
'icon' => 'Timer',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 9,
'status' => 1,
],
[
'name' => '查看任务',
'code' => 'system.tasks.view',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.tasks.index',
'component' => null,
'meta' => null,
'sort' => 1,
'status' => 1,
],
[
'name' => '创建任务',
'code' => 'system.tasks.create',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.tasks.store',
'component' => null,
'meta' => null,
'sort' => 2,
'status' => 1,
],
[
'name' => '编辑任务',
'code' => 'system.tasks.update',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.tasks.update',
'component' => null,
'meta' => null,
'sort' => 3,
'status' => 1,
],
[
'name' => '删除任务',
'code' => 'system.tasks.delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.tasks.destroy',
'component' => null,
'meta' => null,
'sort' => 4,
'status' => 1,
],
[
'name' => '批量删除任务',
'code' => 'system.tasks.batch-delete',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.tasks.batch-delete',
'component' => null,
'meta' => null,
'sort' => 5,
'status' => 1,
],
[
'name' => '执行任务',
'code' => 'system.tasks.execute',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.tasks.execute',
'component' => null,
'meta' => null,
'sort' => 6,
'status' => 1,
],
[
'name' => '启用任务',
'code' => 'system.tasks.enable',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.tasks.enable',
'component' => null,
'meta' => null,
'sort' => 7,
'status' => 1,
],
[
'name' => '禁用任务',
'code' => 'system.tasks.disable',
'type' => 'button',
'parent_id' => 0,
'route' => 'admin.tasks.disable',
'component' => null,
'meta' => null,
'sort' => 8,
'status' => 1,
],
// 个人中心
[
'name' => '个人中心',
'code' => 'system.profile',
'type' => 'menu',
'parent_id' => 0,
'route' => '/system/profile',
'component' => 'system/profile/index',
'meta' => json_encode([
'icon' => 'UserFilled',
'hidden' => false,
'hiddenBreadcrumb' => false,
]),
'sort' => 10,
'status' => 1,
],
];
foreach ($permissions as $permission) {
Permission::create($permission);
}
}
/**
* 创建系统字典
*/
private function createSystemDictionaries(): void
{
// 创建字典类型
$dictionaries = [
[
'name' => '用户状态',
'code' => 'user_status',
'description' => '用户账号状态',
'sort' => 1,
'status' => 1,
],
[
'name' => '性别',
'code' => 'gender',
'description' => '用户性别',
'sort' => 2,
'status' => 1,
],
[
'name' => '角色状态',
'code' => 'role_status',
'description' => '角色启用状态',
'sort' => 3,
'status' => 1,
],
[
'name' => '字典状态',
'code' => 'dictionary_status',
'description' => '数据字典状态',
'sort' => 4,
'status' => 1,
],
[
'name' => '任务状态',
'code' => 'task_status',
'description' => '定时任务状态',
'sort' => 5,
'status' => 1,
],
[
'name' => '日志类型',
'code' => 'log_type',
'description' => '系统日志类型',
'sort' => 6,
'status' => 1,
],
[
'name' => '是否',
'code' => 'yes_no',
'description' => '是否选项',
'sort' => 7,
'status' => 1,
],
];
foreach ($dictionaries as $dictionary) {
$dict = Dictionary::create($dictionary);
$this->createDictionaryItems($dict);
}
}
/**
* 创建字典项
*/
private function createDictionaryItems(Dictionary $dictionary): void
{
$items = [];
switch ($dictionary->code) {
case 'user_status':
$items = [
['label' => '正常', 'value' => 1, 'sort' => 1, 'status' => 1],
['label' => '禁用', 'value' => 0, 'sort' => 2, 'status' => 1],
];
break;
case 'gender':
$items = [
['label' => '男', 'value' => 1, 'sort' => 1, 'status' => 1],
['label' => '女', 'value' => 2, 'sort' => 2, 'status' => 1],
['label' => '保密', 'value' => 0, 'sort' => 3, 'status' => 1],
];
break;
case 'role_status':
$items = [
['label' => '启用', 'value' => 1, 'sort' => 1, 'status' => 1],
['label' => '禁用', 'value' => 0, 'sort' => 2, 'status' => 1],
];
break;
case 'dictionary_status':
$items = [
['label' => '启用', 'value' => 1, 'sort' => 1, 'status' => 1],
['label' => '禁用', 'value' => 0, 'sort' => 2, 'status' => 1],
];
break;
case 'task_status':
$items = [
['label' => '待执行', 'value' => 0, 'sort' => 1, 'status' => 1],
['label' => '执行中', 'value' => 1, 'sort' => 2, 'status' => 1],
['label' => '已完成', 'value' => 2, 'sort' => 3, 'status' => 1],
['label' => '失败', 'value' => 3, 'sort' => 4, 'status' => 1],
];
break;
case 'log_type':
$items = [
['label' => '登录日志', 'value' => 'login', 'sort' => 1, 'status' => 1],
['label' => '操作日志', 'value' => 'operation', 'sort' => 2, 'status' => 1],
['label' => '异常日志', 'value' => 'error', 'sort' => 3, 'status' => 1],
['label' => '系统日志', 'value' => 'system', 'sort' => 4, 'status' => 1],
];
break;
case 'yes_no':
$items = [
['label' => '是', 'value' => 1, 'sort' => 1, 'status' => 1],
['label' => '否', 'value' => 0, 'sort' => 2, 'status' => 1],
];
break;
}
foreach ($items as $item) {
$dictionary->items()->create($item);
}
}
/**
* 创建系统配置
*/
private function createSystemConfigs(): void
{
$configs = [
[
'group' => 'basic',
'key' => 'site_name',
'name' => '网站名称',
'value' => 'Laravel Swoole 管理系统',
'default_value' => 'Laravel Swoole 管理系统',
'type' => 'input',
'description' => '系统显示的网站名称',
'sort' => 1,
'is_system' => true,
'status' => true,
],
[
'group' => 'basic',
'key' => 'site_logo',
'name' => '网站Logo',
'value' => '',
'default_value' => '',
'type' => 'image',
'description' => '系统Logo图片地址',
'sort' => 2,
'is_system' => true,
'status' => true,
],
[
'group' => 'basic',
'key' => 'site_copyright',
'name' => '版权信息',
'value' => '© 2024 Laravel Swoole Admin',
'default_value' => '© 2024 Laravel Swoole Admin',
'type' => 'input',
'description' => '网站底部版权信息',
'sort' => 3,
'is_system' => true,
'status' => true,
],
[
'group' => 'basic',
'key' => 'site_icp',
'name' => '备案号',
'value' => '',
'default_value' => '',
'type' => 'input',
'description' => '网站备案号',
'sort' => 4,
'is_system' => true,
'status' => true,
],
[
'group' => 'upload',
'key' => 'upload_max_size',
'name' => '上传最大限制',
'value' => '10',
'default_value' => '10',
'type' => 'number',
'description' => '文件上传最大限制(MB',
'sort' => 1,
'is_system' => true,
'status' => true,
],
[
'group' => 'upload',
'key' => 'upload_allowed_types',
'name' => '允许上传类型',
'value' => 'jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx',
'default_value' => 'jpg,jpeg,png,gif,pdf,doc,docx,xls,xlsx',
'type' => 'input',
'description' => '允许上传的文件扩展名',
'sort' => 2,
'is_system' => true,
'status' => true,
],
[
'group' => 'system',
'key' => 'user_default_avatar',
'name' => '默认头像',
'value' => '',
'default_value' => '',
'type' => 'image',
'description' => '用户默认头像地址',
'sort' => 1,
'is_system' => true,
'status' => true,
],
[
'group' => 'system',
'key' => 'system_timezone',
'name' => '系统时区',
'value' => 'Asia/Shanghai',
'default_value' => 'Asia/Shanghai',
'type' => 'input',
'description' => '系统默认时区',
'sort' => 2,
'is_system' => true,
'status' => true,
],
[
'group' => 'system',
'key' => 'system_language',
'name' => '系统语言',
'value' => 'zh-CN',
'default_value' => 'zh-CN',
'type' => 'input',
'description' => '系统默认语言',
'sort' => 3,
'is_system' => true,
'status' => true,
],
];
foreach ($configs as $config) {
Config::create($config);
}
}
}
+337
View File
@@ -0,0 +1,337 @@
# 日志模块实现总结
## 实现概述
本次优化完善了后端日志模块,实现了自动化的请求日志记录功能,所有后台管理 API 请求都会被自动记录到数据库中。
## 实现内容
### 1. 新增文件
#### 中间件
- **app/Http/Middleware/LogRequestMiddleware.php**
- 自动拦截所有经过的请求
- 记录请求和响应信息
- 计算请求执行时间
- 提取用户信息和操作详情
- 自动过滤敏感参数(密码、token等)
- 获取客户端真实 IP(支持代理)
#### 请求验证
- **app/Http/Requests/LogRequest.php**
- 统一的请求参数验证
- 支持列表查询、批量删除、清理等操作的参数验证
- 自定义错误消息
- 自动设置默认值
#### 文档
- **docs/README_LOG.md**
- 完整的模块文档
- API 接口说明
- 数据库表结构
- 使用示例
- 前端集成代码
- 常见问题解答
### 2. 修改文件
#### 控制器
- **app/Http/Controllers/System/Admin/Log.php**
- 添加 `export` 方法:支持导出日志数据为 Excel
- 使用 `LogRequest` 进行参数验证
- 优化响应格式
#### 服务层
- **app/Services/System/LogService.php**
- 添加 `getListQuery` 方法:提供查询构建器(用于导出等场景)
- 新增 `buildQuery` 方法:统一的查询构建逻辑
- 代码重构,减少重复代码
#### 路由配置
- **routes/admin.php**
- 添加 `POST /admin/logs/export` 导出路由
- 在所有需要认证的路由组中应用 `log.request` 中间件
#### 中间件配置
- **bootstrap/app.php**
- 注册 `log.request` 中间件别名
- 创建 `admin.log` 中间件组
## 功能特性
### 自动日志记录
- ✅ 所有后台管理 API 请求自动记录
- ✅ 记录用户信息(ID、用户名)
- ✅ 记录请求信息(方法、URL、参数)
- ✅ 记录响应信息(状态码、执行时间)
- ✅ 记录客户端信息(IP、User-Agent
- ✅ 错误请求记录详细错误信息
### 敏感信息保护
- ✅ 自动过滤密码字段
- ✅ 自动过滤 token 字段
- ✅ 自动过滤 secret 字段
- ✅ 自动过滤 key 字段
### 日志管理功能
- ✅ 多维度查询(用户、模块、操作、状态、时间、IP)
- ✅ 分页查询
- ✅ 日志详情查看
- ✅ 日志统计(总数、成功数、失败数)
- ✅ 单条删除
- ✅ 批量删除
- ✅ 定期清理(按天数)
- ✅ 导出为 Excel
### 性能优化
- ✅ 日志记录在请求处理后执行
- ✅ 不影响业务响应速度
- ✅ 异常处理,记录失败不影响业务
- ✅ 支持分页查询,避免一次性加载过多数据
## API 接口列表
| 接口 | 方法 | 说明 |
|------|------|------|
| `/admin/logs` | GET | 获取日志列表 |
| `/admin/logs/{id}` | GET | 获取日志详情 |
| `/admin/logs/statistics` | GET | 获取日志统计 |
| `/admin/logs/export` | POST | 导出日志(Excel |
| `/admin/logs/{id}` | DELETE | 删除单条日志 |
| `/admin/logs/batch-delete` | POST | 批量删除日志 |
| `/admin/logs/clear` | POST | 清理历史日志 |
## 数据库表结构
### system_logs 表
已存在的表结构,包含以下字段:
- id: 主键
- user_id: 用户 ID
- username: 用户名
- module: 模块名称
- action: 操作名称
- method: 请求方法
- url: 请求 URL
- ip: 客户端 IP
- user_agent: 用户代理
- params: 请求参数(JSON
- result: 响应结果
- status_code: HTTP 状态码
- status: 状态(success/error
- error_message: 错误信息
- execution_time: 执行时间(毫秒)
- created_at: 创建时间
- updated_at: 更新时间
## 中间件应用范围
### 已应用的路由
- ✅ 所有 `/admin/*` 路由(除登录接口)
- ✅ 认证相关(登出、刷新、个人信息、修改密码)
- ✅ 用户管理
- ✅ 角色管理
- ✅ 权限管理
- ✅ 部门管理
- ✅ 在线用户管理
- ✅ 系统配置管理
- ✅ 数据字典管理
- ✅ 任务管理
- ✅ 城市数据管理
- ✅ 文件上传管理
### 未应用的路由
- ❌ 登录接口(`POST /admin/auth/login`
- ❌ 健康检查接口(`GET /up`
## 使用示例
### 后端使用
中间件会自动记录所有请求,无需手动调用:
```php
// 任何经过 log.request 中间件的请求都会被自动记录
Route::middleware(['auth.check:admin', 'log.request'])->group(function () {
Route::apiResource('users', UserController::class);
// 其他路由...
});
```
### 前端调用示例
```javascript
// 获取日志列表
const response = await request.get('/admin/logs', {
params: {
username: 'admin',
module: 'users',
status: 'success',
page: 1,
page_size: 20
}
})
// 导出日志
await request.post('/admin/logs/export', {
username: 'admin',
status: 'error'
}, {
responseType: 'blob'
})
// 批量删除
await request.post('/admin/logs/batch-delete', {
ids: [1, 2, 3, 4, 5]
})
// 清理历史日志
await request.post('/admin/logs/clear', {
days: 30
})
```
## 日志记录示例
### 成功请求日志
```json
{
"id": 1,
"user_id": 1,
"username": "admin",
"module": "users",
"action": "创建 users",
"method": "POST",
"url": "http://example.com/admin/users",
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"params": {
"name": "test",
"email": "test@example.com",
"password": "******"
},
"result": null,
"status_code": 200,
"status": "success",
"error_message": null,
"execution_time": 125,
"created_at": "2024-01-01 12:00:00"
}
```
### 失败请求日志
```json
{
"id": 2,
"user_id": 1,
"username": "admin",
"module": "users",
"action": "删除 users",
"method": "DELETE",
"url": "http://example.com/admin/users/999",
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"params": {},
"result": "{\"code\":404,\"message\":\"用户不存在\"}",
"status_code": 404,
"status": "error",
"error_message": "用户不存在",
"execution_time": 45,
"created_at": "2024-01-01 12:01:00"
}
```
## 注意事项
### 1. 性能考虑
- 日志记录在请求处理后执行,不影响响应速度
- 大量日志会增加数据库写入压力
- 建议定期清理历史日志
### 2. 数据安全
- 敏感信息已自动过滤
- 日志数据应妥善保管
- 建议定期备份重要日志
### 3. 权限控制
- 日志管理接口需要相应权限
- 建议只允许管理员查看和操作日志
### 4. 数据库优化
- 确保查询字段有索引
- 使用分页查询避免加载过多数据
- 定期清理历史日志
## 后续优化建议
### 1. 异步队列
考虑使用 Laravel 队列异步处理日志记录,进一步减少对响应时间的影响。
### 2. 日志归档
实现日志归档功能,将历史日志移动到归档表或文件存储。
### 3. 日志分析
集成日志分析工具,提供可视化仪表盘和趋势分析。
### 4. 定时清理
配置 Laravel 任务调度器,自动清理指定天数前的日志:
```php
// app/Console/Kernel.php
$schedule->call(function () {
app(LogService::class)->clearLogs(90);
})->dailyAt('02:00');
```
### 5. 日志级别
增加日志级别(info、warning、error、critical),便于分类管理。
## 测试建议
### 功能测试
1. 测试各种请求是否被正确记录
2. 测试敏感信息是否被正确过滤
3. 测试日志查询和筛选功能
4. 测试日志导出功能
5. 测试批量删除和清理功能
### 性能测试
1. 测试日志记录对响应时间的影响
2. 测试大量日志数据的查询性能
3. 测试并发写入的性能
### 边界测试
1. 测试异常情况下的日志记录
2. 测试超长参数的处理
3. 测试特殊字符的处理
## 文件清单
### 新增文件
```
app/Http/Middleware/LogRequestMiddleware.php
app/Http/Requests/LogRequest.php
docs/README_LOG.md
docs/LOG_IMPLEMENTATION_SUMMARY.md
```
### 修改文件
```
app/Http/Controllers/System/Admin/Log.php
app/Services/System/LogService.php
routes/admin.php
bootstrap/app.php
```
## 总结
本次日志模块优化完善实现了:
- ✅ 全自动化的请求日志记录
- ✅ 完善的日志管理功能
- ✅ 敏感信息保护
- ✅ 多维度查询和筛选
- ✅ 数据导出功能
- ✅ 批量操作支持
- ✅ 完整的文档说明
日志模块现已完全集成到项目中,所有后台管理 API 请求都会被自动记录,管理员可以通过日志管理功能进行系统监控、审计和问题排查。
+638
View File
@@ -0,0 +1,638 @@
# Auth 基础模块文档
## 概述
Auth 基础模块提供了完整的后台管理系统的认证授权功能,包括用户管理、角色管理、权限管理、部门管理等功能。
## 控制器结构
```
app/Http/Controllers/Auth/
└── Admin/
├── Auth.php # 认证控制器
├── User.php # 用户管理控制器
├── Role.php # 角色管理控制器
├── Permission.php # 权限管理控制器
└── Department.php # 部门管理控制器
```
**命名空间**: `App\Http\Controllers\Auth\Admin`
**路由前缀**: `/admin`
**中间件**: `auth.check:admin` (后台管理认证)
## 技术栈
- PHP 8.1+
- Laravel 11
- JWT 认证 (tymon/jwt-auth)
- Redis 缓存
- Laravel Excel (maatwebsite/excel)
## 数据库表结构
### auth_users (用户表)
- `id`: 主键
- `username`: 用户名(唯一)
- `password`: 密码(加密)
- `real_name`: 真实姓名
- `email`: 邮箱
- `phone`: 手机号
- `avatar`: 头像
- `department_id`: 部门ID
- `status`: 状态(0:禁用, 1:启用)
- `last_login_at`: 最后登录时间
- `last_login_ip`: 最后登录IP
- `last_active_at`: 最后活跃时间
- `created_at`, `updated_at`: 时间戳
### auth_roles (角色表)
- `id`: 主键
- `name`: 角色名称
- `code`: 角色编码(唯一)
- `description`: 角色描述
- `sort`: 排序
- `status`: 状态
- `created_at`, `updated_at`: 时间戳
### auth_permissions (权限表)
- `id`: 主键
- `name`: 权限名称
- `code`: 权限编码(唯一,格式:模块.功能.操作)
- `type`: 类型(menu:菜单, api:接口, button:按钮)
- `route`: 路由
- `component`: 组件路径
- `icon`: 图标
- `parent_id`: 父级ID
- `meta`: 元数据(JSON格式)
- `sort`: 排序
- `status`: 状态
- `created_at`, `updated_at`: 时间戳
### auth_role_permission (角色权限关联表)
- `id`: 主键
- `role_id`: 角色ID
- `permission_id`: 权限ID
### auth_user_role (用户角色关联表)
- `id`: 主键
- `user_id`: 用户ID
- `role_id`: 角色ID
### auth_departments (部门表)
- `id`: 主键
- `name`: 部门名称
- `parent_id`: 父级ID
- `leader`: 负责人
- `phone`: 联系电话
- `sort`: 排序
- `status`: 状态
- `created_at`, `updated_at`: 时间戳
## API 接口文档
### 中间件说明
### AuthCheckMiddleware
这是一个通用的认证中间件,支持通过参数指定不同的路由守卫和权限验证。
#### 基本用法
**1. 只进行登录认证(不检查权限)**
```php
// 使用 admin 守卫
Route::middleware(['auth.check:admin'])->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
// 使用 api 守卫
Route::middleware(['auth.check:api'])->group(function () {
Route::get('/profile', [UserController::class, 'profile']);
});
```
**2. 登录认证 + 权限验证**
```php
// 验证用户是否有特定权限
Route::middleware(['auth.check:admin,system.user.list'])->group(function () {
Route::get('/users', [UserController::class, 'index']);
});
// 验证多个权限
Route::middleware(['auth.check:admin,system.user.create'])->group(function () {
Route::post('/users', [UserController::class, 'store']);
});
```
**3. 单个路由应用中间件**
```php
Route::middleware('auth.check:admin,system.user.delete')->delete('/users/{id}', [UserController::class, 'destroy']);
```
#### 参数说明
- `guard`: 认证守卫名称(必需),例如:`admin``api`
- `permission`: 权限编码(可选),如果指定则会验证用户是否有该权限
#### 功能特性
- ✅ 支持多个路由守卫(admin、api等)
- ✅ 支持登录状态检查
- ✅ 支持用户状态检查(禁用用户无法访问)
- ✅ 支持权限验证
- ✅ 自动将用户信息注入到请求中($request->auth_user
- ✅ 自动更新用户最后活跃时间
#### 认证相关
#### 登录
- **接口**: `POST /admin/auth/login`
- **参数**:
```json
{
"username": "admin",
"password": "123456"
}
```
- **返回**:
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"user": {...},
"menu": {...},
"permissions": {...}
}
}
```
#### 登出
- **接口**: `POST /admin/auth/logout`
- **Header**: `Authorization: Bearer {token}`
#### 刷新Token
- **接口**: `POST /admin/auth/refresh`
- **Header**: `Authorization: Bearer {token}`
#### 获取当前用户信息
- **接口**: `GET /admin/auth/me`
- **Header**: `Authorization: Bearer {token}`
#### 修改密码
- **接口**: `POST /admin/auth/change-password`
- **参数**:
```json
{
"old_password": "123456",
"new_password": "654321"
}
```
### 用户管理
#### 获取用户列表
- **接口**: `GET /admin/users`
- **参数**:
- `page`: 页码(默认1
- `page_size`: 每页数量(默认20
- `keyword`: 搜索关键词(用户名/真实姓名/邮箱)
- `status`: 状态(0:禁用, 1:启用)
- `department_id`: 部门ID
- `role_id`: 角色ID
- `order_by`: 排序字段(默认id
- `order_direction`: 排序方向(asc/desc,默认asc
#### 获取用户详情
- **接口**: `GET /admin/users/{id}`
#### 创建用户
- **接口**: `POST /admin/users`
- **参数**:
```json
{
"username": "test001",
"password": "123456",
"real_name": "测试用户",
"email": "test@example.com",
"phone": "13800138000",
"department_id": 1,
"role_ids": [1, 2],
"status": 1
}
```
#### 更新用户
- **接口**: `PUT /admin/users/{id}`
- **参数**: 同创建用户(所有字段都是可选的)
#### 删除用户
- **接口**: `DELETE /admin/users/{id}`
#### 批量删除用户
- **接口**: `POST /admin/users/batch-delete`
- **参数**:
```json
{
"ids": [1, 2, 3]
}
```
#### 批量更新用户状态
- **接口**: `POST /admin/users/batch-status`
- **参数**:
```json
{
"ids": [1, 2, 3],
"status": 1
}
```
#### 批量分配部门
- **接口**: `POST /admin/users/batch-department`
- **参数**:
```json
{
"ids": [1, 2, 3],
"department_id": 1
}
```
#### 批量分配角色
- **接口**: `POST /admin/users/batch-roles`
- **参数**:
```json
{
"ids": [1, 2, 3],
"role_ids": [1, 2]
}
```
#### 导出用户
- **接口**: `POST /admin/users/export`
- **参数**:
```json
{
"ids": [1, 2, 3] // 可选,不传则导出所有用户
}
```
#### 导入用户
- **接口**: `POST /admin/users/import`
- **参数**: `file` (multipart/form-data, xlsx/xls文件)
- **返回**:
```json
{
"code": 200,
"message": "导入完成,成功 10 条,失败 0 条",
"data": {
"success_count": 10,
"error_count": 0,
"errors": []
}
}
```
#### 下载用户导入模板
- **接口**: `GET /admin/users/download-template`
### 在线用户管理
#### 获取在线用户数量
- **接口**: `GET /admin/online-users/count`
#### 获取在线用户列表
- **接口**: `GET /admin/online-users`
- **参数**:
- `limit`: 数量限制(默认100
#### 获取用户的所有会话
- **接口**: `GET /admin/online-users/{userId}/sessions`
#### 强制用户下线(单个会话)
- **接口**: `POST /admin/online-users/{userId}/offline`
- **参数**:
```json
{
"token": "用户token"
}
```
#### 强制用户所有设备下线
- **接口**: `POST /admin/online-users/{userId}/offline-all`
### 角色管理
#### 获取角色列表
- **接口**: `GET /admin/roles`
- **参数**:
- `page`, `page_size`, `keyword`, `status`, `order_by`, `order_direction`
#### 获取所有角色(不分页)
- **接口**: `GET /admin/roles/all`
#### 获取角色详情
- **接口**: `GET /admin/roles/{id}`
#### 创建角色
- **接口**: `POST /admin/roles`
- **参数**:
```json
{
"name": "编辑",
"code": "editor",
"description": "编辑角色",
"sort": 1,
"status": 1,
"permission_ids": [1, 2, 3]
}
```
#### 更新角色
- **接口**: `PUT /admin/roles/{id}`
#### 删除角色
- **接口**: `DELETE /admin/roles/{id}`
#### 批量删除角色
- **接口**: `POST /admin/roles/batch-delete`
#### 批量更新角色状态
- **接口**: `POST /admin/roles/batch-status`
#### 分配权限
- **接口**: `POST /admin/roles/{id}/permissions`
- **参数**:
```json
{
"permission_ids": [1, 2, 3]
}
```
#### 获取角色的权限列表
- **接口**: `GET /admin/roles/{id}/permissions`
#### 复制角色
- **接口**: `POST /admin/roles/{id}/copy`
- **参数**:
```json
{
"name": "新角色名称",
"code": "new_role_code",
"description": "新角色描述",
"status": 1
}
```
#### 批量复制角色
- **接口**: `POST /admin/roles/batch-copy`
- **参数**:
```json
{
"ids": [1, 2, 3],
"name": "新角色名称(可选)",
"code": "new_code(可选)"
}
```
### 权限管理
#### 获取权限列表
- **接口**: `GET /admin/permissions`
- **参数**:
- `page`, `page_size`, `keyword`, `type`, `status`, `order_by`, `order_direction`
#### 获取权限树
- **接口**: `GET /admin/permissions/tree`
#### 获取菜单树
- **接口**: `GET /admin/permissions/menu`
- **返回**: 当前登录用户的菜单树
#### 获取权限详情
- **接口**: `GET /admin/permissions/{id}`
#### 创建权限
- **接口**: `POST /admin/permissions`
- **参数**:
```json
{
"name": "用户列表",
"code": "system.user.list",
"type": "api",
"route": "/admin/users",
"component": "",
"icon": "user",
"parent_id": 0,
"sort": 1,
"status": 1,
"meta": {
"title": "用户列表",
"keepAlive": true
}
}
```
#### 更新权限
- **接口**: `PUT /admin/permissions/{id}`
#### 删除权限
- **接口**: `DELETE /admin/permissions/{id}`
#### 批量删除权限
- **接口**: `POST /admin/permissions/batch-delete`
#### 批量更新权限状态
- **接口**: `POST /admin/permissions/batch-status`
### 部门管理
#### 获取部门列表
- **接口**: `GET /admin/departments`
- **参数**:
- `page`, `page_size`, `keyword`, `status`, `order_by`, `order_direction`
#### 获取部门树
- **接口**: `GET /admin/departments/tree`
#### 获取所有部门(不分页)
- **接口**: `GET /admin/departments/all`
#### 获取部门详情
- **接口**: `GET /admin/departments/{id}`
#### 创建部门
- **接口**: `POST /admin/departments`
- **参数**:
```json
{
"name": "技术部",
"parent_id": 0,
"leader": "张三",
"phone": "13800138000",
"sort": 1,
"status": 1
}
```
#### 更新部门
- **接口**: `PUT /admin/departments/{id}`
#### 删除部门
- **接口**: `DELETE /admin/departments/{id}`
#### 批量删除部门
- **接口**: `POST /admin/departments/batch-delete`
#### 批量更新部门状态
- **接口**: `POST /admin/departments/batch-status`
#### 导出部门
- **接口**: `POST /admin/departments/export`
#### 导入部门
- **接口**: `POST /admin/departments/import`
- **参数**: `file` (multipart/form-data, xlsx/xls文件)
#### 下载部门导入模板
- **接口**: `GET /admin/departments/download-template`
## 权限设计
### 权限编码规则
权限编码采用 `模块.功能.操作` 的格式,例如:
- `system.user.list` - 系统管理-用户-列表
- `system.user.create` - 系统管理-用户-创建
- `system.user.update` - 系统管理-用户-更新
- `system.user.delete` - 系统管理-用户-删除
### 权限类型
- **menu**: 菜单类型,用于前端路由配置
- **api**: API接口类型,用于后端权限验证
- **button**: 按钮类型,用于前端按钮权限控制
## 缓存机制
### 用户在线状态缓存
- **缓存键**: `user_online:{userId}:{tokenHash}`
- **过期时间**: 5分钟
- **用途**: 跟踪用户在线状态、最后活跃时间、登录设备信息
### 权限缓存
- **用户权限列表**: `permission:user:{userId}:permissions` (60分钟)
- **用户权限编码**: `permission:user:{userId}:permission_codes` (60分钟)
- **用户菜单树**: `permission:user:{userId}:menu_tree` (60分钟)
- **角色权限**: `permission:role:{roleId}:permissions` (60分钟)
### 缓存更新时机
- 用户登录/刷新token时,更新在线状态
- 用户角色变化时,清除用户权限缓存
- 角色权限变化时,清除角色和所有关联用户的权限缓存
- 权限本身变化时,清除所有权限缓存
## 导入导出
### 用户导入模板字段
- 用户名*(必填)
- 密码*(必填)
- 真实姓名*(必填)
- 邮箱
- 手机号
- 部门名称
- 角色名称(多个用逗号分隔)
- 备注
### 部门导入模板字段
- 部门名称*(必填)
- 上级部门名称
- 负责人
- 联系电话
- 排序
- 备注
### 导出功能
- 支持导出全部数据
- 支持按选中的ID导出
- 导出文件为Excel格式
- 下载后自动删除临时文件
## 初始化数据
运行数据库迁移和填充命令:
```bash
# 执行迁移
php artisan migrate
# 填充初始数据
php artisan db:seed --class=AuthSeeder
```
初始数据包括:
- 超级管理员账号(username: admin, password: 123456
- 基础角色(超级管理员、管理员)
- 完整的权限菜单
- 默认部门
## 注意事项
1. **Swoole环境注意事项**:
- 避免使用静态变量存储状态
- 避免使用全局变量
- 正确管理数据库连接
- 使用Redis缓存时注意连接池配置
2. **安全注意事项**:
- 所有密码必须加密存储
- 使用JWT进行身份认证
- 敏感操作需要记录日志
- 定期清理过期的导出文件
3. **性能优化**:
- 使用Redis缓存权限数据
- 大批量操作使用队列处理
- 分页查询避免一次性加载过多数据
4. **权限验证**:
- 在中间件中验证用户权限
- 前端根据权限控制按钮显示
- 使用权限缓存减少数据库查询
## 扩展建议
1. **日志审计**: 添加操作日志记录
2. **数据权限**: 实现基于部门的数据权限控制
3. **多租户**: 支持多租户场景
4. **SSO登录**: 支持第三方单点登录
5. **动态权限**: 支持运行时动态添加权限
## 常见问题
### Q: 如何添加新的权限模块?
A: 在Seed文件中添加新的权限数据,或者通过API创建新的权限节点。
### Q: 导入大量数据时超时怎么办?
A: 使用队列处理导入任务,分批导入数据。
### Q: 如何清理权限缓存?
A: 调用 `PermissionCacheService::clearAllPermissionCache()` 方法。
### Q: Swoole环境下如何热重载?
A: 运行 `php bin/laravels reload` 命令进行平滑重启。
### Q: Excel文件支持哪些格式?
A: 支持.xlsx和.xls格式文件。
+608
View File
@@ -0,0 +1,608 @@
# 系统操作日志模块文档
## 概述
系统操作日志模块用于记录后台管理系统的所有操作请求,包括用户操作、API 调用、错误信息等,方便管理员进行系统监控、审计和问题排查。
## 技术特性
- **自动记录**: 通过中间件自动记录所有请求,无需手动调用
- **详细信息**: 记录用户信息、请求参数、响应结果、执行时间等
- **敏感信息保护**: 自动过滤密码等敏感信息
- **性能优化**: 不影响业务响应速度
- **多维度查询**: 支持按用户、模块、操作、状态、时间等多维度筛选
- **数据导出**: 支持导出日志数据为 Excel 文件
- **批量操作**: 支持批量删除和定期清理
## 数据库表结构
### system_logs 表
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | bigint | 主键 ID |
| user_id | bigint | 用户 ID |
| username | varchar(100) | 用户名 |
| module | varchar(50) | 模块名称 |
| action | varchar(100) | 操作名称 |
| method | varchar(10) | 请求方法 (GET/POST/PUT/DELETE) |
| url | text | 请求 URL |
| ip | varchar(45) | 客户端 IP 地址 |
| user_agent | text | 用户代理 |
| params | json | 请求参数 |
| result | text | 响应结果(仅错误时记录) |
| status_code | int | HTTP 状态码 |
| status | varchar(20) | 状态 (success/error) |
| error_message | text | 错误信息 |
| execution_time | int | 执行时间(毫秒) |
| created_at | timestamp | 创建时间 |
| updated_at | timestamp | 更新时间 |
## 核心组件
### 1. 中间件 (Middleware)
**LogRequestMiddleware**
位置: `app/Http/Middleware/LogRequestMiddleware.php`
功能:
- 自动拦截所有经过的请求
- 记录请求和响应信息
- 计算请求执行时间
- 提取用户信息和操作详情
- 过滤敏感参数
- 处理异常情况
使用方式:
```php
// 在路由中应用
Route::middleware(['log.request'])->group(function () {
// 需要记录日志的路由
});
```
### 2. 服务层 (Service)
**LogService**
位置: `app/Services/System/LogService.php`
主要方法:
- `create(array $data)`: 创建日志记录
- `getList(array $params)`: 获取日志列表(分页)
- `getListQuery(array $params)`: 获取日志查询构建器
- `getById(int $id)`: 根据 ID 获取日志详情
- `delete(int $id)`: 删除单条日志
- `batchDelete(array $ids)`: 批量删除日志
- `clearLogs(string $days)`: 清理指定天数前的日志
- `getStatistics(array $params)`: 获取日志统计信息
### 3. 控制器 (Controller)
**Log Controller**
位置: `app/Http/Controllers/System/Admin/Log.php`
接口列表:
- `GET /admin/logs`: 获取日志列表
- `GET /admin/logs/{id}`: 获取日志详情
- `GET /admin/logs/statistics`: 获取日志统计
- `POST /admin/logs/export`: 导出日志
- `DELETE /admin/logs/{id}`: 删除单条日志
- `POST /admin/logs/batch-delete`: 批量删除日志
- `POST /admin/logs/clear`: 清理历史日志
### 4. 请求验证 (Request Validation)
**LogRequest**
位置: `app/Http/Requests/LogRequest.php`
验证规则:
- `user_id`: 用户 ID(可选)
- `username`: 用户名(模糊查询,可选)
- `module`: 模块名称(可选)
- `action`: 操作名称(可选)
- `status`: 状态(success/error,可选)
- `start_date`: 开始日期(可选)
- `end_date`: 结束日期(可选)
- `ip`: IP 地址(可选)
- `page`: 页码(默认 1
- `page_size`: 每页数量(默认 20,最大 100)
## API 接口文档
### 1. 获取日志列表
**接口**: `GET /admin/logs`
**请求参数**:
```json
{
"user_id": 1,
"username": "admin",
"module": "users",
"action": "创建 users",
"status": "success",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"ip": "192.168.1.1",
"page": 1,
"page_size": 20
}
```
**响应示例**:
```json
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"id": 1,
"user_id": 1,
"username": "admin",
"module": "users",
"action": "创建 users",
"method": "POST",
"url": "http://example.com/admin/users",
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"params": {
"name": "test",
"email": "test@example.com"
},
"result": null,
"status_code": 200,
"status": "success",
"error_message": null,
"execution_time": 125,
"created_at": "2024-01-01 12:00:00",
"user": {
"id": 1,
"name": "管理员",
"username": "admin"
}
}
],
"total": 100,
"page": 1,
"page_size": 20
}
}
```
### 2. 获取日志详情
**接口**: `GET /admin/logs/{id}`
**响应示例**:
```json
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"user_id": 1,
"username": "admin",
"module": "users",
"action": "创建 users",
"method": "POST",
"url": "http://example.com/admin/users",
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"params": {
"name": "test",
"email": "test@example.com"
},
"result": null,
"status_code": 200,
"status": "success",
"error_message": null,
"execution_time": 125,
"created_at": "2024-01-01 12:00:00",
"user": {
"id": 1,
"name": "管理员",
"username": "admin",
"email": "admin@example.com",
"created_at": "2024-01-01 10:00:00"
}
}
}
```
### 3. 获取日志统计
**接口**: `GET /admin/logs/statistics`
**请求参数**:
```json
{
"start_date": "2024-01-01",
"end_date": "2024-12-31"
}
```
**响应示例**:
```json
{
"code": 200,
"message": "success",
"data": {
"total": 1000,
"success": 950,
"error": 50
}
}
```
### 4. 导出日志
**接口**: `POST /admin/logs/export`
**请求参数**: 与获取日志列表相同的查询参数
**响应**: Excel 文件下载
文件名格式: `系统操作日志_YYYYMMDDHHmmss.xlsx`
包含字段:
- ID
- 用户名
- 模块
- 操作
- 请求方法
- URL
- IP 地址
- 状态码
- 状态
- 错误信息
- 执行时间(ms)
- 创建时间
### 5. 删除单条日志
**接口**: `DELETE /admin/logs/{id}`
**响应示例**:
```json
{
"code": 200,
"message": "删除成功",
"data": null
}
```
### 6. 批量删除日志
**接口**: `POST /admin/logs/batch-delete`
**请求参数**:
```json
{
"ids": [1, 2, 3, 4, 5]
}
```
**响应示例**:
```json
{
"code": 200,
"message": "批量删除成功",
"data": null
}
```
### 7. 清理历史日志
**接口**: `POST /admin/logs/clear`
**请求参数**:
```json
{
"days": 30
}
```
**说明**: 清理指定天数前的所有日志记录,默认清理 30 天前的数据。
**响应示例**:
```json
{
"code": 200,
"message": "清理成功",
"data": null
}
```
## 日志记录规则
### 1. 自动记录的请求
所有经过 `log.request` 中间件的请求都会被自动记录,包括:
- 用户管理操作
- 角色管理操作
- 权限管理操作
- 部门管理操作
- 系统配置操作
- 其他所有后台管理操作
### 2. 不记录的请求
- 登录接口 (`POST /admin/auth/login`)
- 健康检查接口 (`GET /up`)
- 其他明确排除的路由
### 3. 敏感信息过滤
以下字段会被自动过滤,记录为 `******`:
- `password`
- `password_confirmation`
- `token`
- `secret`
- `key`
### 4. 错误日志处理
- 成功请求 (HTTP 状态码 < 400): `status` = `success`
- 失败请求 (HTTP 状态码 >= 400): `status` = `error`
- 错误时记录响应内容和错误消息
- 同时写入 Laravel 日志文件 (`storage/logs/laravel.log`)
## 模块和操作名称解析
### 模块名称
从 URL 路径中解析,例如:
- `/admin/users` → 模块: `users`
- `/admin/roles` → 模块: `roles`
- `/admin/configs` → 模块: `configs`
### 操作名称
根据 HTTP 方法和资源名称生成:
- `GET /admin/users` → 操作: `查询 users`
- `POST /admin/users` → 操作: `创建 users`
- `PUT /admin/users/1` → 操作: `更新 users`
- `DELETE /admin/users/1` → 操作: `删除 users`
## 性能优化建议
### 1. 定期清理日志
建议使用 Laravel 任务调度器定期清理历史日志:
```php
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// 每天凌晨 2 点清理 90 天前的日志
$schedule->call(function () {
app(LogService::class)->clearLogs(90);
})->dailyAt('02:00');
}
```
### 2. 数据库索引
确保以下字段有索引:
- `user_id`
- `username`
- `module`
- `status`
- `created_at`
### 3. 分页查询
列表查询必须使用分页,避免一次加载过多数据。
### 4. 异步记录
日志记录操作应放在请求处理后,不影响响应速度。
## 前端集成示例
### Vue3 + Ant Design Vue
```vue
<template>
<a-card title="操作日志">
<!-- 搜索表单 -->
<a-form layout="inline" :model="searchParams">
<a-form-item label="用户名">
<a-input v-model:value="searchParams.username" placeholder="请输入用户名" />
</a-form-item>
<a-form-item label="模块">
<a-input v-model:value="searchParams.module" placeholder="请输入模块名" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model:value="searchParams.status" placeholder="请选择状态">
<a-select-option value="success">成功</a-select-option>
<a-select-option value="error">失败</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
<a-button @click="handleExport">导出</a-button>
</a-form-item>
</a-form>
<!-- 数据表格 -->
<a-table
:columns="columns"
:data-source="logs"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
>
<template #status="{ record }">
<a-tag :color="record.status === 'success' ? 'green' : 'red'">
{{ record.status === 'success' ? '成功' : '失败' }}
</a-tag>
</template>
<template #action="{ record }">
<a-button type="link" @click="handleView(record)">查看</a-button>
<a-button type="link" danger @click="handleDelete(record.id)">删除</a-button>
</template>
</a-table>
</a-card>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import request from '@/utils/request'
const logs = ref([])
const loading = ref(false)
const searchParams = reactive({
username: '',
module: '',
status: null,
page: 1,
page_size: 20
})
const pagination = reactive({
total: 0,
current: 1,
pageSize: 20
})
const columns = [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '用户名', dataIndex: 'username', width: 120 },
{ title: '模块', dataIndex: 'module', width: 100 },
{ title: '操作', dataIndex: 'action', width: 150 },
{ title: '请求方法', dataIndex: 'method', width: 100 },
{ title: 'IP 地址', dataIndex: 'ip', width: 150 },
{ title: '状态', dataIndex: 'status', slots: { customRender: 'status' }, width: 100 },
{ title: '执行时间', dataIndex: 'execution_time', width: 100 },
{ title: '创建时间', dataIndex: 'created_at', width: 180 },
{ title: '操作', slots: { customRender: 'action' }, width: 150, fixed: 'right' }
]
// 获取日志列表
const fetchLogs = async () => {
loading.value = true
try {
const res = await request.get('/admin/logs', { params: searchParams })
logs.value = res.data.list
pagination.total = res.data.total
pagination.current = res.data.page
pagination.pageSize = res.data.page_size
} catch (error) {
message.error('获取日志失败')
} finally {
loading.value = false
}
}
// 查询
const handleSearch = () => {
searchParams.page = 1
fetchLogs()
}
// 重置
const handleReset = () => {
searchParams.username = ''
searchParams.module = ''
searchParams.status = null
searchParams.page = 1
fetchLogs()
}
// 导出
const handleExport = async () => {
try {
const res = await request.post('/admin/logs/export', searchParams, {
responseType: 'blob'
})
const url = window.URL.createObjectURL(new Blob([res]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `操作日志_${new Date().getTime()}.xlsx`)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
message.success('导出成功')
} catch (error) {
message.error('导出失败')
}
}
// 查看详情
const handleView = (record) => {
// 打开详情对话框
console.log('查看日志', record)
}
// 删除
const handleDelete = async (id) => {
try {
await request.delete(`/admin/logs/${id}`)
message.success('删除成功')
fetchLogs()
} catch (error) {
message.error('删除失败')
}
}
// 表格分页变化
const handleTableChange = (pag) => {
searchParams.page = pag.current
searchParams.page_size = pag.pageSize
fetchLogs()
}
onMounted(() => {
fetchLogs()
})
</script>
```
## 注意事项
1. **权限控制**: 日志管理接口需要相应的权限才能访问
2. **数据安全**: 敏感信息已自动过滤,但仍需注意日志数据的安全存储
3. **性能影响**: 虽然日志记录不影响响应速度,但大量日志会增加数据库负载
4. **定期备份**: 重要日志数据建议定期备份
5. **日志分析**: 可结合 BI 工具对日志数据进行深度分析
## 常见问题
### Q1: 为什么某些请求没有被记录?
A: 检查路由是否应用了 `log.request` 中间件,或者在中间件中是否被排除了。
### Q2: 日志数据过多怎么办?
A: 使用 `clearLogs` 方法定期清理历史日志,或设置任务调度器自动清理。
### Q3: 如何自定义日志记录规则?
A: 修改 `LogRequestMiddleware` 中的 `parseModule``parseAction` 方法。
### Q4: 日志记录会影响性能吗?
A: 日志记录在请求处理后执行,不影响响应速度。但大量日志会增加数据库写入压力。
### Q5: 如何查看完整的请求参数?
A: 在日志详情接口中,`params` 字段包含了完整的请求参数(敏感信息已过滤)。
## 更新日志
### v1.0.0 (2024-01-01)
- 初始版本
- 实现基础日志记录功能
- 支持多维度查询和筛选
- 支持数据导出
- 支持批量删除和清理
+792
View File
@@ -0,0 +1,792 @@
# System 基础模块文档
## 概述
System 基础模块提供了完整的系统管理功能,包括系统配置、数据字典、操作日志、任务管理、城市数据和文件上传等功能。
## 控制器结构
```
app/Http/Controllers/System/
├── Admin/ # 后台管理控制器
│ ├── Config.php # 系统配置控制器
│ ├── Log.php # 操作日志控制器
│ ├── Dictionary.php # 数据字典控制器
│ ├── Task.php # 任务管理控制器
│ ├── City.php # 城市数据控制器
│ └── Upload.php # 文件上传控制器
└── Api/ # 公共API控制器
├── Config.php # 系统配置API
├── Dictionary.php # 数据字典API
├── City.php # 城市数据API
└── Upload.php # 文件上传API
```
### Admin 控制器
**命名空间**: `App\Http\Controllers\System\Admin`
**路由前缀**: `/admin`
**中间件**: `auth.check:admin` (后台管理认证)
### Api 控制器
**命名空间**: `App\Http\Controllers\System\Api`
**路由前缀**: `/api`
**中间件**: `auth:api` (API认证,部分接口为公开接口)
## 技术栈
- PHP 8.1+
- Laravel 11
- Redis 缓存
- Intervention Image (图像处理)
## 数据库表结构
### system_configs (系统配置表)
- `id`: 主键
- `group`: 配置分组(如:system, site, upload
- `key`: 配置键(唯一)
- `value`: 配置值(JSON格式)
- `type`: 数据类型(string, number, boolean, array, image, file
- `name`: 配置名称
- `description`: 配置描述
- `options`: 可选值(JSON数组)
- `sort`: 排序
- `status`: 状态(0:禁用, 1:启用)
- `created_at`, `updated_at`: 时间戳
### system_dictionaries (数据字典分类表)
- `id`: 主键
- `name`: 字典名称
- `code`: 字典编码(唯一)
- `description`: 描述
- `sort`: 排序
- `status`: 状态
- `created_at`, `updated_at`: 时间戳
### system_dictionary_items (数据字典项表)
- `id`: 主键
- `dictionary_id`: 字典ID
- `label`: 显示标签
- `value`: 实际值
- `sort`: 排序
- `status`: 状态
- `created_at`, `updated_at`: 时间戳
### system_logs (操作日志表)
- `id`: 主键
- `user_id`: 用户ID
- `username`: 用户名
- `module`: 模块
- `action`: 操作
- `method`: 请求方法
- `url`: 请求URL
- `ip`: IP地址
- `user_agent`: 用户代理
- `request_data`: 请求数据(JSON
- `response_data`: 响应数据(JSON
- `duration`: 执行时间(毫秒)
- `status_code`: 状态码
- `created_at`: 创建时间
### system_tasks (任务表)
- `id`: 主键
- `name`: 任务名称
- `code`: 任务编码(唯一)
- `command`: 命令
- `description`: 描述
- `cron_expression`: Cron表达式
- `status`: 状态(0:禁用, 1:启用, 2:运行中, 3:失败)
- `last_run_at`: 最后运行时间
- `next_run_at`: 下次运行时间
- `run_count`: 运行次数
- `fail_count`: 失败次数
- `last_message`: 最后运行消息
- `sort`: 排序
- `created_at`, `updated_at`: 时间戳
### system_cities (城市表)
- `id`: 主键
- `name`: 城市名称
- `code`: 城市编码
- `level`: 级别(1:省, 2:市, 3:区县)
- `parent_id`: 父级ID
- `adcode`: 行政区划编码
- `sort`: 排序
- `status`: 状态
- `created_at`, `updated_at`: 时间戳
## Admin API 接口文档
### 系统配置管理
#### 获取配置列表
- **接口**: `GET /admin/configs`
- **参数**:
- `page`: 页码(默认1
- `page_size`: 每页数量(默认20
- `keyword`: 搜索关键词
- `group`: 配置分组
- `status`: 状态
- `order_by`, `order_direction`: 排序
#### 获取所有配置分组
- **接口**: `GET /admin/configs/groups`
#### 按分组获取配置
- **接口**: `GET /admin/configs/all`
- **参数**:
- `group`: 配置分组(可选)
#### 获取配置详情
- **接口**: `GET /admin/configs/{id}`
#### 创建配置
- **接口**: `POST /admin/configs`
- **参数**:
```json
{
"group": "site",
"key": "site_name",
"name": "网站名称",
"description": "网站显示的名称",
"type": "string",
"value": "\"我的网站\"",
"options": null,
"sort": 1,
"status": 1
}
```
- **数据类型说明**:
- `string`: 字符串
- `number`: 数字
- `boolean`: 布尔值
- `array`: 数组
- `image`: 图片
- `file`: 文件
#### 更新配置
- **接口**: `PUT /admin/configs/{id}`
#### 删除配置
- **接口**: `DELETE /admin/configs/{id}`
#### 批量删除配置
- **接口**: `POST /admin/configs/batch-delete`
- **参数**:
```json
{
"ids": [1, 2, 3]
}
```
#### 批量更新配置状态
- **接口**: `POST /admin/configs/batch-status`
- **参数**:
```json
{
"ids": [1, 2, 3],
"status": 1
}
```
### 操作日志管理
#### 获取日志列表
- **接口**: `GET /admin/logs`
- **参数**:
- `page`, `page_size`
- `keyword`: 搜索关键词(用户名/模块/操作)
- `module`: 模块
- `action`: 操作
- `user_id`: 用户ID
- `start_date`: 开始日期
- `end_date`: 结束日期
- `order_by`, `order_direction`
#### 获取日志详情
- **接口**: `GET /admin/logs/{id}`
#### 删除日志
- **接口**: `DELETE /admin/logs/{id}`
#### 批量删除日志
- **接口**: `POST /admin/logs/batch-delete`
- **参数**:
```json
{
"ids": [1, 2, 3]
}
```
#### 清理日志
- **接口**: `POST /admin/logs/clear`
- **参数**:
```json
{
"days": 30
}
```
- **说明**: 删除指定天数之前的日志记录
#### 获取日志统计
- **接口**: `GET /admin/logs/statistics`
- **参数**:
- `start_date`: 开始日期
- `end_date`: 结束日期
- **返回**:
```json
{
"code": 200,
"message": "success",
"data": {
"total_count": 1000,
"module_stats": [
{
"module": "user",
"count": 500
}
],
"user_stats": [
{
"user_id": 1,
"username": "admin",
"count": 800
}
]
}
}
```
### 数据字典管理
#### 获取字典列表
- **接口**: `GET /admin/dictionaries`
- **参数**:
- `page`, `page_size`, `keyword`, `status`, `order_by`, `order_direction`
#### 获取所有字典
- **接口**: `GET /admin/dictionaries/all`
#### 获取字典详情
- **接口**: `GET /admin/dictionaries/{id}`
#### 创建字典
- **接口**: `POST /admin/dictionaries`
- **参数**:
```json
{
"name": "用户状态",
"code": "user_status",
"description": "用户状态字典",
"sort": 1,
"status": 1
}
```
#### 更新字典
- **接口**: `PUT /admin/dictionaries/{id}`
#### 删除字典
- **接口**: `DELETE /admin/dictionaries/{id}`
#### 批量删除字典
- **接口**: `POST /admin/dictionaries/batch-delete`
#### 批量更新字典状态
- **接口**: `POST /admin/dictionaries/batch-status`
### 数据字典项管理
#### 获取字典项列表
- **接口**: `GET /admin/dictionary-items`
- **参数**:
- `page`, `page_size`
- `dictionary_id`: 字典ID(必需)
- `keyword`: 搜索关键词
- `status`, `order_by`, `order_direction`
#### 创建字典项
- **接口**: `POST /admin/dictionary-items`
- **参数**:
```json
{
"dictionary_id": 1,
"label": "正常",
"value": "1",
"sort": 1,
"status": 1
}
```
#### 更新字典项
- **接口**: `PUT /admin/dictionary-items/{id}`
#### 删除字典项
- **接口**: `DELETE /admin/dictionary-items/{id}`
#### 批量删除字典项
- **接口**: `POST /admin/dictionary-items/batch-delete`
#### 批量更新字典项状态
- **接口**: `POST /admin/dictionary-items/batch-status`
### 任务管理
#### 获取任务列表
- **接口**: `GET /admin/tasks`
- **参数**:
- `page`, `page_size`, `keyword`, `status`, `order_by`, `order_direction`
#### 获取所有任务
- **接口**: `GET /admin/tasks/all`
#### 获取任务详情
- **接口**: `GET /admin/tasks/{id}`
#### 创建任务
- **接口**: `POST /admin/tasks`
- **参数**:
```json
{
"name": "清理临时文件",
"code": "cleanup_temp_files",
"command": "cleanup:temp",
"description": "每天清理过期临时文件",
"cron_expression": "0 2 * * *",
"sort": 1,
"status": 1
}
```
- **Cron表达式说明**:
- 格式:`分 时 日 月 周`
- 示例:`0 2 * * *` (每天凌晨2点执行)
- 示例:`*/5 * * * *` (每5分钟执行一次)
#### 更新任务
- **接口**: `PUT /admin/tasks/{id}`
#### 删除任务
- **接口**: `DELETE /admin/tasks/{id}`
#### 批量删除任务
- **接口**: `POST /admin/tasks/batch-delete`
#### 批量更新任务状态
- **接口**: `POST /admin/tasks/batch-status`
#### 手动执行任务
- **接口**: `POST /admin/tasks/{id}/run`
- **说明**: 立即执行指定任务
#### 获取任务统计
- **接口**: `GET /admin/tasks/statistics`
- **返回**:
```json
{
"code": 200,
"message": "success",
"data": {
"total": 10,
"enabled": 8,
"disabled": 2,
"running": 0,
"failed": 0
}
}
```
### 城市数据管理
#### 获取城市列表
- **接口**: `GET /admin/cities`
- **参数**:
- `page`, `page_size`, `keyword`, `level`, `parent_id`, `status`
- `order_by`, `order_direction`
#### 获取城市树
- **接口**: `GET /admin/cities/tree`
- **说明**: 从缓存中获取完整的三级城市树
#### 获取城市详情
- **接口**: `GET /admin/cities/{id}`
#### 获取子级城市
- **接口**: `GET /admin/cities/{id}/children`
- **说明**: 获取指定城市的下级城市
#### 获取所有省份
- **接口**: `GET /admin/cities/provinces`
#### 获取指定省份的城市
- **接口**: `GET /admin/cities/{provinceId}/cities`
#### 获取指定城市的区县
- **接口**: `GET /admin/cities/{cityId}/districts`
#### 创建城市
- **接口**: `POST /admin/cities`
- **参数**:
```json
{
"name": "北京市",
"code": "110000",
"level": 1,
"parent_id": 0,
"adcode": "110000",
"sort": 1,
"status": 1
}
```
- **级别说明**:
- `1`: 省级
- `2`: 市级
- `3`: 区县级
#### 更新城市
- **接口**: `PUT /admin/cities/{id}`
#### 删除城市
- **接口**: `DELETE /admin/cities/{id}`
#### 批量删除城市
- **接口**: `POST /admin/cities/batch-delete`
#### 批量更新城市状态
- **接口**: `POST /admin/cities/batch-status`
### 文件上传管理
#### 单文件上传
- **接口**: `POST /admin/upload`
- **参数**:
- `file`: 文件(multipart/form-data,最大10MB
- `directory`: 存储目录(默认:uploads
- `compress`: 是否压缩图片(默认:false
- `quality`: 图片质量(1-100,默认:80
- `width`: 压缩宽度(可选)
- `height`: 压缩高度(可选)
- **返回**:
```json
{
"code": 200,
"message": "上传成功",
"data": {
"url": "http://example.com/uploads/2024/01/xxx.jpg",
"path": "uploads/2024/01/xxx.jpg",
"name": "xxx.jpg",
"size": 102400,
"mime_type": "image/jpeg"
}
}
```
#### 多文件上传
- **接口**: `POST /admin/upload/multiple`
- **参数**:
- `files`: 文件数组(multipart/form-data
- 其他参数同单文件上传
- **返回**: 文件数组
#### Base64上传
- **接口**: `POST /admin/upload/base64`
- **参数**:
```json
{
"base64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ...",
"directory": "uploads",
"file_name": "image.jpg"
}
```
#### 删除文件
- **接口**: `POST /admin/upload/delete`
- **参数**:
```json
{
"path": "uploads/2024/01/xxx.jpg"
}
```
#### 批量删除文件
- **接口**: `POST /admin/upload/batch-delete`
- **参数**:
```json
{
"paths": [
"uploads/2024/01/xxx.jpg",
"uploads/2024/01/yyy.jpg"
]
}
```
## Public API 接口文档
### 系统配置 API
#### 获取所有配置
- **接口**: `GET /api/system/configs`
- **认证**: 公开接口(无需认证)
- **返回**:
```json
{
"code": 200,
"message": "success",
"data": {
"site_name": "我的网站",
"site_logo": "/uploads/logo.png"
}
}
```
#### 按分组获取配置
- **接口**: `GET /api/system/configs/group`
- **参数**:
- `group`: 配置分组
- **认证**: 公开接口
#### 根据键获取配置
- **接口**: `GET /api/system/configs/key`
- **参数**:
- `key`: 配置键
- **认证**: 公开接口
### 数据字典 API
#### 获取所有字典
- **接口**: `GET /api/system/dictionaries`
- **认证**: 公开接口
- **返回**:
```json
{
"code": 200,
"message": "success",
"data": [
{
"id": 1,
"code": "user_status",
"name": "用户状态",
"items": [...]
}
]
}
```
#### 根据编码获取字典项
- **接口**: `GET /api/system/dictionaries/code`
- **参数**:
- `code`: 字典编码
- **认证**: 公开接口
- **返回**:
```json
{
"code": 200,
"message": "success",
"data": {
"code": "user_status",
"items": [
{"label": "正常", "value": "1"},
{"label": "禁用", "value": "0"}
]
}
}
```
#### 获取字典详情
- **接口**: `GET /api/system/dictionaries/{id}`
- **认证**: 公开接口
### 城市数据 API
#### 获取城市树
- **接口**: `GET /api/system/cities/tree`
- **认证**: 公开接口
- **说明**: 从缓存中获取完整的三级城市树
#### 获取所有省份
- **接口**: `GET /api/system/cities/provinces`
- **认证**: 公开接口
#### 获取指定省份的城市
- **接口**: `GET /api/system/cities/{provinceId}/cities`
- **认证**: 公开接口
#### 获取指定城市的区县
- **接口**: `GET /api/system/cities/{cityId}/districts`
- **认证**: 公开接口
#### 获取城市详情
- **接口**: `GET /api/system/cities/{id}`
- **认证**: 公开接口
### 文件上传 API
#### 单文件上传
- **接口**: `POST /api/system/upload`
- **认证**: 需要认证(`auth:api`
- **参数**: 同Admin上传接口
#### 多文件上传
- **接口**: `POST /api/system/upload/multiple`
- **认证**: 需要认证(`auth:api`
- **参数**: 同Admin上传接口
#### Base64上传
- **接口**: `POST /api/system/upload/base64`
- **认证**: 需要认证(`auth:api`
- **参数**: 同Admin上传接口
## 缓存机制
### 城市数据缓存
- **缓存键**: `city:tree`
- **过期时间**: 永久(手动清除)
- **更新时机**: 城市数据增删改时自动清除
### 系统配置缓存
- **缓存键**: `config:all``config:group:{group}`
- **过期时间**: 60分钟
- **更新时机**: 配置数据增删改时自动清除
### 数据字典缓存
- **缓存键**: `dictionary:all``dictionary:code:{code}`
- **过期时间**: 60分钟
- **更新时机**: 字典数据增删改时自动清除
## 服务层说明
### ConfigService
提供系统配置的CRUD操作和缓存管理。
**主要方法**:
- `getList()`: 获取配置列表
- `getByGroup()`: 按分组获取配置
- `getConfigValue()`: 获取配置值
- `create()`: 创建配置
- `update()`: 更新配置
- `delete()`: 删除配置
### LogService
提供操作日志的记录、查询和清理功能。
**主要方法**:
- `getList()`: 获取日志列表
- `getStatistics()`: 获取统计数据
- `clearLogs()`: 清理过期日志
- `record()`: 记录日志(由中间件自动调用)
### DictionaryService
提供数据字典和字典项的管理功能。
**主要方法**:
- `getList()`: 获取字典列表
- `getItemsByCode()`: 按编码获取字典项
- `create()`: 创建字典
- `createItem()`: 创建字典项
- `update()`: 更新字典
- `updateItem()`: 更新字典项
### TaskService
提供任务的管理和执行功能。
**主要方法**:
- `getList()`: 获取任务列表
- `run()`: 手动执行任务
- `getStatistics()`: 获取任务统计
- `updateStatus()`: 更新任务状态
### CityService
提供城市数据的管理和缓存功能。
**主要方法**:
- `getList()`: 获取城市列表
- `getCachedTree()`: 从缓存获取城市树
- `getProvinces()`: 获取省份列表
- `getCities()`: 获取城市列表
- `getDistricts()`: 获取区县列表
### UploadService
提供文件上传和删除功能。
**主要方法**:
- `upload()`: 单文件上传
- `uploadMultiple()`: 多文件上传
- `uploadBase64()`: Base64上传
- `delete()`: 删除文件
- `compressImage()`: 图片压缩
## 初始化数据
```bash
# 执行迁移
php artisan migrate
# 填充初始数据
php artisan db:seed --class=SystemSeeder
```
初始数据包括:
- 基础系统配置
- 常用数据字典
- 全国省市区数据
## 注意事项
1. **Swoole环境注意事项**:
- 文件上传时注意临时文件清理
- 使用Redis缓存避免内存泄漏
- 图片压缩使用协程安全的方式
2. **安全注意事项**:
- 文件上传必须验证文件类型和大小
- 敏感操作必须记录日志
- 配置数据不要存储密码等敏感信息
3. **性能优化**:
- 城市数据使用Redis缓存
- 大量日志数据定期清理
- 图片上传时进行压缩处理
4. **文件上传**:
- 限制文件上传大小
- 验证文件MIME类型
- 定期清理临时文件
## 扩展建议
1. **日志告警**: 添加日志异常告警功能
2. **配置加密**: 敏感配置数据加密存储
3. **多语言**: 支持配置数据的多语言
4. **任务监控**: 添加任务执行监控和通知
5. **CDN集成**: 文件上传支持CDN分发
## 常见问题
### Q: 如何清除城市数据缓存?
A: 调用 `CityService::clearCache()` 方法或运行 `php artisan cache:forget city:tree`
### Q: 图片上传后如何压缩?
A: 上传时设置 `compress=true``quality` 参数,系统会自动压缩。
### Q: 如何配置定时任务?
A: 在Admin后台创建任务,设置Cron表达式,系统会自动调度执行。
### Q: 数据字典如何使用?
A: 通过Public API获取字典数据,前端根据数据渲染下拉框等组件。
### Q: 日志数据过多如何处理?
A: 定期使用 `/admin/logs/clear` 接口清理过期日志,或在后台设置自动清理任务。
+684
View File
@@ -0,0 +1,684 @@
# WebSocket 功能文档
## 概述
本项目基于 Laravel-S 和 Swoole 实现了完整的 WebSocket 功能,支持实时通信、消息推送、广播等功能。
## 功能特性
- ✅ 实时双向通信
- ✅ 用户连接管理
- ✅ 点对点消息发送
- ✅ 群发消息/广播
- ✅ 频道订阅/取消订阅
- ✅ 心跳机制
- ✅ 自动重连
- ✅ 在线状态管理
- ✅ 系统通知推送
- ✅ 数据更新推送
## 架构设计
### 后端组件
#### 1. WebSocketHandler (`app/Services/WebSocket/WebSocketHandler.php`)
WebSocket 处理器,实现了 Swoole 的 `WebSocketHandlerInterface` 接口。
**主要方法:**
- `onOpen()`: 处理连接建立事件
- `onMessage()`: 处理消息接收事件
- `onClose()`: 处理连接关闭事件
**支持的消息类型:**
- `ping/pong`: 心跳检测
- `heartbeat`: 心跳确认
- `chat`: 私聊消息
- `broadcast`: 广播消息
- `subscribe/unsubscribe`: 频道订阅/取消订阅
#### 2. WebSocketService (`app/Services/WebSocket/WebSocketService.php`)
WebSocket 服务类,提供便捷的 WebSocket 操作方法。
**主要方法:**
- `sendToUser($userId, $data)`: 发送消息给指定用户
- `sendToUsers($userIds, $data)`: 发送消息给多个用户
- `broadcast($data, $excludeUserId)`: 广播消息给所有用户
- `sendToChannel($channel, $data)`: 发送消息给指定频道
- `getOnlineUserCount()`: 获取在线用户数
- `isUserOnline($userId)`: 检查用户是否在线
- `sendSystemNotification()`: 发送系统通知
- `pushDataUpdate()`: 推送数据更新
#### 3. WebSocketController (`app/Http/Controllers/System/WebSocket.php`)
WebSocket API 控制器,提供 HTTP 接口用于管理 WebSocket 连接。
### 前端组件
#### WebSocketClient (`resources/admin/src/utils/websocket.js`)
WebSocket 客户端封装类。
**功能:**
- 自动连接和重连
- 心跳机制
- 消息类型路由
- 事件监听
- 连接状态管理
## 配置说明
### Laravel-S 配置 (`config/laravels.php`)
```php
'websocket' => [
'enable' => env('LARAVELS_WEBSOCKET', true),
'handler' => \App\Services\WebSocket\WebSocketHandler::class,
],
'swoole_tables' => [
'wsTable' => [
'size' => 102400,
'column' => [
['name' => 'value', 'type' => \Swoole\Table::TYPE_STRING, 'size' => 1024],
['name' => 'expiry', 'type' => \Swoole\Table::TYPE_INT, 'size' => 4],
],
],
],
```
### 环境变量
`.env` 文件中添加:
```env
LARAVELS_WEBSOCKET=true
```
## API 接口
### 1. 获取在线用户数
```
GET /admin/websocket/online-count
```
**响应:**
```json
{
"code": 200,
"message": "success",
"data": {
"online_count": 10
}
}
```
### 2. 获取在线用户列表
```
GET /admin/websocket/online-users
```
**响应:**
```json
{
"code": 200,
"message": "success",
"data": {
"user_ids": [1, 2, 3, 4, 5],
"count": 5
}
}
```
### 3. 检查用户在线状态
```
POST /admin/websocket/check-online
```
**请求参数:**
```json
{
"user_id": 1
}
```
**响应:**
```json
{
"code": 200,
"message": "success",
"data": {
"user_id": 1,
"is_online": true
}
}
```
### 4. 发送消息给指定用户
```
POST /admin/websocket/send-to-user
```
**请求参数:**
```json
{
"user_id": 1,
"type": "notification",
"data": {
"title": "新消息",
"message": "您有一条新消息"
}
}
```
### 5. 发送消息给多个用户
```
POST /admin/websocket/send-to-users
```
**请求参数:**
```json
{
"user_ids": [1, 2, 3],
"type": "notification",
"data": {
"title": "系统通知",
"message": "系统将在今晚进行维护"
}
}
```
### 6. 广播消息
```
POST /admin/websocket/broadcast
```
**请求参数:**
```json
{
"type": "notification",
"data": {
"title": "公告",
"message": "欢迎使用新版本"
},
"exclude_user_id": 1 // 可选:排除某个用户
}
```
### 7. 发送消息到频道
```
POST /admin/websocket/send-to-channel
```
**请求参数:**
```json
{
"channel": "orders",
"type": "data_update",
"data": {
"order_id": 123,
"status": "paid"
}
}
```
### 8. 发送系统通知
```
POST /admin/websocket/send-notification
```
**请求参数:**
```json
{
"title": "系统维护",
"message": "系统将于今晚 23:00-24:00 进行维护",
"type": "warning",
"extra_data": {
"start_time": "23:00",
"end_time": "24:00"
}
}
```
### 9. 发送通知给指定用户
```
POST /admin/websocket/send-notification-to-users
```
**请求参数:**
```json
{
"user_ids": [1, 2, 3],
"title": "订单更新",
"message": "您的订单已发货",
"type": "success"
}
```
### 10. 推送数据更新
```
POST /admin/websocket/push-data-update
```
**请求参数:**
```json
{
"user_ids": [1, 2, 3],
"resource_type": "order",
"action": "update",
"data": {
"id": 123,
"status": "shipped"
}
}
```
### 11. 推送数据更新到频道
```
POST /admin/websocket/push-data-update-channel
```
**请求参数:**
```json
{
"channel": "orders",
"resource_type": "order",
"action": "create",
"data": {
"id": 124,
"customer": "张三",
"amount": 100.00
}
}
```
### 12. 断开用户连接
```
POST /admin/websocket/disconnect-user
```
**请求参数:**
```json
{
"user_id": 1
}
```
## 前端使用示例
### 1. 基本连接
```javascript
import { getWebSocket, closeWebSocket } from '@/utils/websocket'
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
// 连接 WebSocket
const ws = getWebSocket(userStore.userInfo.id, userStore.token, {
onOpen: (event) => {
console.log('WebSocket 已连接')
},
onMessage: (message) => {
console.log('收到消息:', message)
},
onError: (error) => {
console.error('WebSocket 错误:', error)
},
onClose: (event) => {
console.log('WebSocket 已关闭')
}
})
// 连接
ws.connect()
```
### 2. 监听特定消息类型
```javascript
// 监听通知消息
ws.on('notification', (data) => {
message.success(data.title, data.message)
})
// 监听数据更新
ws.on('data_update', (data) => {
console.log('数据更新:', data.resource_type, data.action)
// 刷新数据
loadData()
})
```
### 3. 发送消息
```javascript
// 发送心跳
ws.send('heartbeat', { timestamp: Date.now() })
// 发送私聊消息
ws.send('chat', {
to_user_id: 2,
content: '你好,这是一条私聊消息'
})
// 订阅频道
ws.send('subscribe', { channel: 'orders' })
// 取消订阅
ws.send('unsubscribe', { channel: 'orders' })
```
### 4. 发送广播消息
```javascript
ws.send('broadcast', {
message: '这是一条广播消息'
})
```
### 5. 断开连接
```javascript
// 断开连接
ws.disconnect()
// 或使用全局方法
closeWebSocket()
```
### 6. 在 Vue 组件中使用
```vue
<template>
<div>
<a-button @click="connectWebSocket">连接 WebSocket</a-button>
<a-button @click="disconnectWebSocket">断开连接</a-button>
<a-button @click="sendMessage">发送消息</a-button>
<div>连接状态: {{ connectionStatus }}</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { getWebSocket } from '@/utils/websocket'
import { useUserStore } from '@/stores/modules/user'
const userStore = useUserStore()
const ws = ref(null)
const connectionStatus = ref('未连接')
const connectWebSocket = () => {
ws.value = getWebSocket(userStore.userInfo.id, userStore.token, {
onOpen: () => {
connectionStatus.value = '已连接'
},
onMessage: (message) => {
handleMessage(message)
},
onClose: () => {
connectionStatus.value = '已断开'
}
})
ws.value.connect()
}
const disconnectWebSocket = () => {
if (ws.value) {
ws.value.disconnect()
connectionStatus.value = '已断开'
}
}
const sendMessage = () => {
if (ws.value && ws.value.isConnected) {
ws.value.send('chat', {
to_user_id: 2,
content: '测试消息'
})
}
}
const handleMessage = (message) => {
switch (message.type) {
case 'notification':
message.success(message.data.title, message.data.message)
break
case 'data_update':
// 处理数据更新
break
case 'chat':
// 处理聊天消息
break
}
}
onMounted(() => {
connectWebSocket()
})
onUnmounted(() => {
disconnectWebSocket()
})
</script>
```
## 消息格式
### 服务端发送的消息格式
```json
{
"type": "notification",
"data": {
"title": "标题",
"message": "内容",
"type": "info",
"timestamp": 1641234567
}
}
```
### 客户端发送的消息格式
```json
{
"type": "chat",
"data": {
"to_user_id": 2,
"content": "消息内容"
}
}
```
## 启动和停止
### 启动 Laravel-S 服务
```bash
php bin/laravels start
```
### 停止 Laravel-S 服务
```bash
php bin/laravels stop
```
### 重启 Laravel-S 服务
```bash
php bin/laravels restart
```
### 重载 Laravel-S 服务(平滑重启)
```bash
php bin/laravels reload
```
### 查看服务状态
```bash
php bin/laravels status
```
## WebSocket 连接地址
### 开发环境
```
ws://localhost:5200/ws?user_id={user_id}&token={token}
```
### 生产环境
```
wss://yourdomain.com/ws?user_id={user_id}&token={token}
```
## Nginx 配置示例
```nginx
server {
listen 80;
server_name yourdomain.com;
root /path/to/your/project/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# WebSocket 代理配置
location /ws {
proxy_pass http://127.0.0.1:5200;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
```
## 使用场景
### 1. 实时通知
```php
// 发送系统通知
$webSocketService->sendSystemNotification(
'系统维护',
'系统将于今晚进行维护',
'warning'
);
```
### 2. 订单状态更新
```php
// 推送订单状态更新给相关人员
$webSocketService->pushDataUpdate(
[$order->user_id],
'order',
'update',
[
'id' => $order->id,
'status' => $order->status,
'updated_at' => $order->updated_at
]
);
```
### 3. 实时聊天
```javascript
// 发送私聊消息
ws.send('chat', {
to_user_id: 2,
content: '你好'
})
```
### 4. 数据监控
```php
// 推送系统监控数据到特定频道
$webSocketService->sendToChannel('system_monitor', 'monitor', [
'cpu_usage' => 75,
'memory_usage' => 80,
'disk_usage' => 60
]);
```
## 注意事项
1. **连接认证**: WebSocket 连接时需要提供 `user_id``token` 参数
2. **心跳机制**: 客户端默认每 30 秒发送一次心跳
3. **自动重连**: 连接断开后会自动尝试重连,最多重试 5 次
4. **并发限制**: Swoole Table 最多支持 102,400 个连接
5. **内存管理**: 注意内存泄漏问题,定期重启服务
6. **安全性**: 生产环境建议使用 WSS (WebSocket Secure)
7. **日志监控**: 查看日志文件 `storage/logs/swoole-YYYY-MM.log`
## 故障排查
### 1. 无法连接 WebSocket
- 检查 Laravel-S 服务是否启动
- 检查端口 5200 是否被占用
- 检查防火墙设置
- 查看日志文件
### 2. 连接频繁断开
- 检查网络稳定性
- 调整心跳间隔
- 检查服务器资源使用情况
### 3. 消息发送失败
- 检查用户是否在线
- 检查消息格式是否正确
- 查看错误日志
## 参考资料
- [Laravel-S 文档](https://github.com/hhxsv5/laravel-s)
- [Swoole 文档](https://www.swoole.com/)
- [WebSocket API](https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket)
## 更新日志
### 2024-02-08
- ✅ 初始版本发布
- ✅ 实现基础 WebSocket 功能
- ✅ 实现消息推送功能
- ✅ 实现频道订阅功能
- ✅ 实现前端客户端封装
- ✅ 实现管理 API 接口
+18
View File
@@ -0,0 +1,18 @@
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"laravel-vite-plugin": "^1.0",
"vite": "^5.0"
},
"dependencies": {
"axios": "^1.7.9",
"vue": "^3.5.13",
"vuex": "^4.1.0"
}
}
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
<php>
<env name="APP_ENV" value="testing"/>
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
<env name="BCRYPT_ROUNDS" value="4"/>
<env name="BROADCAST_CONNECTION" value="null"/>
<env name="CACHE_STORE" value="array"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="PULSE_ENABLED" value="false"/>
<env name="TELESCOPE_ENABLED" value="false"/>
<env name="NIGHTWATCH_ENABLED" value="false"/>
</php>
</phpunit>
+25
View File
@@ -0,0 +1,25 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Handle X-XSRF-Token Header
RewriteCond %{HTTP:x-xsrf-token} .
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Send Requests To Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>
View File
+20
View File
@@ -0,0 +1,20 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
/** @var Application $app */
$app = require_once __DIR__.'/../bootstrap/app.php';
$app->handleRequest(Request::capture());
+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow:
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
+5
View File
@@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

Some files were not shown because too many files have changed in this diff Show More