Compare commits

...

41 Commits

Author SHA1 Message Date
molong db606434bc Demo模块 2026-02-22 08:59:14 +08:00
molong 2625ee94bf Merge branch 'main' of http://git.tensent.cn/molong/laravel_swoole 2026-02-22 08:58:24 +08:00
molong 9197bc6f29 重置 2026-02-22 08:58:19 +08:00
molong 78507ac7a7 update 2026-02-22 08:56:39 +08:00
molong 22824550d6 解决文件名大小写的问题 2026-02-22 08:53:58 +08:00
molong 6d118287db 更新 2026-02-21 14:57:05 +08:00
molong 6b92ffbfeb 更新 2026-02-21 14:50:16 +08:00
molong c0d27be99b 移动端更新 2026-02-21 14:47:54 +08:00
molong 7f8e68bb0a 修复单复数的bug 2026-02-21 10:35:26 +08:00
molong 350f368f6c 更新模块配置 2026-02-21 10:31:00 +08:00
molong 3d7f508a6b 更新 2026-02-21 09:54:56 +08:00
molong 843f5bfab9 模块Demo 2026-02-21 09:52:27 +08:00
molong ca0dd554d3 更新 2026-02-19 22:51:23 +08:00
molong e26ba12150 更新调整修复 2026-02-19 14:05:30 +08:00
molong f0af965412 更新配置页面 2026-02-19 12:06:13 +08:00
molong f0f0763ceb 前端代码格式化 2026-02-19 11:46:27 +08:00
molong d310a29c03 调整数据库表的单复数 2026-02-19 10:39:38 +08:00
molong 736a41a718 优化代码 2026-02-18 22:36:40 +08:00
molong 0ecb088569 优化更新 2026-02-18 22:28:08 +08:00
molong b6c133952b 格式化代码,websocket功能完善 2026-02-18 21:50:05 +08:00
molong 6543e2ccdd 更新 2026-02-18 19:41:03 +08:00
molong a0c2350662 更新websocket功能 2026-02-18 18:05:33 +08:00
molong e679a9402f 更新 2026-02-18 17:54:07 +08:00
molong 378b9bd71f 更新完善字典相关功能 2026-02-18 17:15:33 +08:00
molong 5450777bd7 更新用户权限模块功能 2026-02-18 16:17:11 +08:00
molong 790b3140a7 更新功能:数据字典和定时任务 2026-02-18 10:43:25 +08:00
molong 6623c656f4 更新 2026-02-17 13:55:30 +08:00
molong f90afaddca 更新 2026-02-11 22:59:22 +08:00
molong b0ae1bb68c 更新 2026-02-11 22:42:37 +08:00
molong e265bcc28d 优化更新 2026-02-11 17:13:18 +08:00
molong ada5e027fa 更新优化调整 2026-02-11 15:49:19 +08:00
molong 1bfe30651e 更新 2026-02-11 15:16:11 +08:00
molong 2720de7f44 更新代码 2026-02-11 14:35:58 +08:00
molong 9339cefae0 更新后端规则文档 2026-02-11 09:51:21 +08:00
molong a2ca64d909 更新后天规则文档 2026-02-11 09:39:02 +08:00
molong 1969669f0b 更新功能 2026-02-10 22:36:05 +08:00
molong 2248d51887 解决登录token的bug,移除refreshtoken 2026-02-10 21:20:21 +08:00
molong 7aa428d932 更新 2026-02-10 21:15:11 +08:00
molong abfc2a953c 更新 2026-02-10 09:24:47 +08:00
molong 121a971e91 Merge branch 'main' of http://git.tensent.cn/molong/laravel_swoole 2026-02-10 09:12:53 +08:00
molong 5d51656fd4 更新配置 2026-02-10 09:12:51 +08:00
237 changed files with 75661 additions and 11081 deletions
+636 -40
View File
@@ -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
View File
@@ -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;
}
}
}
+9 -3
View File
@@ -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 : '',
];
}
}
+85
View File
@@ -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;
}
}
+67
View File
@@ -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 : '',
];
}
}
+2 -2
View File
@@ -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',
+95 -8
View File
@@ -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();
}
}
+64 -9
View File
@@ -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();
}
}
+5 -4
View File
@@ -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',
+2 -2
View File
@@ -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',
+8 -1
View File
@@ -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 {
+4 -33
View File
@@ -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
+5 -3
View File
@@ -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
*/
+166
View File
@@ -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;
}
}
+129
View File
@@ -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;
}
}
+3 -2
View File
@@ -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',
+3 -2
View File
@@ -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',
+3 -2
View File
@@ -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',
+11 -2
View File
@@ -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
View File
@@ -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);
}
}
+3 -2
View File
@@ -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',
+4 -2
View File
@@ -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',
];
+3 -1
View File
@@ -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',
+3 -1
View File
@@ -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',
+154
View File
@@ -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 => '无操作',
];
}
}
+3 -2
View File
@@ -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
View File
@@ -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;
}
}
+44
View File
@@ -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);
}
}
}
}
+164
View File
@@ -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(),
];
}
/**
* 删除导出文件
*/
+95 -1
View File
@@ -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,
]
);
}
}
+98 -59
View File
@@ -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);
}
}
+2 -2
View File
@@ -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();
+229 -23
View File
@@ -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);
}
}
+77 -1
View File
@@ -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;
}
}
+563
View File
@@ -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)
);
}
}
+124
View File
@@ -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 [];
}
}
}
+7 -8
View File
@@ -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
+394 -366
View File
@@ -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);
}
}
}
}
+246 -242
View File
@@ -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' => [
+230
View File
@@ -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;
}
}
+298
View File
@@ -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;
}
}
+43
View File
@@ -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
View File
@@ -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
View File
@@ -101,7 +101,7 @@
|
*/
'ttl' => env('JWT_TTL', 60 * 60 * 24 * 7),
'ttl' => (int) env('JWT_TTL', 60 * 60 * 24 * 7),
/*
|--------------------------------------------------------------------------
+3 -3
View File
@@ -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,
+302
View File
@@ -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');
}
};
+47 -47
View File
@@ -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
View File
@@ -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
-337
View File
@@ -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
-608
View File
@@ -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
View File
@@ -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 模块现已完全集成到项目中,提供了完整的系统管理功能和优秀的用户体验。
-684
View File
@@ -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(){}
}
View File
@@ -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'));
}
}
+30
View File
@@ -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/"
}
}
}
View File
+5
View File
@@ -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([]);
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "Demo",
"alias": "demo",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\Demo\\Providers\\DemoServiceProvider"
],
"files": []
}
+15
View File
@@ -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>
View File
+4
View File
@@ -0,0 +1,4 @@
<?php
use Illuminate\Support\Facades\Route;
+4
View File
@@ -0,0 +1,4 @@
<?php
use Illuminate\Support\Facades\Route;
+8
View File
@@ -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');
});
View File
View File
+57
View File
@@ -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',
//];
+3
View File
@@ -0,0 +1,3 @@
{
"Demo": true
}
+8
View File
@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 300,
"useTabs": true,
"tabWidth": 4
}
+40 -235
View File
@@ -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>
+14
View File
@@ -0,0 +1,14 @@
// 配置覆盖文件
// 此文件中的配置项会覆盖 src/config/index.js 中的默认配置
// 修改后无需重新构建,只需刷新页面即可生效
window.SY_CONFIG = {
// 项目名称
APP_NAME: '系统管理后台',
// 示例:覆盖接口地址
// API_URL: 'http://your-domain.com/admin/',
// 示例:覆盖超时时间
// TIMEOUT: 30000,
}
-1
View File
@@ -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

+42 -3
View File
@@ -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
View File
@@ -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