Compare commits
41 Commits
64c086431a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| db606434bc | |||
| 2625ee94bf | |||
| 9197bc6f29 | |||
| 78507ac7a7 | |||
| 22824550d6 | |||
| 6d118287db | |||
| 6b92ffbfeb | |||
| c0d27be99b | |||
| 7f8e68bb0a | |||
| 350f368f6c | |||
| 3d7f508a6b | |||
| 843f5bfab9 | |||
| ca0dd554d3 | |||
| e26ba12150 | |||
| f0af965412 | |||
| f0f0763ceb | |||
| d310a29c03 | |||
| 736a41a718 | |||
| 0ecb088569 | |||
| b6c133952b | |||
| 6543e2ccdd | |||
| a0c2350662 | |||
| e679a9402f | |||
| 378b9bd71f | |||
| 5450777bd7 | |||
| 790b3140a7 | |||
| 6623c656f4 | |||
| f90afaddca | |||
| b0ae1bb68c | |||
| e265bcc28d | |||
| ada5e027fa | |||
| 1bfe30651e | |||
| 2720de7f44 | |||
| 9339cefae0 | |||
| a2ca64d909 | |||
| 1969669f0b | |||
| 2248d51887 | |||
| 7aa428d932 | |||
| abfc2a953c | |||
| 121a971e91 | |||
| 5d51656fd4 |
+636
-40
@@ -59,6 +59,18 @@ ### 1. 项目结构
|
||||
│ │ └── index.vue # 主布局
|
||||
│ ├── pages/ # 页面组件
|
||||
│ │ ├── auth/ # 认证页面
|
||||
│ │ │ ├── users/ # 用户管理
|
||||
│ │ │ │ ├── index.vue # 主页面
|
||||
│ │ │ │ └── components/ # 页面私有组件
|
||||
│ │ │ │ ├── SaveDialog.vue # 新增/编辑弹窗
|
||||
│ │ │ │ ├── RoleDialog.vue # 角色设置弹窗
|
||||
│ │ │ │ └── DetailDialog.vue # 详情弹窗
|
||||
│ │ │ ├── roles/ # 角色管理
|
||||
│ │ │ │ ├── index.vue
|
||||
│ │ │ │ └── components/
|
||||
│ │ │ │ ├── SaveDialog.vue
|
||||
│ │ │ │ └── PermissionDialog.vue
|
||||
│ │ │ └── ...
|
||||
│ │ ├── home/ # 首页
|
||||
│ │ ├── login/ # 登录页
|
||||
│ │ ├── system/ # 系统管理
|
||||
@@ -84,13 +96,431 @@ ### 1. 项目结构
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
### 2. 组件开发规范
|
||||
### 2. 页面组件开发规范
|
||||
|
||||
#### 页面组件目录结构
|
||||
|
||||
每个页面模块应遵循以下目录结构:
|
||||
|
||||
```
|
||||
pages/
|
||||
└── {module}/ # 模块目录(如 auth, system 等)
|
||||
└── {resource}/ # 资源目录(如 users, roles 等)
|
||||
├── index.vue # 主页面(列表页)
|
||||
└── components/ # 页面私有组件目录
|
||||
├── SaveDialog.vue # 新增/编辑弹窗(字段较少时使用)
|
||||
├── SaveDrawer.vue # 新增/编辑抽屉(字段较多时使用)
|
||||
├── DetailDialog.vue # 详情弹窗
|
||||
├── RoleDialog.vue # 角色设置弹窗
|
||||
├── SearchDrawer.vue # 高级搜索抽屉
|
||||
└── ... # 其他页面专用组件
|
||||
```
|
||||
|
||||
**目录结构说明**:
|
||||
- `index.vue`: 页面的主入口组件,通常是列表展示页面
|
||||
- `components/`: 存放该页面专用的私有组件
|
||||
- 弹窗/抽屉组件:根据表单字段数量选择使用弹窗或抽屉
|
||||
- 字段较少(3-5个):使用 `*Dialog.vue` 弹窗组件
|
||||
- 字段较多(5个以上):使用 `*Drawer.vue` 抽屉组件
|
||||
- `SaveDialog.vue` / `SaveDrawer.vue`: 用于新增和编辑数据的表单组件
|
||||
- `DetailDialog.vue`: 用于查看数据详情的弹窗
|
||||
- `SearchDrawer.vue`: 用于高级搜索条件的抽屉组件
|
||||
- 其他与该页面紧密关联的组件
|
||||
|
||||
**组件命名规范**:
|
||||
- 页面私有组件使用 PascalCase 命名
|
||||
- 建议以功能命名:
|
||||
- 表单类:`SaveDialog.vue`, `SaveDrawer.vue`, `EditDialog.vue` 等
|
||||
- 功能类:`RoleDialog.vue`, `PermissionDialog.vue` 等
|
||||
- 搜索类:`SearchDrawer.vue`
|
||||
- 避免 `Dialog.vue` 或 `Drawer.vue` 这样过于通用的命名
|
||||
|
||||
#### 页面布局规范
|
||||
|
||||
**基本布局要求**:
|
||||
|
||||
1. **使用 Flex 布局**: 页面容器必须使用 Flex 布局,确保占满全部内容区域
|
||||
2. **表格高度自适应**: 表格使用封装的 `scTable` 组件,高度占满剩余内容空间
|
||||
|
||||
**标准页面布局模板**:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="pages {module}-{resource}-page">
|
||||
<!-- 工具栏区域 -->
|
||||
<div class="tool-bar">
|
||||
<div class="left-panel">
|
||||
<!-- 搜索表单 -->
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<!-- 操作按钮 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格内容区域 -->
|
||||
<div class="table-content">
|
||||
<scTable
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:row-key="rowKey"
|
||||
@refresh="refreshTable"
|
||||
@paginationChange="handlePaginationChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.{module}-{resource}-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
.tool-bar {
|
||||
// 工具栏样式
|
||||
}
|
||||
|
||||
.table-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**布局样式说明**:
|
||||
- 页面根元素使用 `display: flex` 和 `flex-direction: column` 实现垂直布局
|
||||
- `height: 100%` 确保页面占满父容器高度
|
||||
- `padding: 0` 去除内边距(由布局组件控制外边距)
|
||||
- `table-content` 使用 `flex: 1` 占据剩余空间
|
||||
- `overflow: hidden` 防止出现滚动条(表格内部自己处理滚动)
|
||||
|
||||
**侧边栏布局示例** (如用户管理页面):
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="pages user-page">
|
||||
<!-- 左侧部门树 -->
|
||||
<div class="left-box">
|
||||
<div class="header">
|
||||
<!-- 搜索框 -->
|
||||
</div>
|
||||
<div class="body">
|
||||
<!-- 树形组件 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="right-box">
|
||||
<!-- 工具栏 -->
|
||||
<div class="tool-bar">...</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<div class="table-content">...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.user-page {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
.left-box {
|
||||
width: 260px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
.header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.table-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
#### 列表搜索区域规范
|
||||
|
||||
**搜索区域设计原则**:
|
||||
|
||||
1. **精简显示**: 搜索条件不全部显示,只显示最常用的 1-2 项
|
||||
2. **去除 label 属性**: 搜索输入框不使用 label 属性,通过 placeholder 直接说明用途,保持界面简洁
|
||||
3. **优先使用表格筛选**: 对于状态等枚举类型的筛选,优先使用 Ant Design Vue Table 的自定义过滤器功能
|
||||
4. **高级搜索抽屉**: 其他复杂搜索条件使用抽屉弹出(需要在页面 components 目录下创建 SearchDrawer.vue 组件)
|
||||
5. **保持界面整洁**: 避免搜索区域占用过多空间
|
||||
|
||||
**实现方式**:
|
||||
|
||||
**方式一: 使用表格自定义筛选(推荐)**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="pages-base-layout role-page">
|
||||
<div class="tool-bar">
|
||||
<div class="left-panel">
|
||||
<a-space>
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="角色名称" allow-clear style="width: 180px" />
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><search-outlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><redo-outlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="right-panel">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><plus-outlined /></template>
|
||||
新增
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-content">
|
||||
<scTable
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@refresh="refreshTable"
|
||||
@paginationChange="handlePaginationChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useTable } from '@/hooks/useTable'
|
||||
|
||||
// 使用 useTable Hook
|
||||
const {
|
||||
tableRef,
|
||||
searchForm,
|
||||
tableData,
|
||||
loading,
|
||||
pagination,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePaginationChange,
|
||||
refreshTable
|
||||
} = useTable({
|
||||
api: api.roles.list.get,
|
||||
searchForm: {
|
||||
keyword: '',
|
||||
status: null
|
||||
},
|
||||
columns: [],
|
||||
needPagination: true
|
||||
})
|
||||
|
||||
// 表格列配置(包含筛选器)
|
||||
const columns = [
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
filters: [
|
||||
{ text: '正常', value: 1 },
|
||||
{ text: '禁用', value: 0 }
|
||||
],
|
||||
filterMultiple: false,
|
||||
slot: 'status'
|
||||
},
|
||||
// ...其他列
|
||||
]
|
||||
</script>
|
||||
```
|
||||
|
||||
**方式二: 使用高级搜索抽屉(复杂搜索条件)**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="pages-base-layout user-page">
|
||||
<div class="tool-bar">
|
||||
<div class="left-panel">
|
||||
<a-space>
|
||||
<a-input v-model:value="searchForm.username" placeholder="用户名" allow-clear style="width: 140px" />
|
||||
<a-button type="primary" @click="handleSearch">搜索</a-button>
|
||||
<a-button @click="handleReset">重置</a-button>
|
||||
<a-button @click="showAdvancedSearch = true">高级搜索</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-content">
|
||||
<scTable
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
@refresh="refreshTable"
|
||||
@paginationChange="handlePaginationChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高级搜索抽屉 -->
|
||||
<SearchDrawer
|
||||
v-model:visible="showAdvancedSearch"
|
||||
:form-data="searchForm"
|
||||
@confirm="handleAdvancedSearch"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useTable } from '@/hooks/useTable'
|
||||
import SearchDrawer from './components/SearchDrawer.vue'
|
||||
|
||||
const showAdvancedSearch = ref(false)
|
||||
|
||||
const handleAdvancedSearch = (formData) => {
|
||||
// 更新搜索表单数据
|
||||
Object.assign(searchForm, formData)
|
||||
showAdvancedSearch.value = false
|
||||
handleSearch()
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**SearchDrawer.vue 组件示例**:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<a-drawer v-model:open="visible" title="高级搜索" placement="right" width="400">
|
||||
<a-form :model="formData" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<a-form-item label="姓名">
|
||||
<a-input v-model:value="formData.real_name" placeholder="请输入姓名" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号">
|
||||
<a-input v-model:value="formData.phone" placeholder="请输入手机号" allow-clear />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-select v-model:value="formData.status" placeholder="请选择状态" allow-clear>
|
||||
<a-select-option :value="1">正常</a-select-option>
|
||||
<a-select-option :value="0">禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template #footer>
|
||||
<a-space>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" @click="handleConfirm">确定</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
visible: Boolean,
|
||||
formData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible', 'confirm'])
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm', { ...formData })
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
**搜索条件选择建议**:
|
||||
|
||||
常用搜索条件(显示在工具栏):
|
||||
- 主要关键字搜索(如用户名、角色名称)
|
||||
- 无需筛选的简单字段
|
||||
|
||||
表格筛选(推荐用于):
|
||||
- 状态、类型等枚举值字段
|
||||
- 不需要组合条件的单一筛选
|
||||
|
||||
高级搜索抽屉(用于):
|
||||
- 次要字段(如邮箱、手机号)
|
||||
- 日期范围筛选
|
||||
- 多条件组合筛选
|
||||
- 复杂的搜索逻辑
|
||||
|
||||
**scTable 组件筛选器优化说明**:
|
||||
|
||||
scTable 组件已支持 Ant Design Vue Table 的筛选功能,使用方式:
|
||||
|
||||
1. 在 `columns` 配置中添加 `filters` 属性
|
||||
2. 通过 `@filterChange` 事件处理筛选变化
|
||||
3. 将筛选条件合并到搜索表单中提交
|
||||
|
||||
```javascript
|
||||
// 在 useTable Hook 中处理筛选变化
|
||||
const handleFilterChange = (pagination, filters) => {
|
||||
// 将筛选条件添加到搜索表单
|
||||
if (filters.status) {
|
||||
searchForm.status = filters.status[0] // filterMultiple: false 时取第一个值
|
||||
}
|
||||
handleSearch()
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 组件开发规范
|
||||
|
||||
#### 组件命名
|
||||
|
||||
- **单文件组件**: 使用 PascalCase 命名,如 `UserList.vue`
|
||||
- **公共组件**: 以 `sc` 开头,如 `scTable.vue`
|
||||
- **页面组件**: 使用语义化命名,如 `UserManagement.vue`
|
||||
- **页面私有组件**: 使用语义化命名,如 `SaveDialog.vue`, `RoleDialog.vue`
|
||||
|
||||
#### 组件结构
|
||||
|
||||
@@ -121,7 +551,7 @@ #### 组件结构
|
||||
</style>
|
||||
```
|
||||
|
||||
### 3. API 接口开发规范
|
||||
### 4. API 接口开发规范
|
||||
|
||||
#### API 文件组织
|
||||
|
||||
@@ -189,7 +619,7 @@ #### 使用示例
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 路由开发规范
|
||||
### 5. 路由开发规范
|
||||
|
||||
#### 路由类型
|
||||
|
||||
@@ -243,7 +673,23 @@ #### 动态路由加载
|
||||
|
||||
**后端菜单数据格式**:
|
||||
|
||||
菜单权限节点需要遵循以下规范:
|
||||
菜单权限节点数据结构如下:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | number | 权限ID |
|
||||
| title | string | 权限标题(用于菜单显示) |
|
||||
| name | string | 权限编码(唯一标识) |
|
||||
| type | string | 权限类型:menu(菜单)、api(接口)、button(按钮)、url(链接) |
|
||||
| parent_id | number | 父级ID,顶级菜单为 0 |
|
||||
| path | string | 路由路径(如 `/system/users`) |
|
||||
| component | string | 前端组件路径(如 `system/users/index`) |
|
||||
| meta | object | 元数据(包含 icon、hidden、keepAlive 等) |
|
||||
| sort | number | 排序 |
|
||||
| status | number | 状态:1启用 0禁用 |
|
||||
| children | array | 子权限列表(树形结构) |
|
||||
|
||||
**菜单节点规范**:
|
||||
|
||||
1. **顶级菜单**(parent_id=0)不需要 component 值
|
||||
- 顶级菜单作为分组容器,不需要指定组件路径
|
||||
@@ -260,47 +706,77 @@ #### 动态路由加载
|
||||
- 前端会将菜单数据转换为路由,所有页面路由都会挂载到 Layout 布局下
|
||||
- 不需要在前端维护嵌套的路由结构
|
||||
|
||||
**数据示例**:
|
||||
|
||||
```javascript
|
||||
// 示例:顶级菜单(无 component)
|
||||
{
|
||||
name: '系统管理',
|
||||
code: 'system',
|
||||
id: 1,
|
||||
title: '系统管理',
|
||||
name: 'system',
|
||||
type: 'menu',
|
||||
parent_id: 0,
|
||||
route: '/system',
|
||||
path: '/system',
|
||||
component: null, // 顶级菜单不需要 component
|
||||
meta: {
|
||||
icon: 'Setting',
|
||||
hidden: false
|
||||
hidden: false,
|
||||
hiddenBreadcrumb: false,
|
||||
keepAlive: false
|
||||
},
|
||||
sort: 1
|
||||
sort: 1,
|
||||
status: 1,
|
||||
children: []
|
||||
}
|
||||
|
||||
// 示例:最后一级菜单(有 component)
|
||||
{
|
||||
name: '用户管理',
|
||||
code: 'system.users',
|
||||
id: 2,
|
||||
title: '用户管理',
|
||||
name: 'system.users',
|
||||
type: 'menu',
|
||||
parent_id: 0,
|
||||
route: '/system/users',
|
||||
path: '/system/users',
|
||||
component: 'system/users/index', // 最后一级菜单需要 component
|
||||
meta: {
|
||||
icon: 'User',
|
||||
hidden: false
|
||||
hidden: false,
|
||||
hiddenBreadcrumb: false,
|
||||
keepAlive: true
|
||||
},
|
||||
sort: 1
|
||||
sort: 1,
|
||||
status: 1,
|
||||
children: []
|
||||
}
|
||||
|
||||
// 示例:按钮权限(无 component)
|
||||
{
|
||||
name: '查看用户',
|
||||
code: 'system.users.view',
|
||||
id: 3,
|
||||
title: '查看用户',
|
||||
name: 'system.users.view',
|
||||
type: 'button',
|
||||
parent_id: 0,
|
||||
route: 'admin.users.index',
|
||||
parent_id: 2,
|
||||
path: null,
|
||||
component: null, // 非菜单类型不需要 component
|
||||
meta: null,
|
||||
sort: 1
|
||||
sort: 1,
|
||||
status: 1,
|
||||
children: []
|
||||
}
|
||||
|
||||
// 示例:API 权限
|
||||
{
|
||||
id: 4,
|
||||
title: '获取用户列表',
|
||||
name: 'system.users.list',
|
||||
type: 'api',
|
||||
parent_id: 0,
|
||||
path: 'admin.users.index',
|
||||
component: null,
|
||||
meta: null,
|
||||
sort: 1,
|
||||
status: 1,
|
||||
children: []
|
||||
}
|
||||
```
|
||||
|
||||
@@ -313,12 +789,12 @@ #### 动态路由加载
|
||||
function transformMenusToRoutes(menus) {
|
||||
return menus.map(menu => {
|
||||
const route = {
|
||||
path: menu.route,
|
||||
name: menu.name || menu.code,
|
||||
path: menu.path,
|
||||
name: menu.name,
|
||||
meta: {
|
||||
title: menu.name,
|
||||
title: menu.title,
|
||||
icon: menu.meta?.icon,
|
||||
hidden: menu.meta?.hidden,
|
||||
hidden: menu.meta?.hidden || false,
|
||||
keepAlive: menu.meta?.keepAlive || false
|
||||
}
|
||||
}
|
||||
@@ -346,9 +822,12 @@ #### 动态路由加载
|
||||
**菜单拉平处理说明**:
|
||||
|
||||
由于前端将所有页面菜单拉平挂载在 Layout 下,因此:
|
||||
- 后端菜单的 parent_id 主要用于构建菜单树的层级关系(侧边栏显示)
|
||||
- 后端菜单的 `parent_id` 主要用于构建菜单树的层级关系(侧边栏显示)
|
||||
- 路由层面不需要维护嵌套结构,所有页面路由都在同一层级
|
||||
- component 值只需要在最后一级菜单(叶子节点)中设置
|
||||
- `component` 值只需要在最后一级菜单(叶子节点)中设置
|
||||
- 路由 `path` 使用后端返回的 `path` 字段
|
||||
- 路由 `name` 使用后端返回的 `name` 字段(权限编码)
|
||||
- 路由 `meta.title` 使用后端返回的 `title` 字段(权限标题)
|
||||
|
||||
#### 路由守卫
|
||||
|
||||
@@ -387,7 +866,7 @@ #### 路由守卫
|
||||
})
|
||||
```
|
||||
|
||||
### 5. 状态管理规范
|
||||
### 6. 状态管理规范
|
||||
|
||||
#### Pinia Store 定义
|
||||
|
||||
@@ -547,7 +1026,7 @@ #### 权限指令
|
||||
</script>
|
||||
```
|
||||
|
||||
### 6. 表格开发规范
|
||||
### 7. 表格开发规范
|
||||
|
||||
#### 使用 useTable Hook
|
||||
|
||||
@@ -594,7 +1073,7 @@ #### scTable 组件使用
|
||||
</template>
|
||||
```
|
||||
|
||||
### 7. 表单开发规范
|
||||
### 8. 表单开发规范
|
||||
|
||||
#### scForm 组件使用
|
||||
|
||||
@@ -650,7 +1129,7 @@ #### scForm 组件使用
|
||||
</script>
|
||||
```
|
||||
|
||||
### 8. 图标使用规范
|
||||
### 9. 图标使用规范
|
||||
|
||||
#### Ant Design Vue Icons
|
||||
|
||||
@@ -680,7 +1159,7 @@ #### 常用图标
|
||||
- `info-circle`: 信息
|
||||
- `warning`: 警告
|
||||
|
||||
### 9. 国际化 (i18n) 规范
|
||||
### 10. 国际化 (i18n) 规范
|
||||
|
||||
#### 使用 i18n
|
||||
|
||||
@@ -711,7 +1190,7 @@ #### 语言文件组织
|
||||
}
|
||||
```
|
||||
|
||||
### 10. 文件上传规范
|
||||
### 11. 文件上传规范
|
||||
|
||||
#### scUpload 组件使用
|
||||
|
||||
@@ -732,7 +1211,7 @@ #### scUpload 组件使用
|
||||
</script>
|
||||
```
|
||||
|
||||
### 11. 富文本编辑器规范
|
||||
### 12. 富文本编辑器规范
|
||||
|
||||
#### scEditor 组件使用
|
||||
|
||||
@@ -757,12 +1236,129 @@ #### scEditor 组件使用
|
||||
</script>
|
||||
```
|
||||
|
||||
### 12. 样式规范
|
||||
### 13. 样式规范
|
||||
|
||||
#### 全局样式
|
||||
|
||||
在 `src/style.css` 中定义全局样式。
|
||||
|
||||
#### 页面公共样式
|
||||
|
||||
页面布局的公共样式已提取到全局样式文件中,各页面可直接使用:
|
||||
|
||||
**在 `src/assets/style/pages.scss` 中定义的公共类:**
|
||||
|
||||
```scss
|
||||
// 标准页面布局(垂直布局)
|
||||
.pages-base-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
.tool-bar {
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.left-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏布局(左右分栏)
|
||||
.pages-sidebar-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
.left-box {
|
||||
width: 260px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
.header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.right-box {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.tool-bar {
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.left-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.right-panel {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
|
||||
```vue
|
||||
<!-- 标准页面布局 -->
|
||||
<style scoped lang="scss">
|
||||
.user-page {
|
||||
@extend .pages-base-layout;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 侧边栏布局 -->
|
||||
<style scoped lang="scss">
|
||||
.user-page {
|
||||
@extend .pages-sidebar-layout;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
#### 组件样式
|
||||
|
||||
使用 `scoped` 避免样式污染:
|
||||
@@ -784,7 +1380,7 @@ #### 命名规范
|
||||
- 使用 BEM 命名法
|
||||
- 类名使用 kebab-case
|
||||
|
||||
### 13. 工具函数使用
|
||||
### 14. 工具函数使用
|
||||
|
||||
#### request.js (HTTP 请求)
|
||||
|
||||
@@ -816,7 +1412,7 @@ #### tool.js (工具函数)
|
||||
deepClone(originalObject)
|
||||
```
|
||||
|
||||
### 14. 开发流程
|
||||
### 15. 开发流程
|
||||
|
||||
#### 登录流程
|
||||
|
||||
@@ -875,7 +1471,7 @@ #### 添加新组件
|
||||
3. 添加必要的 Props 和 Emits
|
||||
4. 编写组件文档
|
||||
|
||||
### 15. 常用命令
|
||||
### 16. 常用命令
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
@@ -897,7 +1493,7 @@ # 代码检查
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### 16. 注意事项
|
||||
### 17. 注意事项
|
||||
|
||||
1. **禁止重复引入图标**: Ant Design Vue 图标已全局引入,直接使用即可
|
||||
2. **使用组合式 API**: 新代码统一使用 `<script setup>` 语法
|
||||
@@ -908,7 +1504,7 @@ ### 16. 注意事项
|
||||
7. **不要编写 demo**: 开发过程中不编写示例代码
|
||||
8. **测试提示**: 如需测试,提示用户是否运行测试,不主动运行
|
||||
|
||||
### 17. 代码质量
|
||||
### 18. 代码质量
|
||||
|
||||
- 遵循 Vue 3 官方风格指南
|
||||
- 保持代码简洁、可读
|
||||
@@ -916,7 +1512,7 @@ ### 17. 代码质量
|
||||
- 使用语义化的变量和函数命名
|
||||
- 避免过多的嵌套层级
|
||||
|
||||
### 18. 性能优化
|
||||
### 19. 性能优化
|
||||
|
||||
- 合理使用 `v-if` 和 `v-show`
|
||||
- 列表渲染必须设置 `key`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+61
-13
@@ -521,6 +521,22 @@ ### 统一响应格式
|
||||
}
|
||||
```
|
||||
|
||||
**树形响应:**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{...,children: [
|
||||
{...}
|
||||
]},
|
||||
{...,children: [
|
||||
{...}
|
||||
]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP 状态码规范
|
||||
|
||||
- `200 OK`: 请求成功
|
||||
@@ -612,7 +628,7 @@ #### 基础模块迁移
|
||||
|
||||
**表命名示例:**
|
||||
- Auth 模块: `auth_users`, `auth_roles`, `auth_permissions`, `auth_role_permission`
|
||||
- System 模块: `system_configs`, `system_logs`, `system_dictionaries`
|
||||
- System 模块: `system_setting`, `system_logs`, `system_dictionaries`
|
||||
|
||||
**迁移文件示例:**
|
||||
```php
|
||||
@@ -671,7 +687,22 @@ #### 业务模块数据填充
|
||||
|
||||
#### 菜单权限节点 Seed 规范
|
||||
|
||||
在 Seeder 文件中创建菜单权限节点时,需要遵循以下规范:
|
||||
在 Seeder 文件中创建菜单权限节点时,需要遵循以下规范。
|
||||
|
||||
**数据库字段说明 (auth_permissions 表):**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | number | 权限ID |
|
||||
| title | string | 权限标题(用于菜单显示) |
|
||||
| name | string | 权限编码(唯一标识) |
|
||||
| type | string | 权限类型:menu(菜单)、api(接口)、button(按钮)、url(链接) |
|
||||
| parent_id | number | 父级ID,顶级菜单为 0 |
|
||||
| path | string | 路由路径(如 `/system/users`) |
|
||||
| component | string | 前端组件路径(如 `system/users/index`) |
|
||||
| meta | json | 元数据(包含 icon、hidden、keepAlive 等) |
|
||||
| sort | number | 排序 |
|
||||
| status | number | 状态:1启用 0禁用 |
|
||||
|
||||
**菜单结构规范:**
|
||||
|
||||
@@ -690,7 +721,7 @@ #### 菜单权限节点 Seed 规范
|
||||
- 前端会将菜单数据转换为路由,所有页面路由都会挂载到 Layout 布局下
|
||||
- 不需要在前端维护嵌套的路由结构
|
||||
|
||||
**Seeder 示例:**
|
||||
** Seeder 示例:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
@@ -700,16 +731,17 @@ #### 菜单权限节点 Seed 规范
|
||||
$permissions = [
|
||||
// 顶级菜单(无 component)
|
||||
[
|
||||
'name' => '系统管理',
|
||||
'code' => 'system',
|
||||
'title' => '系统管理',
|
||||
'name' => 'system',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0,
|
||||
'route' => '/system',
|
||||
'path' => '/system',
|
||||
'component' => null, // 顶级菜单不需要 component
|
||||
'meta' => json_encode([
|
||||
'icon' => 'Setting',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
'keepAlive' => false
|
||||
]),
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
@@ -717,16 +749,17 @@ #### 菜单权限节点 Seed 规范
|
||||
|
||||
// 最后一级菜单(有 component)
|
||||
[
|
||||
'name' => '用户管理',
|
||||
'code' => 'system.users',
|
||||
'title' => '用户管理',
|
||||
'name' => 'system.users',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0,
|
||||
'route' => '/system/users',
|
||||
'path' => '/system/users',
|
||||
'component' => 'system/users/index', // 最后一级菜单需要 component
|
||||
'meta' => json_encode([
|
||||
'icon' => 'User',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
'keepAlive' => true
|
||||
]),
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
@@ -734,16 +767,29 @@ #### 菜单权限节点 Seed 规范
|
||||
|
||||
// 按钮权限(无 component)
|
||||
[
|
||||
'name' => '查看用户',
|
||||
'code' => 'system.users.view',
|
||||
'title' => '查看用户',
|
||||
'name' => 'system.users.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0,
|
||||
'route' => 'admin.users.index',
|
||||
'path' => 'admin.users.index',
|
||||
'component' => null, // 非菜单类型不需要 component
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
],
|
||||
|
||||
// API 权限
|
||||
[
|
||||
'title' => '获取用户列表',
|
||||
'name' => 'system.users.list',
|
||||
'type' => 'api',
|
||||
'parent_id' => 0,
|
||||
'path' => 'admin.users.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
@@ -754,8 +800,10 @@ #### 菜单权限节点 Seed 规范
|
||||
|
||||
**重要说明:**
|
||||
|
||||
- `title` 用于显示在菜单中的标题
|
||||
- `name` 是权限的唯一标识编码,用于前端路由和权限验证
|
||||
- `parent_id` 主要用于构建菜单树的层级关系(侧边栏显示)
|
||||
- 路由层面不需要维护嵌套结构,所有页面路由都在同一层级
|
||||
- `path` 用于菜单类型的路由路径,或 API 类型的接口路由名称
|
||||
- `component` 值只需要在最后一级菜单(叶子节点)中设置
|
||||
- 前端会根据 `type` 字段判断是否需要加载组件
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\System\NotificationService;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RetryUnsentNotifications extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'notifications:retry-unsent {--limit=100 : Maximum number of notifications to retry}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Retry sending unsent notifications via WebSocket';
|
||||
|
||||
/**
|
||||
* @var NotificationService
|
||||
*/
|
||||
protected $notificationService;
|
||||
|
||||
/**
|
||||
* RetryUnsentNotifications constructor
|
||||
*/
|
||||
public function __construct(NotificationService $notificationService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->notificationService = $notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$limit = (int) $this->option('limit');
|
||||
|
||||
$this->info('开始重试未发送的通知...');
|
||||
$this->info("最大处理数量: {$limit}");
|
||||
|
||||
try {
|
||||
$sentCount = $this->notificationService->retryUnsentNotifications($limit);
|
||||
|
||||
if ($sentCount > 0) {
|
||||
$this->info("成功发送 {$sentCount} 条通知");
|
||||
} else {
|
||||
$this->info('没有需要重试的通知');
|
||||
}
|
||||
|
||||
Log::info('重试未发送通知完成', [
|
||||
'sent_count' => $sentCount,
|
||||
'limit' => $limit
|
||||
]);
|
||||
|
||||
return self::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$this->error('重试未发送通知失败: ' . $e->getMessage());
|
||||
Log::error('重试未发送通知失败', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,15 +53,21 @@ public function headings(): array
|
||||
*/
|
||||
public function map($department): array
|
||||
{
|
||||
$parentName = '';
|
||||
if ($department->parent_id) {
|
||||
$parent = Department::find($department->parent_id);
|
||||
$parentName = $parent ? $parent->name : '';
|
||||
}
|
||||
|
||||
return [
|
||||
$department->id,
|
||||
$department->name,
|
||||
$department->parent_id ? Department::find($department->parent_id)?->name : '',
|
||||
$parentName,
|
||||
$department->leader,
|
||||
$department->phone,
|
||||
$department->sort,
|
||||
(int)$department->sort,
|
||||
$department->status == 1 ? '启用' : '禁用',
|
||||
$department->created_at ? $department->created_at->toDateTimeString() : '',
|
||||
$department->created_at ? (string)$department->created_at : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
|
||||
class PermissionExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize
|
||||
{
|
||||
protected $permissionIds;
|
||||
|
||||
public function __construct(array $permissionIds = [])
|
||||
{
|
||||
$this->permissionIds = $permissionIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据集合
|
||||
*/
|
||||
public function collection()
|
||||
{
|
||||
$query = Permission::query();
|
||||
|
||||
if (!empty($this->permissionIds)) {
|
||||
$query->whereIn('id', $this->permissionIds);
|
||||
}
|
||||
|
||||
return $query->orderBy('sort')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表头
|
||||
*/
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'ID',
|
||||
'权限标题',
|
||||
'权限编码',
|
||||
'权限类型',
|
||||
'父级ID',
|
||||
'路由路径',
|
||||
'前端组件',
|
||||
'排序',
|
||||
'状态',
|
||||
'创建时间',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射数据
|
||||
*/
|
||||
public function map($permission): array
|
||||
{
|
||||
return [
|
||||
$permission->id,
|
||||
$permission->title,
|
||||
$permission->name,
|
||||
$this->getTypeName($permission->type),
|
||||
$permission->parent_id ?: 0,
|
||||
$permission->path,
|
||||
$permission->component,
|
||||
(int)$permission->sort,
|
||||
$permission->status == 1 ? '启用' : '禁用',
|
||||
$permission->created_at ? (string)$permission->created_at : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类型名称
|
||||
*/
|
||||
protected function getTypeName($type): string
|
||||
{
|
||||
$types = [
|
||||
'menu' => '菜单',
|
||||
'api' => 'API接口',
|
||||
'button' => '按钮',
|
||||
'url' => '链接',
|
||||
];
|
||||
return $types[$type] ?? $type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exports;
|
||||
|
||||
use App\Models\Auth\Role;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
|
||||
class RoleExport implements FromCollection, WithHeadings, WithMapping, ShouldAutoSize
|
||||
{
|
||||
protected $roleIds;
|
||||
|
||||
public function __construct(array $roleIds = [])
|
||||
{
|
||||
$this->roleIds = $roleIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据集合
|
||||
*/
|
||||
public function collection()
|
||||
{
|
||||
$query = Role::with(['permissions']);
|
||||
|
||||
if (!empty($this->roleIds)) {
|
||||
$query->whereIn('id', $this->roleIds);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置表头
|
||||
*/
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'ID',
|
||||
'角色名称',
|
||||
'角色编码',
|
||||
'描述',
|
||||
'权限',
|
||||
'排序',
|
||||
'状态',
|
||||
'创建时间',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射数据
|
||||
*/
|
||||
public function map($role): array
|
||||
{
|
||||
return [
|
||||
$role->id,
|
||||
$role->name,
|
||||
$role->code,
|
||||
$role->description,
|
||||
$role->permissions->pluck('title')->implode(','),
|
||||
(int)$role->sort,
|
||||
$role->status == 1 ? '启用' : '禁用',
|
||||
$role->created_at ? (string)$role->created_at : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,8 @@ public function map($user): array
|
||||
$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() : '',
|
||||
$user->last_login_at ? (string)$user->last_login_at : '',
|
||||
$user->created_at ? (string)$user->created_at : '',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,15 @@ public function index(Request $request)
|
||||
/**
|
||||
* 获取部门树
|
||||
*/
|
||||
public function tree()
|
||||
public function tree(Request $request)
|
||||
{
|
||||
$result = $this->departmentService->getTree();
|
||||
$params = $request->only(['keyword', 'status']);
|
||||
$result = $this->departmentService->getTree($params);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => ['tree' => $result],
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'parent_id' => 'nullable|integer|exists:auth_departments,id',
|
||||
'parent_id' => 'nullable|integer',
|
||||
'leader' => 'nullable|string|max:50',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
@@ -107,7 +108,7 @@ public function update(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'nullable|string|max:50',
|
||||
'parent_id' => 'nullable|integer|exists:auth_departments,id',
|
||||
'parent_id' => 'nullable|integer',
|
||||
'leader' => 'nullable|string|max:50',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
|
||||
@@ -4,15 +4,20 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Auth\PermissionService;
|
||||
use App\Services\Auth\ImportExportService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Permission extends Controller
|
||||
{
|
||||
protected $permissionService;
|
||||
protected $importExportService;
|
||||
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
public function __construct(
|
||||
PermissionService $permissionService,
|
||||
ImportExportService $importExportService
|
||||
) {
|
||||
$this->permissionService = $permissionService;
|
||||
$this->importExportService = $importExportService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +46,7 @@ public function tree(Request $request)
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => ['tree' => $result],
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -55,7 +60,7 @@ public function menu(Request $request)
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => ['tree' => $result],
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -80,16 +85,32 @@ public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'required|string|max:50',
|
||||
'name' => 'required|string|max:100|unique:auth_permissions,name',
|
||||
'name' => 'required|string|max:100|unique:auth_permission,name',
|
||||
'type' => 'required|in:menu,api,button',
|
||||
'route' => 'nullable|string|max:200',
|
||||
'component' => 'nullable|string|max:200',
|
||||
'parent_id' => 'nullable|integer|exists:auth_permissions,id',
|
||||
'parent_id' => 'nullable|integer|min:0',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
'status' => 'nullable|integer|in:0,1',
|
||||
'meta' => 'nullable|array',
|
||||
], [], [
|
||||
'parent_id.exists' => '父级权限不存在',
|
||||
]);
|
||||
|
||||
// 额外验证:如果 parent_id 不为 0,则必须存在
|
||||
if (!empty($validated['parent_id']) && $validated['parent_id'] != 0) {
|
||||
$parent = \App\Models\Auth\Permission::find($validated['parent_id']);
|
||||
if (!$parent) {
|
||||
return response()->json([
|
||||
'code' => 422,
|
||||
'message' => '验证失败',
|
||||
'data' => [
|
||||
'parent_id' => ['父级权限不存在']
|
||||
]
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->permissionService->create($validated);
|
||||
|
||||
return response()->json([
|
||||
@@ -106,16 +127,32 @@ public function update(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'title' => 'nullable|string|max:50',
|
||||
'name' => 'nullable|string|max:100|unique:auth_permissions,name,' . $id,
|
||||
'name' => 'nullable|string|max:100|unique:auth_permission,name,' . $id,
|
||||
'type' => 'nullable|in:menu,api,button',
|
||||
'route' => 'nullable|string|max:200',
|
||||
'component' => 'nullable|string|max:200',
|
||||
'parent_id' => 'nullable|integer|exists:auth_permissions,id',
|
||||
'parent_id' => 'nullable|integer|min:0',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
'status' => 'nullable|integer|in:0,1',
|
||||
'meta' => 'nullable|array',
|
||||
], [], [
|
||||
'parent_id.exists' => '父级权限不存在',
|
||||
]);
|
||||
|
||||
// 额外验证:如果 parent_id 不为 0,则必须存在
|
||||
if (isset($validated['parent_id']) && !empty($validated['parent_id']) && $validated['parent_id'] != 0) {
|
||||
$parent = \App\Models\Auth\Permission::find($validated['parent_id']);
|
||||
if (!$parent) {
|
||||
return response()->json([
|
||||
'code' => 422,
|
||||
'message' => '验证失败',
|
||||
'data' => [
|
||||
'parent_id' => ['父级权限不存在']
|
||||
]
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->permissionService->update($id, $validated);
|
||||
|
||||
return response()->json([
|
||||
@@ -177,4 +214,54 @@ public function batchUpdateStatus(Request $request)
|
||||
'data' => ['count' => $count],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出权限
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'ids' => 'nullable|array',
|
||||
'ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$filename = $this->importExportService->exportPermissions($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->importPermissions($filename, $realPath);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => "导入完成,成功 {$result['success_count']} 条,失败 {$result['error_count']} 条",
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载权限导入模板
|
||||
*/
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$filename = $this->importExportService->downloadPermissionTemplate();
|
||||
$filePath = $this->importExportService->getExportFilePath($filename);
|
||||
|
||||
return response()->download($filePath, $filename)->deleteFileAfterSend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,20 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Auth\RoleService;
|
||||
use App\Services\Auth\ImportExportService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Role extends Controller
|
||||
{
|
||||
protected $roleService;
|
||||
protected $importExportService;
|
||||
|
||||
public function __construct(RoleService $roleService)
|
||||
{
|
||||
public function __construct(
|
||||
RoleService $roleService,
|
||||
ImportExportService $importExportService
|
||||
) {
|
||||
$this->roleService = $roleService;
|
||||
$this->importExportService = $importExportService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,7 +45,7 @@ public function getAll()
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => ['list' => $result],
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -65,12 +70,12 @@ public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'code' => 'required|string|max:50|unique:auth_roles,code',
|
||||
'code' => 'required|string|max:50|unique:auth_role,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',
|
||||
'permission_ids.*' => 'integer|exists:auth_permission,id',
|
||||
]);
|
||||
|
||||
$result = $this->roleService->create($validated);
|
||||
@@ -89,12 +94,12 @@ public function update(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'nullable|string|max:50',
|
||||
'code' => 'nullable|string|max:50|unique:auth_roles,code,' . $id,
|
||||
'code' => 'nullable|string|max:50|unique:auth_role,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',
|
||||
'permission_ids.*' => 'integer|exists:auth_permission,id',
|
||||
]);
|
||||
|
||||
$result = $this->roleService->update($id, $validated);
|
||||
@@ -166,7 +171,7 @@ public function assignPermissions(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'permission_ids' => 'required|array',
|
||||
'permission_ids.*' => 'integer|exists:auth_permissions,id',
|
||||
'permission_ids.*' => 'integer|exists:auth_permission,id',
|
||||
]);
|
||||
|
||||
$this->roleService->assignPermissions($id, $validated['permission_ids']);
|
||||
@@ -199,7 +204,7 @@ public function copy(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
'code' => 'required|string|max:50|unique:auth_roles,code',
|
||||
'code' => 'required|string|max:50|unique:auth_role,code',
|
||||
'description' => 'nullable|string|max:200',
|
||||
'sort' => 'nullable|integer|min:0',
|
||||
'status' => 'nullable|integer|in:0,1',
|
||||
@@ -237,4 +242,54 @@ public function batchCopy(Request $request)
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出角色
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'ids' => 'nullable|array',
|
||||
'ids.*' => 'integer',
|
||||
]);
|
||||
|
||||
$filename = $this->importExportService->exportRoles($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->importRoles($filename, $realPath);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => "导入完成,成功 {$result['success_count']} 条,失败 {$result['error_count']} 条",
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载角色导入模板
|
||||
*/
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$filename = $this->importExportService->downloadRoleTemplate();
|
||||
$filePath = $this->importExportService->getExportFilePath($filename);
|
||||
|
||||
return response()->download($filePath, $filename)->deleteFileAfterSend();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,10 @@ public function show(Request $request, $id)
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'username' => 'required|string|max:50|unique:auth_users,username',
|
||||
'username' => 'required|string|max:50|unique:auth_user,username',
|
||||
'password' => 'required|string|min:6',
|
||||
'real_name' => 'required|string|max:50',
|
||||
'email' => 'nullable|email|unique:auth_users,email',
|
||||
'email' => 'nullable|email|unique:auth_user,email',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'department_id' => 'nullable|integer|exists:auth_departments,id',
|
||||
'role_ids' => 'nullable|array',
|
||||
@@ -86,11 +86,12 @@ public function store(Request $request)
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'username' => 'nullable|string|max:50|unique:auth_users,username,' . $id,
|
||||
'username' => 'nullable|string|max:50|unique:auth_user,username,' . $id,
|
||||
'password' => 'nullable|string|min:6',
|
||||
'real_name' => 'nullable|string|max:50',
|
||||
'email' => 'nullable|email|unique:auth_users,email,' . $id,
|
||||
'email' => 'nullable|email|unique:auth_user,email,' . $id,
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'avatar' => 'nullable|string|max:500',
|
||||
'department_id' => 'nullable|integer|exists:auth_departments,id',
|
||||
'role_ids' => 'nullable|array',
|
||||
'role_ids.*' => 'integer|exists:auth_roles,id',
|
||||
|
||||
@@ -130,9 +130,9 @@ public function batchUpdateStatus(Request $request)
|
||||
]);
|
||||
}
|
||||
|
||||
public function children(int $parentId)
|
||||
public function children(string $parentCode)
|
||||
{
|
||||
$children = $this->cityService->getChildren($parentId);
|
||||
$children = $this->cityService->getChildren($parentCode);
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
|
||||
@@ -132,7 +132,14 @@ public function getGroups()
|
||||
|
||||
public function getByGroup(Request $request)
|
||||
{
|
||||
$configs = $this->configService->getByGroup($request->input('group'));
|
||||
$group = $request->input('group');
|
||||
// 如果没有指定分组,返回所有配置项
|
||||
if (empty($group)) {
|
||||
$result = $this->configService->getList([]);
|
||||
$configs = $result['list'] ?? [];
|
||||
} else {
|
||||
$configs = $this->configService->getByGroup($group);
|
||||
}
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
|
||||
@@ -140,6 +140,16 @@ public function getItemsList(Request $request)
|
||||
]);
|
||||
}
|
||||
|
||||
public function getAllItems()
|
||||
{
|
||||
$items = $this->dictionaryService->getAllItems();
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => $items
|
||||
]);
|
||||
}
|
||||
|
||||
public function storeItem(Request $request)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\LogRequest;
|
||||
use App\Services\System\LogService;
|
||||
use Illuminate\Http\Request;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\GenericExport;
|
||||
|
||||
@@ -30,39 +31,9 @@ public function index(LogRequest $request)
|
||||
public function export(LogRequest $request)
|
||||
{
|
||||
$params = $request->validated();
|
||||
$pageSize = $params['page_size'] ?? 10000; // 导出时默认获取更多数据
|
||||
$filePath = $this->logService->export($params);
|
||||
|
||||
// 获取所有符合条件的日志(不分页)
|
||||
$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);
|
||||
return response()->download($filePath)->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
public function show(int $id)
|
||||
@@ -93,7 +64,7 @@ public function destroy(int $id)
|
||||
]);
|
||||
}
|
||||
|
||||
public function batchDelete(Request $request)
|
||||
public function batchDelete(\Illuminate\Http\Request $request)
|
||||
{
|
||||
$this->logService->batchDelete($request->input('ids', []));
|
||||
return response()->json([
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\System\Admin;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Services\System\NotificationService;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class Notification extends Controller
|
||||
{
|
||||
/**
|
||||
* @var NotificationService
|
||||
*/
|
||||
protected $notificationService;
|
||||
|
||||
/**
|
||||
* Notification constructor
|
||||
*/
|
||||
public function __construct(NotificationService $notificationService)
|
||||
{
|
||||
$this->notificationService = $notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知列表
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$params = $request->all();
|
||||
|
||||
// 如果没有指定user_id,使用当前登录用户
|
||||
if (empty($params['user_id'])) {
|
||||
$params['user_id'] = auth('admin')->id();
|
||||
}
|
||||
|
||||
$result = $this->notificationService->getList($params);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读通知
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function unread(Request $request): JsonResponse
|
||||
{
|
||||
$userId = auth('admin')->id();
|
||||
$limit = $request->input('limit', $request->input('page_size', 10));
|
||||
$page = $request->input('page', 1);
|
||||
$type = $request->input('type');
|
||||
|
||||
$result = $this->notificationService->getUnreadNotifications($userId, $limit, $page, $type);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读通知数量
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function unreadCount(): JsonResponse
|
||||
{
|
||||
$userId = auth('admin')->id();
|
||||
$count = $this->notificationService->getUnreadCount($userId);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => [
|
||||
'count' => $count
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知详情
|
||||
*
|
||||
* @param int $id
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$notification = $this->notificationService->getById($id);
|
||||
|
||||
if (!$notification) {
|
||||
return response()->json([
|
||||
'code' => 404,
|
||||
'message' => 'Notification not found',
|
||||
'data' => null
|
||||
], 404);
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if ($notification->user_id !== auth('admin')->id()) {
|
||||
return response()->json([
|
||||
'code' => 403,
|
||||
'message' => 'Access denied',
|
||||
'data' => null
|
||||
], 403);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => $notification
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记通知为已读
|
||||
*
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function markAsRead(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$userId = auth('admin')->id();
|
||||
$result = $this->notificationService->markAsRead($id, $userId);
|
||||
|
||||
if (!$result) {
|
||||
return response()->json([
|
||||
'code' => 404,
|
||||
'message' => 'Notification not found or access denied',
|
||||
'data' => null
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'Notification marked as read',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量标记通知为已读
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function batchMarkAsRead(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array',
|
||||
'ids.*' => 'integer'
|
||||
]);
|
||||
|
||||
$userId = auth('admin')->id();
|
||||
$ids = $request->input('ids');
|
||||
|
||||
$count = $this->notificationService->batchMarkAsRead($ids, $userId);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'Notifications marked as read',
|
||||
'data' => [
|
||||
'count' => $count
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记所有通知为已读
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function markAllAsRead(): JsonResponse
|
||||
{
|
||||
$userId = auth('admin')->id();
|
||||
$count = $this->notificationService->markAllAsRead($userId);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'All notifications marked as read',
|
||||
'data' => [
|
||||
'count' => $count
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除通知
|
||||
*
|
||||
* @param int $id
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
$userId = auth('admin')->id();
|
||||
$result = $this->notificationService->delete($id, $userId);
|
||||
|
||||
if (!$result) {
|
||||
return response()->json([
|
||||
'code' => 404,
|
||||
'message' => 'Notification not found or access denied',
|
||||
'data' => null
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'Notification deleted',
|
||||
'data' => null
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除通知
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function batchDelete(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'ids' => 'required|array',
|
||||
'ids.*' => 'integer'
|
||||
]);
|
||||
|
||||
$userId = auth('admin')->id();
|
||||
$ids = $request->input('ids');
|
||||
|
||||
$count = $this->notificationService->batchDelete($ids, $userId);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'Notifications deleted',
|
||||
'data' => [
|
||||
'count' => $count
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空已读通知
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function clearRead(): JsonResponse
|
||||
{
|
||||
$userId = auth('admin')->id();
|
||||
$count = $this->notificationService->clearReadNotifications($userId);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'Read notifications cleared',
|
||||
'data' => [
|
||||
'count' => $count
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知统计
|
||||
*
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function statistics(): JsonResponse
|
||||
{
|
||||
$userId = auth('admin')->id();
|
||||
$stats = $this->notificationService->getStatistics($userId);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => $stats
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通知(管理员功能)
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function send(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'user_ids' => 'nullable|array',
|
||||
'user_ids.*' => 'integer',
|
||||
'title' => 'required|string|max:200',
|
||||
'content' => 'required|string',
|
||||
'type' => 'required|string|in:info,success,warning,error,task,system',
|
||||
'category' => 'nullable|string|in:system,task,message,reminder,announcement',
|
||||
'data' => 'nullable|array',
|
||||
'action_type' => 'nullable|string|in:link,modal,none',
|
||||
'action_data' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$userIds = $request->input('user_ids');
|
||||
$title = $request->input('title');
|
||||
$content = $request->input('content');
|
||||
$type = $request->input('type', 'info');
|
||||
$category = $request->input('category', 'system');
|
||||
$extraData = $request->input('data', []);
|
||||
|
||||
// 如果没有指定user_ids,则发送给所有用户
|
||||
if (empty($userIds)) {
|
||||
$result = $this->notificationService->broadcast(
|
||||
$title,
|
||||
$content,
|
||||
$type,
|
||||
$category,
|
||||
$extraData
|
||||
);
|
||||
} else {
|
||||
$result = $this->notificationService->sendToUsers(
|
||||
$userIds,
|
||||
$title,
|
||||
$content,
|
||||
$type,
|
||||
$category,
|
||||
$extraData
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'Notification sent successfully',
|
||||
'data' => [
|
||||
'count' => count($result)
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试发送未发送的通知(管理员功能)
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function retryUnsent(Request $request): JsonResponse
|
||||
{
|
||||
$limit = $request->input('limit', 100);
|
||||
$count = $this->notificationService->retryUnsentNotifications($limit);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'Unsent notifications retried',
|
||||
'data' => [
|
||||
'count' => $count
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,28 @@ public function index()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字典数据(包含字典项)
|
||||
* 用于前端登录后缓存所有字典数据
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
$dictionaries = $this->dictionaryService->getAll();
|
||||
|
||||
// 为每个字典添加 items 字段
|
||||
$result = array_map(function($dictionary) {
|
||||
$items = $this->dictionaryService->getItemsByCode($dictionary['code']);
|
||||
$dictionary['items'] = $items;
|
||||
return $dictionary;
|
||||
}, $dictionaries);
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'message' => 'success',
|
||||
'data' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
public function getByCode(Request $request)
|
||||
{
|
||||
$code = $request->input('code');
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class AuthCheckMiddleware
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
class LogRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
* Determine if user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -19,7 +19,7 @@ public function authorize(): bool
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
* Get validation rules that apply to request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -34,6 +34,7 @@ public function rules(): array
|
||||
'username' => 'nullable|string|max:50',
|
||||
'module' => 'nullable|string|max:50',
|
||||
'action' => 'nullable|string|max:100',
|
||||
'method' => 'nullable|in:GET,POST,PUT,DELETE,PATCH',
|
||||
'status' => 'nullable|in:success,error',
|
||||
'start_date' => 'nullable|date',
|
||||
'end_date' => 'nullable|date|after_or_equal:start_date',
|
||||
@@ -77,6 +78,7 @@ public function messages(): array
|
||||
'username.max' => '用户名最多50个字符',
|
||||
'module.max' => '模块名最多50个字符',
|
||||
'action.max' => '操作名最多100个字符',
|
||||
'method.in' => '请求方式必须是 GET、POST、PUT、DELETE 或 PATCH',
|
||||
'status.in' => '状态值必须是 success 或 error',
|
||||
'start_date.date' => '开始日期格式不正确',
|
||||
'end_date.date' => '结束日期格式不正确',
|
||||
@@ -121,7 +123,7 @@ protected function failedValidation(Validator $validator): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
* Prepare for validation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||
|
||||
class PermissionImport 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 = Permission::where('name', $row['权限编码'])->exists();
|
||||
if ($exists) {
|
||||
$this->addError($index + 2, '权限编码已存在');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 查找父级权限
|
||||
$parentId = null;
|
||||
if (!empty($row['父级ID']) && $row['父级ID'] != 0) {
|
||||
$parent = Permission::find($row['父级ID']);
|
||||
if (!$parent) {
|
||||
$this->addError($index + 2, '父级权限不存在');
|
||||
continue;
|
||||
}
|
||||
$parentId = $parent->id;
|
||||
}
|
||||
|
||||
// 解析类型
|
||||
$type = $this->parseType($row['权限类型']);
|
||||
|
||||
// 解析元数据
|
||||
$meta = null;
|
||||
if ($type === 'menu' && !empty($row['元数据'])) {
|
||||
// 元数据格式: icon:Setting,hidden:false,keepAlive:false
|
||||
$meta = $this->parseMeta($row['元数据']);
|
||||
}
|
||||
|
||||
// 创建权限
|
||||
Permission::create([
|
||||
'title' => $row['权限标题'],
|
||||
'name' => $row['权限编码'],
|
||||
'type' => $type,
|
||||
'parent_id' => $parentId,
|
||||
'path' => $row['路由路径'] ?? null,
|
||||
'component' => $row['前端组件'] ?? null,
|
||||
'meta' => $meta,
|
||||
'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',
|
||||
'权限编码' => 'required|string|max:100',
|
||||
'权限类型' => 'required|in:菜单,API接口,按钮,链接,menu,api,button,url',
|
||||
'父级ID' => 'nullable|integer|min:0',
|
||||
'路由路径' => 'nullable|string|max:200',
|
||||
'前端组件' => 'nullable|string|max:200',
|
||||
'排序' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证消息
|
||||
*/
|
||||
public function customValidationMessages(): array
|
||||
{
|
||||
return [
|
||||
'权限标题.required' => '权限标题不能为空',
|
||||
'权限编码.required' => '权限编码不能为空',
|
||||
'权限类型.required' => '权限类型不能为空',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析类型
|
||||
*/
|
||||
protected function parseType($type): string
|
||||
{
|
||||
$typeMap = [
|
||||
'菜单' => 'menu',
|
||||
'API接口' => 'api',
|
||||
'接口' => 'api',
|
||||
'按钮' => 'button',
|
||||
'链接' => 'url',
|
||||
];
|
||||
|
||||
return $typeMap[$type] ?? $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析元数据
|
||||
*/
|
||||
protected function parseMeta($metaString): ?string
|
||||
{
|
||||
if (empty($metaString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 简单解析,实际项目中可能需要更复杂的解析逻辑
|
||||
return $metaString;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加错误
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use App\Models\Auth\Role;
|
||||
use App\Models\Auth\Permission;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||
|
||||
class RoleImport 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 = Role::where('code', $row['角色编码'])->exists();
|
||||
if ($exists) {
|
||||
$this->addError($index + 2, '角色编码已存在');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 查找权限
|
||||
$permissionIds = [];
|
||||
if (!empty($row['权限(多个用逗号分隔)'])) {
|
||||
$permissionNames = array_map('trim', explode(',', $row['权限(多个用逗号分隔)']));
|
||||
$permissions = Permission::whereIn('title', $permissionNames)->get();
|
||||
|
||||
if ($permissions->count() != count($permissionNames)) {
|
||||
$existingNames = $permissions->pluck('title')->toArray();
|
||||
$notFound = array_diff($permissionNames, $existingNames);
|
||||
$this->addError($index + 2, '权限不存在: ' . implode(', ', $notFound));
|
||||
continue;
|
||||
}
|
||||
$permissionIds = $permissions->pluck('id')->toArray();
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
$role = Role::create([
|
||||
'name' => $row['角色名称'],
|
||||
'code' => $row['角色编码'],
|
||||
'description' => $row['描述'] ?? null,
|
||||
'sort' => $row['排序'] ?? 0,
|
||||
'status' => 1,
|
||||
]);
|
||||
|
||||
// 分配权限
|
||||
if (!empty($permissionIds)) {
|
||||
$role->permissions()->attach($permissionIds);
|
||||
}
|
||||
|
||||
$this->successCount++;
|
||||
} catch (\Exception $e) {
|
||||
$this->addError($index + 2, $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'角色名称' => 'required|string|max:50',
|
||||
'角色编码' => 'required|string|max:50',
|
||||
'描述' => 'nullable|string|max:200',
|
||||
'排序' => 'nullable|integer|min:0',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证消息
|
||||
*/
|
||||
public function customValidationMessages(): array
|
||||
{
|
||||
return [
|
||||
'角色名称.required' => '角色名称不能为空',
|
||||
'角色编码.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;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
namespace App\Models\Auth;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Department extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'auth_departments';
|
||||
protected $table = 'auth_department';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
namespace App\Models\Auth;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Permission extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'auth_permissions';
|
||||
protected $table = 'auth_permission';
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
namespace App\Models\Auth;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'auth_roles';
|
||||
protected $table = 'auth_role';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models\Auth;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
@@ -11,9 +12,9 @@
|
||||
|
||||
class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
use SoftDeletes;
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'auth_users';
|
||||
protected $table = 'auth_user';
|
||||
|
||||
protected $fillable = [
|
||||
'username',
|
||||
@@ -87,6 +88,14 @@ public function hasRole(string $roleCode): bool
|
||||
return $this->roles()->where('code', $roleCode)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否为超级管理员
|
||||
*/
|
||||
public function isSuperAdmin(): bool
|
||||
{
|
||||
return $this->hasRole('super_admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 JWT 标识符
|
||||
*
|
||||
|
||||
+21
-24
@@ -2,45 +2,42 @@
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class City extends Model
|
||||
{
|
||||
protected $table = 'system_cities';
|
||||
use ModelTrait;
|
||||
protected $table = 'system_city';
|
||||
|
||||
protected $fillable = [
|
||||
'parent_id',
|
||||
'name',
|
||||
'title',
|
||||
'code',
|
||||
'pinyin',
|
||||
'pinyin_short',
|
||||
'level',
|
||||
'sort',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'parent_id' => 'integer',
|
||||
'level' => 'integer',
|
||||
'sort' => 'integer',
|
||||
'status' => 'boolean',
|
||||
'parent_code',
|
||||
];
|
||||
|
||||
/**
|
||||
* 子级城市
|
||||
*/
|
||||
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');
|
||||
return $this->hasMany(City::class, 'parent_code', 'code');
|
||||
}
|
||||
|
||||
/**
|
||||
* 父级城市
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(City::class, 'parent_id');
|
||||
return $this->belongsTo(City::class, 'parent_code', 'code');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否是顶级城市(省/直辖市/自治区)
|
||||
*/
|
||||
public function isTopLevel(): bool
|
||||
{
|
||||
return empty($this->parent_code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Config extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'system_configs';
|
||||
protected $table = 'system_setting';
|
||||
|
||||
protected $fillable = [
|
||||
'group',
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Dictionary extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'system_dictionaries';
|
||||
protected $table = 'system_dictionary';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'code',
|
||||
'description',
|
||||
'value_type',
|
||||
'status',
|
||||
'sort',
|
||||
];
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DictionaryItem extends Model
|
||||
{
|
||||
protected $table = 'system_dictionary_items';
|
||||
use ModelTrait;
|
||||
protected $table = 'system_dictionary_item';
|
||||
|
||||
protected $fillable = [
|
||||
'dictionary_id',
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Log extends Model
|
||||
{
|
||||
protected $table = 'system_logs';
|
||||
use ModelTrait;
|
||||
protected $table = 'system_log';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'system_notification';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'title',
|
||||
'content',
|
||||
'type',
|
||||
'category',
|
||||
'data',
|
||||
'action_type',
|
||||
'action_data',
|
||||
'is_read',
|
||||
'read_at',
|
||||
'sent_via_websocket',
|
||||
'sent_at',
|
||||
'retry_count',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'data' => 'array',
|
||||
'is_read' => 'boolean',
|
||||
'sent_via_websocket' => 'boolean',
|
||||
'read_at' => 'datetime',
|
||||
'sent_at' => 'datetime',
|
||||
'retry_count' => 'integer',
|
||||
];
|
||||
|
||||
/**
|
||||
* 通知类型常量
|
||||
*/
|
||||
const TYPE_INFO = 'info';
|
||||
const TYPE_SUCCESS = 'success';
|
||||
const TYPE_WARNING = 'warning';
|
||||
const TYPE_ERROR = 'error';
|
||||
const TYPE_TASK = 'task';
|
||||
const TYPE_SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* 通知分类常量
|
||||
*/
|
||||
const CATEGORY_SYSTEM = 'system';
|
||||
const CATEGORY_TASK = 'task';
|
||||
const CATEGORY_MESSAGE = 'message';
|
||||
const CATEGORY_REMINDER = 'reminder';
|
||||
const CATEGORY_ANNOUNCEMENT = 'announcement';
|
||||
|
||||
/**
|
||||
* 操作类型常量
|
||||
*/
|
||||
const ACTION_LINK = 'link';
|
||||
const ACTION_MODAL = 'modal';
|
||||
const ACTION_NONE = 'none';
|
||||
|
||||
/**
|
||||
* 关联用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Models\Auth\User::class, 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为已读
|
||||
*/
|
||||
public function markAsRead(): bool
|
||||
{
|
||||
$this->is_read = true;
|
||||
$this->read_at = now();
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为未读
|
||||
*/
|
||||
public function markAsUnread(): bool
|
||||
{
|
||||
$this->is_read = false;
|
||||
$this->read_at = null;
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记已通过WebSocket发送
|
||||
*/
|
||||
public function markAsSent(): bool
|
||||
{
|
||||
$this->sent_via_websocket = true;
|
||||
$this->sent_at = now();
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加重试次数
|
||||
*/
|
||||
public function incrementRetry(): bool
|
||||
{
|
||||
$this->increment('retry_count');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知类型选项
|
||||
*/
|
||||
public static function getTypeOptions(): array
|
||||
{
|
||||
return [
|
||||
self::TYPE_INFO => '信息',
|
||||
self::TYPE_SUCCESS => '成功',
|
||||
self::TYPE_WARNING => '警告',
|
||||
self::TYPE_ERROR => '错误',
|
||||
self::TYPE_TASK => '任务',
|
||||
self::TYPE_SYSTEM => '系统',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知分类选项
|
||||
*/
|
||||
public static function getCategoryOptions(): array
|
||||
{
|
||||
return [
|
||||
self::CATEGORY_SYSTEM => '系统通知',
|
||||
self::CATEGORY_TASK => '任务通知',
|
||||
self::CATEGORY_MESSAGE => '消息通知',
|
||||
self::CATEGORY_REMINDER => '提醒通知',
|
||||
self::CATEGORY_ANNOUNCEMENT => '公告通知',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作类型选项
|
||||
*/
|
||||
public static function getActionTypeOptions(): array
|
||||
{
|
||||
return [
|
||||
self::ACTION_LINK => '跳转链接',
|
||||
self::ACTION_MODAL => '弹窗显示',
|
||||
self::ACTION_NONE => '无操作',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
namespace App\Models\System;
|
||||
|
||||
use App\Traits\ModelTrait;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Task extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
use ModelTrait, SoftDeletes;
|
||||
|
||||
protected $table = 'system_tasks';
|
||||
protected $table = 'system_task';
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
|
||||
+214
-203
@@ -4,246 +4,257 @@
|
||||
|
||||
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;
|
||||
protected $permissionService;
|
||||
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*/
|
||||
public function login(array $credentials): array
|
||||
{
|
||||
$user = User::where('username', $credentials['username'])->first();
|
||||
/**
|
||||
* 管理员登录
|
||||
*/
|
||||
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 || !Hash::check($credentials['password'], $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'username' => ['用户名或密码错误'],
|
||||
]);
|
||||
}
|
||||
|
||||
if ($user->status !== 1) {
|
||||
throw ValidationException::withMessages([
|
||||
'username' => ['账号已被禁用'],
|
||||
]);
|
||||
}
|
||||
if ($user->status !== 1) {
|
||||
throw ValidationException::withMessages([
|
||||
'username' => ['账号已被禁用'],
|
||||
]);
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
$user->update([
|
||||
'last_login_at' => now(),
|
||||
'last_login_ip' => request()->ip(),
|
||||
]);
|
||||
// 更新登录信息
|
||||
$user->update([
|
||||
'last_login_at' => now(),
|
||||
'last_login_ip' => request()->ip(),
|
||||
]);
|
||||
|
||||
// 生成token
|
||||
$token = auth('admin')->login($user);
|
||||
// 生成token
|
||||
$token = auth('admin')->login($user);
|
||||
|
||||
// 生成refresh token
|
||||
$refreshToken = auth('admin')->refresh();
|
||||
// 获取用户菜单
|
||||
$menu = $this->getUserMenu($user);
|
||||
|
||||
// 获取用户菜单
|
||||
$menu = $this->getUserMenu($user);
|
||||
// 获取用户权限列表
|
||||
$permissions = $this->getUserPermissions($user);
|
||||
|
||||
// 获取用户权限列表
|
||||
$permissions = $this->getUserPermissions($user);
|
||||
return [
|
||||
'token' => $token,
|
||||
'expires_in' => auth('admin')->factory()->getTTL() * 60,
|
||||
'user' => $this->getUserInfo($user),
|
||||
'menu' => $menu,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'refreshToken' => $refreshToken,
|
||||
'user' => $this->getUserInfo($user),
|
||||
'menu' => $menu,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
}
|
||||
/**
|
||||
* 管理员登出
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
auth('admin')->logout();
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登出
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
auth('admin')->logout();
|
||||
}
|
||||
/**
|
||||
* 刷新token
|
||||
*/
|
||||
public function refresh(): array
|
||||
{
|
||||
$newToken = auth('admin')->refresh();
|
||||
$user = auth('admin')->user();
|
||||
|
||||
/**
|
||||
* 刷新token
|
||||
*/
|
||||
public function refresh(): array
|
||||
{
|
||||
$newToken = auth('admin')->refresh();
|
||||
$user = auth('admin')->user();
|
||||
// 生成新的refresh token
|
||||
$newRefreshToken = auth('admin')->refresh();
|
||||
|
||||
// 生成新的refresh token
|
||||
$newRefreshToken = auth('admin')->refresh();
|
||||
// 获取用户菜单
|
||||
$menu = $this->getUserMenu($user);
|
||||
|
||||
// 获取用户菜单
|
||||
$menu = $this->getUserMenu($user);
|
||||
// 获取用户权限列表
|
||||
$permissions = $this->getUserPermissions($user);
|
||||
|
||||
// 获取用户权限列表
|
||||
$permissions = $this->getUserPermissions($user);
|
||||
return [
|
||||
'token' => $newToken,
|
||||
'refreshToken' => $newRefreshToken,
|
||||
'user' => $this->getUserInfo($user),
|
||||
'menu' => $menu,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'token' => $newToken,
|
||||
'refreshToken' => $newRefreshToken,
|
||||
'user' => $this->getUserInfo($user),
|
||||
'menu' => $menu,
|
||||
'permissions' => $permissions,
|
||||
];
|
||||
}
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
public function me(): array
|
||||
{
|
||||
$user = auth('admin')->user();
|
||||
return $this->getUserInfo($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
public function me(): array
|
||||
{
|
||||
$user = auth('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();
|
||||
|
||||
/**
|
||||
* 找回密码
|
||||
*/
|
||||
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' => ['用户不存在'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$user) {
|
||||
throw ValidationException::withMessages([
|
||||
'username' => ['用户不存在'],
|
||||
]);
|
||||
}
|
||||
$user->update([
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
}
|
||||
|
||||
$user->update([
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
public function changePassword(array $data): void
|
||||
{
|
||||
$user = auth('admin')->user();
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
public function changePassword(array $data): void
|
||||
{
|
||||
$user = auth('admin')->user();
|
||||
if (!Hash::check($data['old_password'], $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'old_password' => ['原密码错误'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (!Hash::check($data['old_password'], $user->password)) {
|
||||
throw ValidationException::withMessages([
|
||||
'old_password' => ['原密码错误'],
|
||||
]);
|
||||
}
|
||||
$user->update([
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
}
|
||||
|
||||
$user->update([
|
||||
'password' => Hash::make($data['password']),
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* 获取用户信息详情
|
||||
*/
|
||||
private function getUserInfo(User $user): array
|
||||
{
|
||||
$user->load(['department', 'roles.permissions']);
|
||||
|
||||
/**
|
||||
* 获取用户信息详情
|
||||
*/
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
// 超级管理员获取所有菜单
|
||||
if ($user->isSuperAdmin()) {
|
||||
$menuPermissions = \App\Models\Auth\Permission::where('type', 'menu')
|
||||
->where('status', 1)
|
||||
->orderBy('sort', 'asc')
|
||||
->get();
|
||||
} else {
|
||||
// 获取用户的所有权限
|
||||
$permissionIds = [];
|
||||
foreach ($user->roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
$permissionIds[] = $permission->id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户菜单
|
||||
*/
|
||||
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();
|
||||
}
|
||||
|
||||
// 查询菜单类型的权限
|
||||
$menuPermissions = \App\Models\Auth\Permission::whereIn('id', $permissionIds)
|
||||
->where('type', 'menu')
|
||||
->where('status', 1)
|
||||
->orderBy('sort', 'asc')
|
||||
->get();
|
||||
// 构建菜单树
|
||||
return $this->buildMenuTree($menuPermissions);
|
||||
}
|
||||
|
||||
// 构建菜单树
|
||||
return $this->buildMenuTree($menuPermissions);
|
||||
}
|
||||
/**
|
||||
* 构建菜单树
|
||||
*/
|
||||
private function buildMenuTree($permissions, $parentId = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($permissions as $permission) {
|
||||
if ($permission->parent_id == $parentId) {
|
||||
$node = [
|
||||
'path' => $permission->path,
|
||||
'name' => $permission->name,
|
||||
'title' => $permission->title,
|
||||
'meta' => $permission->meta ?: [],
|
||||
];
|
||||
|
||||
/**
|
||||
* 构建菜单树
|
||||
*/
|
||||
private function buildMenuTree($permissions, $parentId = 0): array
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($permissions as $permission) {
|
||||
if ($permission->parent_id == $parentId) {
|
||||
$node = [
|
||||
'path' => $permission->path,
|
||||
'name' => $permission->name,
|
||||
'title' => $permission->title,
|
||||
'meta' => $permission->meta ? json_decode($permission->meta, true) : [],
|
||||
];
|
||||
// 添加组件路径
|
||||
if ($permission->component) {
|
||||
$node['component'] = $permission->component;
|
||||
}
|
||||
|
||||
// 添加组件路径
|
||||
if ($permission->component) {
|
||||
$node['component'] = $permission->component;
|
||||
}
|
||||
// 添加重定向
|
||||
if (!empty($node['meta']['redirect'])) {
|
||||
$node['redirect'] = $node['meta']['redirect'];
|
||||
}
|
||||
|
||||
// 添加重定向
|
||||
if (!empty($node['meta']['redirect'])) {
|
||||
$node['redirect'] = $node['meta']['redirect'];
|
||||
}
|
||||
// 递归构建子菜单
|
||||
$children = $this->buildMenuTree($permissions, $permission->id);
|
||||
if (!empty($children)) {
|
||||
$node['children'] = $children;
|
||||
}
|
||||
|
||||
// 递归构建子菜单
|
||||
$children = $this->buildMenuTree($permissions, $permission->id);
|
||||
if (!empty($children)) {
|
||||
$node['children'] = $children;
|
||||
}
|
||||
$tree[] = $node;
|
||||
}
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
$tree[] = $node;
|
||||
}
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
/**
|
||||
* 获取用户权限列表
|
||||
*/
|
||||
private function getUserPermissions(User $user): array
|
||||
{
|
||||
// 超级管理员获取所有权限
|
||||
if ($user->isSuperAdmin()) {
|
||||
return \App\Models\Auth\Permission::where('status', 1)
|
||||
->pluck('name')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限列表
|
||||
*/
|
||||
private function getUserPermissions(User $user): array
|
||||
{
|
||||
$permissions = [];
|
||||
foreach ($user->roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
if (!in_array($permission->name, $permissions)) {
|
||||
$permissions[] = $permission->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $permissions;
|
||||
}
|
||||
$permissions = [];
|
||||
foreach ($user->roles as $role) {
|
||||
foreach ($role->permissions as $permission) {
|
||||
if (!in_array($permission->name, $permissions)) {
|
||||
$permissions[] = $permission->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $permissions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,14 @@ public function getTree(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'] . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if (isset($params['status']) && $params['status'] !== '') {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
@@ -316,4 +324,40 @@ private function isDescendant($id, $childId): bool
|
||||
}
|
||||
return $this->isDescendant($id, $child->parent_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门及所有子部门的ID列表
|
||||
*
|
||||
* @param int $departmentId 部门ID
|
||||
* @param array $departments 所有部门数据
|
||||
* @return array
|
||||
*/
|
||||
public function getDepartmentAndChildrenIds(int $departmentId, array $departments = null): array
|
||||
{
|
||||
if ($departments === null) {
|
||||
$departments = Department::where('status', 1)->get()->keyBy('id')->toArray();
|
||||
}
|
||||
|
||||
$ids = [$departmentId];
|
||||
$this->collectChildrenIds($departmentId, $departments, $ids);
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集子部门ID
|
||||
*
|
||||
* @param int $parentId 父部门ID
|
||||
* @param array $departments 所有部门数据
|
||||
* @param array &$ids ID收集数组
|
||||
*/
|
||||
private function collectChildrenIds(int $parentId, array $departments, array &$ids): void
|
||||
{
|
||||
foreach ($departments as $department) {
|
||||
if ($department['parent_id'] == $parentId) {
|
||||
$ids[] = $department['id'];
|
||||
$this->collectChildrenIds($department['id'], $departments, $ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,12 @@
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\UserExport;
|
||||
use App\Exports\DepartmentExport;
|
||||
use App\Exports\RoleExport;
|
||||
use App\Exports\PermissionExport;
|
||||
use App\Imports\UserImport;
|
||||
use App\Imports\DepartmentImport;
|
||||
use App\Imports\RoleImport;
|
||||
use App\Imports\PermissionImport;
|
||||
|
||||
class ImportExportService
|
||||
{
|
||||
@@ -187,6 +191,166 @@ public function getExportFilePath(string $filename): string
|
||||
return storage_path('app/exports/' . $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载角色导入模板
|
||||
*/
|
||||
public function downloadRoleTemplate(): string
|
||||
{
|
||||
$filename = 'role_import_template_' . date('YmdHis') . '.xlsx';
|
||||
|
||||
// 确保目录存在
|
||||
if (!is_dir(storage_path('app/exports'))) {
|
||||
mkdir(storage_path('app/exports'), 0755, true);
|
||||
}
|
||||
|
||||
$templateData = [
|
||||
[
|
||||
'角色名称*',
|
||||
'角色编码*',
|
||||
'描述',
|
||||
'权限(多个用逗号分隔)',
|
||||
'排序',
|
||||
],
|
||||
[
|
||||
'测试角色',
|
||||
'test_role',
|
||||
'这是一个测试角色',
|
||||
'查看用户,编辑用户',
|
||||
'1',
|
||||
],
|
||||
];
|
||||
|
||||
Excel::store(new \App\Exports\GenericExport($templateData), 'exports/' . $filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出角色数据
|
||||
*/
|
||||
public function exportRoles(array $roleIds = []): string
|
||||
{
|
||||
$filename = 'roles_export_' . date('YmdHis') . '.xlsx';
|
||||
|
||||
// 确保目录存在
|
||||
if (!is_dir(storage_path('app/exports'))) {
|
||||
mkdir(storage_path('app/exports'), 0755, true);
|
||||
}
|
||||
|
||||
Excel::store(new RoleExport($roleIds), 'exports/' . $filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入角色数据
|
||||
*/
|
||||
public function importRoles(string $filePath, string $realPath): array
|
||||
{
|
||||
if (!file_exists($realPath)) {
|
||||
throw ValidationException::withMessages([
|
||||
'file' => ['文件不存在'],
|
||||
]);
|
||||
}
|
||||
|
||||
$import = new RoleImport();
|
||||
Excel::import($import, $realPath);
|
||||
|
||||
// 删除临时文件
|
||||
if (file_exists($realPath)) {
|
||||
unlink($realPath);
|
||||
}
|
||||
|
||||
return [
|
||||
'success_count' => $import->getSuccessCount(),
|
||||
'error_count' => $import->getErrorCount(),
|
||||
'errors' => $import->getErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载权限导入模板
|
||||
*/
|
||||
public function downloadPermissionTemplate(): string
|
||||
{
|
||||
$filename = 'permission_import_template_' . date('YmdHis') . '.xlsx';
|
||||
|
||||
// 确保目录存在
|
||||
if (!is_dir(storage_path('app/exports'))) {
|
||||
mkdir(storage_path('app/exports'), 0755, true);
|
||||
}
|
||||
|
||||
$templateData = [
|
||||
[
|
||||
'权限标题*',
|
||||
'权限编码*',
|
||||
'权限类型*',
|
||||
'父级ID',
|
||||
'路由路径',
|
||||
'前端组件',
|
||||
'元数据',
|
||||
'排序',
|
||||
],
|
||||
[
|
||||
'系统管理',
|
||||
'system',
|
||||
'菜单',
|
||||
'0',
|
||||
'/system',
|
||||
null,
|
||||
'icon:Setting',
|
||||
'1',
|
||||
],
|
||||
];
|
||||
|
||||
Excel::store(new \App\Exports\GenericExport($templateData), 'exports/' . $filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出权限数据
|
||||
*/
|
||||
public function exportPermissions(array $permissionIds = []): string
|
||||
{
|
||||
$filename = 'permissions_export_' . date('YmdHis') . '.xlsx';
|
||||
|
||||
// 确保目录存在
|
||||
if (!is_dir(storage_path('app/exports'))) {
|
||||
mkdir(storage_path('app/exports'), 0755, true);
|
||||
}
|
||||
|
||||
Excel::store(new PermissionExport($permissionIds), 'exports/' . $filename);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入权限数据
|
||||
*/
|
||||
public function importPermissions(string $filePath, string $realPath): array
|
||||
{
|
||||
if (!file_exists($realPath)) {
|
||||
throw ValidationException::withMessages([
|
||||
'file' => ['文件不存在'],
|
||||
]);
|
||||
}
|
||||
|
||||
$import = new PermissionImport();
|
||||
Excel::import($import, $realPath);
|
||||
|
||||
// 删除临时文件
|
||||
if (file_exists($realPath)) {
|
||||
unlink($realPath);
|
||||
}
|
||||
|
||||
return [
|
||||
'success_count' => $import->getSuccessCount(),
|
||||
'error_count' => $import->getErrorCount(),
|
||||
'errors' => $import->getErrors(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除导出文件
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
namespace App\Services\Auth;
|
||||
|
||||
use App\Models\Auth\User;
|
||||
use App\Models\System\Notification;
|
||||
use App\Services\System\NotificationService;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
@@ -14,6 +17,23 @@
|
||||
|
||||
class UserService
|
||||
{
|
||||
protected $departmentService;
|
||||
protected $notificationService;
|
||||
|
||||
public function __construct(DepartmentService $departmentService, NotificationService $notificationService)
|
||||
{
|
||||
$this->departmentService = $departmentService;
|
||||
$this->notificationService = $notificationService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户ID
|
||||
*/
|
||||
protected function getCurrentUserId(): int
|
||||
{
|
||||
return Auth::guard('admin')->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
@@ -32,7 +52,9 @@ public function getList(array $params): array
|
||||
}
|
||||
|
||||
if (!empty($params['department_id'])) {
|
||||
$query->where('department_id', $params['department_id']);
|
||||
// 获取部门及所有子部门的ID
|
||||
$departmentIds = $this->departmentService->getDepartmentAndChildrenIds($params['department_id']);
|
||||
$query->whereIn('department_id', $departmentIds);
|
||||
}
|
||||
|
||||
if (isset($params['status']) && $params['status'] !== '') {
|
||||
@@ -198,6 +220,12 @@ public function update(int $id, array $data): User
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
// 发送更新通知(如果更新的是其他用户)
|
||||
if ($id !== $this->getCurrentUserId()) {
|
||||
$this->sendUserUpdateNotification($user, $data);
|
||||
}
|
||||
|
||||
return $user;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
@@ -226,6 +254,15 @@ public function delete(int $id): void
|
||||
*/
|
||||
public function batchDelete(array $ids): int
|
||||
{
|
||||
$currentUserId = $this->getCurrentUserId();
|
||||
|
||||
// 检查是否包含当前用户
|
||||
if (in_array($currentUserId, $ids)) {
|
||||
throw ValidationException::withMessages([
|
||||
'ids' => ['不能删除当前登录用户'],
|
||||
]);
|
||||
}
|
||||
|
||||
return User::whereIn('id', $ids)->delete();
|
||||
}
|
||||
|
||||
@@ -234,6 +271,15 @@ public function batchDelete(array $ids): int
|
||||
*/
|
||||
public function batchUpdateStatus(array $ids, int $status): int
|
||||
{
|
||||
$currentUserId = $this->getCurrentUserId();
|
||||
|
||||
// 如果是禁用操作,检查是否包含当前用户
|
||||
if ($status === 0 && in_array($currentUserId, $ids)) {
|
||||
throw ValidationException::withMessages([
|
||||
'ids' => ['不能禁用当前登录用户'],
|
||||
]);
|
||||
}
|
||||
|
||||
return User::whereIn('id', $ids)->update(['status' => $status]);
|
||||
}
|
||||
|
||||
@@ -317,4 +363,52 @@ private function formatUserInfo(User $user): array
|
||||
'updated_at' => $user->updated_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送用户更新通知
|
||||
*/
|
||||
private function sendUserUpdateNotification(User $user, array $data): void
|
||||
{
|
||||
// 收集被更新的字段
|
||||
$changes = [];
|
||||
|
||||
$fieldLabels = [
|
||||
'username' => '用户名',
|
||||
'real_name' => '姓名',
|
||||
'email' => '邮箱',
|
||||
'phone' => '手机号',
|
||||
'department_id' => '所属部门',
|
||||
'avatar' => '头像',
|
||||
'status' => '状态',
|
||||
'password' => '密码',
|
||||
'role_ids' => '角色',
|
||||
];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (isset($fieldLabels[$key])) {
|
||||
$changes[] = $fieldLabels[$key];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($changes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成通知内容
|
||||
$content = '您的账户信息已被管理员更新,更新的内容:' . implode('、', $changes);
|
||||
|
||||
// 发送通知
|
||||
$this->notificationService->sendToUser(
|
||||
$user->id,
|
||||
'个人信息已更新',
|
||||
$content,
|
||||
Notification::TYPE_INFO,
|
||||
Notification::CATEGORY_SYSTEM,
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'updated_fields' => $changes,
|
||||
'action_type' => Notification::ACTION_NONE,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,27 +12,18 @@ 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['parent_code'])) {
|
||||
$query->where('parent_code', $params['parent_code']);
|
||||
}
|
||||
|
||||
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'] . '%');
|
||||
$q->where('title', 'like', '%' . $params['keyword'] . '%')
|
||||
->orWhere('code', 'like', '%' . $params['keyword'] . '%');
|
||||
});
|
||||
}
|
||||
|
||||
if (isset($params['status']) && $params['status'] !== '') {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
$query->orderBy('sort')->orderBy('id');
|
||||
$query->orderBy('id');
|
||||
|
||||
$pageSize = $params['page_size'] ?? 20;
|
||||
$list = $query->paginate($pageSize);
|
||||
@@ -47,14 +38,13 @@ public function getList(array $params): array
|
||||
|
||||
public function getTree(): array
|
||||
{
|
||||
return $this->buildTree(City::where('status', true)->orderBy('sort')->get());
|
||||
return $this->buildTree(City::orderBy('id')->get());
|
||||
}
|
||||
|
||||
public function getChildren(int $parentId): array
|
||||
public function getChildren(string $parentCode): array
|
||||
{
|
||||
return City::where('parent_id', $parentId)
|
||||
->where('status', true)
|
||||
->orderBy('sort')
|
||||
return City::where('parent_code', $parentCode)
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
@@ -69,21 +59,12 @@ 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',
|
||||
'title' => 'required|string|max:100',
|
||||
'code' => 'required|string|max:50|unique:system_city,code',
|
||||
'parent_code' => 'sometimes|nullable|exists:system_city,code',
|
||||
])->validate();
|
||||
|
||||
$city = City::create($data);
|
||||
@@ -96,12 +77,16 @@ 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',
|
||||
'title' => 'sometimes|required|string|max:100',
|
||||
'code' => 'sometimes|required|string|max:50|unique:system_city,code,' . $id,
|
||||
'parent_code' => 'sometimes|nullable|exists:system_city,code',
|
||||
])->validate();
|
||||
|
||||
// 防止循环引用
|
||||
if (isset($data['parent_code']) && $data['parent_code'] === $city->code) {
|
||||
throw new \Exception('不能将城市设置为自己的子级');
|
||||
}
|
||||
|
||||
$city->update($data);
|
||||
$this->clearCache();
|
||||
return $city;
|
||||
@@ -127,19 +112,69 @@ public function batchDelete(array $ids): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
public function batchUpdateStatus(array $ids, bool $status): bool
|
||||
/**
|
||||
* 获取所有顶级城市(省/直辖市/自治区)
|
||||
*/
|
||||
public function getTopLevel(): array
|
||||
{
|
||||
City::whereIn('id', $ids)->update(['status' => $status]);
|
||||
$this->clearCache();
|
||||
return true;
|
||||
return City::whereNull('parent_code')
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function buildTree(array $cities, int $parentId = 0): array
|
||||
/**
|
||||
* 根据父级编码获取城市
|
||||
*/
|
||||
public function getByParentCode(string $parentCode): array
|
||||
{
|
||||
return City::where('parent_code', $parentCode)
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取城市的完整路径(从顶级到当前)
|
||||
*/
|
||||
public function getPath(string $code): array
|
||||
{
|
||||
$path = [];
|
||||
$city = $this->getByCode($code);
|
||||
|
||||
while ($city) {
|
||||
array_unshift($path, $city);
|
||||
$city = $city->parent;
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取城市的所有子孙
|
||||
*/
|
||||
public function getDescendants(string $code): array
|
||||
{
|
||||
$descendants = [];
|
||||
$this->collectDescendants($code, $descendants);
|
||||
return $descendants;
|
||||
}
|
||||
|
||||
private function collectDescendants(string $parentCode, array &$result): void
|
||||
{
|
||||
$children = City::where('parent_code', $parentCode)->get();
|
||||
foreach ($children as $child) {
|
||||
$result[] = $child;
|
||||
$this->collectDescendants($child->code, $result);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildTree($cities, string $parentCode = ''): array
|
||||
{
|
||||
$tree = [];
|
||||
foreach ($cities as $city) {
|
||||
if ($city['parent_id'] == $parentId) {
|
||||
$children = $this->buildTree($cities, $city['id']);
|
||||
if ($city->parent_code == $parentCode) {
|
||||
$children = $this->buildTree($cities, $city->code);
|
||||
if (!empty($children)) {
|
||||
$city['children'] = $children;
|
||||
}
|
||||
@@ -167,32 +202,36 @@ public function getCachedTree(): array
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取省份(兼容旧接口)
|
||||
*/
|
||||
public function getProvinces(): array
|
||||
{
|
||||
return City::where('level', 1)
|
||||
->where('status', true)
|
||||
->orderBy('sort')
|
||||
->get()
|
||||
->toArray();
|
||||
return $this->getTopLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取城市(兼容旧接口)
|
||||
*/
|
||||
public function getCities(int $provinceId): array
|
||||
{
|
||||
return City::where('parent_id', $provinceId)
|
||||
->where('level', 2)
|
||||
->where('status', true)
|
||||
->orderBy('sort')
|
||||
->get()
|
||||
->toArray();
|
||||
// 由于新结构使用code关联,这里需要根据id找到对应的code
|
||||
$province = City::find($provinceId);
|
||||
if (!$province) {
|
||||
return [];
|
||||
}
|
||||
return $this->getByParentCode($province->code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取区县(兼容旧接口)
|
||||
*/
|
||||
public function getDistricts(int $cityId): array
|
||||
{
|
||||
return City::where('parent_id', $cityId)
|
||||
->where('level', 3)
|
||||
->where('status', true)
|
||||
->orderBy('sort')
|
||||
->get()
|
||||
->toArray();
|
||||
$city = City::find($cityId);
|
||||
if (!$city) {
|
||||
return [];
|
||||
}
|
||||
return $this->getByParentCode($city->code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public function create(array $data): Config
|
||||
{
|
||||
Validator::make($data, [
|
||||
'group' => 'required|string|max:50',
|
||||
'key' => 'required|string|max:100|unique:system_configs,key',
|
||||
'key' => 'required|string|max:100|unique:system_setting,key',
|
||||
'name' => 'required|string|max:100',
|
||||
'type' => 'required|string|in:string,text,number,boolean,select,radio,checkbox,file,json',
|
||||
])->validate();
|
||||
@@ -104,7 +104,7 @@ public function update(int $id, array $data): Config
|
||||
|
||||
Validator::make($data, [
|
||||
'group' => 'sometimes|required|string|max:50',
|
||||
'key' => 'sometimes|required|string|max:100|unique:system_configs,key,' . $id,
|
||||
'key' => 'sometimes|required|string|max:100|unique:system_setting,key,' . $id,
|
||||
'name' => 'sometimes|required|string|max:100',
|
||||
'type' => 'sometimes|required|string|in:string,text,number,boolean,select,radio,checkbox,file,json',
|
||||
])->validate();
|
||||
|
||||
@@ -4,11 +4,19 @@
|
||||
|
||||
use App\Models\System\Dictionary;
|
||||
use App\Models\System\DictionaryItem;
|
||||
use App\Services\WebSocket\WebSocketService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class DictionaryService
|
||||
{
|
||||
protected $webSocketService;
|
||||
|
||||
public function __construct(WebSocketService $webSocketService)
|
||||
{
|
||||
$this->webSocketService = $webSocketService;
|
||||
}
|
||||
|
||||
public function getList(array $params): array
|
||||
{
|
||||
$query = Dictionary::query();
|
||||
@@ -20,7 +28,8 @@ public function getList(array $params): array
|
||||
});
|
||||
}
|
||||
|
||||
if (isset($params['status']) && $params['status'] !== '') {
|
||||
// 处理状态筛选:只接受布尔值或数字1/0
|
||||
if (array_key_exists('status', $params) && is_bool($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
@@ -37,20 +46,61 @@ public function getList(array $params): array
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
return Dictionary::where('status', true)
|
||||
->orderBy('sort')
|
||||
->get()
|
||||
->toArray();
|
||||
$cacheKey = 'system:dictionary:all';
|
||||
$dictionary = Cache::get($cacheKey);
|
||||
|
||||
if ($dictionary === null) {
|
||||
$dictionary = Dictionary::where('status', true)
|
||||
->with(['activeItems'])
|
||||
->orderBy('sort')
|
||||
->get()
|
||||
->toArray();
|
||||
Cache::put($cacheKey, $dictionary, 3600);
|
||||
}
|
||||
|
||||
return $dictionary;
|
||||
}
|
||||
|
||||
public function getById(int $id): ?Dictionary
|
||||
public function getById(int $id): ?array
|
||||
{
|
||||
return Dictionary::with('items')->find($id);
|
||||
$cacheKey = 'system:dictionary:' . $id;
|
||||
$dictionary = Cache::get($cacheKey);
|
||||
|
||||
if ($dictionary === null) {
|
||||
$dictionary = Dictionary::with('items')->find($id);
|
||||
if ($dictionary) {
|
||||
$dictionary = $dictionary->toArray();
|
||||
// 格式化字典项值
|
||||
if (!empty($dictionary['items'])) {
|
||||
$dictionary['items'] = $this->formatItemsByType($dictionary['items'], $dictionary['value_type']);
|
||||
}
|
||||
Cache::put($cacheKey, $dictionary, 3600);
|
||||
}
|
||||
}
|
||||
|
||||
return $dictionary ?? null;
|
||||
}
|
||||
|
||||
public function getByCode(string $code): ?Dictionary
|
||||
public function getByCode(string $code): ?array
|
||||
{
|
||||
return Dictionary::where('code', $code)->first();
|
||||
$cacheKey = 'system:dictionary:code:' . $code;
|
||||
$dictionary = Cache::get($cacheKey);
|
||||
|
||||
if ($dictionary === null) {
|
||||
$dictionaryModel = Dictionary::with('items')->where('code', $code)->first();
|
||||
if ($dictionaryModel) {
|
||||
$dictionary = $dictionaryModel->toArray();
|
||||
// 格式化字典项值
|
||||
if (!empty($dictionary['items'])) {
|
||||
$dictionary['items'] = $this->formatItemsByType($dictionary['items'], $dictionary['value_type']);
|
||||
}
|
||||
Cache::put($cacheKey, $dictionary, 3600);
|
||||
} else {
|
||||
$dictionary = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $dictionary;
|
||||
}
|
||||
|
||||
public function getItemsByCode(string $code): array
|
||||
@@ -66,6 +116,8 @@ public function getItemsByCode(string $code): array
|
||||
->orderBy('sort')
|
||||
->get()
|
||||
->toArray();
|
||||
// 格式化字典项值
|
||||
$items = $this->formatItemsByType($items, $dictionary->value_type);
|
||||
Cache::put($cacheKey, $items, 3600);
|
||||
} else {
|
||||
$items = [];
|
||||
@@ -79,11 +131,12 @@ public function create(array $data): Dictionary
|
||||
{
|
||||
Validator::make($data, [
|
||||
'name' => 'required|string|max:100',
|
||||
'code' => 'required|string|max:50|unique:system_dictionaries,code',
|
||||
'code' => 'required|string|max:50|unique:system_dictionary,code',
|
||||
])->validate();
|
||||
|
||||
$dictionary = Dictionary::create($data);
|
||||
$this->clearCache();
|
||||
$this->notifyDictionaryUpdate('create', $dictionary->toArray());
|
||||
return $dictionary;
|
||||
}
|
||||
|
||||
@@ -93,20 +146,23 @@ public function update(int $id, array $data): Dictionary
|
||||
|
||||
Validator::make($data, [
|
||||
'name' => 'sometimes|required|string|max:100',
|
||||
'code' => 'sometimes|required|string|max:50|unique:system_dictionaries,code,' . $id,
|
||||
'code' => 'sometimes|required|string|max:50|unique:system_dictionary,code,' . $id,
|
||||
])->validate();
|
||||
|
||||
$dictionary->update($data);
|
||||
$this->clearCache();
|
||||
$this->notifyDictionaryUpdate('update', $dictionary->toArray());
|
||||
return $dictionary;
|
||||
}
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$dictionary = Dictionary::findOrFail($id);
|
||||
$dictionaryData = $dictionary->toArray();
|
||||
DictionaryItem::where('dictionary_id', $id)->delete();
|
||||
$dictionary->delete();
|
||||
$this->clearCache();
|
||||
$this->notifyDictionaryUpdate('delete', $dictionaryData);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -115,6 +171,7 @@ public function batchDelete(array $ids): bool
|
||||
DictionaryItem::whereIn('dictionary_id', $ids)->delete();
|
||||
Dictionary::whereIn('id', $ids)->delete();
|
||||
$this->clearCache();
|
||||
$this->notifyDictionaryUpdate('batch_delete', ['ids' => $ids]);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -122,14 +179,30 @@ public function batchUpdateStatus(array $ids, bool $status): bool
|
||||
{
|
||||
Dictionary::whereIn('id', $ids)->update(['status' => $status]);
|
||||
$this->clearCache();
|
||||
$this->notifyDictionaryUpdate('batch_update_status', ['ids' => $ids, 'status' => $status]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private function clearCache(): void
|
||||
private function clearCache($dictionaryId = null): void
|
||||
{
|
||||
$codes = Dictionary::pluck('code')->toArray();
|
||||
foreach ($codes as $code) {
|
||||
Cache::forget('system:dictionary:' . $code);
|
||||
// 清理所有字典列表缓存
|
||||
Cache::forget('system:dictionary:all');
|
||||
|
||||
if ($dictionaryId) {
|
||||
// 清理特定字典的缓存
|
||||
$dictionary = Dictionary::find($dictionaryId);
|
||||
if ($dictionary) {
|
||||
Cache::forget('system:dictionary:' . $dictionaryId);
|
||||
Cache::forget('system:dictionary:code:' . $dictionary->code);
|
||||
Cache::forget('system:dictionary:' . $dictionary->code);
|
||||
}
|
||||
} else {
|
||||
// 清理所有字典缓存
|
||||
$codes = Dictionary::pluck('code')->toArray();
|
||||
foreach ($codes as $code) {
|
||||
Cache::forget('system:dictionary:' . $code);
|
||||
Cache::forget('system:dictionary:code:' . $code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +214,8 @@ public function getItemsList(array $params): array
|
||||
$query->where('dictionary_id', $params['dictionary_id']);
|
||||
}
|
||||
|
||||
if (isset($params['status']) && $params['status'] !== '') {
|
||||
// 处理状态筛选:只接受布尔值或数字1/0
|
||||
if (array_key_exists('status', $params) && is_bool($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
|
||||
@@ -161,13 +235,14 @@ public function getItemsList(array $params): array
|
||||
public function createItem(array $data): DictionaryItem
|
||||
{
|
||||
Validator::make($data, [
|
||||
'dictionary_id' => 'required|exists:system_dictionaries,id',
|
||||
'dictionary_id' => 'required|exists:system_dictionary,id',
|
||||
'label' => 'required|string|max:100',
|
||||
'value' => 'required|string|max:100',
|
||||
])->validate();
|
||||
|
||||
$item = DictionaryItem::create($data);
|
||||
$this->clearCache();
|
||||
$this->clearCache($data['dictionary_id']);
|
||||
$this->notifyDictionaryItemUpdate('create', $item->toArray());
|
||||
return $item;
|
||||
}
|
||||
|
||||
@@ -176,35 +251,166 @@ public function updateItem(int $id, array $data): DictionaryItem
|
||||
$item = DictionaryItem::findOrFail($id);
|
||||
|
||||
Validator::make($data, [
|
||||
'dictionary_id' => 'sometimes|required|exists:system_dictionaries,id',
|
||||
'dictionary_id' => 'sometimes|required|exists:system_dictionary,id',
|
||||
'label' => 'sometimes|required|string|max:100',
|
||||
'value' => 'sometimes|required|string|max:100',
|
||||
])->validate();
|
||||
|
||||
$item->update($data);
|
||||
$this->clearCache();
|
||||
$this->clearCache($item->dictionary_id);
|
||||
$this->notifyDictionaryItemUpdate('update', $item->toArray());
|
||||
return $item;
|
||||
}
|
||||
|
||||
public function deleteItem(int $id): bool
|
||||
{
|
||||
$item = DictionaryItem::findOrFail($id);
|
||||
$dictionaryId = $item->dictionary_id;
|
||||
$itemData = $item->toArray();
|
||||
$item->delete();
|
||||
$this->clearCache();
|
||||
$this->clearCache($dictionaryId);
|
||||
$this->notifyDictionaryItemUpdate('delete', $itemData);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function batchDeleteItems(array $ids): bool
|
||||
{
|
||||
$items = DictionaryItem::whereIn('id', $ids)->get();
|
||||
$dictionaryIds = $items->pluck('dictionary_id')->unique()->toArray();
|
||||
|
||||
DictionaryItem::whereIn('id', $ids)->delete();
|
||||
$this->clearCache();
|
||||
|
||||
// 清理相关字典的缓存
|
||||
foreach ($dictionaryIds as $dictionaryId) {
|
||||
$this->clearCache($dictionaryId);
|
||||
}
|
||||
|
||||
$this->notifyDictionaryItemUpdate('batch_delete', ['ids' => $ids, 'dictionary_ids' => $dictionaryIds]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function batchUpdateItemsStatus(array $ids, bool $status): bool
|
||||
{
|
||||
$items = DictionaryItem::whereIn('id', $ids)->get();
|
||||
$dictionaryIds = $items->pluck('dictionary_id')->unique()->toArray();
|
||||
|
||||
DictionaryItem::whereIn('id', $ids)->update(['status' => $status]);
|
||||
$this->clearCache();
|
||||
|
||||
// 清理相关字典的缓存
|
||||
foreach ($dictionaryIds as $dictionaryId) {
|
||||
$this->clearCache($dictionaryId);
|
||||
}
|
||||
|
||||
$this->notifyDictionaryItemUpdate('batch_update_status', ['ids' => $ids, 'dictionary_ids' => $dictionaryIds, 'status' => $status]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有字典项(按字典分类)
|
||||
* @return array 按字典code分类的字典项数据
|
||||
*/
|
||||
public function getAllItems(): array
|
||||
{
|
||||
$cacheKey = 'system:dictionary-items:all';
|
||||
$allItems = Cache::get($cacheKey);
|
||||
|
||||
if ($allItems === null) {
|
||||
// 获取所有启用的字典
|
||||
$dictionary = Dictionary::where('status', true)
|
||||
->orderBy('sort')
|
||||
->get();
|
||||
|
||||
$result = [];
|
||||
foreach ($dictionary as $dictionary) {
|
||||
$items = $dictionary->activeItems->toArray();
|
||||
// 格式化字典项值
|
||||
$items = $this->formatItemsByType($items, $dictionary->value_type);
|
||||
|
||||
$result[] = [
|
||||
'code' => $dictionary->code,
|
||||
'name' => $dictionary->name,
|
||||
'description' => $dictionary->description,
|
||||
'value_type' => $dictionary->value_type,
|
||||
'items' => $items
|
||||
];
|
||||
}
|
||||
|
||||
$allItems = $result;
|
||||
Cache::put($cacheKey, $allItems, 3600);
|
||||
}
|
||||
|
||||
return $allItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知前端字典分类已更新
|
||||
*
|
||||
* @param string $action 操作类型:create, update, delete, batch_delete, batch_update_status
|
||||
* @param array $data 字典数据
|
||||
*/
|
||||
private function notifyDictionaryUpdate(string $action, array $data): void
|
||||
{
|
||||
$this->webSocketService->broadcast([
|
||||
'type' => 'dictionary_update',
|
||||
'data' => [
|
||||
'action' => $action,
|
||||
'resource_type' => 'dictionary',
|
||||
'data' => $data,
|
||||
'timestamp' => time()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知前端字典项已更新
|
||||
*
|
||||
* @param string $action 操作类型:create, update, delete, batch_delete, batch_update_status
|
||||
* @param array $data 字典项数据
|
||||
*/
|
||||
private function notifyDictionaryItemUpdate(string $action, array $data): void
|
||||
{
|
||||
$this->webSocketService->broadcast([
|
||||
'type' => 'dictionary_item_update',
|
||||
'data' => [
|
||||
'action' => $action,
|
||||
'resource_type' => 'dictionary_item',
|
||||
'data' => $data,
|
||||
'timestamp' => time()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据值类型格式化字典项
|
||||
* @param array $items 字典项数组
|
||||
* @param string $valueType 值类型:string, number, boolean, json
|
||||
* @return array 格式化后的字典项数组
|
||||
*/
|
||||
private function formatItemsByType(array $items, string $valueType): array
|
||||
{
|
||||
return array_map(function ($item) use ($valueType) {
|
||||
switch ($valueType) {
|
||||
case 'number':
|
||||
// 数字类型:将值转换为数字
|
||||
$item['value'] = is_numeric($item['value']) ? (strpos($item['value'], '.') !== false ? (float)$item['value'] : (int)$item['value']) : $item['value'];
|
||||
break;
|
||||
case 'boolean':
|
||||
// 布尔类型:将 '1', 'true', 'yes' 转换为 true,其他为 false
|
||||
$item['value'] = in_array(strtolower($item['value']), ['1', 'true', 'yes', 'on']);
|
||||
break;
|
||||
case 'json':
|
||||
// JSON类型:尝试解析JSON字符串,失败则保持原值
|
||||
$decoded = json_decode($item['value'], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$item['value'] = $decoded;
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
default:
|
||||
// 字符串类型:保持原值
|
||||
break;
|
||||
}
|
||||
return $item;
|
||||
}, $items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public function getListQuery(array $params)
|
||||
*/
|
||||
protected function buildQuery(array $params)
|
||||
{
|
||||
$query = Log::query()->with('user:id,name,username');
|
||||
$query = Log::query()->with('user:id,username');
|
||||
|
||||
if (!empty($params['user_id'])) {
|
||||
$query->where('user_id', $params['user_id']);
|
||||
@@ -66,6 +66,10 @@ protected function buildQuery(array $params)
|
||||
$query->where('action', $params['action']);
|
||||
}
|
||||
|
||||
if (!empty($params['method'])) {
|
||||
$query->where('method', $params['method']);
|
||||
}
|
||||
|
||||
if (!empty($params['status'])) {
|
||||
$query->where('status', $params['status']);
|
||||
}
|
||||
@@ -108,6 +112,14 @@ public function getStatistics(array $params = []): array
|
||||
{
|
||||
$query = Log::query();
|
||||
|
||||
if (!empty($params['method'])) {
|
||||
$query->where('method', $params['method']);
|
||||
}
|
||||
|
||||
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']]);
|
||||
}
|
||||
@@ -116,10 +128,74 @@ public function getStatistics(array $params = []): array
|
||||
$successCount = (clone $query)->where('status', 'success')->count();
|
||||
$errorCount = (clone $query)->where('status', 'error')->count();
|
||||
|
||||
// 计算平均响应时间
|
||||
$avgTime = (clone $query)->avg('execution_time');
|
||||
$avgTime = $avgTime ? round($avgTime, 2) : 0;
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'success' => $successCount,
|
||||
'error' => $errorCount,
|
||||
'avg_time' => $avgTime,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出日志
|
||||
*
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
public function export(array $params): string
|
||||
{
|
||||
$query = $this->buildQuery($params);
|
||||
$query->orderBy('created_at', 'desc');
|
||||
|
||||
$logs = $query->limit(10000)->get();
|
||||
|
||||
// 创建导出数据
|
||||
$data = [];
|
||||
foreach ($logs as $log) {
|
||||
$data[] = [
|
||||
'ID' => $log->id,
|
||||
'用户名' => $log->username,
|
||||
'模块' => $log->module,
|
||||
'操作' => $log->action,
|
||||
'请求方式' => $log->method,
|
||||
'URL' => $log->url,
|
||||
'IP地址' => $log->ip,
|
||||
'状态码' => $log->status_code,
|
||||
'状态' => $log->status === 'success' ? '成功' : '失败',
|
||||
'执行时间' => $log->execution_time . 'ms',
|
||||
'创建时间' => $log->created_at,
|
||||
];
|
||||
}
|
||||
|
||||
// 生成CSV文件
|
||||
$filename = '系统日志_' . date('YmdHis') . '.csv';
|
||||
$filepath = storage_path('app/exports/' . $filename);
|
||||
|
||||
// 确保目录存在
|
||||
if (!file_exists(dirname($filepath))) {
|
||||
mkdir(dirname($filepath), 0755, true);
|
||||
}
|
||||
|
||||
$file = fopen($filepath, 'w');
|
||||
// 添加BOM以支持Excel中文显示
|
||||
fprintf($file, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
|
||||
// 写入表头
|
||||
if (!empty($data)) {
|
||||
fputcsv($file, array_keys($data[0]));
|
||||
|
||||
// 写入数据
|
||||
foreach ($data as $row) {
|
||||
fputcsv($file, $row);
|
||||
}
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
|
||||
return $filepath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\System;
|
||||
|
||||
use App\Models\System\Notification;
|
||||
use App\Services\WebSocket\WebSocketService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class NotificationService
|
||||
{
|
||||
/**
|
||||
* @var WebSocketService
|
||||
*/
|
||||
protected $webSocketService;
|
||||
|
||||
/**
|
||||
* NotificationService constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->webSocketService = app(WebSocketService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知列表
|
||||
*
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function getList(array $params): array
|
||||
{
|
||||
$query = Notification::query();
|
||||
|
||||
// 过滤用户ID
|
||||
if (!empty($params['user_id'])) {
|
||||
$query->where('user_id', $params['user_id']);
|
||||
}
|
||||
|
||||
// 关键字搜索
|
||||
if (!empty($params['keyword'])) {
|
||||
$query->where(function ($q) use ($params) {
|
||||
$q->where('title', 'like', '%' . $params['keyword'] . '%')
|
||||
->orWhere('content', 'like', '%' . $params['keyword'] . '%');
|
||||
});
|
||||
}
|
||||
|
||||
// 过滤阅读状态
|
||||
if (isset($params['is_read']) && $params['is_read'] !== '') {
|
||||
$query->where('is_read', $params['is_read']);
|
||||
}
|
||||
|
||||
// 过滤通知类型
|
||||
if (!empty($params['type'])) {
|
||||
$query->where('type', $params['type']);
|
||||
}
|
||||
|
||||
// 过滤通知分类
|
||||
if (!empty($params['category'])) {
|
||||
$query->where('category', $params['category']);
|
||||
}
|
||||
|
||||
// 日期范围
|
||||
if (!empty($params['start_date'])) {
|
||||
$query->where('created_at', '>=', $params['start_date']);
|
||||
}
|
||||
if (!empty($params['end_date'])) {
|
||||
$query->where('created_at', '<=', $params['end_date']);
|
||||
}
|
||||
|
||||
$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 int $userId
|
||||
* @param int $limit
|
||||
* @param int $page
|
||||
* @param string|null $type
|
||||
* @return array
|
||||
*/
|
||||
public function getUnreadNotifications(int $userId, int $limit = 10, int $page = 1, ?string $type = null): array
|
||||
{
|
||||
$query = Notification::where('user_id', $userId)
|
||||
->where('is_read', false);
|
||||
|
||||
// 按类型过滤
|
||||
if (!empty($type)) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
|
||||
$query->orderBy('created_at', 'desc');
|
||||
|
||||
// 分页处理
|
||||
$list = $query->paginate($limit, ['*'], 'page', $page);
|
||||
|
||||
return [
|
||||
'list' => $list->items(),
|
||||
'total' => $list->total(),
|
||||
'page' => $list->currentPage(),
|
||||
'page_size' => $list->perPage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未读通知数量
|
||||
*
|
||||
* @param int $userId
|
||||
* @return int
|
||||
*/
|
||||
public function getUnreadCount(int $userId): int
|
||||
{
|
||||
return Notification::where('user_id', $userId)
|
||||
->where('is_read', false)
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取通知
|
||||
*
|
||||
* @param int $id
|
||||
* @return Notification|null
|
||||
*/
|
||||
public function getById(int $id): ?Notification
|
||||
{
|
||||
return Notification::find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通知
|
||||
*
|
||||
* @param array $data
|
||||
* @return Notification
|
||||
*/
|
||||
public function create(array $data): Notification
|
||||
{
|
||||
$notification = Notification::create($data);
|
||||
|
||||
// 如果用户在线,立即通过WebSocket发送
|
||||
if ($this->webSocketService->isUserOnline($data['user_id'])) {
|
||||
$this->sendViaWebSocket($notification);
|
||||
}
|
||||
|
||||
return $notification;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建通知
|
||||
*
|
||||
* @param array $notificationsData
|
||||
* @return array
|
||||
*/
|
||||
public function batchCreate(array $notificationsData): array
|
||||
{
|
||||
$notifications = [];
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($notificationsData as $data) {
|
||||
$notification = Notification::create($data);
|
||||
$notifications[] = $notification;
|
||||
|
||||
// 如果用户在线,立即通过WebSocket发送
|
||||
if ($this->webSocketService->isUserOnline($data['user_id'])) {
|
||||
$this->sendViaWebSocket($notification);
|
||||
}
|
||||
}
|
||||
DB::commit();
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error('批量创建通知失败', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统通知给单个用户
|
||||
*
|
||||
* @param int $userId
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @param string $type
|
||||
* @param string $category
|
||||
* @param array $extraData
|
||||
* @return Notification
|
||||
*/
|
||||
public function sendToUser(
|
||||
int $userId,
|
||||
string $title,
|
||||
string $content,
|
||||
string $type = Notification::TYPE_INFO,
|
||||
string $category = Notification::CATEGORY_SYSTEM,
|
||||
array $extraData = []
|
||||
): Notification {
|
||||
$data = [
|
||||
'user_id' => $userId,
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'type' => $type,
|
||||
'category' => $category,
|
||||
'data' => $extraData,
|
||||
'is_read' => false,
|
||||
];
|
||||
|
||||
return $this->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统通知给多个用户
|
||||
*
|
||||
* @param array $userIds
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @param string $type
|
||||
* @param string $category
|
||||
* @param array $extraData
|
||||
* @return array
|
||||
*/
|
||||
public function sendToUsers(
|
||||
array $userIds,
|
||||
string $title,
|
||||
string $content,
|
||||
string $type = Notification::TYPE_INFO,
|
||||
string $category = Notification::CATEGORY_SYSTEM,
|
||||
array $extraData = []
|
||||
): array {
|
||||
$notificationsData = [];
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
$notificationsData[] = [
|
||||
'user_id' => $userId,
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'type' => $type,
|
||||
'category' => $category,
|
||||
'data' => $extraData,
|
||||
'is_read' => false,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->batchCreate($notificationsData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统广播通知(所有用户)
|
||||
*
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @param string $type
|
||||
* @param string $category
|
||||
* @param array $extraData
|
||||
* @return array
|
||||
*/
|
||||
public function broadcast(
|
||||
string $title,
|
||||
string $content,
|
||||
string $type = Notification::TYPE_INFO,
|
||||
string $category = Notification::CATEGORY_ANNOUNCEMENT,
|
||||
array $extraData = []
|
||||
): array {
|
||||
// 获取所有用户ID
|
||||
$userIds = \App\Models\Auth\User::where('status', 1)->pluck('id')->toArray();
|
||||
|
||||
return $this->sendToUsers($userIds, $title, $content, $type, $category, $extraData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过WebSocket发送通知
|
||||
*
|
||||
* @param Notification $notification
|
||||
* @return bool
|
||||
*/
|
||||
protected function sendViaWebSocket(Notification $notification): bool
|
||||
{
|
||||
$data = [
|
||||
'type' => 'notification',
|
||||
'data' => [
|
||||
'id' => $notification->id,
|
||||
'title' => $notification->title,
|
||||
'content' => $notification->content,
|
||||
'type' => $notification->type,
|
||||
'category' => $notification->category,
|
||||
'data' => $notification->data,
|
||||
'action_type' => $notification->action_type,
|
||||
'action_data' => $notification->action_data,
|
||||
'timestamp' => $notification->created_at->timestamp,
|
||||
]
|
||||
];
|
||||
|
||||
$result = $this->webSocketService->sendToUser($notification->user_id, $data);
|
||||
|
||||
if ($result) {
|
||||
$notification->markAsSent();
|
||||
} else {
|
||||
$notification->incrementRetry();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试发送未发送的通知
|
||||
*
|
||||
* @param int $limit
|
||||
* @return int
|
||||
*/
|
||||
public function retryUnsentNotifications(int $limit = 100): int
|
||||
{
|
||||
$notifications = Notification::where('sent_via_websocket', false)
|
||||
->where('retry_count', '<', 3)
|
||||
->where('created_at', '>', now()->subHours(24))
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
$sentCount = 0;
|
||||
|
||||
foreach ($notifications as $notification) {
|
||||
if ($this->webSocketService->isUserOnline($notification->user_id)) {
|
||||
if ($this->sendViaWebSocket($notification)) {
|
||||
$sentCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记通知为已读
|
||||
*
|
||||
* @param int $id
|
||||
* @param int $userId
|
||||
* @return bool
|
||||
*/
|
||||
public function markAsRead(int $id, int $userId): bool
|
||||
{
|
||||
$notification = Notification::where('id', $id)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
|
||||
if (!$notification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $notification->markAsRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量标记通知为已读
|
||||
*
|
||||
* @param array $ids
|
||||
* @param int $userId
|
||||
* @return int
|
||||
*/
|
||||
public function batchMarkAsRead(array $ids, int $userId): int
|
||||
{
|
||||
return Notification::where('user_id', $userId)
|
||||
->whereIn('id', $ids)
|
||||
->update([
|
||||
'is_read' => true,
|
||||
'read_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记所有通知为已读
|
||||
*
|
||||
* @param int $userId
|
||||
* @return int
|
||||
*/
|
||||
public function markAllAsRead(int $userId): int
|
||||
{
|
||||
return Notification::where('user_id', $userId)
|
||||
->where('is_read', false)
|
||||
->update([
|
||||
'is_read' => true,
|
||||
'read_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除通知
|
||||
*
|
||||
* @param int $id
|
||||
* @param int $userId
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(int $id, int $userId): bool
|
||||
{
|
||||
$notification = Notification::where('id', $id)
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
|
||||
if (!$notification) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $notification->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除通知
|
||||
*
|
||||
* @param array $ids
|
||||
* @param int $userId
|
||||
* @return int
|
||||
*/
|
||||
public function batchDelete(array $ids, int $userId): int
|
||||
{
|
||||
return Notification::where('user_id', $userId)
|
||||
->whereIn('id', $ids)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空已读通知
|
||||
*
|
||||
* @param int $userId
|
||||
* @return int
|
||||
*/
|
||||
public function clearReadNotifications(int $userId): int
|
||||
{
|
||||
return Notification::where('user_id', $userId)
|
||||
->where('is_read', true)
|
||||
->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知统计
|
||||
*
|
||||
* @param int $userId
|
||||
* @return array
|
||||
*/
|
||||
public function getStatistics(int $userId): array
|
||||
{
|
||||
$total = Notification::where('user_id', $userId)->count();
|
||||
$unread = Notification::where('user_id', $userId)->where('is_read', false)->count();
|
||||
$read = Notification::where('user_id', $userId)->where('is_read', true)->count();
|
||||
|
||||
// 按类型统计
|
||||
$byType = Notification::where('user_id', $userId)
|
||||
->selectRaw('type, COUNT(*) as count')
|
||||
->groupBy('type')
|
||||
->pluck('count', 'type')
|
||||
->toArray();
|
||||
|
||||
// 按分类统计
|
||||
$byCategory = Notification::where('user_id', $userId)
|
||||
->selectRaw('category, COUNT(*) as count')
|
||||
->groupBy('category')
|
||||
->pluck('count', 'category')
|
||||
->toArray();
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'unread' => $unread,
|
||||
'read' => $read,
|
||||
'by_type' => $byType,
|
||||
'by_category' => $byCategory,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送任务通知
|
||||
*
|
||||
* @param int $userId
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @param array $taskData
|
||||
* @return Notification
|
||||
*/
|
||||
public function sendTaskNotification(int $userId, string $title, string $content, array $taskData = []): Notification
|
||||
{
|
||||
return $this->sendToUser(
|
||||
$userId,
|
||||
$title,
|
||||
$content,
|
||||
Notification::TYPE_TASK,
|
||||
Notification::CATEGORY_TASK,
|
||||
array_merge(['task' => $taskData], $taskData)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统维护通知
|
||||
*
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @param array $maintenanceData
|
||||
* @return array
|
||||
*/
|
||||
public function sendMaintenanceNotification(string $title, string $content, array $maintenanceData = []): array
|
||||
{
|
||||
return $this->broadcast(
|
||||
$title,
|
||||
$content,
|
||||
Notification::TYPE_WARNING,
|
||||
Notification::CATEGORY_ANNOUNCEMENT,
|
||||
array_merge(['maintenance' => $maintenanceData], $maintenanceData)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送新消息通知
|
||||
*
|
||||
* @param int $userId
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @param array $messageData
|
||||
* @return Notification
|
||||
*/
|
||||
public function sendNewMessageNotification(int $userId, string $title, string $content, array $messageData = []): Notification
|
||||
{
|
||||
return $this->sendToUser(
|
||||
$userId,
|
||||
$title,
|
||||
$content,
|
||||
Notification::TYPE_INFO,
|
||||
Notification::CATEGORY_MESSAGE,
|
||||
array_merge(['message' => $messageData], $messageData)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送提醒通知
|
||||
*
|
||||
* @param int $userId
|
||||
* @param string $title
|
||||
* @param string $content
|
||||
* @param array $reminderData
|
||||
* @return Notification
|
||||
*/
|
||||
public function sendReminderNotification(int $userId, string $title, string $content, array $reminderData = []): Notification
|
||||
{
|
||||
return $this->sendToUser(
|
||||
$userId,
|
||||
$title,
|
||||
$content,
|
||||
Notification::TYPE_WARNING,
|
||||
Notification::CATEGORY_REMINDER,
|
||||
array_merge(['reminder' => $reminderData], $reminderData)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,25 @@
|
||||
namespace App\Services\System;
|
||||
|
||||
use App\Models\System\Task;
|
||||
use App\Services\System\NotificationService;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TaskService
|
||||
{
|
||||
/**
|
||||
* @var NotificationService
|
||||
*/
|
||||
protected $notificationService;
|
||||
|
||||
/**
|
||||
* TaskService constructor
|
||||
*/
|
||||
public function __construct(NotificationService $notificationService)
|
||||
{
|
||||
$this->notificationService = $notificationService;
|
||||
}
|
||||
|
||||
public function getList(array $params): array
|
||||
{
|
||||
$query = Task::query();
|
||||
@@ -144,6 +159,9 @@ public function run(int $id): array
|
||||
'last_output' => substr($output, 0, 10000),
|
||||
]);
|
||||
|
||||
// 发送任务执行结果通知
|
||||
$this->sendTaskNotification($task, $status, $errorMessage, $executionTime);
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'output' => $output,
|
||||
@@ -164,4 +182,110 @@ public function getStatistics(): array
|
||||
'inactive' => $inactive,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送任务执行结果通知
|
||||
*
|
||||
* @param Task $task
|
||||
* @param string $status
|
||||
* @param string|null $errorMessage
|
||||
* @param int $executionTime
|
||||
* @return void
|
||||
*/
|
||||
protected function sendTaskNotification(Task $task, string $status, ?string $errorMessage, int $executionTime): void
|
||||
{
|
||||
try {
|
||||
// 只对失败的任务或重要的成功任务发送通知
|
||||
// 这里可以根据实际需求调整通知策略
|
||||
|
||||
if ($status === 'error') {
|
||||
// 任务失败通知
|
||||
$title = '任务执行失败: ' . $task->name;
|
||||
$content = sprintf(
|
||||
"任务 %s 执行失败,错误信息:%s\n执行时间:%d 毫秒",
|
||||
$task->name,
|
||||
$errorMessage ?: '未知错误',
|
||||
$executionTime
|
||||
);
|
||||
|
||||
// 获取管理员用户ID列表
|
||||
$adminUserIds = $this->getAdminUserIds();
|
||||
|
||||
if (!empty($adminUserIds)) {
|
||||
$this->notificationService->sendToUsers(
|
||||
$adminUserIds,
|
||||
$title,
|
||||
$content,
|
||||
\App\Models\System\Notification::TYPE_ERROR,
|
||||
\App\Models\System\Notification::CATEGORY_TASK,
|
||||
[
|
||||
'task_id' => $task->id,
|
||||
'task_name' => $task->name,
|
||||
'command' => $task->command,
|
||||
'error_message' => $errorMessage,
|
||||
'execution_time' => $executionTime,
|
||||
'last_run_at' => $task->last_run_at?->toDateTimeString()
|
||||
]
|
||||
);
|
||||
}
|
||||
} elseif ($executionTime > 60000) {
|
||||
// 执行时间超过1分钟的成功任务,发送通知
|
||||
$title = '任务执行完成: ' . $task->name;
|
||||
$content = sprintf(
|
||||
"任务 %s 执行成功,耗时:%.2f 秒",
|
||||
$task->name,
|
||||
$executionTime / 1000
|
||||
);
|
||||
|
||||
$adminUserIds = $this->getAdminUserIds();
|
||||
|
||||
if (!empty($adminUserIds)) {
|
||||
$this->notificationService->sendToUsers(
|
||||
$adminUserIds,
|
||||
$title,
|
||||
$content,
|
||||
\App\Models\System\Notification::TYPE_SUCCESS,
|
||||
\App\Models\System\Notification::CATEGORY_TASK,
|
||||
[
|
||||
'task_id' => $task->id,
|
||||
'task_name' => $task->name,
|
||||
'execution_time' => $executionTime,
|
||||
'last_run_at' => $task->last_run_at?->toDateTimeString()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('发送任务通知失败', [
|
||||
'task_id' => $task->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取管理员用户ID列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getAdminUserIds(): array
|
||||
{
|
||||
try {
|
||||
// 获取拥有管理员权限的用户
|
||||
// 这里可以根据实际业务逻辑调整,例如获取特定角色的用户
|
||||
$adminUserIds = \App\Models\Auth\User::where('status', 1)
|
||||
->whereHas('roles', function ($query) {
|
||||
$query->where('name', 'admin');
|
||||
})
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
return $adminUserIds;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('获取管理员用户列表失败', [
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Facades\Image;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
|
||||
class UploadService
|
||||
{
|
||||
@@ -17,6 +18,7 @@ class UploadService
|
||||
public function __construct()
|
||||
{
|
||||
$this->disk = Storage::disk('public');
|
||||
$this->imageManager = new ImageManager(new Driver());
|
||||
}
|
||||
|
||||
public function upload(UploadedFile $file, string $directory = 'uploads', array $options = []): array
|
||||
@@ -135,17 +137,14 @@ private function compressImage(UploadedFile $file, string $filePath, array $opti
|
||||
$width = $options['width'] ?? null;
|
||||
$height = $options['height'] ?? null;
|
||||
|
||||
$image = Image::make($file);
|
||||
$image = $this->imageManager->read($file);
|
||||
|
||||
if ($width || $height) {
|
||||
$image->resize($width, $height, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
$constraint->upsize();
|
||||
});
|
||||
$image->scale($width, $height);
|
||||
}
|
||||
|
||||
$image->encode(null, $quality);
|
||||
$this->disk->put($filePath, (string) $image);
|
||||
$encoded = $image->toJpeg(quality: $quality);
|
||||
$this->disk->put($filePath, (string) $encoded);
|
||||
}
|
||||
|
||||
public function getFileUrl(string $path): string
|
||||
|
||||
@@ -4,448 +4,476 @@
|
||||
|
||||
use Hhxsv5\LaravelS\Swoole\WebSocketHandlerInterface;
|
||||
use Swoole\Http\Request;
|
||||
use Swoole\Http\Response;
|
||||
use Swoole\WebSocket\Frame;
|
||||
use Swoole\WebSocket\Server;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Services\Auth\UserOnlineService;
|
||||
use Tymon\JWTAuth\Facades\JWTAuth;
|
||||
|
||||
/**
|
||||
* WebSocket Handler
|
||||
* WebSocket 处理器
|
||||
*
|
||||
* Handles WebSocket connections, messages, and disconnections
|
||||
* 处理 WebSocket 连接事件:onOpen, onMessage, onClose
|
||||
*/
|
||||
class WebSocketHandler implements WebSocketHandlerInterface
|
||||
{
|
||||
/**
|
||||
* @var UserOnlineService
|
||||
*/
|
||||
protected $userOnlineService;
|
||||
|
||||
/**
|
||||
* WebSocketHandler constructor
|
||||
* WebSocketHandlerInterface 需要的构造函数
|
||||
* wsTable 直接从 handler 方法的 $server 参数中访问
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->userOnlineService = app(UserOnlineService::class);
|
||||
// 空构造函数 - wsTable 从 server 参数中访问
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WebSocket connection open event
|
||||
* 处理 WebSocket 握手(可选)
|
||||
*
|
||||
* @param Server $server
|
||||
* @param Request $request
|
||||
* @param Request $request 请求对象
|
||||
* @param Response $response 响应对象
|
||||
* @return void
|
||||
*/
|
||||
// public function onHandShake(Request $request, Response $response)
|
||||
// {
|
||||
// // 自定义握手逻辑(如果需要)
|
||||
// // 握手成功后,onOpen 事件会自动触发
|
||||
// }
|
||||
|
||||
/**
|
||||
* 处理连接打开事件
|
||||
*
|
||||
* @param Server $server WebSocket 服务器对象
|
||||
* @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'] ?? '/';
|
||||
// 从服务器获取 wsTable
|
||||
$wsTable = $server->wsTable;
|
||||
|
||||
Log::info('WebSocket connection opened', [
|
||||
'fd' => $fd,
|
||||
'path' => $path,
|
||||
'ip' => $request->server['remote_addr'] ?? 'unknown'
|
||||
]);
|
||||
// 从查询字符串获取 user_id 和 token
|
||||
$userId = (int)($request->get['user_id'] ?? 0);
|
||||
$token = $request->get['token'] ?? '';
|
||||
|
||||
// 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([
|
||||
// 用户认证
|
||||
if (!$userId || !$token) {
|
||||
$this->safePush($server, $request->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => 'Authentication required. Please provide user_id and token.',
|
||||
'message' => '认证失败:缺少 user_id 或 token',
|
||||
'code' => 401
|
||||
]
|
||||
]));
|
||||
$server->disconnect($request->fd);
|
||||
return;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('WebSocket onOpen error', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
|
||||
// 验证 JWT token
|
||||
try {
|
||||
$payload = JWTAuth::setToken($token)->getPayload();
|
||||
|
||||
// 验证 token 中的用户 ID 是否匹配
|
||||
$tokenUserId = $payload['sub'] ?? null;
|
||||
if ($tokenUserId != $userId) {
|
||||
$this->safePush($server, $request->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '认证失败:用户 ID 不匹配',
|
||||
'code' => 401
|
||||
]
|
||||
]));
|
||||
$server->disconnect($request->fd);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 token 是否过期
|
||||
if (isset($payload['exp']) && $payload['exp'] < time()) {
|
||||
$this->safePush($server, $request->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '认证失败:token 已过期',
|
||||
'code' => 401
|
||||
]
|
||||
]));
|
||||
$server->disconnect($request->fd);
|
||||
return;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->safePush($server, $request->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '认证失败:无效的 token',
|
||||
'code' => 401
|
||||
]
|
||||
]));
|
||||
$server->disconnect($request->fd);
|
||||
return;
|
||||
}
|
||||
|
||||
// 存储连接映射:uid:{userId} -> fd
|
||||
$wsTable->set('uid:' . $userId, [
|
||||
'value' => $request->fd,
|
||||
'expiry' => time() + 3600 // 1 小时过期
|
||||
]);
|
||||
|
||||
// 存储反向映射:fd:{fd} -> userId
|
||||
$wsTable->set('fd:' . $request->fd, [
|
||||
'value' => $userId,
|
||||
'expiry' => time() + 3600
|
||||
]);
|
||||
|
||||
// 发送欢迎消息
|
||||
$this->safePush($server, $request->fd, json_encode([
|
||||
'type' => 'connected',
|
||||
'data' => [
|
||||
'message' => '欢迎连接到 LaravelS WebSocket',
|
||||
'user_id' => $userId,
|
||||
'fd' => $request->fd,
|
||||
'timestamp' => time()
|
||||
]
|
||||
]));
|
||||
} catch (\Exception $e) {
|
||||
$this->safePush($server, $request->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '连接错误:' . $e->getMessage(),
|
||||
'code' => 500
|
||||
]
|
||||
]));
|
||||
$server->disconnect($request->fd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle WebSocket message event
|
||||
* 安全的推送消息(检查连接是否建立)
|
||||
*
|
||||
* @param Server $server
|
||||
* @param Frame $frame
|
||||
* @param Server $server WebSocket 服务器对象
|
||||
* @param int $fd 文件描述符
|
||||
* @param string $data 要发送的数据
|
||||
* @return bool 是否发送成功
|
||||
*/
|
||||
protected function safePush(Server $server, int $fd, string $data): bool
|
||||
{
|
||||
try {
|
||||
if ($server->isEstablished($fd)) {
|
||||
return $server->push($fd, $data);
|
||||
}
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收消息事件
|
||||
*
|
||||
* @param Server $server WebSocket 服务器对象
|
||||
* @param Frame $frame WebSocket 帧对象
|
||||
* @return void
|
||||
*/
|
||||
public function onMessage(Server $server, Frame $frame): void
|
||||
{
|
||||
try {
|
||||
$fd = $frame->fd;
|
||||
$data = $frame->data;
|
||||
// 从服务器获取 wsTable
|
||||
$wsTable = $server->wsTable;
|
||||
|
||||
Log::info('WebSocket message received', [
|
||||
'fd' => $fd,
|
||||
'data' => $data,
|
||||
'opcode' => $frame->opcode
|
||||
]);
|
||||
// 从 fd 映射获取 user_id
|
||||
$fdInfo = $wsTable->get('fd:' . $frame->fd);
|
||||
if ($fdInfo === false) {
|
||||
$server->disconnect($frame->fd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse incoming message
|
||||
$message = json_decode($data, true);
|
||||
$userId = (int)$fdInfo['value'];
|
||||
|
||||
if (!$message) {
|
||||
$server->push($fd, json_encode([
|
||||
// 解析消息
|
||||
$message = json_decode($frame->data, true);
|
||||
|
||||
if (!$message || !isset($message['type'])) {
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => 'Invalid JSON format',
|
||||
'message' => '无效的消息格式',
|
||||
'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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
$type = $message['type'];
|
||||
$data = $message['data'] ?? [];
|
||||
|
||||
/**
|
||||
* 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':
|
||||
// 响应 ping
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'pong',
|
||||
'data' => $data
|
||||
]));
|
||||
break;
|
||||
|
||||
switch ($type) {
|
||||
case 'ping':
|
||||
// Respond to ping with pong
|
||||
$server->push($fd, json_encode([
|
||||
'type' => 'pong',
|
||||
'data' => [
|
||||
'timestamp' => time()
|
||||
]
|
||||
]));
|
||||
break;
|
||||
case 'heartbeat':
|
||||
// 心跳确认
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'heartbeat_ack',
|
||||
'data' => array_merge($data, [
|
||||
'timestamp' => time()
|
||||
])
|
||||
]));
|
||||
break;
|
||||
|
||||
case 'heartbeat':
|
||||
// Handle heartbeat
|
||||
$server->push($fd, json_encode([
|
||||
'type' => 'heartbeat_ack',
|
||||
'data' => [
|
||||
'timestamp' => time()
|
||||
]
|
||||
]));
|
||||
break;
|
||||
case 'chat':
|
||||
// 私聊消息
|
||||
$this->handleChatMessage($server, $wsTable, $frame, $userId, $data);
|
||||
break;
|
||||
|
||||
case 'chat':
|
||||
// Handle chat message
|
||||
$this->handleChatMessage($server, $fd, $data);
|
||||
break;
|
||||
case 'broadcast':
|
||||
// 广播消息给所有用户
|
||||
$this->handleBroadcast($server, $wsTable, $userId, $data);
|
||||
break;
|
||||
|
||||
case 'broadcast':
|
||||
// Handle broadcast message (admin only)
|
||||
$this->handleBroadcast($server, $fd, $data);
|
||||
break;
|
||||
case 'subscribe':
|
||||
// 订阅频道
|
||||
$this->handleSubscribe($server, $wsTable, $frame, $userId, $data);
|
||||
break;
|
||||
|
||||
case 'subscribe':
|
||||
// Handle channel subscription
|
||||
$this->handleSubscribe($server, $fd, $data);
|
||||
break;
|
||||
case 'unsubscribe':
|
||||
// 取消订阅频道
|
||||
$this->handleUnsubscribe($server, $wsTable, $frame, $userId, $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);
|
||||
default:
|
||||
// 未知消息类型
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '未知的消息类型:' . $type,
|
||||
'code' => 400
|
||||
]
|
||||
]));
|
||||
break;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
// 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
|
||||
* @param Server $server WebSocket 服务器对象
|
||||
* @param int $fd 文件描述符
|
||||
* @param int $reactorId 反应器 ID
|
||||
* @return void
|
||||
*/
|
||||
public function onClose(Server $server, $fd, $reactorId): void
|
||||
{
|
||||
try {
|
||||
Log::info('WebSocket connection closed', [
|
||||
'fd' => $fd,
|
||||
'reactor_id' => $reactorId
|
||||
]);
|
||||
// 从服务器获取 wsTable
|
||||
$wsTable = $server->wsTable;
|
||||
|
||||
// Get user ID from wsTable
|
||||
$userId = $server->wsTable->get('fd:' . $fd)['value'] ?? null;
|
||||
// 从 fd 映射获取 user_id
|
||||
$fdInfo = $wsTable->get('fd:' . $fd);
|
||||
|
||||
if ($userId) {
|
||||
// Remove user connection mapping
|
||||
$server->wsTable->del('uid:' . $userId);
|
||||
$server->wsTable->del('fd:' . $fd);
|
||||
if ($fdInfo !== false) {
|
||||
$userId = (int)$fdInfo['value'];
|
||||
|
||||
// Update user online status
|
||||
$this->userOnlineService->updateUserOnlineStatus($userId, $fd, false);
|
||||
// 删除 uid 映射
|
||||
$wsTable->del('uid:' . $userId);
|
||||
|
||||
Log::info('User disconnected from WebSocket', [
|
||||
'user_id' => $userId,
|
||||
'fd' => $fd
|
||||
]);
|
||||
// 删除该用户的所有频道订阅
|
||||
$this->removeUserFromAllChannels($wsTable, $userId, $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
|
||||
// 删除 fd 映射
|
||||
$wsTable->del('fd:' . $fd);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('WebSocket onClose error', [
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理私聊消息
|
||||
*
|
||||
* @param Server $server WebSocket 服务器对象
|
||||
* @param \Swoole\Table $wsTable WebSocket 表
|
||||
* @param Frame $frame WebSocket 帧对象
|
||||
* @param int $fromUserId 发送者用户 ID
|
||||
* @param array $data 消息数据
|
||||
* @return void
|
||||
*/
|
||||
protected function handleChatMessage(Server $server, \Swoole\Table $wsTable, Frame $frame, int $fromUserId, array $data): void
|
||||
{
|
||||
$toUserId = $data['to_user_id'] ?? 0;
|
||||
|
||||
if (!$toUserId) {
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '缺少 to_user_id',
|
||||
'code' => 400
|
||||
]
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取接收者的 fd
|
||||
$recipientInfo = $wsTable->get('uid:' . $toUserId);
|
||||
|
||||
if ($recipientInfo === false) {
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '用户不在线',
|
||||
'to_user_id' => $toUserId,
|
||||
'code' => 404
|
||||
]
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
$toFd = (int)$recipientInfo['value'];
|
||||
|
||||
// 发送消息给接收者
|
||||
$this->safePush($server, $toFd, json_encode([
|
||||
'type' => 'chat',
|
||||
'data' => array_merge($data, [
|
||||
'from_user_id' => $fromUserId,
|
||||
'timestamp' => time()
|
||||
])
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理广播消息
|
||||
*
|
||||
* @param Server $server WebSocket 服务器对象
|
||||
* @param \Swoole\Table $wsTable WebSocket 表
|
||||
* @param int $userId 用户 ID
|
||||
* @param array $data 消息数据
|
||||
* @return void
|
||||
*/
|
||||
protected function handleBroadcast(Server $server, \Swoole\Table $wsTable, int $userId, array $data): void
|
||||
{
|
||||
$excludeUserId = $data['exclude_user_id'] ?? null;
|
||||
$message = json_encode([
|
||||
'type' => 'broadcast',
|
||||
'data' => array_merge($data, [
|
||||
'from_user_id' => $userId,
|
||||
'timestamp' => time()
|
||||
])
|
||||
]);
|
||||
|
||||
// 发送消息给所有连接的用户
|
||||
foreach ($wsTable as $key => $row) {
|
||||
if (strpos($key, 'uid:') === 0) {
|
||||
$targetUserId = (int)substr($key, 4); // 移除 'uid:' 前缀
|
||||
$fd = (int)$row['value'];
|
||||
|
||||
// 跳过排除的用户
|
||||
if ($excludeUserId && $targetUserId == $excludeUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->safePush($server, $fd, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理频道订阅
|
||||
*
|
||||
* @param Server $server WebSocket 服务器对象
|
||||
* @param \Swoole\Table $wsTable WebSocket 表
|
||||
* @param Frame $frame WebSocket 帧对象
|
||||
* @param int $userId 用户 ID
|
||||
* @param array $data 消息数据
|
||||
* @return void
|
||||
*/
|
||||
protected function handleSubscribe(Server $server, \Swoole\Table $wsTable, Frame $frame, int $userId, array $data): void
|
||||
{
|
||||
$channel = $data['channel'] ?? '';
|
||||
|
||||
if (!$channel) {
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '缺少频道名称',
|
||||
'code' => 400
|
||||
]
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
// 存储频道订阅
|
||||
$channelKey = 'channel:' . $channel . ':fd:' . $frame->fd;
|
||||
$wsTable->set($channelKey, [
|
||||
'value' => $userId,
|
||||
'expiry' => time() + 3600
|
||||
]);
|
||||
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'subscribed',
|
||||
'data' => [
|
||||
'channel' => $channel,
|
||||
'message' => '成功订阅频道:' . $channel,
|
||||
'timestamp' => time()
|
||||
]
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理频道取消订阅
|
||||
*
|
||||
* @param Server $server WebSocket 服务器对象
|
||||
* @param \Swoole\Table $wsTable WebSocket 表
|
||||
* @param Frame $frame WebSocket 帧对象
|
||||
* @param int $userId 用户 ID
|
||||
* @param array $data 消息数据
|
||||
* @return void
|
||||
*/
|
||||
protected function handleUnsubscribe(Server $server, \Swoole\Table $wsTable, Frame $frame, int $userId, array $data): void
|
||||
{
|
||||
$channel = $data['channel'] ?? '';
|
||||
|
||||
if (!$channel) {
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'error',
|
||||
'data' => [
|
||||
'message' => '缺少频道名称',
|
||||
'code' => 400
|
||||
]
|
||||
]));
|
||||
return;
|
||||
}
|
||||
|
||||
// 删除频道订阅
|
||||
$channelKey = 'channel:' . $channel . ':fd:' . $frame->fd;
|
||||
$wsTable->del($channelKey);
|
||||
|
||||
$this->safePush($server, $frame->fd, json_encode([
|
||||
'type' => 'unsubscribed',
|
||||
'data' => [
|
||||
'channel' => $channel,
|
||||
'message' => '成功取消订阅频道:' . $channel,
|
||||
'timestamp' => time()
|
||||
]
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从所有频道中移除用户
|
||||
*
|
||||
* @param \Swoole\Table $wsTable WebSocket 表
|
||||
* @param int $userId 用户 ID
|
||||
* @param int $fd 文件描述符
|
||||
* @return void
|
||||
*/
|
||||
protected function removeUserFromAllChannels(\Swoole\Table $wsTable, int $userId, int $fd): void
|
||||
{
|
||||
foreach ($wsTable as $key => $row) {
|
||||
if (strpos($key, 'channel:') === 0 && strpos($key, ':fd:' . $fd) !== false) {
|
||||
$wsTable->del($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,76 +2,82 @@
|
||||
|
||||
namespace App\Services\WebSocket;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Swoole\WebSocket\Server;
|
||||
|
||||
/**
|
||||
* WebSocket Service
|
||||
* WebSocket 服务
|
||||
*
|
||||
* Provides helper functions for WebSocket operations
|
||||
* 提供 WebSocket 操作的便捷方法
|
||||
*/
|
||||
class WebSocketService
|
||||
{
|
||||
/**
|
||||
* Get Swoole WebSocket Server instance
|
||||
* 获取 Swoole Server 实例
|
||||
*
|
||||
* @return Server|null
|
||||
* @return Server
|
||||
*/
|
||||
public function getServer(): ?Server
|
||||
protected function getServer(): Server
|
||||
{
|
||||
return app('swoole.server');
|
||||
/** @var Server $server */
|
||||
$server = app('swoole');
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to a specific user
|
||||
* 获取 WebSocket 表
|
||||
*
|
||||
* @param int $userId
|
||||
* @param array $data
|
||||
* @return \Swoole\Table
|
||||
*/
|
||||
protected function getWsTable(): \Swoole\Table
|
||||
{
|
||||
return app('swoole')->wsTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定用户
|
||||
*
|
||||
* @param int $userId 用户 ID
|
||||
* @param array $data 消息数据
|
||||
* @return bool
|
||||
*/
|
||||
public function sendToUser(int $userId, array $data): bool
|
||||
{
|
||||
$server = $this->getServer();
|
||||
try {
|
||||
$wsTable = $this->getWsTable();
|
||||
$server = $this->getServer();
|
||||
|
||||
if (!$server || !isset($server->wsTable)) {
|
||||
Log::warning('WebSocket server not available', ['user_id' => $userId]);
|
||||
// 获取用户的 fd
|
||||
$fdInfo = $wsTable->get('uid:' . $userId);
|
||||
|
||||
if ($fdInfo === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fd = (int)$fdInfo['value'];
|
||||
|
||||
// 检查连接是否仍然建立
|
||||
if (!$server->isEstablished($fd)) {
|
||||
// 删除过期连接
|
||||
$wsTable->del('uid:' . $userId);
|
||||
$wsTable->del('fd:' . $fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
$result = $server->push($fd, json_encode($data));
|
||||
|
||||
return $result;
|
||||
} catch (\Exception $e) {
|
||||
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
|
||||
* @param array $userIds 用户 ID 数组
|
||||
* @param array $data 消息数据
|
||||
* @return array 成功发送的用户 ID 数组
|
||||
*/
|
||||
public function sendToUsers(array $userIds, array $data): array
|
||||
{
|
||||
@@ -87,239 +93,223 @@ public function sendToUsers(array $userIds, array $data): array
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param array $data 消息数据
|
||||
* @param int|null $excludeUserId 要排除的用户 ID
|
||||
* @return int 成功发送的用户数量
|
||||
*/
|
||||
public function broadcast(array $data, ?int $excludeUserId = null): int
|
||||
{
|
||||
$server = $this->getServer();
|
||||
try {
|
||||
$wsTable = $this->getWsTable();
|
||||
$server = $this->getServer();
|
||||
|
||||
if (!$server) {
|
||||
Log::warning('WebSocket server not available for broadcast');
|
||||
return 0;
|
||||
}
|
||||
$message = json_encode($data);
|
||||
$count = 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) {
|
||||
foreach ($wsTable as $key => $row) {
|
||||
// 只处理用户映射(uid:*)
|
||||
if (strpos($key, 'uid:') !== 0) {
|
||||
continue;
|
||||
}
|
||||
$userId = (int)substr($key, 4); // 移除 'uid:' 前缀
|
||||
$fd = (int)$row['value'];
|
||||
|
||||
// 跳过排除的用户
|
||||
if ($excludeUserId && $userId == $excludeUserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查连接是否已建立并发送
|
||||
if ($server->isEstablished($fd)) {
|
||||
if ($server->push($fd, $message)) {
|
||||
$count++;
|
||||
}
|
||||
} else {
|
||||
// 删除过期连接
|
||||
$wsTable->del('uid:' . $userId);
|
||||
$wsTable->del('fd:' . $fd);
|
||||
}
|
||||
}
|
||||
|
||||
$server->push($fd, $message);
|
||||
$count++;
|
||||
return $count;
|
||||
} catch (\Exception $e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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
|
||||
* @param string $channel 频道名称
|
||||
* @param array $data 消息数据
|
||||
* @return int 成功发送的订阅者数量
|
||||
*/
|
||||
public function sendToChannel(string $channel, array $data): int
|
||||
{
|
||||
$server = $this->getServer();
|
||||
try {
|
||||
$wsTable = $this->getWsTable();
|
||||
$server = $this->getServer();
|
||||
|
||||
if (!$server || !isset($server->wsTable)) {
|
||||
Log::warning('WebSocket server not available for channel broadcast', ['channel' => $channel]);
|
||||
$message = json_encode($data);
|
||||
$count = 0;
|
||||
$channelPrefix = 'channel:' . $channel . ':fd:';
|
||||
|
||||
foreach ($wsTable as $key => $row) {
|
||||
// 只处理该频道的订阅
|
||||
if (strpos($key, $channelPrefix) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fd = (int)substr($key, strlen($channelPrefix));
|
||||
|
||||
// 检查连接是否已建立并发送
|
||||
if ($server->isEstablished($fd)) {
|
||||
if ($server->push($fd, $message)) {
|
||||
$count++;
|
||||
}
|
||||
} else {
|
||||
// 删除过期订阅
|
||||
$wsTable->del($key);
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
} catch (\Exception $e) {
|
||||
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();
|
||||
try {
|
||||
$wsTable = $this->getWsTable();
|
||||
$count = 0;
|
||||
|
||||
if (!$server || !isset($server->wsTable)) {
|
||||
foreach ($wsTable as $key => $row) {
|
||||
if (strpos($key, 'uid:') === 0) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
} catch (\Exception $e) {
|
||||
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
|
||||
* @param int $userId 用户 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function isUserOnline(int $userId): bool
|
||||
{
|
||||
$server = $this->getServer();
|
||||
try {
|
||||
$wsTable = $this->getWsTable();
|
||||
$fdInfo = $wsTable->get('uid:' . $userId);
|
||||
|
||||
if (!$server || !isset($server->wsTable)) {
|
||||
if ($fdInfo === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$server = $this->getServer();
|
||||
$fd = (int)$fdInfo['value'];
|
||||
|
||||
return $server->isEstablished($fd);
|
||||
} catch (\Exception $e) {
|
||||
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
|
||||
* 获取在线用户 ID 列表
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getOnlineUserIds(): array
|
||||
{
|
||||
$server = $this->getServer();
|
||||
try {
|
||||
$wsTable = $this->getWsTable();
|
||||
$userIds = [];
|
||||
|
||||
if (!$server || !isset($server->wsTable)) {
|
||||
foreach ($wsTable as $key => $row) {
|
||||
if (strpos($key, 'uid:') === 0) {
|
||||
$userId = (int)substr($key, 4); // 移除 'uid:' 前缀
|
||||
$userIds[] = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
return $userIds;
|
||||
} catch (\Exception $e) {
|
||||
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
|
||||
* 断开用户 WebSocket 连接
|
||||
*
|
||||
* @param string $title
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @param array $extraData
|
||||
* @return int
|
||||
* @param int $userId 用户 ID
|
||||
* @return bool
|
||||
*/
|
||||
public function sendSystemNotification(string $title, string $message, string $type = 'info', array $extraData = []): int
|
||||
public function disconnectUser(int $userId): bool
|
||||
{
|
||||
try {
|
||||
$wsTable = $this->getWsTable();
|
||||
$server = $this->getServer();
|
||||
|
||||
// 获取用户的 fd
|
||||
$fdInfo = $wsTable->get('uid:' . $userId);
|
||||
|
||||
if ($fdInfo === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fd = (int)$fdInfo['value'];
|
||||
|
||||
// 断开连接
|
||||
$server->disconnect($fd);
|
||||
|
||||
// 删除映射
|
||||
$wsTable->del('uid:' . $userId);
|
||||
$wsTable->del('fd:' . $fd);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送系统通知
|
||||
*
|
||||
* @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
|
||||
'type' => $type,
|
||||
'data' => $extraData,
|
||||
'timestamp' => time()
|
||||
]
|
||||
];
|
||||
|
||||
@@ -327,47 +317,57 @@ public function sendSystemNotification(string $title, string $message, string $t
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to specific users
|
||||
* 发送通知给指定用户
|
||||
*
|
||||
* @param array $userIds
|
||||
* @param string $title
|
||||
* @param string $message
|
||||
* @param string $type
|
||||
* @param array $extraData
|
||||
* @return array
|
||||
* @param array $userIds 用户 ID 数组
|
||||
* @param string $title 标题
|
||||
* @param string $message 消息内容
|
||||
* @param string $type 类型
|
||||
* @param array $extraData 额外数据
|
||||
* @return int 成功发送的用户数量
|
||||
*/
|
||||
public function sendNotificationToUsers(array $userIds, string $title, string $message, string $type = 'info', array $extraData = []): array
|
||||
{
|
||||
public function sendNotificationToUsers(
|
||||
array $userIds,
|
||||
string $title,
|
||||
string $message,
|
||||
string $type = 'info',
|
||||
array $extraData = []
|
||||
): int {
|
||||
$data = [
|
||||
'type' => 'notification',
|
||||
'data' => [
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'type' => $type,
|
||||
'timestamp' => time(),
|
||||
...$extraData
|
||||
'data' => $extraData,
|
||||
'timestamp' => time()
|
||||
]
|
||||
];
|
||||
|
||||
return $this->sendToUsers($userIds, $data);
|
||||
$sentTo = $this->sendToUsers($userIds, $data);
|
||||
return count($sentTo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push data update to specific users
|
||||
* 推送数据更新
|
||||
*
|
||||
* @param array $userIds
|
||||
* @param string $resourceType
|
||||
* @param string $action
|
||||
* @param array $data
|
||||
* @return array
|
||||
* @param array $userIds 用户 ID 数组
|
||||
* @param string $resourceType 资源类型
|
||||
* @param string $action 操作
|
||||
* @param array $data 数据
|
||||
* @return array 成功推送的用户 ID 数组
|
||||
*/
|
||||
public function pushDataUpdate(array $userIds, string $resourceType, string $action, array $data): 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
|
||||
'resource_type' => $resourceType,
|
||||
'action' => $action,
|
||||
'data' => $data,
|
||||
'timestamp' => time()
|
||||
]
|
||||
@@ -377,16 +377,20 @@ public function pushDataUpdate(array $userIds, string $resourceType, string $act
|
||||
}
|
||||
|
||||
/**
|
||||
* Push data update to a channel
|
||||
* 推送数据更新到频道
|
||||
*
|
||||
* @param string $channel
|
||||
* @param string $resourceType
|
||||
* @param string $action
|
||||
* @param array $data
|
||||
* @return int
|
||||
* @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
|
||||
{
|
||||
public function pushDataUpdateToChannel(
|
||||
string $channel,
|
||||
string $resourceType,
|
||||
string $action,
|
||||
array $data
|
||||
): int {
|
||||
$message = [
|
||||
'type' => 'data_update',
|
||||
'data' => [
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace App\Support;
|
||||
|
||||
class Regex {
|
||||
|
||||
|
||||
/**
|
||||
* 验证用户名
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @param int $minLen 最小长度
|
||||
* @param int $maxLen 最大长度
|
||||
* @param string $type 验证类型,默认‘ALL’,EN.验证英文,CN.验证中文,ALL.验证中文和英文
|
||||
* @return bool
|
||||
*/
|
||||
public static function isUsername($value, $minLen = 2, $maxLen = 48, $type = 'ALL')
|
||||
{
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'EN' :
|
||||
$match = '/^[_\w\d]{' . $minLen . ',' . $maxLen . '}$/iu';
|
||||
break;
|
||||
case 'CN' :
|
||||
$match = '/^[_\x{4e00}-\x{9fa5}\d]{' . $minLen . ',' . $maxLen . '}$/iu';
|
||||
break;
|
||||
default :
|
||||
$match = '/^[_\w\d\x{4e00}-\x{9fa5}]{' . $minLen . ',' . $maxLen . '}$/iu';
|
||||
}
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @param int $minLen 最小长度
|
||||
* @param int $maxLen 最大长度
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPassword($value, $minLen = 6, $maxLen = 16)
|
||||
{
|
||||
$value = trim($value);
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$match = '/^[\\~!@#$%^&*()-_=+|{}\[\],.?\/:;\'\"\d\w]{' . $minLen . ',' . $maxLen . '}$/';
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证eamil
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return bool
|
||||
*/
|
||||
public static function isEmail($value)
|
||||
{
|
||||
$value = trim($value);
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$match = '/^[\w\d]+[\w\d-.]*@[\w\d-.]+\.[\w\d]{2,10}$/i';
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证电话号码
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return bool
|
||||
*/
|
||||
public static function isTelephone($value)
|
||||
{
|
||||
$value = trim($value);
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$match = '/^0[0-9]{2,3}[-]?\d{7,8}$/';
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return bool
|
||||
*/
|
||||
public static function isMobile($value)
|
||||
{
|
||||
$value = trim($value);
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$match = '/^[(86)|0]?(1\d{10})$/';
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮政编码
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return bool
|
||||
*/
|
||||
public static function isPostCode($value)
|
||||
{
|
||||
$value = trim($value);
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$match = '/\d{6}/';
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证IP
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isIp($value)
|
||||
{
|
||||
$value = trim($value);
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$match = '/^(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])' .
|
||||
'\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)' .
|
||||
'\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)' .
|
||||
'\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])$/';
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证身份证号码
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isIDCard($value)
|
||||
{
|
||||
$value = trim($value);
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($value) > 18) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$match = '/^\d{6}((1[89])|(2\d))\d{2}((0\d)|(1[0-2]))((3[01])|([0-2]\d))\d{3}(\d|X)$/i';
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证URL
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isUrl($value)
|
||||
{
|
||||
$value = strtolower(trim($value));
|
||||
if (empty ($value)) {
|
||||
return false;
|
||||
}
|
||||
$match = '/^(http:\/\/)?(https:\/\/)?([\w\d-]+\.)+[\w-]+(\/[\d\w-.\/?%&=]*)?$/';
|
||||
|
||||
return preg_match($match, $value) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有数字
|
||||
* 说明:如果字符串中含有非法字符返回假,没有返回真
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return int
|
||||
*/
|
||||
public static function hasNumber($value)
|
||||
{
|
||||
return preg_match("/[0-9]/", $value) != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否含有英文
|
||||
* 说明:如果字符串中含有非法字符返回假,没有返回真
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasEnglish($value)
|
||||
{
|
||||
return preg_match("/[a-zA-Z]/", $value) != false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有中文
|
||||
* 说明:如果字符串中含有非法字符返回假,没有返回真
|
||||
*
|
||||
* @param string $value 验证的值
|
||||
* @return bool
|
||||
*/
|
||||
public static function hasChinese($value)
|
||||
{
|
||||
return preg_match("/[\x7f-\xff]/", $value) != false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class Time {
|
||||
|
||||
/**
|
||||
* 返回今日开始和结束的时间戳
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function today($data = '') {
|
||||
[$y, $m, $d] = explode('-', $data ? $data : date('Y-m-d'));
|
||||
|
||||
return [
|
||||
mktime(0, 0, 0, $m, $d, $y),
|
||||
mktime(23, 59, 59, $m, $d, $y),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回昨日开始和结束的时间戳
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function yesterday() {
|
||||
$yesterday = date('d') - 1;
|
||||
|
||||
return [
|
||||
mktime(0, 0, 0, date('m'), $yesterday, date('Y')),
|
||||
mktime(23, 59, 59, date('m'), $yesterday, date('Y')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回本周开始和结束的时间戳
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function week() {
|
||||
[$y, $m, $d, $w] = explode('-', date('Y-m-d-w'));
|
||||
if ($w == 0) $w = 7; //修正周日的问题
|
||||
|
||||
return [
|
||||
mktime(0, 0, 0, $m, $d - $w + 1, $y), mktime(23, 59, 59, $m, $d - $w + 7, $y),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上周开始和结束的时间戳
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function lastWeek() {
|
||||
$timestamp = time();
|
||||
|
||||
return [
|
||||
strtotime(date('Y-m-d', strtotime("last week Monday", $timestamp))),
|
||||
strtotime(date('Y-m-d', strtotime("last week Sunday", $timestamp))) + 24 * 3600 - 1,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回本月开始和结束的时间戳
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function month($data = '') {
|
||||
$res = [];
|
||||
$data = $data ? $data : date('Y-m-d');
|
||||
$nextMoth = date('Y-m-d', strtotime($data . ' +1 month'));
|
||||
|
||||
return [
|
||||
mktime(0, 0, 0, date('m', strtotime($data)), 1, date('Y', strtotime($data))),
|
||||
mktime(0, 0, 0, date('m', strtotime($nextMoth)), 1, date('Y', strtotime($nextMoth))),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上个月开始和结束的时间戳
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function lastMonth() {
|
||||
$y = date('Y');
|
||||
$m = date('m');
|
||||
$begin = mktime(0, 0, 0, $m - 1, 1, $y);
|
||||
$end = mktime(23, 59, 59, $m - 1, date('t', $begin), $y);
|
||||
|
||||
return [$begin, $end];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回今年开始和结束的时间戳
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function year() {
|
||||
$y = date('Y');
|
||||
|
||||
return [
|
||||
mktime(0, 0, 0, 1, 1, $y),
|
||||
mktime(23, 59, 59, 12, 31, $y),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回去年开始和结束的时间戳
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function lastYear() {
|
||||
$year = date('Y') - 1;
|
||||
|
||||
return [
|
||||
mktime(0, 0, 0, 1, 1, $year),
|
||||
mktime(23, 59, 59, 12, 31, $year),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取几天前零点到现在/昨日结束的时间戳
|
||||
*
|
||||
* @param int $day 天数
|
||||
* @param bool $now 返回现在或者昨天结束时间戳
|
||||
* @return array
|
||||
*/
|
||||
public static function dayToNow($day = 1, $now = true) {
|
||||
$end = time();
|
||||
if (!$now) {
|
||||
[$foo, $end] = self::yesterday();
|
||||
}
|
||||
|
||||
return [
|
||||
mktime(0, 0, 0, date('m'), date('d') - $day, date('Y')),
|
||||
$end,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回几天前的时间戳
|
||||
*
|
||||
* @param int $day
|
||||
* @return int
|
||||
*/
|
||||
public static function daysAgo($day = 1) {
|
||||
$nowTime = time();
|
||||
|
||||
return $nowTime - self::daysToSecond($day);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回几天后的时间戳
|
||||
*
|
||||
* @param int $day
|
||||
* @return int
|
||||
*/
|
||||
public static function daysAfter($day = 1) {
|
||||
$nowTime = time();
|
||||
|
||||
return $nowTime + self::daysToSecond($day);
|
||||
}
|
||||
|
||||
/**
|
||||
* 天数转换成秒数
|
||||
*
|
||||
* @param int $day
|
||||
* @return int
|
||||
*/
|
||||
public static function daysToSecond($day = 1) {
|
||||
return $day * 86400;
|
||||
}
|
||||
|
||||
/**
|
||||
* 周数转换成秒数
|
||||
*
|
||||
* @param int $week
|
||||
* @return int
|
||||
*/
|
||||
public static function weekToSecond($week = 1) {
|
||||
return self::daysToSecond() * 7 * $week;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取毫秒级别的时间戳
|
||||
*/
|
||||
public static function getMillisecond() {
|
||||
$time = explode(" ", microtime());
|
||||
$time = $time[1] . ($time[0] * 1000);
|
||||
$time2 = explode(".", $time);
|
||||
$time = $time2[0];
|
||||
|
||||
return $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对时间
|
||||
*
|
||||
* @param int $timeStamp
|
||||
* @return string
|
||||
*/
|
||||
public static function formatRelative($timeStamp) {
|
||||
$currentTime = time();
|
||||
|
||||
// 判断传入时间戳是否早于当前时间戳
|
||||
$isEarly = $timeStamp <= $currentTime;
|
||||
|
||||
// 获取两个时间戳差值
|
||||
$diff = abs($currentTime - $timeStamp);
|
||||
|
||||
$dirStr = $isEarly ? '前' : '后';
|
||||
|
||||
if ($diff < 60) { // 一分钟之内
|
||||
$resStr = $diff . '秒' . $dirStr;
|
||||
} elseif ($diff >= 60 && $diff < 3600) { // 多于59秒,少于等于59分钟59秒
|
||||
$resStr = floor($diff / 60) . '分钟' . $dirStr;
|
||||
} elseif ($diff >= 3600 && $diff < 86400) { // 多于59分钟59秒,少于等于23小时59分钟59秒
|
||||
$resStr = floor($diff / 3600) . '小时' . $dirStr;
|
||||
} elseif ($diff >= 86400 && $diff < 2623860) { // 多于23小时59分钟59秒,少于等于29天59分钟59秒
|
||||
$resStr = floor($diff / 86400) . '天' . $dirStr;
|
||||
} elseif ($diff >= 2623860 && $diff <= 31567860 && $isEarly) { // 多于29天59分钟59秒,少于364天23小时59分钟59秒,且传入的时间戳早于当前
|
||||
$resStr = date('m-d H:i', $timeStamp);
|
||||
} else {
|
||||
$resStr = date('Y-m-d', $timeStamp);
|
||||
}
|
||||
|
||||
return $resStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 范围日期转换时间戳
|
||||
*
|
||||
* @param string $rangeDatetime
|
||||
* @param int $maxRange 最大时间间隔
|
||||
* @param string $delimiter
|
||||
* @return array
|
||||
*/
|
||||
public static function parseRange($rangeDatetime, $maxRange = 0, $delimiter = ' - ') {
|
||||
$rangeDatetime = explode($delimiter, $rangeDatetime, 2);
|
||||
$rangeDatetime[0] = strtotime($rangeDatetime[0]);
|
||||
$rangeDatetime[1] = isset($rangeDatetime[1]) ? strtotime($rangeDatetime[1]) : time();
|
||||
$rangeDatetime = [
|
||||
min($rangeDatetime[0], $rangeDatetime[1]),
|
||||
max($rangeDatetime[0], $rangeDatetime[1]),
|
||||
];
|
||||
|
||||
// 如果结束时间小于或等于开始时间 直接返回null
|
||||
// if ($rangeDatetime[1] < $rangeDatetime[0]) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// 如果大于最大时间间隔 则用结束时间减去最大时间间隔获得开始时间
|
||||
if ($maxRange > 0 && $rangeDatetime[1] - $rangeDatetime[0] > $maxRange) {
|
||||
$rangeDatetime[0] = $rangeDatetime[1] - $maxRange;
|
||||
}
|
||||
|
||||
return $rangeDatetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间范围内的日期数组
|
||||
* @param int $startTime
|
||||
* @param int $endTime
|
||||
* @return \Carbon\CarbonPeriod
|
||||
*/
|
||||
public static function daysUntilOfTimestamp($startTime, $endTime) {
|
||||
$startTime = Carbon::createFromTimestamp($startTime);
|
||||
$endTime = Carbon::createFromTimestamp($endTime);
|
||||
|
||||
return $startTime->daysUntil($endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间排序
|
||||
*
|
||||
* @param array $times
|
||||
* @return array
|
||||
*/
|
||||
public static function sort($times) {
|
||||
usort($times, function ($com1, $com2) {
|
||||
$com1 = strtotime($com1);
|
||||
$com2 = strtotime($com2);
|
||||
|
||||
return $com1 < $com2 ? -1 : 1;
|
||||
});
|
||||
|
||||
return $times;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | SentCMS [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2024 http://www.tensent.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: molong <molong@tensent.cn> <http://www.tensent.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
trait ModelTrait {
|
||||
|
||||
protected function casts(): array {
|
||||
return [
|
||||
'created_at' => 'datetime:Y-m-d H:i:s',
|
||||
'updated_at' => 'datetime:Y-m-d H:i:s',
|
||||
'deleted_at' => 'datetime:Y-m-d H:i:s',
|
||||
];
|
||||
}
|
||||
|
||||
protected function serializeDate($data){
|
||||
return $data->timezone('Asia/Shanghai')->format('Y-m-d H:i:s');
|
||||
}
|
||||
/**
|
||||
* 过滤移除非当前表的字段参数
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function setFilterFields(array $params) : array {
|
||||
$fields = Schema::getColumnListing($this->getTable());
|
||||
foreach ($params as $key => $param) {
|
||||
if ( !in_array($key, $fields) ) unset($params[$key]);
|
||||
}
|
||||
// 同时过滤时间戳字段【时间戳只允许自动更改,不允许手动设置】
|
||||
if ( $this->timestamps === true && isset($params[self::CREATED_AT]) ) unset($params[self::CREATED_AT]);
|
||||
if ( $this->timestamps === true && isset($params[self::UPDATED_AT]) ) unset($params[self::UPDATED_AT]);
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -8,6 +8,7 @@
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"hhxsv5/laravel-s": "^3.8",
|
||||
"intervention/image": "^3.11",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"nwidart/laravel-modules": "^12.0",
|
||||
@@ -73,7 +74,12 @@
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"merge-plugin": {
|
||||
"include": [
|
||||
"modules/*/composer.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
|
||||
+1
-1
@@ -101,7 +101,7 @@
|
||||
|
|
||||
*/
|
||||
|
||||
'ttl' => env('JWT_TTL', 60 * 60 * 24 * 7),
|
||||
'ttl' => (int) env('JWT_TTL', 60 * 60 * 24 * 7),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
+3
-3
@@ -32,7 +32,7 @@
|
||||
|
|
||||
*/
|
||||
|
||||
'listen_port' => env('LARAVELS_LISTEN_PORT', 8080),
|
||||
'listen_port' => env('LARAVELS_LISTEN_PORT', 8000),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
@@ -221,7 +221,7 @@
|
||||
|
||||
'swoole_tables' => [
|
||||
// WebSocket table for storing user connections
|
||||
'wsTable' => [
|
||||
'ws' => [
|
||||
'size' => 102400, // Maximum number of rows
|
||||
'column' => [
|
||||
['name' => 'value', 'type' => \Swoole\Table::TYPE_STRING, 'size' => 1024],
|
||||
@@ -292,7 +292,7 @@
|
||||
|
||||
'swoole' => [
|
||||
'daemonize' => env('LARAVELS_DAEMONIZE', false),
|
||||
'dispatch_mode' => env('LARAVELS_DISPATCH_MODE', 3),
|
||||
'dispatch_mode' => env('LARAVELS_DISPATCH_MODE', 2),
|
||||
'worker_num' => env('LARAVELS_WORKER_NUM', 30),
|
||||
//'task_worker_num' => env('LARAVELS_TASK_WORKER_NUM', 10),
|
||||
'task_ipc_mode' => 1,
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
use Nwidart\Modules\Activators\FileActivator;
|
||||
use Nwidart\Modules\Providers\ConsoleServiceProvider;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Module Namespace
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Default module namespace.
|
||||
|
|
||||
*/
|
||||
'namespace' => 'Modules',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Module Stubs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Default module stubs.
|
||||
|
|
||||
*/
|
||||
'stubs' => [
|
||||
'enabled' => false,
|
||||
'path' => base_path('vendor/nwidart/laravel-modules/src/Commands/stubs'),
|
||||
'files' => [
|
||||
'routes/web' => 'routes/web.php',
|
||||
'routes/api' => 'routes/api.php',
|
||||
'routes/admin' => 'routes/admin.php',
|
||||
'views/index' => 'resources/views/index.blade.php',
|
||||
// 'views/master' => 'resources/views/components/layouts/master.blade.php',
|
||||
'scaffold/config' => 'config/config.php',
|
||||
'composer' => 'composer.json',
|
||||
// 'assets/js/app' => 'resources/assets/js/app.js',
|
||||
// 'assets/sass/app' => 'resources/assets/sass/app.scss',
|
||||
// 'vite' => 'vite.config.js',
|
||||
// 'package' => 'package.json',
|
||||
],
|
||||
'replacements' => [
|
||||
/**
|
||||
* Define custom replacements for each section.
|
||||
* You can specify a closure for dynamic values.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 'composer' => [
|
||||
* 'CUSTOM_KEY' => fn (\Nwidart\Modules\Generators\ModuleGenerator $generator) => $generator->getModule()->getLowerName() . '-module',
|
||||
* 'CUSTOM_KEY2' => fn () => 'custom text',
|
||||
* 'LOWER_NAME',
|
||||
* 'STUDLY_NAME',
|
||||
* // ...
|
||||
* ],
|
||||
*
|
||||
* Note: Keys should be in UPPERCASE.
|
||||
*/
|
||||
'routes/web' => ['LOWER_NAME', 'STUDLY_NAME', 'PLURAL_LOWER_NAME', 'KEBAB_NAME', 'MODULE_NAMESPACE', 'CONTROLLER_NAMESPACE'],
|
||||
'routes/api' => ['LOWER_NAME', 'STUDLY_NAME', 'PLURAL_LOWER_NAME', 'KEBAB_NAME', 'MODULE_NAMESPACE', 'CONTROLLER_NAMESPACE'],
|
||||
'routes/admin' => ['LOWER_NAME', 'STUDLY_NAME', 'MODULE_NAMESPACE', 'CONTROLLER_NAMESPACE'],
|
||||
'vite' => ['LOWER_NAME', 'STUDLY_NAME', 'KEBAB_NAME'],
|
||||
'json' => ['LOWER_NAME', 'STUDLY_NAME', 'KEBAB_NAME', 'MODULE_NAMESPACE', 'PROVIDER_NAMESPACE'],
|
||||
'views/index' => ['LOWER_NAME'],
|
||||
'views/master' => ['LOWER_NAME', 'STUDLY_NAME', 'KEBAB_NAME'],
|
||||
'scaffold/config' => ['STUDLY_NAME'],
|
||||
'composer' => [
|
||||
'LOWER_NAME',
|
||||
'STUDLY_NAME',
|
||||
'VENDOR',
|
||||
'AUTHOR_NAME',
|
||||
'AUTHOR_EMAIL',
|
||||
'MODULE_NAMESPACE',
|
||||
'PROVIDER_NAMESPACE',
|
||||
'APP_FOLDER_NAME',
|
||||
],
|
||||
],
|
||||
'gitkeep' => true,
|
||||
],
|
||||
'paths' => [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Modules path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This path is used to save the generated module.
|
||||
| This path will also be added automatically to the list of scanned folders.
|
||||
|
|
||||
*/
|
||||
'modules' => base_path('modules'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Modules assets path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may update the modules' assets path.
|
||||
|
|
||||
*/
|
||||
'assets' => public_path('modules'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| The migrations' path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Where you run the 'module:publish-migration' command, where do you publish the
|
||||
| the migration files?
|
||||
|
|
||||
*/
|
||||
'migration' => base_path('database/migrations'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| The app path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| app folder name
|
||||
| for example can change it to 'src' or 'App'
|
||||
*/
|
||||
'app_folder' => 'app/',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Generator path
|
||||
|--------------------------------------------------------------------------
|
||||
| Customise the paths where the folders will be generated.
|
||||
| Setting the generate key to false will not generate that folder
|
||||
*/
|
||||
'generator' => [
|
||||
// app/
|
||||
'actions' => ['path' => 'app/Actions', 'generate' => false],
|
||||
'casts' => ['path' => 'app/Casts', 'generate' => false],
|
||||
'channels' => ['path' => 'app/Broadcasting', 'generate' => false],
|
||||
'class' => ['path' => 'app/Classes', 'generate' => false],
|
||||
'command' => ['path' => 'app/Console', 'generate' => false],
|
||||
'component-class' => ['path' => 'app/View/Components', 'generate' => false],
|
||||
'emails' => ['path' => 'app/Emails', 'generate' => false],
|
||||
'event' => ['path' => 'app/Events', 'generate' => false],
|
||||
'enums' => ['path' => 'app/Enums', 'generate' => false],
|
||||
'exceptions' => ['path' => 'app/Exceptions', 'generate' => false],
|
||||
'jobs' => ['path' => 'app/Jobs', 'generate' => false],
|
||||
'helpers' => ['path' => 'app/Helpers', 'generate' => false],
|
||||
'interfaces' => ['path' => 'app/Interfaces', 'generate' => false],
|
||||
'listener' => ['path' => 'app/Listeners', 'generate' => false],
|
||||
'model' => ['path' => 'app/Models', 'generate' => true],
|
||||
'notifications' => ['path' => 'app/Notifications', 'generate' => false],
|
||||
'observer' => ['path' => 'app/Observers', 'generate' => false],
|
||||
'policies' => ['path' => 'app/Policies', 'generate' => false],
|
||||
'provider' => ['path' => 'app/Providers', 'generate' => true],
|
||||
'repository' => ['path' => 'app/Repositories', 'generate' => false],
|
||||
'resource' => ['path' => 'app/Transformers', 'generate' => false],
|
||||
'route-provider' => ['path' => 'app/Providers', 'generate' => true],
|
||||
'rules' => ['path' => 'app/Rules', 'generate' => false],
|
||||
'services' => ['path' => 'app/Services', 'generate' => true],
|
||||
'scopes' => ['path' => 'app/Models/Scopes', 'generate' => false],
|
||||
'traits' => ['path' => 'app/Traits', 'generate' => false],
|
||||
|
||||
// app/Http/
|
||||
'controller' => ['path' => 'app/Http/Controllers', 'generate' => true],
|
||||
'filter' => ['path' => 'app/Http/Middleware', 'generate' => false],
|
||||
'request' => ['path' => 'app/Http/Requests', 'generate' => false],
|
||||
|
||||
// config/
|
||||
'config' => ['path' => 'config', 'generate' => true],
|
||||
|
||||
// database/
|
||||
'factory' => ['path' => 'database/factories', 'generate' => true],
|
||||
'migration' => ['path' => 'database/migrations', 'generate' => true],
|
||||
'seeder' => ['path' => 'database/seeders', 'generate' => true],
|
||||
|
||||
// lang/
|
||||
'lang' => ['path' => 'lang', 'generate' => false],
|
||||
|
||||
// resource/
|
||||
'assets' => ['path' => 'resources/assets', 'generate' => false],
|
||||
'component-view' => ['path' => 'resources/views/components', 'generate' => false],
|
||||
'views' => ['path' => 'resources/views', 'generate' => false],
|
||||
|
||||
// routes/
|
||||
'routes' => ['path' => 'routes', 'generate' => true],
|
||||
|
||||
// tests/
|
||||
'test-feature' => ['path' => 'tests/Feature', 'generate' => false],
|
||||
'test-unit' => ['path' => 'tests/Unit', 'generate' => false],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Auto Discover of Modules
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you configure auto discover of module
|
||||
| This is useful for simplify module providers.
|
||||
|
|
||||
*/
|
||||
'auto-discover' => [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migrations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option for register migration automatically.
|
||||
|
|
||||
*/
|
||||
'migrations' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Translations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option for register lang file automatically.
|
||||
|
|
||||
*/
|
||||
'translations' => false,
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Package commands
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can define which commands will be visible and used in your
|
||||
| application. You can add your own commands to merge section.
|
||||
|
|
||||
*/
|
||||
'commands' => ConsoleServiceProvider::defaultCommands()
|
||||
->merge([
|
||||
// New commands go here
|
||||
])->toArray(),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Scan Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you define which folder will be scanned. By default will scan vendor
|
||||
| directory. This is useful if you host the package in packagist website.
|
||||
|
|
||||
*/
|
||||
'scan' => [
|
||||
'enabled' => false,
|
||||
'paths' => [
|
||||
base_path('vendor/*/*'),
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Composer File Template
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is the config for the composer.json file, generated by this package
|
||||
|
|
||||
*/
|
||||
'composer' => [
|
||||
'vendor' => env('MODULE_VENDOR', 'tensent'),
|
||||
'author' => [
|
||||
'name' => env('MODULE_AUTHOR_NAME', 'molong'),
|
||||
'email' => env('MODULE_AUTHOR_EMAIL', 'molong@tensent.cn'),
|
||||
],
|
||||
'composer-output' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Choose what laravel-modules will register as custom namespaces.
|
||||
| Setting one to false will require you to register that part
|
||||
| in your own Service Provider class.
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
'register' => [
|
||||
'translations' => true,
|
||||
/**
|
||||
* load files on boot or register method
|
||||
*/
|
||||
'files' => 'register',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Activators
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You can define new types of activators here, file, database, etc. The only
|
||||
| required parameter is 'class'.
|
||||
| The file activator will store the activation status in storage/installed_modules
|
||||
*/
|
||||
'activators' => [
|
||||
'file' => [
|
||||
'class' => FileActivator::class,
|
||||
'statuses-file' => base_path('storage/modules_statuses.json'),
|
||||
'cache-key' => 'activator.installed',
|
||||
'cache-lifetime' => 604800,
|
||||
],
|
||||
],
|
||||
|
||||
'activator' => 'file',
|
||||
];
|
||||
@@ -12,7 +12,7 @@
|
||||
public function up(): void
|
||||
{
|
||||
// 管理员表
|
||||
Schema::create('auth_users', function (Blueprint $table) {
|
||||
Schema::create('auth_user', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('username', 50)->unique()->comment('用户名');
|
||||
$table->string('password')->comment('密码');
|
||||
@@ -32,7 +32,7 @@ public function up(): void
|
||||
});
|
||||
|
||||
// 部门表
|
||||
Schema::create('auth_departments', function (Blueprint $table) {
|
||||
Schema::create('auth_department', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 100)->comment('部门名称');
|
||||
$table->unsignedBigInteger('parent_id')->default(0)->comment('父部门ID');
|
||||
@@ -48,7 +48,7 @@ public function up(): void
|
||||
});
|
||||
|
||||
// 角色表
|
||||
Schema::create('auth_roles', function (Blueprint $table) {
|
||||
Schema::create('auth_role', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 50)->unique()->comment('角色名称');
|
||||
$table->string('code', 50)->unique()->comment('角色编码');
|
||||
@@ -62,7 +62,7 @@ public function up(): void
|
||||
});
|
||||
|
||||
// 权限表
|
||||
Schema::create('auth_permissions', function (Blueprint $table) {
|
||||
Schema::create('auth_permission', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title', 100)->comment('权限标题');
|
||||
$table->string('name', 100)->unique()->comment('权限编码');
|
||||
@@ -114,9 +114,9 @@ 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');
|
||||
Schema::dropIfExists('auth_permission');
|
||||
Schema::dropIfExists('auth_role');
|
||||
Schema::dropIfExists('auth_department');
|
||||
Schema::dropIfExists('auth_user');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
public function up()
|
||||
{
|
||||
// 系统配置表
|
||||
Schema::create('system_configs', function (Blueprint $table) {
|
||||
Schema::create('system_setting', function (Blueprint $table) {
|
||||
$table->comment('系统配置表');
|
||||
$table->id();
|
||||
$table->string('group')->comment('配置分组');
|
||||
@@ -32,7 +32,7 @@ public function up()
|
||||
});
|
||||
|
||||
// 系统日志表
|
||||
Schema::create('system_logs', function (Blueprint $table) {
|
||||
Schema::create('system_log', function (Blueprint $table) {
|
||||
$table->comment('系统日志表');
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id')->nullable()->comment('用户ID');
|
||||
@@ -58,12 +58,13 @@ public function up()
|
||||
});
|
||||
|
||||
// 系统字典表
|
||||
Schema::create('system_dictionaries', function (Blueprint $table) {
|
||||
Schema::create('system_dictionary', function (Blueprint $table) {
|
||||
$table->comment('系统字典表');
|
||||
$table->id();
|
||||
$table->string('name')->comment('字典名称');
|
||||
$table->string('code')->unique()->comment('字典编码');
|
||||
$table->text('description')->nullable()->comment('字典描述');
|
||||
$table->string('value_type')->default('string')->comment('值类型:string, number, boolean, json');
|
||||
$table->boolean('status')->default(true)->comment('状态');
|
||||
$table->integer('sort')->default(0)->comment('排序');
|
||||
$table->timestamps();
|
||||
@@ -71,7 +72,7 @@ public function up()
|
||||
});
|
||||
|
||||
// 系统字典项表
|
||||
Schema::create('system_dictionary_items', function (Blueprint $table) {
|
||||
Schema::create('system_dictionary_item', function (Blueprint $table) {
|
||||
$table->comment('系统字典项表');
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('dictionary_id')->comment('字典ID');
|
||||
@@ -84,12 +85,12 @@ public function up()
|
||||
$table->integer('sort')->default(0)->comment('排序');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('dictionary_id')->references('id')->on('system_dictionaries')->onDelete('cascade');
|
||||
$table->foreign('dictionary_id')->references('id')->on('system_dictionary')->onDelete('cascade');
|
||||
$table->index('dictionary_id');
|
||||
});
|
||||
|
||||
// 系统任务表
|
||||
Schema::create('system_tasks', function (Blueprint $table) {
|
||||
Schema::create('system_task', function (Blueprint $table) {
|
||||
$table->comment('系统任务表');
|
||||
$table->id();
|
||||
$table->string('name')->comment('任务名称');
|
||||
@@ -114,32 +115,54 @@ public function up()
|
||||
});
|
||||
|
||||
// 系统城市表
|
||||
Schema::create('system_cities', function (Blueprint $table) {
|
||||
Schema::create('system_city', function (Blueprint $table) {
|
||||
$table->comment('系统城市表');
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('parent_id')->default(0)->comment('父级ID');
|
||||
$table->string('name')->comment('城市名称');
|
||||
$table->string('title')->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->string('parent_code')->nullable()->comment('父级编码');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('parent_id');
|
||||
$table->index('code');
|
||||
$table->index('level');
|
||||
$table->index('parent_code');
|
||||
});
|
||||
|
||||
// 系统通知表
|
||||
Schema::create('system_notification', function (Blueprint $table) {
|
||||
$table->comment('系统通知表');
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id')->comment('用户ID');
|
||||
$table->string('title')->comment('通知标题');
|
||||
$table->text('content')->comment('通知内容');
|
||||
$table->string('type')->default('info')->comment('通知类型:info, success, warning, error, task, system');
|
||||
$table->string('category')->nullable()->comment('通知分类:system, task, message, reminder, announcement');
|
||||
$table->json('data')->nullable()->comment('附加数据(JSON格式)');
|
||||
$table->string('action_type')->nullable()->comment('操作类型:link, modal, none');
|
||||
$table->text('action_data')->nullable()->comment('操作数据');
|
||||
$table->boolean('is_read')->default(false)->comment('是否已读');
|
||||
$table->timestamp('read_at')->nullable()->comment('阅读时间');
|
||||
$table->boolean('sent_via_websocket')->default(false)->comment('是否已通过WebSocket发送');
|
||||
$table->timestamp('sent_at')->nullable()->comment('发送时间');
|
||||
$table->integer('retry_count')->default(0)->comment('重试次数');
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('user_id');
|
||||
$table->index('is_read');
|
||||
$table->index('type');
|
||||
$table->index('category');
|
||||
$table->index('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
Schema::dropIfExists('system_notification');
|
||||
Schema::dropIfExists('system_dictionary_item');
|
||||
Schema::dropIfExists('system_dictionary');
|
||||
Schema::dropIfExists('system_setting');
|
||||
Schema::dropIfExists('system_log');
|
||||
Schema::dropIfExists('system_task');
|
||||
Schema::dropIfExists('system_city');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,12 +63,12 @@ private function createPermissions(): void
|
||||
'parent_id' => 0,
|
||||
'path' => '/home',
|
||||
'component' => null,
|
||||
'meta' => json_encode([
|
||||
'meta' => [
|
||||
'icon' => 'Dashboard',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
'affix' => true,
|
||||
]),
|
||||
],
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -80,12 +80,12 @@ private function createPermissions(): void
|
||||
'parent_id' => 0, // 稍后更新为首页菜单的ID
|
||||
'path' => '/dashboard',
|
||||
'component' => 'home/index',
|
||||
'meta' => json_encode([
|
||||
'meta' => [
|
||||
'icon' => 'DataLine',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
'affix' => true,
|
||||
]),
|
||||
],
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -97,11 +97,11 @@ private function createPermissions(): void
|
||||
'parent_id' => 0, // 稍后更新为首页菜单的ID
|
||||
'path' => '/ucenter',
|
||||
'component' => 'ucenter/index',
|
||||
'meta' => json_encode([
|
||||
'meta' => [
|
||||
'icon' => 'User',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 2,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -147,11 +147,11 @@ private function createPermissions(): void
|
||||
'parent_id' => 0,
|
||||
'path' => '/auth',
|
||||
'component' => null,
|
||||
'meta' => json_encode([
|
||||
'meta' => [
|
||||
'icon' => 'User',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 2,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -161,13 +161,13 @@ private function createPermissions(): void
|
||||
'name' => 'auth.users',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为权限菜单的ID
|
||||
'path' => '/auth/users',
|
||||
'component' => 'auth/users/index',
|
||||
'meta' => json_encode([
|
||||
'path' => '/auth/user',
|
||||
'component' => 'auth/user/index',
|
||||
'meta' => [
|
||||
'icon' => 'UserOutlined',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -176,7 +176,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.users.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为用户管理菜单的ID
|
||||
'path' => 'admin.users.index',
|
||||
'path' => 'admin.user.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
@@ -187,7 +187,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.users.create',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为用户管理菜单的ID
|
||||
'path' => 'admin.users.store',
|
||||
'path' => 'admin.user.store',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
@@ -198,7 +198,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.users.update',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为用户管理菜单的ID
|
||||
'path' => 'admin.users.update',
|
||||
'path' => 'admin.user.update',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
@@ -209,7 +209,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.users.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为用户管理菜单的ID
|
||||
'path' => 'admin.users.destroy',
|
||||
'path' => 'admin.user.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
@@ -220,7 +220,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.users.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为用户管理菜单的ID
|
||||
'path' => 'admin.users.batch-delete',
|
||||
'path' => 'admin.user.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 5,
|
||||
@@ -231,7 +231,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.users.export',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为用户管理菜单的ID
|
||||
'path' => 'admin.users.export',
|
||||
'path' => 'admin.user.export',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 6,
|
||||
@@ -242,7 +242,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.users.import',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为用户管理菜单的ID
|
||||
'path' => 'admin.users.import',
|
||||
'path' => 'admin.user.import',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 7,
|
||||
@@ -254,13 +254,13 @@ private function createPermissions(): void
|
||||
'name' => 'auth.roles',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为权限菜单的ID
|
||||
'path' => '/auth/roles',
|
||||
'component' => 'auth/roles/index',
|
||||
'meta' => json_encode([
|
||||
'path' => '/auth/role',
|
||||
'component' => 'auth/role/index',
|
||||
'meta' => [
|
||||
'icon' => 'UserFilled',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 2,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -269,7 +269,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.roles.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为角色管理菜单的ID
|
||||
'path' => 'admin.roles.index',
|
||||
'path' => 'admin.role.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
@@ -280,7 +280,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.roles.create',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为角色管理菜单的ID
|
||||
'path' => 'admin.roles.store',
|
||||
'path' => 'admin.role.store',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
@@ -291,7 +291,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.roles.update',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为角色管理菜单的ID
|
||||
'path' => 'admin.roles.update',
|
||||
'path' => 'admin.role.update',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
@@ -302,7 +302,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.roles.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为角色管理菜单的ID
|
||||
'path' => 'admin.roles.destroy',
|
||||
'path' => 'admin.role.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
@@ -313,7 +313,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.roles.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为角色管理菜单的ID
|
||||
'path' => 'admin.roles.batch-delete',
|
||||
'path' => 'admin.role.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 5,
|
||||
@@ -324,7 +324,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.roles.assign-permissions',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为角色管理菜单的ID
|
||||
'path' => 'admin.roles.assign-permissions',
|
||||
'path' => 'admin.role.assign-permissions',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 6,
|
||||
@@ -336,13 +336,13 @@ private function createPermissions(): void
|
||||
'name' => 'auth.permissions',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为权限菜单的ID
|
||||
'path' => '/auth/permissions',
|
||||
'component' => 'auth/permissions/index',
|
||||
'meta' => json_encode([
|
||||
'path' => '/auth/permission',
|
||||
'component' => 'auth/permission/index',
|
||||
'meta' => [
|
||||
'icon' => 'Lock',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 3,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -351,7 +351,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.permissions.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为权限管理菜单的ID
|
||||
'path' => 'admin.permissions.index',
|
||||
'path' => 'admin.permission.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
@@ -362,7 +362,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.permissions.create',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为权限管理菜单的ID
|
||||
'path' => 'admin.permissions.store',
|
||||
'path' => 'admin.permission.store',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
@@ -373,7 +373,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.permissions.update',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为权限管理菜单的ID
|
||||
'path' => 'admin.permissions.update',
|
||||
'path' => 'admin.permission.update',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
@@ -384,7 +384,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.permissions.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为权限管理菜单的ID
|
||||
'path' => 'admin.permissions.destroy',
|
||||
'path' => 'admin.permission.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
@@ -395,7 +395,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.permissions.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为权限管理菜单的ID
|
||||
'path' => 'admin.permissions.batch-delete',
|
||||
'path' => 'admin.permission.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 5,
|
||||
@@ -407,13 +407,13 @@ private function createPermissions(): void
|
||||
'name' => 'auth.departments',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为权限菜单的ID
|
||||
'path' => '/auth/departments',
|
||||
'component' => 'auth/departments/index',
|
||||
'meta' => json_encode([
|
||||
'path' => '/auth/department',
|
||||
'component' => 'auth/department/index',
|
||||
'meta' => [
|
||||
'icon' => 'OfficeBuilding',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 4,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -422,7 +422,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.departments.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为部门管理菜单的ID
|
||||
'path' => 'admin.departments.index',
|
||||
'path' => 'admin.department.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
@@ -433,7 +433,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.departments.create',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为部门管理菜单的ID
|
||||
'path' => 'admin.departments.store',
|
||||
'path' => 'admin.department.store',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
@@ -444,7 +444,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.departments.update',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为部门管理菜单的ID
|
||||
'path' => 'admin.departments.update',
|
||||
'path' => 'admin.department.update',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
@@ -455,7 +455,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.departments.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为部门管理菜单的ID
|
||||
'path' => 'admin.departments.destroy',
|
||||
'path' => 'admin.department.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
@@ -466,7 +466,7 @@ private function createPermissions(): void
|
||||
'name' => 'auth.departments.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为部门管理菜单的ID
|
||||
'path' => 'admin.departments.batch-delete',
|
||||
'path' => 'admin.department.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 5,
|
||||
|
||||
+292
-167
@@ -56,36 +56,36 @@ private function createSystemPermissions(): void
|
||||
'parent_id' => 0,
|
||||
'path' => '/system',
|
||||
'component' => null,
|
||||
'meta' => json_encode([
|
||||
'meta' => [
|
||||
'icon' => 'Setting',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 3,
|
||||
'status' => 1,
|
||||
],
|
||||
// 系统配置
|
||||
[
|
||||
'title' => '系统配置',
|
||||
'name' => 'system.config',
|
||||
'name' => 'system.setting',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为系统菜单的ID
|
||||
'path' => '/system/config',
|
||||
'component' => 'system/config/index',
|
||||
'meta' => json_encode([
|
||||
'path' => '/system/setting',
|
||||
'component' => 'system/setting/index',
|
||||
'meta' => [
|
||||
'icon' => 'SettingFilled',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '查看配置',
|
||||
'name' => 'system.config.view',
|
||||
'name' => 'system.setting.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统配置菜单的ID
|
||||
'path' => 'admin.config.index',
|
||||
'path' => 'admin.setting.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
@@ -93,10 +93,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '创建配置',
|
||||
'name' => 'system.config.create',
|
||||
'name' => 'system.setting.create',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统配置菜单的ID
|
||||
'path' => 'admin.config.store',
|
||||
'path' => 'admin.setting.store',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
@@ -104,10 +104,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '编辑配置',
|
||||
'name' => 'system.config.update',
|
||||
'name' => 'system.setting.update',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统配置菜单的ID
|
||||
'path' => 'admin.config.update',
|
||||
'path' => 'admin.setting.update',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
@@ -115,10 +115,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '删除配置',
|
||||
'name' => 'system.config.delete',
|
||||
'name' => 'system.setting.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统配置菜单的ID
|
||||
'path' => 'admin.config.destroy',
|
||||
'path' => 'admin.setting.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
@@ -126,10 +126,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '批量删除配置',
|
||||
'name' => 'system.config.batch-delete',
|
||||
'name' => 'system.setting.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统配置菜单的ID
|
||||
'path' => 'admin.config.batch-delete',
|
||||
'path' => 'admin.setting.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 5,
|
||||
@@ -139,25 +139,25 @@ private function createSystemPermissions(): void
|
||||
// 系统日志
|
||||
[
|
||||
'title' => '系统日志',
|
||||
'name' => 'system.logs',
|
||||
'name' => 'system.log',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为系统菜单的ID
|
||||
'path' => '/system/logs',
|
||||
'component' => 'system/logs/index',
|
||||
'meta' => json_encode([
|
||||
'path' => '/system/log',
|
||||
'component' => 'system/log/index',
|
||||
'meta' => [
|
||||
'icon' => 'DocumentCopy',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 2,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '查看日志',
|
||||
'name' => 'system.logs.view',
|
||||
'name' => 'system.log.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统日志菜单的ID
|
||||
'path' => 'admin.logs.index',
|
||||
'path' => 'admin.log.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
@@ -165,10 +165,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '删除日志',
|
||||
'name' => 'system.logs.delete',
|
||||
'name' => 'system.log.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统日志菜单的ID
|
||||
'path' => 'admin.logs.destroy',
|
||||
'path' => 'admin.log.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
@@ -176,10 +176,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '批量删除日志',
|
||||
'name' => 'system.logs.batch-delete',
|
||||
'name' => 'system.log.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统日志菜单的ID
|
||||
'path' => 'admin.logs.batch-delete',
|
||||
'path' => 'admin.log.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
@@ -187,10 +187,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '导出日志',
|
||||
'name' => 'system.logs.export',
|
||||
'name' => 'system.log.export',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为系统日志菜单的ID
|
||||
'path' => 'admin.logs.export',
|
||||
'path' => 'admin.log.export',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
@@ -200,25 +200,25 @@ private function createSystemPermissions(): void
|
||||
// 数据字典
|
||||
[
|
||||
'title' => '数据字典',
|
||||
'name' => 'system.dictionaries',
|
||||
'name' => 'system.dictionary',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为系统菜单的ID
|
||||
'path' => '/system/dictionaries',
|
||||
'component' => 'system/dictionaries/index',
|
||||
'meta' => json_encode([
|
||||
'path' => '/system/dictionary',
|
||||
'component' => 'system/dictionary/index',
|
||||
'meta' => [
|
||||
'icon' => 'Notebook',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 3,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '查看字典',
|
||||
'name' => 'system.dictionaries.view',
|
||||
'name' => 'system.dictionary.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为数据字典菜单的ID
|
||||
'path' => 'admin.dictionaries.index',
|
||||
'path' => 'admin.dictionary.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
@@ -226,10 +226,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '创建字典',
|
||||
'name' => 'system.dictionaries.create',
|
||||
'name' => 'system.dictionary.create',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为数据字典菜单的ID
|
||||
'path' => 'admin.dictionaries.store',
|
||||
'path' => 'admin.dictionary.store',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
@@ -237,10 +237,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '编辑字典',
|
||||
'name' => 'system.dictionaries.update',
|
||||
'name' => 'system.dictionary.update',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为数据字典菜单的ID
|
||||
'path' => 'admin.dictionaries.update',
|
||||
'path' => 'admin.dictionary.update',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
@@ -248,10 +248,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '删除字典',
|
||||
'name' => 'system.dictionaries.delete',
|
||||
'name' => 'system.dictionary.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为数据字典菜单的ID
|
||||
'path' => 'admin.dictionaries.destroy',
|
||||
'path' => 'admin.dictionary.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
@@ -259,10 +259,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '批量删除字典',
|
||||
'name' => 'system.dictionaries.batch-delete',
|
||||
'name' => 'system.dictionary.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为数据字典菜单的ID
|
||||
'path' => 'admin.dictionaries.batch-delete',
|
||||
'path' => 'admin.dictionary.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 5,
|
||||
@@ -272,25 +272,25 @@ private function createSystemPermissions(): void
|
||||
// 定时任务
|
||||
[
|
||||
'title' => '定时任务',
|
||||
'name' => 'system.tasks',
|
||||
'name' => 'system.task',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为系统菜单的ID
|
||||
'path' => '/system/tasks',
|
||||
'component' => 'system/tasks/index',
|
||||
'meta' => json_encode([
|
||||
'path' => '/system/task',
|
||||
'component' => 'system/task/index',
|
||||
'meta' => [
|
||||
'icon' => 'Timer',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
]),
|
||||
],
|
||||
'sort' => 4,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '查看任务',
|
||||
'name' => 'system.tasks.view',
|
||||
'name' => 'system.task.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为定时任务菜单的ID
|
||||
'path' => 'admin.tasks.index',
|
||||
'path' => 'admin.task.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
@@ -298,10 +298,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '创建任务',
|
||||
'name' => 'system.tasks.create',
|
||||
'name' => 'system.task.create',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为定时任务菜单的ID
|
||||
'path' => 'admin.tasks.store',
|
||||
'path' => 'admin.task.store',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
@@ -309,10 +309,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '编辑任务',
|
||||
'name' => 'system.tasks.update',
|
||||
'name' => 'system.task.update',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为定时任务菜单的ID
|
||||
'path' => 'admin.tasks.update',
|
||||
'path' => 'admin.task.update',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
@@ -320,10 +320,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '删除任务',
|
||||
'name' => 'system.tasks.delete',
|
||||
'name' => 'system.task.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为定时任务菜单的ID
|
||||
'path' => 'admin.tasks.destroy',
|
||||
'path' => 'admin.task.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
@@ -331,10 +331,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '批量删除任务',
|
||||
'name' => 'system.tasks.batch-delete',
|
||||
'name' => 'system.task.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为定时任务菜单的ID
|
||||
'path' => 'admin.tasks.batch-delete',
|
||||
'path' => 'admin.task.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 5,
|
||||
@@ -342,10 +342,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '执行任务',
|
||||
'name' => 'system.tasks.execute',
|
||||
'name' => 'system.task.execute',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为定时任务菜单的ID
|
||||
'path' => 'admin.tasks.execute',
|
||||
'path' => 'admin.task.execute',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 6,
|
||||
@@ -353,10 +353,10 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '启用任务',
|
||||
'name' => 'system.tasks.enable',
|
||||
'name' => 'system.task.enable',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为定时任务菜单的ID
|
||||
'path' => 'admin.tasks.enable',
|
||||
'path' => 'admin.task.enable',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 7,
|
||||
@@ -364,15 +364,87 @@ private function createSystemPermissions(): void
|
||||
],
|
||||
[
|
||||
'title' => '禁用任务',
|
||||
'name' => 'system.tasks.disable',
|
||||
'name' => 'system.task.disable',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为定时任务菜单的ID
|
||||
'path' => 'admin.tasks.disable',
|
||||
'path' => 'admin.task.disable',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 8,
|
||||
'status' => 1,
|
||||
],
|
||||
|
||||
// 城市管理
|
||||
[
|
||||
'title' => '城市管理',
|
||||
'name' => 'system.city',
|
||||
'type' => 'menu',
|
||||
'parent_id' => 0, // 稍后更新为系统菜单的ID
|
||||
'path' => '/system/city',
|
||||
'component' => 'system/city/index',
|
||||
'meta' => [
|
||||
'icon' => 'EnvironmentOutlined',
|
||||
'hidden' => false,
|
||||
'hiddenBreadcrumb' => false,
|
||||
],
|
||||
'sort' => 5,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '查看城市',
|
||||
'name' => 'system.city.view',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为城市管理菜单的ID
|
||||
'path' => 'admin.city.index',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '创建城市',
|
||||
'name' => 'system.city.create',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为城市管理菜单的ID
|
||||
'path' => 'admin.city.store',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 2,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '编辑城市',
|
||||
'name' => 'system.city.update',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为城市管理菜单的ID
|
||||
'path' => 'admin.city.update',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 3,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '删除城市',
|
||||
'name' => 'system.city.delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为城市管理菜单的ID
|
||||
'path' => 'admin.city.destroy',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 4,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'title' => '批量删除城市',
|
||||
'name' => 'system.city.batch-delete',
|
||||
'type' => 'button',
|
||||
'parent_id' => 0, // 稍后更新为城市管理菜单的ID
|
||||
'path' => 'admin.city.batch-delete',
|
||||
'component' => null,
|
||||
'meta' => null,
|
||||
'sort' => 5,
|
||||
'status' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
@@ -394,128 +466,156 @@ private function updateParentIds(): void
|
||||
$systemMenu = $permissions->where('name', 'system')->first();
|
||||
|
||||
// 获取系统子菜单ID
|
||||
$configMenu = $permissions->where('name', 'system.config')->first();
|
||||
$logsMenu = $permissions->where('name', 'system.logs')->first();
|
||||
$dictionariesMenu = $permissions->where('name', 'system.dictionaries')->first();
|
||||
$tasksMenu = $permissions->where('name', 'system.tasks')->first();
|
||||
$settingMenu = $permissions->where('name', 'system.setting')->first();
|
||||
$logMenu = $permissions->where('name', 'system.log')->first();
|
||||
$dictionaryMenu = $permissions->where('name', 'system.dictionary')->first();
|
||||
$taskMenu = $permissions->where('name', 'system.task')->first();
|
||||
$cityMenu = $permissions->where('name', 'system.city')->first();
|
||||
|
||||
// 更新系统子菜单的parent_id
|
||||
if ($systemMenu) {
|
||||
if ($configMenu) {
|
||||
$configMenu->update(['parent_id' => $systemMenu->id]);
|
||||
if ($settingMenu) {
|
||||
$settingMenu->update(['parent_id' => $systemMenu->id]);
|
||||
}
|
||||
if ($logsMenu) {
|
||||
$logsMenu->update(['parent_id' => $systemMenu->id]);
|
||||
if ($logMenu) {
|
||||
$logMenu->update(['parent_id' => $systemMenu->id]);
|
||||
}
|
||||
if ($dictionariesMenu) {
|
||||
$dictionariesMenu->update(['parent_id' => $systemMenu->id]);
|
||||
if ($dictionaryMenu) {
|
||||
$dictionaryMenu->update(['parent_id' => $systemMenu->id]);
|
||||
}
|
||||
if ($tasksMenu) {
|
||||
$tasksMenu->update(['parent_id' => $systemMenu->id]);
|
||||
if ($taskMenu) {
|
||||
$taskMenu->update(['parent_id' => $systemMenu->id]);
|
||||
}
|
||||
if ($cityMenu) {
|
||||
$cityMenu->update(['parent_id' => $systemMenu->id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新按钮权限的parent_id - 系统配置
|
||||
$configViewBtn = $permissions->where('name', 'system.config.view')->first();
|
||||
$configCreateBtn = $permissions->where('name', 'system.config.create')->first();
|
||||
$configUpdateBtn = $permissions->where('name', 'system.config.update')->first();
|
||||
$configDeleteBtn = $permissions->where('name', 'system.config.delete')->first();
|
||||
$configBatchDeleteBtn = $permissions->where('name', 'system.config.batch-delete')->first();
|
||||
if ($configMenu) {
|
||||
if ($configViewBtn) {
|
||||
$configViewBtn->update(['parent_id' => $configMenu->id]);
|
||||
$settingViewBtn = $permissions->where('name', 'system.setting.view')->first();
|
||||
$settingCreateBtn = $permissions->where('name', 'system.setting.create')->first();
|
||||
$settingUpdateBtn = $permissions->where('name', 'system.setting.update')->first();
|
||||
$settingDeleteBtn = $permissions->where('name', 'system.setting.delete')->first();
|
||||
$settingBatchDeleteBtn = $permissions->where('name', 'system.setting.batch-delete')->first();
|
||||
if ($settingMenu) {
|
||||
if ($settingViewBtn) {
|
||||
$settingViewBtn->update(['parent_id' => $settingMenu->id]);
|
||||
}
|
||||
if ($configCreateBtn) {
|
||||
$configCreateBtn->update(['parent_id' => $configMenu->id]);
|
||||
if ($settingCreateBtn) {
|
||||
$settingCreateBtn->update(['parent_id' => $settingMenu->id]);
|
||||
}
|
||||
if ($configUpdateBtn) {
|
||||
$configUpdateBtn->update(['parent_id' => $configMenu->id]);
|
||||
if ($settingUpdateBtn) {
|
||||
$settingUpdateBtn->update(['parent_id' => $settingMenu->id]);
|
||||
}
|
||||
if ($configDeleteBtn) {
|
||||
$configDeleteBtn->update(['parent_id' => $configMenu->id]);
|
||||
if ($settingDeleteBtn) {
|
||||
$settingDeleteBtn->update(['parent_id' => $settingMenu->id]);
|
||||
}
|
||||
if ($configBatchDeleteBtn) {
|
||||
$configBatchDeleteBtn->update(['parent_id' => $configMenu->id]);
|
||||
if ($settingBatchDeleteBtn) {
|
||||
$settingBatchDeleteBtn->update(['parent_id' => $settingMenu->id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新按钮权限的parent_id - 系统日志
|
||||
$logsViewBtn = $permissions->where('name', 'system.logs.view')->first();
|
||||
$logsDeleteBtn = $permissions->where('name', 'system.logs.delete')->first();
|
||||
$logsBatchDeleteBtn = $permissions->where('name', 'system.logs.batch-delete')->first();
|
||||
$logsExportBtn = $permissions->where('name', 'system.logs.export')->first();
|
||||
if ($logsMenu) {
|
||||
if ($logsViewBtn) {
|
||||
$logsViewBtn->update(['parent_id' => $logsMenu->id]);
|
||||
$logViewBtn = $permissions->where('name', 'system.log.view')->first();
|
||||
$logDeleteBtn = $permissions->where('name', 'system.log.delete')->first();
|
||||
$logBatchDeleteBtn = $permissions->where('name', 'system.log.batch-delete')->first();
|
||||
$logExportBtn = $permissions->where('name', 'system.log.export')->first();
|
||||
if ($logMenu) {
|
||||
if ($logViewBtn) {
|
||||
$logViewBtn->update(['parent_id' => $logMenu->id]);
|
||||
}
|
||||
if ($logsDeleteBtn) {
|
||||
$logsDeleteBtn->update(['parent_id' => $logsMenu->id]);
|
||||
if ($logDeleteBtn) {
|
||||
$logDeleteBtn->update(['parent_id' => $logMenu->id]);
|
||||
}
|
||||
if ($logsBatchDeleteBtn) {
|
||||
$logsBatchDeleteBtn->update(['parent_id' => $logsMenu->id]);
|
||||
if ($logBatchDeleteBtn) {
|
||||
$logBatchDeleteBtn->update(['parent_id' => $logMenu->id]);
|
||||
}
|
||||
if ($logsExportBtn) {
|
||||
$logsExportBtn->update(['parent_id' => $logsMenu->id]);
|
||||
if ($logExportBtn) {
|
||||
$logExportBtn->update(['parent_id' => $logMenu->id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新按钮权限的parent_id - 数据字典
|
||||
$dictViewBtn = $permissions->where('name', 'system.dictionaries.view')->first();
|
||||
$dictCreateBtn = $permissions->where('name', 'system.dictionaries.create')->first();
|
||||
$dictUpdateBtn = $permissions->where('name', 'system.dictionaries.update')->first();
|
||||
$dictDeleteBtn = $permissions->where('name', 'system.dictionaries.delete')->first();
|
||||
$dictBatchDeleteBtn = $permissions->where('name', 'system.dictionaries.batch-delete')->first();
|
||||
if ($dictionariesMenu) {
|
||||
$dictViewBtn = $permissions->where('name', 'system.dictionary.view')->first();
|
||||
$dictCreateBtn = $permissions->where('name', 'system.dictionary.create')->first();
|
||||
$dictUpdateBtn = $permissions->where('name', 'system.dictionary.update')->first();
|
||||
$dictDeleteBtn = $permissions->where('name', 'system.dictionary.delete')->first();
|
||||
$dictBatchDeleteBtn = $permissions->where('name', 'system.dictionary.batch-delete')->first();
|
||||
if ($dictionaryMenu) {
|
||||
if ($dictViewBtn) {
|
||||
$dictViewBtn->update(['parent_id' => $dictionariesMenu->id]);
|
||||
$dictViewBtn->update(['parent_id' => $dictionaryMenu->id]);
|
||||
}
|
||||
if ($dictCreateBtn) {
|
||||
$dictCreateBtn->update(['parent_id' => $dictionariesMenu->id]);
|
||||
$dictCreateBtn->update(['parent_id' => $dictionaryMenu->id]);
|
||||
}
|
||||
if ($dictUpdateBtn) {
|
||||
$dictUpdateBtn->update(['parent_id' => $dictionariesMenu->id]);
|
||||
$dictUpdateBtn->update(['parent_id' => $dictionaryMenu->id]);
|
||||
}
|
||||
if ($dictDeleteBtn) {
|
||||
$dictDeleteBtn->update(['parent_id' => $dictionariesMenu->id]);
|
||||
$dictDeleteBtn->update(['parent_id' => $dictionaryMenu->id]);
|
||||
}
|
||||
if ($dictBatchDeleteBtn) {
|
||||
$dictBatchDeleteBtn->update(['parent_id' => $dictionariesMenu->id]);
|
||||
$dictBatchDeleteBtn->update(['parent_id' => $dictionaryMenu->id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新按钮权限的parent_id - 定时任务
|
||||
$taskViewBtn = $permissions->where('name', 'system.tasks.view')->first();
|
||||
$taskCreateBtn = $permissions->where('name', 'system.tasks.create')->first();
|
||||
$taskUpdateBtn = $permissions->where('name', 'system.tasks.update')->first();
|
||||
$taskDeleteBtn = $permissions->where('name', 'system.tasks.delete')->first();
|
||||
$taskBatchDeleteBtn = $permissions->where('name', 'system.tasks.batch-delete')->first();
|
||||
$taskExecuteBtn = $permissions->where('name', 'system.tasks.execute')->first();
|
||||
$taskEnableBtn = $permissions->where('name', 'system.tasks.enable')->first();
|
||||
$taskDisableBtn = $permissions->where('name', 'system.tasks.disable')->first();
|
||||
if ($tasksMenu) {
|
||||
$taskViewBtn = $permissions->where('name', 'system.task.view')->first();
|
||||
$taskCreateBtn = $permissions->where('name', 'system.task.create')->first();
|
||||
$taskUpdateBtn = $permissions->where('name', 'system.task.update')->first();
|
||||
$taskDeleteBtn = $permissions->where('name', 'system.task.delete')->first();
|
||||
$taskBatchDeleteBtn = $permissions->where('name', 'system.task.batch-delete')->first();
|
||||
$taskExecuteBtn = $permissions->where('name', 'system.task.execute')->first();
|
||||
$taskEnableBtn = $permissions->where('name', 'system.task.enable')->first();
|
||||
$taskDisableBtn = $permissions->where('name', 'system.task.disable')->first();
|
||||
if ($taskMenu) {
|
||||
if ($taskViewBtn) {
|
||||
$taskViewBtn->update(['parent_id' => $tasksMenu->id]);
|
||||
$taskViewBtn->update(['parent_id' => $taskMenu->id]);
|
||||
}
|
||||
if ($taskCreateBtn) {
|
||||
$taskCreateBtn->update(['parent_id' => $tasksMenu->id]);
|
||||
$taskCreateBtn->update(['parent_id' => $taskMenu->id]);
|
||||
}
|
||||
if ($taskUpdateBtn) {
|
||||
$taskUpdateBtn->update(['parent_id' => $tasksMenu->id]);
|
||||
$taskUpdateBtn->update(['parent_id' => $taskMenu->id]);
|
||||
}
|
||||
if ($taskDeleteBtn) {
|
||||
$taskDeleteBtn->update(['parent_id' => $tasksMenu->id]);
|
||||
$taskDeleteBtn->update(['parent_id' => $taskMenu->id]);
|
||||
}
|
||||
if ($taskBatchDeleteBtn) {
|
||||
$taskBatchDeleteBtn->update(['parent_id' => $tasksMenu->id]);
|
||||
$taskBatchDeleteBtn->update(['parent_id' => $taskMenu->id]);
|
||||
}
|
||||
if ($taskExecuteBtn) {
|
||||
$taskExecuteBtn->update(['parent_id' => $tasksMenu->id]);
|
||||
$taskExecuteBtn->update(['parent_id' => $taskMenu->id]);
|
||||
}
|
||||
if ($taskEnableBtn) {
|
||||
$taskEnableBtn->update(['parent_id' => $tasksMenu->id]);
|
||||
$taskEnableBtn->update(['parent_id' => $taskMenu->id]);
|
||||
}
|
||||
if ($taskDisableBtn) {
|
||||
$taskDisableBtn->update(['parent_id' => $tasksMenu->id]);
|
||||
$taskDisableBtn->update(['parent_id' => $taskMenu->id]);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新按钮权限的parent_id - 城市管理
|
||||
$cityViewBtn = $permissions->where('name', 'system.city.view')->first();
|
||||
$cityCreateBtn = $permissions->where('name', 'system.city.create')->first();
|
||||
$cityUpdateBtn = $permissions->where('name', 'system.city.update')->first();
|
||||
$cityDeleteBtn = $permissions->where('name', 'system.city.delete')->first();
|
||||
$cityBatchDeleteBtn = $permissions->where('name', 'system.city.batch-delete')->first();
|
||||
if ($cityMenu) {
|
||||
if ($cityViewBtn) {
|
||||
$cityViewBtn->update(['parent_id' => $cityMenu->id]);
|
||||
}
|
||||
if ($cityCreateBtn) {
|
||||
$cityCreateBtn->update(['parent_id' => $cityMenu->id]);
|
||||
}
|
||||
if ($cityUpdateBtn) {
|
||||
$cityUpdateBtn->update(['parent_id' => $cityMenu->id]);
|
||||
}
|
||||
if ($cityDeleteBtn) {
|
||||
$cityDeleteBtn->update(['parent_id' => $cityMenu->id]);
|
||||
}
|
||||
if ($cityBatchDeleteBtn) {
|
||||
$cityBatchDeleteBtn->update(['parent_id' => $cityMenu->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -526,11 +626,12 @@ private function updateParentIds(): void
|
||||
private function createSystemDictionaries(): void
|
||||
{
|
||||
// 创建字典类型
|
||||
$dictionaries = [
|
||||
$dictionary = [
|
||||
[
|
||||
'name' => '用户状态',
|
||||
'code' => 'user_status',
|
||||
'description' => '用户账号状态',
|
||||
'value_type' => 'number',
|
||||
'sort' => 1,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -538,6 +639,7 @@ private function createSystemDictionaries(): void
|
||||
'name' => '性别',
|
||||
'code' => 'gender',
|
||||
'description' => '用户性别',
|
||||
'value_type' => 'number',
|
||||
'sort' => 2,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -545,6 +647,7 @@ private function createSystemDictionaries(): void
|
||||
'name' => '角色状态',
|
||||
'code' => 'role_status',
|
||||
'description' => '角色启用状态',
|
||||
'value_type' => 'number',
|
||||
'sort' => 3,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -552,6 +655,7 @@ private function createSystemDictionaries(): void
|
||||
'name' => '字典状态',
|
||||
'code' => 'dictionary_status',
|
||||
'description' => '数据字典状态',
|
||||
'value_type' => 'number',
|
||||
'sort' => 4,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -559,6 +663,7 @@ private function createSystemDictionaries(): void
|
||||
'name' => '任务状态',
|
||||
'code' => 'task_status',
|
||||
'description' => '定时任务状态',
|
||||
'value_type' => 'number',
|
||||
'sort' => 5,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -566,6 +671,7 @@ private function createSystemDictionaries(): void
|
||||
'name' => '日志类型',
|
||||
'code' => 'log_type',
|
||||
'description' => '系统日志类型',
|
||||
'value_type' => 'string',
|
||||
'sort' => 6,
|
||||
'status' => 1,
|
||||
],
|
||||
@@ -573,12 +679,21 @@ private function createSystemDictionaries(): void
|
||||
'name' => '是否',
|
||||
'code' => 'yes_no',
|
||||
'description' => '是否选项',
|
||||
'value_type' => 'boolean',
|
||||
'sort' => 7,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'name' => '配置分组',
|
||||
'code' => 'config_group',
|
||||
'description' => '系统配置分组类型',
|
||||
'value_type' => 'string',
|
||||
'sort' => 8,
|
||||
'status' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($dictionaries as $dictionary) {
|
||||
foreach ($dictionary as $dictionary) {
|
||||
$dict = Dictionary::create($dictionary);
|
||||
$this->createDictionaryItems($dict);
|
||||
}
|
||||
@@ -645,6 +760,14 @@ private function createDictionaryItems(Dictionary $dictionary): void
|
||||
['label' => '否', 'value' => 0, 'sort' => 2, 'status' => 1],
|
||||
];
|
||||
break;
|
||||
|
||||
case 'config_group':
|
||||
$items = [
|
||||
['label' => '网站设置', 'value' => 'site', 'sort' => 1, 'status' => 1],
|
||||
['label' => '上传设置', 'value' => 'upload', 'sort' => 2, 'status' => 1],
|
||||
['label' => '系统设置', 'value' => 'system', 'sort' => 3, 'status' => 1],
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
@@ -659,112 +782,114 @@ private function createSystemConfigs(): void
|
||||
{
|
||||
$configs = [
|
||||
[
|
||||
'group' => 'basic',
|
||||
'group' => 'site',
|
||||
'key' => 'site_name',
|
||||
'name' => '网站名称',
|
||||
'value' => 'Laravel Swoole 管理系统',
|
||||
'default_value' => 'Laravel Swoole 管理系统',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'description' => '系统显示的网站名称',
|
||||
'sort' => 1,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'group' => 'basic',
|
||||
'group' => 'site',
|
||||
'key' => 'site_logo',
|
||||
'name' => '网站Logo',
|
||||
'value' => '',
|
||||
'default_value' => '',
|
||||
'type' => 'image',
|
||||
'type' => 'file',
|
||||
'description' => '系统Logo图片地址',
|
||||
'sort' => 2,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'group' => 'basic',
|
||||
'group' => 'site',
|
||||
'key' => 'site_copyright',
|
||||
'name' => '版权信息',
|
||||
'value' => '© 2024 Laravel Swoole Admin',
|
||||
'default_value' => '© 2024 Laravel Swoole Admin',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'description' => '网站底部版权信息',
|
||||
'sort' => 3,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'group' => 'basic',
|
||||
'group' => 'site',
|
||||
'key' => 'site_icp',
|
||||
'name' => '备案号',
|
||||
'value' => '',
|
||||
'default_value' => '',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'description' => '网站备案号',
|
||||
'sort' => 4,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'group' => 'upload',
|
||||
'key' => 'upload_max_size',
|
||||
'name' => '上传最大限制',
|
||||
'value' => '10',
|
||||
'default_value' => '10',
|
||||
'type' => 'number',
|
||||
'description' => '文件上传最大限制(MB)',
|
||||
'sort' => 1,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'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',
|
||||
'type' => 'string',
|
||||
'description' => '允许上传的文件扩展名',
|
||||
'sort' => 2,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'group' => 'system',
|
||||
'key' => 'user_default_avatar',
|
||||
'name' => '默认头像',
|
||||
'value' => '',
|
||||
'default_value' => '',
|
||||
'type' => 'image',
|
||||
'type' => 'file',
|
||||
'description' => '用户默认头像地址',
|
||||
'sort' => 1,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'group' => 'system',
|
||||
'key' => 'system_timezone',
|
||||
'name' => '系统时区',
|
||||
'value' => 'Asia/Shanghai',
|
||||
'default_value' => 'Asia/Shanghai',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'description' => '系统默认时区',
|
||||
'sort' => 2,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'group' => 'system',
|
||||
'key' => 'system_language',
|
||||
'name' => '系统语言',
|
||||
'value' => 'zh-CN',
|
||||
'default_value' => 'zh-CN',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'description' => '系统默认语言',
|
||||
'sort' => 3,
|
||||
'is_system' => true,
|
||||
'status' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
[
|
||||
'group' => 'system',
|
||||
'key' => 'enable_register',
|
||||
'name' => '开启注册',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
'description' => '是否开启用户注册功能',
|
||||
'sort' => 4,
|
||||
'is_system' => true,
|
||||
'status' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,337 +0,0 @@
|
||||
# 日志模块实现总结
|
||||
|
||||
## 实现概述
|
||||
|
||||
本次优化完善了后端日志模块,实现了自动化的请求日志记录功能,所有后台管理 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 请求都会被自动记录,管理员可以通过日志管理功能进行系统监控、审计和问题排查。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,608 +0,0 @@
|
||||
# 系统操作日志模块文档
|
||||
|
||||
## 概述
|
||||
|
||||
系统操作日志模块用于记录后台管理系统的所有操作请求,包括用户操作、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)
|
||||
- 初始版本
|
||||
- 实现基础日志记录功能
|
||||
- 支持多维度查询和筛选
|
||||
- 支持数据导出
|
||||
- 支持批量删除和清理
|
||||
+789
-71
@@ -44,10 +44,12 @@ ## 技术栈
|
||||
- Laravel 11
|
||||
- Redis 缓存
|
||||
- Intervention Image (图像处理)
|
||||
- Laravel-S / Swoole (WebSocket 通知)
|
||||
- WebSocket (实时消息推送)
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### system_configs (系统配置表)
|
||||
### system_setting (系统配置表)
|
||||
- `id`: 主键
|
||||
- `group`: 配置分组(如:system, site, upload)
|
||||
- `key`: 配置键(唯一)
|
||||
@@ -84,15 +86,18 @@ ### system_logs (操作日志表)
|
||||
- `username`: 用户名
|
||||
- `module`: 模块
|
||||
- `action`: 操作
|
||||
- `method`: 请求方法
|
||||
- `method`: 请求方法 (GET/POST/PUT/DELETE)
|
||||
- `url`: 请求URL
|
||||
- `ip`: IP地址
|
||||
- `user_agent`: 用户代理
|
||||
- `request_data`: 请求数据(JSON)
|
||||
- `response_data`: 响应数据(JSON)
|
||||
- `duration`: 执行时间(毫秒)
|
||||
- `status_code`: 状态码
|
||||
- `params`: 请求参数(JSON)
|
||||
- `result`: 响应结果(仅错误时记录)
|
||||
- `status_code`: HTTP状态码
|
||||
- `status`: 状态 (success/error)
|
||||
- `error_message`: 错误信息
|
||||
- `execution_time`: 执行时间(毫秒)
|
||||
- `created_at`: 创建时间
|
||||
- `updated_at`: 更新时间
|
||||
|
||||
### system_tasks (任务表)
|
||||
- `id`: 主键
|
||||
@@ -197,34 +202,220 @@ #### 批量更新配置状态
|
||||
|
||||
### 操作日志管理
|
||||
|
||||
操作日志模块通过中间件自动记录所有后台管理 API 请求,实现全自动化的日志记录功能。
|
||||
|
||||
#### 中间件说明
|
||||
|
||||
**LogRequestMiddleware**
|
||||
|
||||
位置: `app/Http/Middleware/LogRequestMiddleware.php`
|
||||
|
||||
功能:
|
||||
- 自动拦截所有经过的请求
|
||||
- 记录请求和响应信息
|
||||
- 计算请求执行时间
|
||||
- 提取用户信息和操作详情
|
||||
- 过滤敏感参数(password、token、secret、key)
|
||||
- 获取客户端真实 IP(支持代理)
|
||||
- 异常处理,记录失败不影响业务
|
||||
|
||||
使用方式:
|
||||
```php
|
||||
// 在路由中应用
|
||||
Route::middleware(['auth.check:admin', 'log.request'])->group(function () {
|
||||
// 需要记录日志的路由
|
||||
});
|
||||
```
|
||||
|
||||
#### 日志记录规则
|
||||
|
||||
**自动记录的请求:**
|
||||
所有经过 `log.request` 中间件的请求都会被自动记录,包括:
|
||||
- 用户管理操作
|
||||
- 角色管理操作
|
||||
- 权限管理操作
|
||||
- 部门管理操作
|
||||
- 系统配置操作
|
||||
- 其他所有后台管理操作
|
||||
|
||||
**不记录的请求:**
|
||||
- 登录接口 (`POST /admin/auth/login`)
|
||||
- 健康检查接口 (`GET /up`)
|
||||
- 其他明确排除的路由
|
||||
|
||||
**敏感信息过滤:**
|
||||
以下字段会被自动过滤,记录为 `******`:
|
||||
- `password`
|
||||
- `password_confirmation`
|
||||
- `token`
|
||||
- `secret`
|
||||
- `key`
|
||||
|
||||
**错误日志处理:**
|
||||
- 成功请求 (HTTP 状态码 < 400): `status` = `success`
|
||||
- 失败请求 (HTTP 状态码 >= 400): `status` = `error`
|
||||
- 错误时记录响应内容和错误消息
|
||||
- 同时写入 Laravel 日志文件 (`storage/logs/laravel.log`)
|
||||
|
||||
#### 获取日志列表
|
||||
- **接口**: `GET /admin/logs`
|
||||
- **参数**:
|
||||
- `page`, `page_size`
|
||||
- `keyword`: 搜索关键词(用户名/模块/操作)
|
||||
- `module`: 模块
|
||||
- `action`: 操作
|
||||
- `user_id`: 用户ID
|
||||
- `start_date`: 开始日期
|
||||
- `end_date`: 结束日期
|
||||
- `order_by`, `order_direction`
|
||||
```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"
|
||||
}
|
||||
],
|
||||
"total": 100,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取日志详情
|
||||
- **接口**: `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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除日志
|
||||
#### 获取日志统计
|
||||
- **接口**: `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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 导出日志
|
||||
- **接口**: `POST /admin/logs/export`
|
||||
- **参数**: 与获取日志列表相同的查询参数
|
||||
- **响应**: Excel 文件下载
|
||||
- **文件名格式**: `系统操作日志_YYYYMMDDHHmmss.xlsx`
|
||||
- **包含字段**:
|
||||
- ID
|
||||
- 用户名
|
||||
- 模块
|
||||
- 操作
|
||||
- 请求方法
|
||||
- URL
|
||||
- IP 地址
|
||||
- 状态码
|
||||
- 状态
|
||||
- 错误信息
|
||||
- 执行时间(ms)
|
||||
- 创建时间
|
||||
|
||||
#### 删除单条日志
|
||||
- **接口**: `DELETE /admin/logs/{id}`
|
||||
- **响应示例**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "删除成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 批量删除日志
|
||||
- **接口**: `POST /admin/logs/batch-delete`
|
||||
- **参数**:
|
||||
```json
|
||||
{
|
||||
"ids": [1, 2, 3]
|
||||
"ids": [1, 2, 3, 4, 5]
|
||||
}
|
||||
```
|
||||
- **响应示例**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "批量删除成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 清理日志
|
||||
#### 清理历史日志
|
||||
- **接口**: `POST /admin/logs/clear`
|
||||
- **参数**:
|
||||
```json
|
||||
@@ -232,39 +423,129 @@ #### 清理日志
|
||||
"days": 30
|
||||
}
|
||||
```
|
||||
- **说明**: 删除指定天数之前的日志记录
|
||||
|
||||
#### 获取日志统计
|
||||
- **接口**: `GET /admin/logs/statistics`
|
||||
- **参数**:
|
||||
- `start_date`: 开始日期
|
||||
- `end_date`: 结束日期
|
||||
- **返回**:
|
||||
- **说明**: 清理指定天数前的所有日志记录,默认清理 30 天前的数据
|
||||
- **响应示例**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"total_count": 1000,
|
||||
"module_stats": [
|
||||
{
|
||||
"module": "user",
|
||||
"count": 500
|
||||
}
|
||||
],
|
||||
"user_stats": [
|
||||
{
|
||||
"user_id": 1,
|
||||
"username": "admin",
|
||||
"count": 800
|
||||
}
|
||||
]
|
||||
}
|
||||
"message": "清理成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 数据字典管理
|
||||
|
||||
数据字典模块提供了完整的字典管理功能,包括字典分类和字典项的 CRUD 操作。通过 WebSocket 实现了前后端缓存的实时同步更新。
|
||||
|
||||
#### 字典缓存更新机制
|
||||
|
||||
**概述**
|
||||
|
||||
字典缓存更新机制通过 WebSocket 实现前后端字典缓存的实时同步,确保在字典分类和字典项的增删改等操作后,前端字典缓存能够自动更新。
|
||||
|
||||
**技术实现**
|
||||
|
||||
1. **后端实现**
|
||||
|
||||
在 `app/Services/System/DictionaryService.php` 中添加了 WebSocket 通知功能:
|
||||
|
||||
**通知方法:**
|
||||
- `notifyDictionaryUpdate` - 字典分类更新通知
|
||||
- 触发时机:创建、更新、删除、批量删除、批量更新状态
|
||||
- 消息类型:`dictionary_update`
|
||||
|
||||
- `notifyDictionaryItemUpdate` - 字典项更新通知
|
||||
- 触发时机:创建、更新、删除、批量删除、批量更新状态
|
||||
- 消息类型:`dictionary_item_update`
|
||||
|
||||
**WebSocket 消息格式:**
|
||||
|
||||
字典分类更新消息:
|
||||
```json
|
||||
{
|
||||
"type": "dictionary_update",
|
||||
"data": {
|
||||
"action": "create|update|delete|batch_delete|batch_update_status",
|
||||
"resource_type": "dictionary",
|
||||
"data": {
|
||||
// 字典分类数据
|
||||
},
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字典项更新消息:
|
||||
```json
|
||||
{
|
||||
"type": "dictionary_item_update",
|
||||
"data": {
|
||||
"action": "create|update|delete|batch_delete|batch_update_status",
|
||||
"resource_type": "dictionary_item",
|
||||
"data": {
|
||||
// 字典项数据
|
||||
},
|
||||
"timestamp": 1234567890
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **前端实现**
|
||||
|
||||
创建了 `resources/admin/src/composables/useWebSocket.js` 来处理 WebSocket 连接和消息监听:
|
||||
|
||||
**主要功能:**
|
||||
- 初始化 WebSocket 连接(检查用户登录状态、验证用户信息完整性)
|
||||
- 消息处理器:`handleDictionaryUpdate` 和 `handleDictionaryItemUpdate`
|
||||
- 缓存刷新:接收到更新通知后,自动刷新字典缓存并显示成功提示
|
||||
|
||||
**App.vue 集成:**
|
||||
|
||||
```javascript
|
||||
onMounted(async () => {
|
||||
// 初始化 WebSocket 连接
|
||||
if (userStore.isLoggedIn()) {
|
||||
initWebSocket()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 关闭 WebSocket 连接
|
||||
closeWebSocket()
|
||||
})
|
||||
```
|
||||
|
||||
**工作流程:**
|
||||
|
||||
```
|
||||
用户操作(增删改字典)
|
||||
↓
|
||||
后端 Controller 调用 Service
|
||||
↓
|
||||
Service 执行数据库操作
|
||||
↓
|
||||
Service 清理后端缓存(Redis)
|
||||
↓
|
||||
Service 发送 WebSocket 广播通知
|
||||
↓
|
||||
WebSocket 推送消息到所有在线客户端
|
||||
↓
|
||||
前端接收 WebSocket 消息
|
||||
↓
|
||||
触发相应的消息处理器
|
||||
↓
|
||||
刷新前端字典缓存
|
||||
↓
|
||||
显示成功提示
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
- WebSocket 仅在用户登录后建立连接
|
||||
- 连接失败会自动重试(最多 5 次)
|
||||
- 页面卸载时会自动关闭连接
|
||||
- WebSocket 通知功能依赖于 Laravel-S (Swoole) 环境
|
||||
- 在普通 PHP 环境下运行时,WebSocket 通知会优雅降级(不发送通知,但不影响功能)
|
||||
|
||||
#### 获取字典列表
|
||||
- **接口**: `GET /admin/dictionaries`
|
||||
- **参数**:
|
||||
@@ -658,8 +939,17 @@ ### 系统配置缓存
|
||||
### 数据字典缓存
|
||||
|
||||
- **缓存键**: `dictionary:all` 或 `dictionary:code:{code}`
|
||||
- **过期时间**: 60分钟
|
||||
- **更新时机**: 字典数据增删改时自动清除
|
||||
- **过期时间**: 3600秒(1小时)
|
||||
- **更新时机**:
|
||||
- 字典数据增删改时自动清除后端缓存(Redis)
|
||||
- 通过 WebSocket 通知前端自动刷新缓存
|
||||
|
||||
**字典缓存同步流程:**
|
||||
1. 后端执行字典操作(增删改)
|
||||
2. 清理 Redis 缓存
|
||||
3. 发送 WebSocket 广播通知
|
||||
4. 前端接收通知并自动刷新缓存
|
||||
5. 显示成功提示
|
||||
|
||||
## 服务层说明
|
||||
|
||||
@@ -681,13 +971,24 @@ ### LogService
|
||||
|
||||
**主要方法**:
|
||||
- `getList()`: 获取日志列表
|
||||
- `getById()`: 根据 ID 获取日志详情
|
||||
- `getStatistics()`: 获取统计数据
|
||||
- `getListQuery()`: 获取日志查询构建器
|
||||
- `clearLogs()`: 清理过期日志
|
||||
- `delete()`: 删除单条日志
|
||||
- `batchDelete()`: 批量删除日志
|
||||
- `record()`: 记录日志(由中间件自动调用)
|
||||
|
||||
**特性**:
|
||||
- 自动记录所有经过中间件的请求
|
||||
- 计算请求执行时间
|
||||
- 过滤敏感参数
|
||||
- 获取客户端真实 IP(支持代理)
|
||||
- 异常处理,记录失败不影响业务
|
||||
|
||||
### DictionaryService
|
||||
|
||||
提供数据字典和字典项的管理功能。
|
||||
提供数据字典和字典项的管理功能,包括 WebSocket 通知机制。
|
||||
|
||||
**主要方法**:
|
||||
- `getList()`: 获取字典列表
|
||||
@@ -696,6 +997,13 @@ ### DictionaryService
|
||||
- `createItem()`: 创建字典项
|
||||
- `update()`: 更新字典
|
||||
- `updateItem()`: 更新字典项
|
||||
- `notifyDictionaryUpdate()`: 发送字典更新通知
|
||||
- `notifyDictionaryItemUpdate()`: 发送字典项更新通知
|
||||
|
||||
**缓存机制**:
|
||||
- Redis 缓存字典数据(TTL: 3600秒)
|
||||
- WebSocket 实时通知前端更新
|
||||
- 前端 Pinia + 本地存储持久化
|
||||
|
||||
### TaskService
|
||||
|
||||
@@ -744,49 +1052,459 @@ # 填充初始数据
|
||||
- 常用数据字典
|
||||
- 全国省市区数据
|
||||
|
||||
## 前端集成示例
|
||||
|
||||
### 日志管理页面
|
||||
|
||||
```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. **Swoole环境注意事项**:
|
||||
- 文件上传时注意临时文件清理
|
||||
- 使用Redis缓存避免内存泄漏
|
||||
- 图片压缩使用协程安全的方式
|
||||
### 1. Swoole环境注意事项
|
||||
- 文件上传时注意临时文件清理
|
||||
- 使用Redis缓存避免内存泄漏
|
||||
- 图片压缩使用协程安全的方式
|
||||
- WebSocket 通知依赖于 Laravel-S 环境
|
||||
|
||||
2. **安全注意事项**:
|
||||
- 文件上传必须验证文件类型和大小
|
||||
- 敏感操作必须记录日志
|
||||
- 配置数据不要存储密码等敏感信息
|
||||
### 2. 安全注意事项
|
||||
- 文件上传必须验证文件类型和大小
|
||||
- 敏感操作必须记录日志
|
||||
- 配置数据不要存储密码等敏感信息
|
||||
- 日志敏感信息已自动过滤
|
||||
|
||||
3. **性能优化**:
|
||||
- 城市数据使用Redis缓存
|
||||
- 大量日志数据定期清理
|
||||
- 图片上传时进行压缩处理
|
||||
### 3. 性能优化
|
||||
- 城市数据使用Redis缓存
|
||||
- 大量日志数据定期清理
|
||||
- 图片上传时进行压缩处理
|
||||
- 日志记录在请求处理后执行,不影响响应速度
|
||||
|
||||
4. **文件上传**:
|
||||
- 限制文件上传大小
|
||||
- 验证文件MIME类型
|
||||
- 定期清理临时文件
|
||||
### 4. 文件上传
|
||||
- 限制文件上传大小
|
||||
- 验证文件MIME类型
|
||||
- 定期清理临时文件
|
||||
|
||||
### 5. 日志管理
|
||||
- 定期清理历史日志(建议使用任务调度器)
|
||||
- 确保查询字段有索引
|
||||
- 使用分页查询避免加载过多数据
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 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. 数据库索引
|
||||
|
||||
确保以下字段有索引:
|
||||
- `system_logs`: `user_id`, `username`, `module`, `status`, `created_at`
|
||||
- `system_dictionaries`: `code`, `status`
|
||||
- `system_dictionary_items`: `dictionary_id`, `status`
|
||||
- `system_setting`: `group`, `key`, `status`
|
||||
|
||||
### 3. 分页查询
|
||||
|
||||
列表查询必须使用分页,避免一次加载过多数据。
|
||||
|
||||
### 4. 异步记录
|
||||
|
||||
日志记录操作应放在请求处理后,不影响响应速度。
|
||||
|
||||
### 5. 细粒度缓存更新
|
||||
|
||||
字典缓存当前实现为全量刷新,未来可以优化为增量更新:
|
||||
|
||||
```javascript
|
||||
// 只更新受影响的字典
|
||||
async function handleDictionaryUpdate(data) {
|
||||
const { action, data: dictData } = data
|
||||
|
||||
if (action === 'update' && dictData.code) {
|
||||
// 只更新特定的字典
|
||||
await dictionaryStore.getDictionary(dictData.code, true)
|
||||
} else {
|
||||
// 全量刷新
|
||||
await dictionaryStore.refresh(true)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展建议
|
||||
|
||||
1. **日志告警**: 添加日志异常告警功能
|
||||
2. **配置加密**: 敏感配置数据加密存储
|
||||
3. **多语言**: 支持配置数据的多语言
|
||||
4. **任务监控**: 添加任务执行监控和通知
|
||||
5. **CDN集成**: 文件上传支持CDN分发
|
||||
### 1. 日志告警
|
||||
添加日志异常告警功能,当出现大量错误日志时自动通知管理员。
|
||||
|
||||
### 2. 配置加密
|
||||
敏感配置数据加密存储,提高安全性。
|
||||
|
||||
### 3. 多语言
|
||||
支持配置数据的多语言,便于国际化部署。
|
||||
|
||||
### 4. 任务监控
|
||||
添加任务执行监控和通知,实时掌握任务运行状态。
|
||||
|
||||
### 5. CDN集成
|
||||
文件上传支持CDN分发,提高访问速度。
|
||||
|
||||
### 6. WebSocket 权限控制
|
||||
可以只向有权限的用户发送通知:
|
||||
|
||||
```php
|
||||
// 后端只向有字典管理权限的用户发送
|
||||
$adminUserIds = User::whereHas('roles', function($query) {
|
||||
$query->where('name', 'admin');
|
||||
})->pluck('id')->toArray();
|
||||
|
||||
$this->webSocketService->sendToUsers($adminUserIds, $message);
|
||||
```
|
||||
|
||||
### 7. 消息队列
|
||||
对于高并发场景,可以使用消息队列异步发送 WebSocket 通知:
|
||||
|
||||
```php
|
||||
// 使用 Laravel 队列
|
||||
UpdateDictionaryCacheJob::dispatch($action, $data);
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何清除城市数据缓存?
|
||||
### Q1: 如何清除城市数据缓存?
|
||||
A: 调用 `CityService::clearCache()` 方法或运行 `php artisan cache:forget city:tree`。
|
||||
|
||||
### Q: 图片上传后如何压缩?
|
||||
### Q2: 图片上传后如何压缩?
|
||||
A: 上传时设置 `compress=true` 和 `quality` 参数,系统会自动压缩。
|
||||
|
||||
### Q: 如何配置定时任务?
|
||||
### Q3: 如何配置定时任务?
|
||||
A: 在Admin后台创建任务,设置Cron表达式,系统会自动调度执行。
|
||||
|
||||
### Q: 数据字典如何使用?
|
||||
### Q4: 数据字典如何使用?
|
||||
A: 通过Public API获取字典数据,前端根据数据渲染下拉框等组件。
|
||||
|
||||
### Q: 日志数据过多如何处理?
|
||||
### Q5: 日志数据过多如何处理?
|
||||
A: 定期使用 `/admin/logs/clear` 接口清理过期日志,或在后台设置自动清理任务。
|
||||
|
||||
### Q6: 为什么某些请求没有被记录?
|
||||
A: 检查路由是否应用了 `log.request` 中间件,或者在中间件中是否被排除了。
|
||||
|
||||
### Q7: 字典缓存未更新怎么办?
|
||||
A: 检查以下几点:
|
||||
- 确认 Laravel-S 服务是否启动:`php bin/laravels status`
|
||||
- 检查浏览器控制台是否有 WebSocket 错误
|
||||
- 确认用户已登录且有 token
|
||||
- 手动刷新页面验证 API 是否正常
|
||||
|
||||
### Q8: WebSocket 连接失败会影响功能吗?
|
||||
A: 不会。WebSocket 连接失败不影响页面正常使用,只是不会收到自动更新通知。字典数据仍会正常更新到数据库和 Redis 缓存,只是前端不会收到实时通知,需要手动刷新页面。
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 1. 功能测试
|
||||
1. 测试各种请求是否被正确记录
|
||||
2. 测试敏感信息是否被正确过滤
|
||||
3. 测试日志查询和筛选功能
|
||||
4. 测试日志导出功能
|
||||
5. 测试批量删除和清理功能
|
||||
6. 测试字典 WebSocket 通知是否正确
|
||||
|
||||
### 2. 性能测试
|
||||
1. 测试日志记录对响应时间的影响
|
||||
2. 测试大量日志数据的查询性能
|
||||
3. 测试并发写入的性能
|
||||
4. 测试 WebSocket 广播性能
|
||||
|
||||
### 3. 集成测试(字典 WebSocket)
|
||||
1. 启动后端服务(Laravel-S)
|
||||
2. 启动前端开发服务器
|
||||
3. 在浏览器中登录系统
|
||||
4. 打开开发者工具的 Network -> WS 标签查看 WebSocket 消息
|
||||
5. 执行字典增删改操作
|
||||
6. 验证:
|
||||
- WebSocket 消息是否正确接收
|
||||
- 缓存是否自动刷新
|
||||
- 页面数据是否更新
|
||||
- 提示消息是否显示
|
||||
|
||||
### 4. 并发测试
|
||||
1. 打开多个浏览器窗口并登录
|
||||
2. 在一个窗口中进行字典操作
|
||||
3. 验证所有窗口的缓存是否同步更新
|
||||
|
||||
### 5. 边界测试
|
||||
1. 测试异常情况下的日志记录
|
||||
2. 测试超长参数的处理
|
||||
3. 测试特殊字符的处理
|
||||
4. 测试 WebSocket 断连重连机制
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 核心文件
|
||||
|
||||
**控制器:**
|
||||
```
|
||||
app/Http/Controllers/System/Admin/Config.php
|
||||
app/Http/Controllers/System/Admin/Log.php
|
||||
app/Http/Controllers/System/Admin/Dictionary.php
|
||||
app/Http/Controllers/System/Admin/Task.php
|
||||
app/Http/Controllers/System/Admin/City.php
|
||||
app/Http/Controllers/System/Admin/Upload.php
|
||||
app/Http/Controllers/System/WebSocket.php
|
||||
```
|
||||
|
||||
**中间件:**
|
||||
```
|
||||
app/Http/Middleware/LogRequestMiddleware.php
|
||||
```
|
||||
|
||||
**请求验证:**
|
||||
```
|
||||
app/Http/Requests/LogRequest.php
|
||||
```
|
||||
|
||||
**服务层:**
|
||||
```
|
||||
app/Services/System/ConfigService.php
|
||||
app/Services/System/LogService.php
|
||||
app/Services/System/DictionaryService.php
|
||||
app/Services/System/TaskService.php
|
||||
app/Services/System/CityService.php
|
||||
app/Services/System/UploadService.php
|
||||
app/Services/WebSocket/WebSocketService.php
|
||||
```
|
||||
|
||||
**模型:**
|
||||
```
|
||||
app/Models/System/Config.php
|
||||
app/Models/System/Log.php
|
||||
app/Models/System/Dictionary.php
|
||||
app/Models/System/DictionaryItem.php
|
||||
app/Models/System/Task.php
|
||||
app/Models/System/City.php
|
||||
```
|
||||
|
||||
**路由:**
|
||||
```
|
||||
routes/admin.php (后台管理路由)
|
||||
routes/api.php (公共 API 路由)
|
||||
```
|
||||
|
||||
**前端:**
|
||||
```
|
||||
resources/admin/src/composables/useWebSocket.js
|
||||
resources/admin/src/App.vue (集成 WebSocket)
|
||||
```
|
||||
|
||||
**文档:**
|
||||
```
|
||||
docs/README_SYSTEM.md (本文档)
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
System 基础模块提供了完整的系统管理功能,包括:
|
||||
|
||||
### 核心功能
|
||||
|
||||
- ✅ 系统配置管理(多分组、多类型支持)
|
||||
- ✅ 数据字典管理(分类 + 字典项)
|
||||
- ✅ 操作日志管理(自动记录、多维度查询、导出)
|
||||
- ✅ 任务管理(定时任务、手动执行、统计)
|
||||
- ✅ 城市数据管理(三级联动、缓存优化)
|
||||
- ✅ 文件上传管理(单文件、多文件、Base64、压缩)
|
||||
|
||||
### 高级特性
|
||||
|
||||
- ✅ WebSocket 实时通知(字典缓存自动更新)
|
||||
- ✅ Redis 缓存机制(性能优化)
|
||||
- ✅ 自动化日志记录(中间件拦截)
|
||||
- ✅ 敏感信息保护(自动过滤)
|
||||
- ✅ 数据导出功能(Excel 导出)
|
||||
- ✅ 批量操作支持
|
||||
- ✅ 完整的 API 文档
|
||||
|
||||
### 性能优化
|
||||
|
||||
- ✅ Redis 缓存(城市数据、系统配置、数据字典)
|
||||
- ✅ 日志异步记录(不影响响应速度)
|
||||
- ✅ 分页查询(避免加载过多数据)
|
||||
- ✅ 图片压缩(减少存储空间)
|
||||
- ✅ WebSocket 实时更新(减少不必要的请求)
|
||||
|
||||
### 安全特性
|
||||
|
||||
- ✅ 敏感信息过滤
|
||||
- ✅ 文件类型验证
|
||||
- ✅ 请求日志记录
|
||||
- ✅ IP 地址记录
|
||||
- ✅ 异常处理机制
|
||||
|
||||
System 模块现已完全集成到项目中,提供了完整的系统管理功能和优秀的用户体验。
|
||||
|
||||
@@ -1,684 +0,0 @@
|
||||
# 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 接口
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Demo\Http\Controllers\Admin;
|
||||
|
||||
class Index {
|
||||
|
||||
public function index(){}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Demo\Http\Controllers\Api;
|
||||
|
||||
class Index {
|
||||
|
||||
public function index(){}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Demo\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Nwidart\Modules\Traits\PathNamespace;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
||||
class DemoServiceProvider extends ServiceProvider
|
||||
{
|
||||
use PathNamespace;
|
||||
|
||||
protected string $name = 'Demo';
|
||||
|
||||
protected string $nameLower = 'demo';
|
||||
|
||||
/**
|
||||
* Boot the application events.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->registerCommands();
|
||||
$this->registerCommandSchedules();
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
$this->loadMigrationsFrom(module_path($this->name, 'database/migrations'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the service provider.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(EventServiceProvider::class);
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register commands in the format of Command::class
|
||||
*/
|
||||
protected function registerCommands(): void
|
||||
{
|
||||
// $this->commands([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register command Schedules.
|
||||
*/
|
||||
protected function registerCommandSchedules(): void
|
||||
{
|
||||
// $this->app->booted(function () {
|
||||
// $schedule = $this->app->make(Schedule::class);
|
||||
// $schedule->command('inspire')->hourly();
|
||||
// });
|
||||
}
|
||||
|
||||
/**
|
||||
* Register translations.
|
||||
*/
|
||||
public function registerTranslations(): void
|
||||
{
|
||||
$langPath = resource_path('lang/modules/'.$this->nameLower);
|
||||
|
||||
if (is_dir($langPath)) {
|
||||
$this->loadTranslationsFrom($langPath, $this->nameLower);
|
||||
$this->loadJsonTranslationsFrom($langPath);
|
||||
} else {
|
||||
$this->loadTranslationsFrom(module_path($this->name, 'lang'), $this->nameLower);
|
||||
$this->loadJsonTranslationsFrom(module_path($this->name, 'lang'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register config.
|
||||
*/
|
||||
protected function registerConfig(): void
|
||||
{
|
||||
$configPath = module_path($this->name, config('modules.paths.generator.config.path'));
|
||||
|
||||
if (is_dir($configPath)) {
|
||||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath));
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||
$config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname());
|
||||
$config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config);
|
||||
$segments = explode('.', $this->nameLower.'.'.$config_key);
|
||||
|
||||
// Remove duplicated adjacent segments
|
||||
$normalized = [];
|
||||
foreach ($segments as $segment) {
|
||||
if (end($normalized) !== $segment) {
|
||||
$normalized[] = $segment;
|
||||
}
|
||||
}
|
||||
|
||||
$key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized);
|
||||
|
||||
$this->publishes([$file->getPathname() => config_path($config)], 'config');
|
||||
$this->merge_config_from($file->getPathname(), $key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge config from the given path recursively.
|
||||
*/
|
||||
protected function merge_config_from(string $path, string $key): void
|
||||
{
|
||||
$existing = config($key, []);
|
||||
$module_config = require $path;
|
||||
|
||||
config([$key => array_replace_recursive($existing, $module_config)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register views.
|
||||
*/
|
||||
public function registerViews(): void
|
||||
{
|
||||
$viewPath = resource_path('views/modules/'.$this->nameLower);
|
||||
$sourcePath = module_path($this->name, 'resources/views');
|
||||
|
||||
$this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);
|
||||
|
||||
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower);
|
||||
|
||||
Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the services provided by the provider.
|
||||
*/
|
||||
public function provides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function getPublishableViewPaths(): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach (config('view.paths') as $path) {
|
||||
if (is_dir($path.'/modules/'.$this->nameLower)) {
|
||||
$paths[] = $path.'/modules/'.$this->nameLower;
|
||||
}
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Demo\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event handler mappings for the application.
|
||||
*
|
||||
* @var array<string, array<int, string>>
|
||||
*/
|
||||
protected $listen = [];
|
||||
|
||||
/**
|
||||
* Indicates if events should be discovered.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected static $shouldDiscoverEvents = true;
|
||||
|
||||
/**
|
||||
* Configure the proper event listeners for email verification.
|
||||
*/
|
||||
protected function configureEmailVerification(): void {}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Demo\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $name = 'Demo';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
*/
|
||||
public function map(): void
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
$this->mapWebRoutes();
|
||||
$this->mapAdminRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "web" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*/
|
||||
protected function mapWebRoutes(): void
|
||||
{
|
||||
Route::middleware('web')->group(module_path($this->name, '/routes/web.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*/
|
||||
protected function mapApiRoutes(): void
|
||||
{
|
||||
Route::middleware('api')->prefix('api')->name('api.')->group(module_path($this->name, '/routes/api.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*/
|
||||
protected function mapAdminRoutes(): void
|
||||
{
|
||||
Route::middleware('admin')->prefix('admin')->name('admin.')->group(module_path($this->name, '/routes/admin.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "nwidart/demo",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Widart",
|
||||
"email": "n.widart@gmail.com"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Demo\\": "app/",
|
||||
"Modules\\Demo\\Database\\Factories\\": "database/factories/",
|
||||
"Modules\\Demo\\Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Modules\\Demo\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'Demo',
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Demo\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DemoDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// $this->call([]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "Demo",
|
||||
"alias": "demo",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\Demo\\Providers\\DemoServiceProvider"
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.1.2",
|
||||
"laravel-vite-plugin": "^0.7.5",
|
||||
"sass": "^1.69.5",
|
||||
"postcss": "^8.3.7",
|
||||
"vite": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<title>Demo Module - {{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<meta name="description" content="{{ $description ?? '' }}">
|
||||
<meta name="keywords" content="{{ $keywords ?? '' }}">
|
||||
<meta name="author" content="{{ $author ?? '' }}">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
{{-- Vite CSS --}}
|
||||
{{-- {{ module_vite('build-demo', 'resources/assets/sass/app.scss') }} --}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{ $slot }}
|
||||
|
||||
{{-- Vite JS --}}
|
||||
{{-- {{ module_vite('build-demo', 'resources/assets/js/app.js') }} --}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<x-demo::layouts.master>
|
||||
<h1>Hello World</h1>
|
||||
|
||||
<p>Module: {!! config('demo.name') !!}</p>
|
||||
</x-demo::layouts.master>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Demo\Http\Controllers\DemoController;
|
||||
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::resource('demos', DemoController::class)->names('demo');
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join,relative,dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: '../../public/build-demo',
|
||||
emptyOutDir: true,
|
||||
manifest: true,
|
||||
},
|
||||
plugins: [
|
||||
laravel({
|
||||
publicDirectory: '../../public',
|
||||
buildDirectory: 'build-demo',
|
||||
input: [
|
||||
__dirname + '/resources/assets/sass/app.scss',
|
||||
__dirname + '/resources/assets/js/app.js'
|
||||
],
|
||||
refresh: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
// Scen all resources for assets file. Return array
|
||||
//function getFilePaths(dir) {
|
||||
// const filePaths = [];
|
||||
//
|
||||
// function walkDirectory(currentPath) {
|
||||
// const files = readdirSync(currentPath);
|
||||
// for (const file of files) {
|
||||
// const filePath = join(currentPath, file);
|
||||
// const stats = statSync(filePath);
|
||||
// if (stats.isFile() && !file.startsWith('.')) {
|
||||
// const relativePath = 'Modules/Demo/'+relative(__dirname, filePath);
|
||||
// filePaths.push(relativePath);
|
||||
// } else if (stats.isDirectory()) {
|
||||
// walkDirectory(filePath);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// walkDirectory(dir);
|
||||
// return filePaths;
|
||||
//}
|
||||
|
||||
//const __filename = fileURLToPath(import.meta.url);
|
||||
//const __dirname = dirname(__filename);
|
||||
|
||||
//const assetsDir = join(__dirname, 'resources/assets');
|
||||
//export const paths = getFilePaths(assetsDir);
|
||||
|
||||
|
||||
//export const paths = [
|
||||
// 'Modules/Demo/resources/assets/sass/app.scss',
|
||||
// 'Modules/Demo/resources/assets/js/app.js',
|
||||
//];
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"Demo": true
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 300,
|
||||
"useTabs": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
+40
-235
@@ -3,245 +3,50 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>VueAdmin - 管理后台</title>
|
||||
<script src="/public/config.js"></script>
|
||||
<script>
|
||||
// 获取项目名称配置
|
||||
window.__APP_NAME__ = window.SY_CONFIG?.APP_NAME || 'VueAdmin'
|
||||
document.title = window.__APP_NAME__ + ' - 管理后台'
|
||||
</script>
|
||||
<title></title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
z-index: 9999;
|
||||
transition: opacity 0.5s ease-in-out, visibility 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-container.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.loading-logo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 40px;
|
||||
animation: logoFloat 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.loading-logo img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@keyframes logoFloat {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #ffffff;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.loading-subtitle {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 16px;
|
||||
margin-bottom: 50px;
|
||||
animation: fadeInUp 0.8s ease-out 0.2s backwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
animation: spinnerRotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 4px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: #ffffff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.spinner-circle:nth-child(2) {
|
||||
width: 70%;
|
||||
height: 70%;
|
||||
top: 15%;
|
||||
left: 15%;
|
||||
border-top-color: rgba(255, 255, 255, 0.8);
|
||||
animation: spinnerRotate 1.5s linear infinite reverse;
|
||||
}
|
||||
|
||||
.spinner-circle:nth-child(3) {
|
||||
width: 40%;
|
||||
height: 40%;
|
||||
top: 30%;
|
||||
left: 30%;
|
||||
border-top-color: #ffffff;
|
||||
animation: spinnerRotate 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinnerRotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-tips {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 200px;
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
margin-top: 30px;
|
||||
overflow: hidden;
|
||||
animation: fadeInUp 0.8s ease-out 0.4s backwards;
|
||||
}
|
||||
|
||||
.progress-bar-inner {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
animation: progressLoading 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes progressLoading {
|
||||
0% {
|
||||
width: 0%;
|
||||
margin-left: 0;
|
||||
}
|
||||
50% {
|
||||
width: 70%;
|
||||
margin-left: 15%;
|
||||
}
|
||||
100% {
|
||||
width: 0%;
|
||||
margin-left: 100%;
|
||||
}
|
||||
}
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f5f5f5}
|
||||
#app,#loading{width:100%;height:100vh}
|
||||
#loading{position:fixed;top:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:linear-gradient(135deg,#fff5f0,#ffe8dc);z-index:9999;transition:opacity .3s}
|
||||
#loading.hidden{opacity:0;pointer-events:none}
|
||||
.logo{width:60px;height:60px;background:#fff;border-radius:12px;display:flex;align-items:center;justify-content:center;margin-bottom:20px;animation:scale 1s ease-in-out infinite;box-shadow:0 8px 24px rgba(255,107,53,.15)}
|
||||
.logo svg{width:36px;height:36px}
|
||||
.text{color:#2d1810;font-size:24px;font-weight:600;margin-bottom:30px}
|
||||
.spinner{width:40px;height:40px;border:3px solid rgba(255,107,53,.3);border-top-color:#ff6b35;border-radius:50%;animation:rotate .8s linear infinite}
|
||||
@keyframes rotate{to{transform:rotate(360deg)}}
|
||||
@keyframes scale{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 加载页面 -->
|
||||
<div id="loading" class="loading-container">
|
||||
<div class="loading-logo">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="width: 50px; height: 50px;">
|
||||
<rect width="24" height="24" rx="4" fill="#667eea"/>
|
||||
<path d="M7 8H17V10H7V8Z" fill="white"/>
|
||||
<path d="M7 11H17V13H7V11Z" fill="white"/>
|
||||
<path d="M7 14H14V16H7V14Z" fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="loading-text">VueAdmin</div>
|
||||
<div class="loading-subtitle">正在加载管理后台...</div>
|
||||
<div class="spinner">
|
||||
<div class="spinner-circle"></div>
|
||||
<div class="spinner-circle"></div>
|
||||
<div class="spinner-circle"></div>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-inner"></div>
|
||||
</div>
|
||||
<div class="loading-tips">首次加载可能需要几秒钟,请耐心等待</div>
|
||||
<div id="loading">
|
||||
<div class="logo">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<rect width="24" height="24" rx="4" fill="url(#logoGradient)"/>
|
||||
<path d="M7 8h10v2H7V8Z" fill="white"/>
|
||||
<path d="M7 11h10v2H7v-2Z" fill="white"/>
|
||||
<path d="M7 14h7v2H7v-2Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="logoGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ff6b35"/>
|
||||
<stop offset="100%" stop-color="#ffb347"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Vue 应用挂载点 -->
|
||||
<div id="app"></div>
|
||||
|
||||
<!-- 隐藏加载页面的脚本 -->
|
||||
<script>
|
||||
window.addEventListener('load', function() {
|
||||
setTimeout(function() {
|
||||
var loading = document.getElementById('loading');
|
||||
if (loading) {
|
||||
loading.classList.add('hidden');
|
||||
setTimeout(function() {
|
||||
if (loading.parentNode) {
|
||||
loading.parentNode.removeChild(loading);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<div class="text"><script>document.write(window.__APP_NAME__)</script></div>
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
window.onload=function(){setTimeout(function(){var e=document.getElementById('loading');e&&e.classList.add('hidden')},300)}
|
||||
</script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// 配置覆盖文件
|
||||
// 此文件中的配置项会覆盖 src/config/index.js 中的默认配置
|
||||
// 修改后无需重新构建,只需刷新页面即可生效
|
||||
|
||||
window.SY_CONFIG = {
|
||||
// 项目名称
|
||||
APP_NAME: '系统管理后台',
|
||||
|
||||
// 示例:覆盖接口地址
|
||||
// API_URL: 'http://your-domain.com/admin/',
|
||||
|
||||
// 示例:覆盖超时时间
|
||||
// TIMEOUT: 30000,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,8 +1,11 @@
|
||||
<script setup>
|
||||
import { onMounted, computed, watch, nextTick } from 'vue'
|
||||
import { onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18nStore } from './stores/modules/i18n'
|
||||
import { useLayoutStore } from './stores/modules/layout'
|
||||
import { useUserStore } from './stores/modules/user'
|
||||
import { useMessageStore } from './stores/modules/message'
|
||||
import { useWebSocket } from './hooks/useWebSocket'
|
||||
import { theme } from 'ant-design-vue'
|
||||
import i18n from './i18n'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
@@ -13,7 +16,7 @@ import 'dayjs/locale/en'
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'App'
|
||||
name: 'App',
|
||||
})
|
||||
|
||||
// i18n store
|
||||
@@ -22,6 +25,15 @@ const i18nStore = useI18nStore()
|
||||
// layout store
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// user store
|
||||
const userStore = useUserStore()
|
||||
|
||||
// message store
|
||||
const messageStore = useMessageStore()
|
||||
|
||||
// WebSocket
|
||||
const { initWebSocket, closeWebSocket } = useWebSocket()
|
||||
|
||||
// 解构 themeColor 以确保响应式
|
||||
const { themeColor } = storeToRefs(layoutStore)
|
||||
|
||||
@@ -66,12 +78,29 @@ watch(
|
||||
document.documentElement.style.setProperty('--primary-color', newColor)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 监听用户信息变化,当用户信息完整时初始化 WebSocket
|
||||
watch(
|
||||
() => [userStore.token, userStore.userInfo],
|
||||
() => {
|
||||
if (userStore.isUserInfoComplete()) {
|
||||
initWebSocket()
|
||||
} else if (!userStore.isLoggedIn()) {
|
||||
// 用户未登录,关闭 WebSocket
|
||||
closeWebSocket()
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
|
||||
// 恢复消息数据
|
||||
messageStore.restoreMessages()
|
||||
|
||||
// 从持久化的 store 中读取语言设置并同步到 i18n
|
||||
i18n.global.locale.value = i18nStore.currentLocale
|
||||
|
||||
@@ -82,6 +111,16 @@ onMounted(async () => {
|
||||
if (layoutStore.themeColor) {
|
||||
document.documentElement.style.setProperty('--primary-color', layoutStore.themeColor)
|
||||
}
|
||||
|
||||
// 尝试初始化 WebSocket 连接
|
||||
if (userStore.isUserInfoComplete()) {
|
||||
initWebSocket()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 关闭 WebSocket 连接
|
||||
closeWebSocket()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
+196
-140
@@ -12,11 +12,6 @@ export default {
|
||||
return await request.post('auth/logout')
|
||||
},
|
||||
},
|
||||
refresh: {
|
||||
post: async function () {
|
||||
return await request.post('auth/refresh')
|
||||
},
|
||||
},
|
||||
me: {
|
||||
get: async function () {
|
||||
return await request.get('auth/me')
|
||||
@@ -28,274 +23,335 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
// 文件上传
|
||||
upload: {
|
||||
post: async function (file) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return await request.post('system/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
// 用户管理
|
||||
users: {
|
||||
user: {
|
||||
list: {
|
||||
get: async function (params) {
|
||||
return await request.get('users', { params })
|
||||
return await request.get('auth/user', { params })
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
get: async function (id) {
|
||||
return await request.get(`users/${id}`)
|
||||
return await request.get(`auth/user/${id}`)
|
||||
},
|
||||
},
|
||||
add: {
|
||||
post: async function (params) {
|
||||
return await request.post('users', params)
|
||||
return await request.post('auth/user', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
put: async function (id, params) {
|
||||
return await request.put(`users/${id}`, params)
|
||||
return await request.put(`auth/user/${id}`, params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
delete: async function (id) {
|
||||
return await request.delete(`users/${id}`)
|
||||
return await request.delete(`auth/user/${id}`)
|
||||
},
|
||||
},
|
||||
batchDelete: {
|
||||
post: async function (params) {
|
||||
return await request.post('users/batch-delete', params)
|
||||
return await request.post('auth/user/batch-delete', params)
|
||||
},
|
||||
},
|
||||
batchStatus: {
|
||||
post: async function (params) {
|
||||
return await request.post('users/batch-status', params)
|
||||
return await request.post('auth/user/batch-status', params)
|
||||
},
|
||||
},
|
||||
batchDepartment: {
|
||||
post: async function (params) {
|
||||
return await request.post('users/batch-department', params)
|
||||
return await request.post('auth/user/batch-department', params)
|
||||
},
|
||||
},
|
||||
batchRoles: {
|
||||
post: async function (params) {
|
||||
return await request.post('users/batch-roles', params)
|
||||
return await request.post('auth/user/batch-roles', params)
|
||||
},
|
||||
},
|
||||
export: {
|
||||
post: async function (params) {
|
||||
return await request.post('users/export', params, { responseType: 'blob' })
|
||||
return await request.post('auth/user/export', params, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
},
|
||||
import: {
|
||||
post: async function (formData) {
|
||||
return await request.post('users/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
return await request.post('auth/user/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
},
|
||||
downloadTemplate: {
|
||||
get: async function () {
|
||||
return await request.get('users/download-template', { responseType: 'blob' })
|
||||
return await request.get('auth/user/download-template', {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 在线用户管理
|
||||
onlineUsers: {
|
||||
onlineUser: {
|
||||
count: {
|
||||
get: async function () {
|
||||
return await request.get('online-users/count')
|
||||
return await request.get('auth/online-user/count')
|
||||
},
|
||||
},
|
||||
list: {
|
||||
get: async function (params) {
|
||||
return await request.get('online-users', { params })
|
||||
return await request.get('auth/online-user', { params })
|
||||
},
|
||||
},
|
||||
sessions: {
|
||||
get: async function (userId) {
|
||||
return await request.get(`online-users/${userId}/sessions`)
|
||||
return await request.get(`auth/online-user/${userId}/sessions`)
|
||||
},
|
||||
},
|
||||
offline: {
|
||||
post: async function (userId, params) {
|
||||
return await request.post(`online-users/${userId}/offline`, params)
|
||||
return await request.post(`auth/online-user/${userId}/offline`, params)
|
||||
},
|
||||
},
|
||||
offlineAll: {
|
||||
post: async function (userId) {
|
||||
return await request.post(`online-users/${userId}/offline-all`)
|
||||
return await request.post(`auth/online-user/${userId}/offline-all`)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 角色管理
|
||||
roles: {
|
||||
role: {
|
||||
list: {
|
||||
get: async function (params) {
|
||||
return await request.get('roles', { params })
|
||||
return await request.get('auth/role', { params })
|
||||
},
|
||||
},
|
||||
all: {
|
||||
get: async function () {
|
||||
return await request.get('roles/all')
|
||||
return await request.get('auth/role/all')
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
get: async function (id) {
|
||||
return await request.get(`roles/${id}`)
|
||||
return await request.get(`auth/role/${id}`)
|
||||
},
|
||||
},
|
||||
add: {
|
||||
post: async function (params) {
|
||||
return await request.post('roles', params)
|
||||
return await request.post('auth/role', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
put: async function (id, params) {
|
||||
return await request.put(`roles/${id}`, params)
|
||||
return await request.put(`auth/role/${id}`, params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
delete: async function (id) {
|
||||
return await request.delete(`roles/${id}`)
|
||||
return await request.delete(`auth/role/${id}`)
|
||||
},
|
||||
},
|
||||
batchDelete: {
|
||||
post: async function (params) {
|
||||
return await request.post('roles/batch-delete', params)
|
||||
return await request.post('auth/role/batch-delete', params)
|
||||
},
|
||||
},
|
||||
batchStatus: {
|
||||
post: async function (params) {
|
||||
return await request.post('roles/batch-status', params)
|
||||
return await request.post('auth/role/batch-status', params)
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
get: async function (id) {
|
||||
return await request.get(`roles/${id}/permissions`)
|
||||
return await request.get(`auth/role/${id}/permissions`)
|
||||
},
|
||||
post: async function (id, params) {
|
||||
return await request.post(`roles/${id}/permissions`, params)
|
||||
return await request.post(`auth/role/${id}/permissions`, params)
|
||||
},
|
||||
},
|
||||
copy: {
|
||||
post: async function (id, params) {
|
||||
return await request.post(`roles/${id}/copy`, params)
|
||||
return await request.post(`auth/role/${id}/copy`, params)
|
||||
},
|
||||
},
|
||||
batchCopy: {
|
||||
post: async function (params) {
|
||||
return await request.post('roles/batch-copy', params)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 权限管理
|
||||
permissions: {
|
||||
list: {
|
||||
get: async function (params) {
|
||||
return await request.get('permissions', { params })
|
||||
},
|
||||
},
|
||||
tree: {
|
||||
get: async function () {
|
||||
return await request.get('permissions/tree')
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
get: async function () {
|
||||
return await request.get('permissions/menu')
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
get: async function (id) {
|
||||
return await request.get(`permissions/${id}`)
|
||||
},
|
||||
},
|
||||
add: {
|
||||
post: async function (params) {
|
||||
return await request.post('permissions', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
put: async function (id, params) {
|
||||
return await request.put(`permissions/${id}`, params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
delete: async function (id) {
|
||||
return await request.delete(`permissions/${id}`)
|
||||
},
|
||||
},
|
||||
batchDelete: {
|
||||
post: async function (params) {
|
||||
return await request.post('permissions/batch-delete', params)
|
||||
},
|
||||
},
|
||||
batchStatus: {
|
||||
post: async function (params) {
|
||||
return await request.post('permissions/batch-status', params)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 部门管理
|
||||
departments: {
|
||||
list: {
|
||||
get: async function (params) {
|
||||
return await request.get('departments', { params })
|
||||
},
|
||||
},
|
||||
tree: {
|
||||
get: async function () {
|
||||
return await request.get('departments/tree')
|
||||
},
|
||||
},
|
||||
all: {
|
||||
get: async function () {
|
||||
return await request.get('departments/all')
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
get: async function (id) {
|
||||
return await request.get(`departments/${id}`)
|
||||
},
|
||||
},
|
||||
add: {
|
||||
post: async function (params) {
|
||||
return await request.post('departments', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
put: async function (id, params) {
|
||||
return await request.put(`departments/${id}`, params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
delete: async function (id) {
|
||||
return await request.delete(`departments/${id}`)
|
||||
},
|
||||
},
|
||||
batchDelete: {
|
||||
post: async function (params) {
|
||||
return await request.post('departments/batch-delete', params)
|
||||
},
|
||||
},
|
||||
batchStatus: {
|
||||
post: async function (params) {
|
||||
return await request.post('departments/batch-status', params)
|
||||
return await request.post('auth/role/batch-copy', params)
|
||||
},
|
||||
},
|
||||
export: {
|
||||
post: async function (params) {
|
||||
return await request.post('departments/export', params, { responseType: 'blob' })
|
||||
return await request.post('auth/role/export', params, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
},
|
||||
import: {
|
||||
post: async function (formData) {
|
||||
return await request.post('departments/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
return await request.post('auth/role/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
},
|
||||
downloadTemplate: {
|
||||
get: async function () {
|
||||
return await request.get('departments/download-template', { responseType: 'blob' })
|
||||
return await request.get('auth/role/download-template', {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 权限管理
|
||||
permission: {
|
||||
list: {
|
||||
get: async function (params) {
|
||||
return await request.get('auth/permission', { params })
|
||||
},
|
||||
},
|
||||
tree: {
|
||||
get: async function () {
|
||||
return await request.get('auth/permission/tree')
|
||||
},
|
||||
},
|
||||
menu: {
|
||||
get: async function () {
|
||||
return await request.get('auth/permission/menu')
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
get: async function (id) {
|
||||
return await request.get(`auth/permission/${id}`)
|
||||
},
|
||||
},
|
||||
add: {
|
||||
post: async function (params) {
|
||||
return await request.post('auth/permission', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
put: async function (id, params) {
|
||||
return await request.put(`auth/permission/${id}`, params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
delete: async function (id) {
|
||||
return await request.delete(`auth/permission/${id}`)
|
||||
},
|
||||
},
|
||||
batchDelete: {
|
||||
post: async function (params) {
|
||||
return await request.post('auth/permission/batch-delete', params)
|
||||
},
|
||||
},
|
||||
batchStatus: {
|
||||
post: async function (params) {
|
||||
return await request.post('auth/permission/batch-status', params)
|
||||
},
|
||||
},
|
||||
export: {
|
||||
post: async function (params) {
|
||||
return await request.post('auth/permission/export', params, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
},
|
||||
import: {
|
||||
post: async function (formData) {
|
||||
return await request.post('auth/permission/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
},
|
||||
downloadTemplate: {
|
||||
get: async function () {
|
||||
return await request.get('auth/permission/download-template', {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 部门管理
|
||||
department: {
|
||||
list: {
|
||||
get: async function (params) {
|
||||
return await request.get('auth/department', { params })
|
||||
},
|
||||
},
|
||||
tree: {
|
||||
get: async function (params) {
|
||||
return await request.get('auth/department/tree', { params })
|
||||
},
|
||||
},
|
||||
all: {
|
||||
get: async function () {
|
||||
return await request.get('auth/department/all')
|
||||
},
|
||||
},
|
||||
detail: {
|
||||
get: async function (id) {
|
||||
return await request.get(`auth/department/${id}`)
|
||||
},
|
||||
},
|
||||
add: {
|
||||
post: async function (params) {
|
||||
return await request.post('auth/department', params)
|
||||
},
|
||||
},
|
||||
edit: {
|
||||
put: async function (id, params) {
|
||||
return await request.put(`auth/department/${id}`, params)
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
delete: async function (id) {
|
||||
return await request.delete(`auth/department/${id}`)
|
||||
},
|
||||
},
|
||||
batchDelete: {
|
||||
post: async function (params) {
|
||||
return await request.post('auth/department/batch-delete', params)
|
||||
},
|
||||
},
|
||||
batchStatus: {
|
||||
post: async function (params) {
|
||||
return await request.post('auth/department/batch-status', params)
|
||||
},
|
||||
},
|
||||
export: {
|
||||
post: async function (params) {
|
||||
return await request.post('auth/department/export', params, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
},
|
||||
import: {
|
||||
post: async function (formData) {
|
||||
return await request.post('auth/department/import', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
},
|
||||
},
|
||||
downloadTemplate: {
|
||||
get: async function () {
|
||||
return await request.get('auth/department/download-template', {
|
||||
responseType: 'blob',
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user