init
This commit is contained in:
@@ -0,0 +1,690 @@
|
||||
# AI 模块开发提示词
|
||||
|
||||
## 角色定义
|
||||
|
||||
你是一位经验丰富的全栈开发工程师,精通 Go 语言(Goravel 框架)和 Vue 3(Element Plus)开发。你的任务是按照项目规范,完成一个完整的 CRUD 模块开发,包括后端接口和前端页面。
|
||||
|
||||
## 项目背景
|
||||
|
||||
这是一个基于 **Goravel**(Go 语言 Web 框架)和 **Vue 3** 的后台管理系统项目。
|
||||
|
||||
### 技术栈
|
||||
- **后端**: Goravel Framework (Go)
|
||||
- **前端**: Vue 3 + Element Plus + Vite
|
||||
- **数据库**: MySQL/PostgreSQL
|
||||
- **ORM**: GORM
|
||||
- **API 文档**: Swagger
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
goravel-admin/
|
||||
├── app/
|
||||
│ ├── http/
|
||||
│ │ ├── controllers/admin/ # 后台控制器
|
||||
│ │ ├── requests/admin/ # 请求验证
|
||||
│ │ ├── helpers/ # 辅助函数
|
||||
│ │ └── response/ # 统一响应
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── services/ # 业务逻辑服务
|
||||
│ └── errors/ # 错误定义
|
||||
├── database/migrations/ # 数据库迁移
|
||||
├── routes/admin.go # 后台路由
|
||||
├── html/src/
|
||||
│ ├── api/ # API 客户端
|
||||
│ ├── views/ # 页面组件
|
||||
│ ├── components/ # 通用组件
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ └── router/ # 路由配置
|
||||
└── docs/ # 文档
|
||||
```
|
||||
|
||||
## 开发任务
|
||||
|
||||
根据用户提供的模块需求,完成以下开发任务:
|
||||
|
||||
### 后端开发(7个步骤)
|
||||
|
||||
1. **数据库迁移** - 创建迁移文件
|
||||
2. **创建模型** - 定义数据模型
|
||||
3. **创建服务层** - 实现业务逻辑
|
||||
4. **创建请求验证** - 验证请求参数
|
||||
5. **创建控制器** - 处理 HTTP 请求
|
||||
6. **注册路由** - 配置 API 路由
|
||||
7. **运行迁移** - 执行数据库迁移
|
||||
|
||||
### 前端开发(5个步骤)
|
||||
|
||||
1. **创建 API 客户端** - 封装 API 请求
|
||||
2. **创建列表页面** - 实现列表展示和搜索
|
||||
3. **创建表单组件** - 实现创建/编辑表单
|
||||
4. **注册路由** - 配置前端路由
|
||||
5. **添加国际化文本** - 配置多语言支持
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 命名规范
|
||||
|
||||
- **表名**: 小写复数形式,如 `guestbooks`, `orders`
|
||||
- **模型名**: 单数首字母大写,如 `Guestbook`, `Order`
|
||||
- **服务接口**: `XxxService`,如 `GuestbookService`
|
||||
- **服务实现**: `XxxServiceImpl`,如 `GuestbookServiceImpl`
|
||||
- **控制器**: `XxxController`,如 `GuestbookController`
|
||||
- **请求验证**: `XxxCreate`, `XxxUpdate`,如 `GuestbookCreate`
|
||||
- **路由资源**: 小写复数,如 `guestbooks`, `orders`
|
||||
|
||||
### 代码规范
|
||||
|
||||
#### 后端规范
|
||||
|
||||
1. **统一响应格式**
|
||||
```go
|
||||
// 成功响应
|
||||
return response.Success(ctx, http.Json{
|
||||
"list": data,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
|
||||
// 错误响应
|
||||
return response.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
|
||||
// 验证错误
|
||||
return response.ValidationError(ctx, http.StatusBadRequest, "validation_failed", errors.All())
|
||||
```
|
||||
|
||||
2. **错误处理**
|
||||
```go
|
||||
// 使用项目错误定义
|
||||
if err != nil {
|
||||
return nil, apperrors.ErrNotFound.WithError(err)
|
||||
}
|
||||
```
|
||||
|
||||
3. **时间处理**
|
||||
```go
|
||||
// 时间转换
|
||||
startTime := ""
|
||||
if startTimeStr != "" {
|
||||
startTime = helpers.ConvertTimeToUTC(ctx, startTimeStr)
|
||||
}
|
||||
```
|
||||
|
||||
4. **排序处理**
|
||||
```go
|
||||
orderBy := filters.OrderBy
|
||||
if orderBy == "" {
|
||||
orderBy = "created_at:desc"
|
||||
}
|
||||
query = helpers.ApplySort(query, orderBy, "created_at:desc")
|
||||
```
|
||||
|
||||
5. **Swagger 注解**
|
||||
- 每个控制器方法必须添加完整的 Swagger 注解
|
||||
- 包括 `@Summary`, `@Description`, `@Tags`, `@Param`, `@Success`, `@Failure`, `@Router`, `@Security`
|
||||
|
||||
#### 前端规范
|
||||
|
||||
1. **使用组合式函数**
|
||||
```javascript
|
||||
// 列表页面使用 useListPage
|
||||
const { loading, tableData, pagination, loadData, ... } = useListPage({
|
||||
api: getXxxList,
|
||||
searchFields: [...],
|
||||
tableColumns: [...]
|
||||
})
|
||||
|
||||
// CRUD 操作使用 useCrud
|
||||
const { dialogVisible, editId, handleAdd, handleEdit, handleDelete, handleFormSuccess } = useCrud(formRef)
|
||||
```
|
||||
|
||||
2. **权限控制**
|
||||
```javascript
|
||||
const { getButtonState } = usePermission()
|
||||
// 按钮权限检查
|
||||
:disabled="getButtonState('module.action').disabled"
|
||||
```
|
||||
|
||||
3. **错误处理**
|
||||
```javascript
|
||||
import ErrorHandler from '../../utils/errorHandler'
|
||||
try {
|
||||
// ...
|
||||
} catch (error) {
|
||||
ErrorHandler.handle(error)
|
||||
}
|
||||
```
|
||||
|
||||
4. **国际化**
|
||||
```javascript
|
||||
const { t } = useI18n()
|
||||
// 使用翻译
|
||||
{{ $t('module.field') }}
|
||||
```
|
||||
|
||||
## 开发步骤详解
|
||||
|
||||
### 步骤 1: 数据库迁移
|
||||
|
||||
**文件路径**: `database/migrations/YYYYMMDDHHMMSS_create_xxx_table.go`
|
||||
|
||||
**模板**:
|
||||
```go
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/database/schema"
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
type MYYYYMMDDHHMMSSCreateXxxTable struct{}
|
||||
|
||||
func (r *MYYYYMMDDHHMMSSCreateXxxTable) Signature() string {
|
||||
return "YYYYMMDDHHMMSS_create_xxx_table"
|
||||
}
|
||||
|
||||
func (r *MYYYYMMDDHHMMSSCreateXxxTable) Up() error {
|
||||
if !facades.Schema().HasTable("xxx") {
|
||||
return facades.Schema().Create("xxx", func(table schema.Blueprint) {
|
||||
table.BigIncrements("id")
|
||||
// 添加字段
|
||||
table.Timestamps()
|
||||
table.SoftDeletes()
|
||||
table.Comment("表注释")
|
||||
|
||||
// 添加索引
|
||||
table.Index("status")
|
||||
table.Index("created_at")
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MYYYYMMDDHHMMSSCreateXxxTable) Down() error {
|
||||
return facades.Schema().DropIfExists("xxx")
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 迁移文件名格式: `YYYYMMDDHHMMSS_create_xxx_table.go`
|
||||
- 类名格式: `MYYYYMMDDHHMMSSCreateXxxTable`
|
||||
- 必须包含 `Timestamps()` 和 `SoftDeletes()`
|
||||
- 为常用查询字段添加索引
|
||||
|
||||
### 步骤 2: 创建模型
|
||||
|
||||
**文件路径**: `app/models/xxx.go`
|
||||
|
||||
**模板**:
|
||||
```go
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/database/orm"
|
||||
)
|
||||
|
||||
type Xxx struct {
|
||||
orm.Model
|
||||
// 字段定义
|
||||
orm.SoftDeletes
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 必须嵌入 `orm.Model` 和 `orm.SoftDeletes`
|
||||
- 字段标签包含 `gorm` 和 `json`
|
||||
- 字符串字段指定 `size`
|
||||
- 文本字段使用 `type:text`
|
||||
|
||||
### 步骤 3: 创建服务层
|
||||
|
||||
**文件路径**: `app/services/xxx_service.go`
|
||||
|
||||
**必须实现的方法**:
|
||||
- `GetByID(id uint) (*models.Xxx, error)` - 根据ID获取
|
||||
- `GetList(filters XxxFilters, page, pageSize int) ([]models.Xxx, int64, error)` - 获取列表
|
||||
- `Create(data map[string]any) (*models.Xxx, error)` - 创建
|
||||
- `Update(id uint, data map[string]any) error` - 更新
|
||||
- `Delete(id uint) error` - 删除
|
||||
|
||||
**模板**:
|
||||
```go
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/database/orm"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
apperrors "goravel/app/errors"
|
||||
"goravel/app/http/helpers"
|
||||
"goravel/app/models"
|
||||
)
|
||||
|
||||
type XxxService interface {
|
||||
GetByID(id uint) (*models.Xxx, error)
|
||||
GetList(filters XxxFilters, page, pageSize int) ([]models.Xxx, int64, error)
|
||||
Create(data map[string]any) (*models.Xxx, error)
|
||||
Update(id uint, data map[string]any) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type XxxFilters struct {
|
||||
// 筛选字段
|
||||
OrderBy string
|
||||
}
|
||||
|
||||
type XxxServiceImpl struct{}
|
||||
|
||||
func NewXxxServiceImpl() *XxxServiceImpl {
|
||||
return &XxxServiceImpl{}
|
||||
}
|
||||
|
||||
func (s *XxxServiceImpl) buildQuery(filters XxxFilters) orm.Query {
|
||||
query := facades.Orm().Query().Model(&models.Xxx{})
|
||||
// 构建查询条件
|
||||
return query
|
||||
}
|
||||
|
||||
func (s *XxxServiceImpl) GetList(filters XxxFilters, page, pageSize int) ([]models.Xxx, int64, error) {
|
||||
query := s.buildQuery(filters)
|
||||
|
||||
orderBy := filters.OrderBy
|
||||
if orderBy == "" {
|
||||
orderBy = "created_at:desc"
|
||||
}
|
||||
query = helpers.ApplySort(query, orderBy, "created_at:desc")
|
||||
|
||||
total, err := query.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var list []models.Xxx
|
||||
err = query.Offset((page-1)*pageSize).Limit(pageSize).Find(&list)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return list, total, nil
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 使用 `buildQuery` 方法构建查询条件
|
||||
- 默认排序为 `created_at:desc`
|
||||
- 使用 `helpers.ApplySort` 处理排序
|
||||
- 错误处理使用 `apperrors`
|
||||
|
||||
### 步骤 4: 创建请求验证
|
||||
|
||||
**文件路径**:
|
||||
- `app/http/requests/admin/xxx_create.go`
|
||||
- `app/http/requests/admin/xxx_update.go`
|
||||
|
||||
**模板**:
|
||||
```go
|
||||
package admin
|
||||
|
||||
import (
|
||||
"goravel/app/http/trans"
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
)
|
||||
|
||||
type XxxCreate struct {
|
||||
// 字段定义
|
||||
}
|
||||
|
||||
func (r *XxxCreate) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *XxxCreate) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
// 验证规则
|
||||
}
|
||||
}
|
||||
|
||||
func (r *XxxCreate) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
// 错误消息
|
||||
}
|
||||
}
|
||||
|
||||
func (r *XxxCreate) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
// 字段名称
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 使用 `trans.Get(ctx, "key")` 获取翻译
|
||||
- Create 请求字段通常为 `required`
|
||||
- Update 请求字段通常为可选(不设置 `required`)
|
||||
|
||||
### 步骤 5: 创建控制器
|
||||
|
||||
**文件路径**: `app/http/controllers/admin/xxx_controller.go`
|
||||
|
||||
**必须实现的方法**:
|
||||
- `Index(ctx http.Context) http.Response` - 列表
|
||||
- `Show(ctx http.Context) http.Response` - 详情
|
||||
- `Store(ctx http.Context) http.Response` - 创建
|
||||
- `Update(ctx http.Context) http.Response` - 更新
|
||||
- `Destroy(ctx http.Context) http.Response` - 删除
|
||||
|
||||
**模板**:
|
||||
```go
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
apperrors "goravel/app/errors"
|
||||
adminrequests "goravel/app/http/requests/admin"
|
||||
"goravel/app/http/helpers"
|
||||
"goravel/app/http/response"
|
||||
"goravel/app/services"
|
||||
)
|
||||
|
||||
type XxxController struct {
|
||||
xxxService services.XxxService
|
||||
}
|
||||
|
||||
func NewXxxController() *XxxController {
|
||||
return &XxxController{
|
||||
xxxService: services.NewXxxServiceImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Index 列表
|
||||
// @Summary 获取列表
|
||||
// @Description 分页获取列表
|
||||
// @Tags 模块管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码" default(1)
|
||||
// @Param page_size query int false "每页数量" default(20)
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Router /api/admin/xxx [get]
|
||||
// @Security BearerAuth
|
||||
func (r *XxxController) Index(ctx http.Context) http.Response {
|
||||
page := cast.ToInt(ctx.Request().Query("page", "1"))
|
||||
pageSize := cast.ToInt(ctx.Request().Query("page_size", "20"))
|
||||
|
||||
filters := services.XxxFilters{
|
||||
OrderBy: ctx.Request().Query("order_by", ""),
|
||||
}
|
||||
|
||||
list, total, err := r.xxxService.GetList(filters, page, pageSize)
|
||||
if err != nil {
|
||||
return response.Error(ctx, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return response.Success(ctx, http.Json{
|
||||
"list": list,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- 每个方法必须添加完整的 Swagger 注解
|
||||
- 使用 `response.Success` 和 `response.Error` 返回响应
|
||||
- 使用 `cast` 进行类型转换
|
||||
- 时间字段使用 `helpers.ConvertTimeToUTC` 转换
|
||||
|
||||
### 步骤 6: 注册路由
|
||||
|
||||
**文件路径**: `routes/admin.go`
|
||||
|
||||
**添加代码**:
|
||||
```go
|
||||
// 在 Admin() 函数中添加控制器实例
|
||||
xxxController := admin.NewXxxController()
|
||||
|
||||
// 在需要认证、权限验证的路由组中添加
|
||||
router.Resource("xxx", xxxController)
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- `router.Resource` 会自动注册 RESTful 路由
|
||||
- 自定义路由需要单独添加
|
||||
|
||||
### 步骤 7: 运行迁移
|
||||
|
||||
**命令**:
|
||||
```bash
|
||||
go run . migrate
|
||||
```
|
||||
|
||||
### 步骤 8: 创建 API 客户端
|
||||
|
||||
**文件路径**: `html/src/api/xxx.js`
|
||||
|
||||
**模板**:
|
||||
```javascript
|
||||
import request from '../utils/request'
|
||||
import { createCRUDApi, extendApi } from '../utils/apiFactory'
|
||||
|
||||
// 创建基础 CRUD API
|
||||
const baseXxxApi = createCRUDApi('xxx')
|
||||
|
||||
// 扩展 API,添加自定义方法(可选)
|
||||
const xxxApi = extendApi(baseXxxApi, {
|
||||
// 自定义方法
|
||||
})
|
||||
|
||||
// 导出所有方法
|
||||
export const {
|
||||
list: getXxxList,
|
||||
detail: getXxxDetail,
|
||||
create: createXxx,
|
||||
update: updateXxx,
|
||||
delete: deleteXxx
|
||||
} = xxxApi
|
||||
```
|
||||
|
||||
### 步骤 9: 创建列表页面
|
||||
|
||||
**文件路径**: `html/src/views/xxx/XxxList.vue`
|
||||
|
||||
**关键点**:
|
||||
- 使用 `useListPage` 组合式函数
|
||||
- 使用 `useCrud` 处理 CRUD 操作
|
||||
- 使用 `usePermission` 进行权限控制
|
||||
- 使用 `SearchForm` 组件
|
||||
- 使用 `VxeTable` 组件
|
||||
- 使用 `Pagination` 组件
|
||||
- 使用 `TableActionButtons` 组件
|
||||
|
||||
**模板结构**:
|
||||
```vue
|
||||
<template>
|
||||
<div class="list-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ $t('menu.xxx') }}</span>
|
||||
<el-button @click="handleAdd">
|
||||
{{ $t('xxx.add_xxx') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<SearchForm ... />
|
||||
<VxeTable ... />
|
||||
<Pagination ... />
|
||||
</el-card>
|
||||
|
||||
<XxxForm ... />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useListPage } from '../../composables/useListPage'
|
||||
import { useCrud } from '../../composables/useCrud'
|
||||
import { usePermission } from '../../composables/usePermission'
|
||||
// ...
|
||||
</script>
|
||||
```
|
||||
|
||||
### 步骤 10: 创建表单组件
|
||||
|
||||
**文件路径**: `html/src/views/xxx/XxxForm.vue`
|
||||
|
||||
**关键点**:
|
||||
- 使用 `v-model` 控制对话框显示
|
||||
- 使用 `editId` 区分创建/编辑
|
||||
- 表单验证使用 Element Plus 规则
|
||||
- 提交成功后触发 `success` 事件
|
||||
|
||||
### 步骤 11: 注册路由
|
||||
|
||||
**文件路径**: `html/src/router/index.js`
|
||||
|
||||
**添加路由**:
|
||||
```javascript
|
||||
{
|
||||
path: '/xxx',
|
||||
name: 'XxxList',
|
||||
component: () => import('../views/xxx/XxxList.vue'),
|
||||
meta: {
|
||||
title: 'menu.xxx',
|
||||
permission: 'xxx.index'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 12: 添加国际化文本
|
||||
|
||||
**文件路径**:
|
||||
- `html/src/i18n/locales/zh-CN.json`
|
||||
- `html/src/i18n/locales/en-US.json`
|
||||
|
||||
**添加翻译**:
|
||||
```json
|
||||
{
|
||||
"menu": {
|
||||
"xxx": "模块名称"
|
||||
},
|
||||
"xxx": {
|
||||
"add_xxx": "添加",
|
||||
"edit_xxx": "编辑",
|
||||
// 字段翻译
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 开发检查清单
|
||||
|
||||
完成开发后,请检查以下项目:
|
||||
|
||||
### 后端检查
|
||||
- [ ] 迁移文件已创建并包含所有字段
|
||||
- [ ] 模型定义正确,包含必要的标签
|
||||
- [ ] 服务层实现了所有必需的方法
|
||||
- [ ] 请求验证规则完整
|
||||
- [ ] 控制器方法都有 Swagger 注解
|
||||
- [ ] 路由已正确注册
|
||||
- [ ] 错误处理统一使用 `response.Error`
|
||||
- [ ] 时间字段使用 `helpers.ConvertTimeToUTC` 转换
|
||||
- [ ] 排序使用 `helpers.ApplySort` 处理
|
||||
|
||||
### 前端检查
|
||||
- [ ] API 客户端已创建
|
||||
- [ ] 列表页面使用了 `useListPage`
|
||||
- [ ] CRUD 操作使用了 `useCrud`
|
||||
- [ ] 权限控制使用了 `usePermission`
|
||||
- [ ] 表单验证规则完整
|
||||
- [ ] 路由已注册
|
||||
- [ ] 国际化文本已添加
|
||||
- [ ] 错误处理使用了 `ErrorHandler`
|
||||
- [ ] 加载状态正确显示
|
||||
|
||||
## 常见问题处理
|
||||
|
||||
### 1. 时间字段处理
|
||||
```go
|
||||
// 后端:转换时间
|
||||
startTime := ""
|
||||
if startTimeStr != "" {
|
||||
startTime = helpers.ConvertTimeToUTC(ctx, startTimeStr)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 排序处理
|
||||
```go
|
||||
// 后端:应用排序
|
||||
orderBy := filters.OrderBy
|
||||
if orderBy == "" {
|
||||
orderBy = "created_at:desc"
|
||||
}
|
||||
query = helpers.ApplySort(query, orderBy, "created_at:desc")
|
||||
```
|
||||
|
||||
### 3. 软删除
|
||||
```go
|
||||
// 模型必须包含
|
||||
orm.SoftDeletes
|
||||
|
||||
// 删除使用
|
||||
facades.Orm().Query().Delete(&model)
|
||||
```
|
||||
|
||||
### 4. 关联查询
|
||||
```go
|
||||
// 预加载关联
|
||||
query = query.Preload("RelationName")
|
||||
```
|
||||
|
||||
### 5. 权限控制
|
||||
```javascript
|
||||
// 前端权限检查
|
||||
:disabled="getButtonState('module.action').disabled"
|
||||
|
||||
// 路由权限
|
||||
meta: {
|
||||
permission: 'module.index'
|
||||
}
|
||||
```
|
||||
|
||||
## 开发流程
|
||||
|
||||
1. **理解需求** - 仔细阅读用户提供的模块需求
|
||||
2. **设计数据库** - 设计表结构和字段
|
||||
3. **后端开发** - 按照 7 个步骤完成后端开发
|
||||
4. **前端开发** - 按照 5 个步骤完成前端开发
|
||||
5. **测试验证** - 测试所有功能点
|
||||
6. **代码检查** - 使用检查清单验证代码
|
||||
|
||||
## 输出要求
|
||||
|
||||
1. **代码完整性** - 所有必需的文件都要创建
|
||||
2. **代码规范性** - 遵循项目规范和命名约定
|
||||
3. **注释完整** - 关键代码要有注释
|
||||
4. **错误处理** - 完善的错误处理机制
|
||||
5. **用户体验** - 良好的交互和提示
|
||||
|
||||
## 开始开发
|
||||
|
||||
请根据用户提供的模块需求,按照以上规范和步骤,完成完整的 CRUD 模块开发。在开发过程中,请:
|
||||
|
||||
1. 严格按照项目规范编写代码
|
||||
2. 参考 `docs/DEVELOPMENT_GUIDE.md` 中的留言板示例
|
||||
3. 确保代码质量和完整性
|
||||
4. 添加必要的注释和文档
|
||||
5. 处理所有边界情况
|
||||
|
||||
开始开发前,请先确认:
|
||||
- 模块名称和表名
|
||||
- 数据库字段和类型
|
||||
- 业务逻辑需求
|
||||
- 特殊功能需求(如导出、批量操作等)
|
||||
|
||||
准备好后,请开始按照步骤逐一完成开发任务。
|
||||
|
||||
+995
@@ -0,0 +1,995 @@
|
||||
# API 接口文档
|
||||
|
||||
本文档详细描述了 Goravel Admin 后台管理系统的所有 API 接口。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **Base URL**: `http://localhost:3000/api/admin`
|
||||
- **认证方式**: JWT Bearer Token
|
||||
- **请求格式**: JSON
|
||||
- **响应格式**: JSON
|
||||
|
||||
## 通用响应格式
|
||||
|
||||
### 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
### 分页响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"list": [],
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "错误信息",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 状态码说明
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未认证或 Token 过期 |
|
||||
| 403 | 无权限访问 |
|
||||
| 404 | 资源不存在 |
|
||||
| 422 | 验证失败 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 认证接口
|
||||
|
||||
### 登录
|
||||
|
||||
```http
|
||||
POST /login
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| username | string | 是 | 用户名 |
|
||||
| password | string | 是 | 密码 |
|
||||
| captcha_id | string | 否 | 验证码ID |
|
||||
| captcha_code | string | 否 | 验证码 |
|
||||
|
||||
**请求示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"expires_at": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 退出登录
|
||||
|
||||
```http
|
||||
POST /logout
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "退出成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 刷新 Token
|
||||
|
||||
```http
|
||||
POST /refresh-token
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 获取验证码
|
||||
|
||||
```http
|
||||
GET /captcha
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"captcha_id": "abc123",
|
||||
"captcha_image": "data:image/png;base64,..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 管理员信息
|
||||
|
||||
### 获取当前用户信息
|
||||
|
||||
```http
|
||||
GET /info
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"nickname": "超级管理员",
|
||||
"email": "admin@example.com",
|
||||
"avatar": "",
|
||||
"department_id": 1,
|
||||
"department": {
|
||||
"id": 1,
|
||||
"name": "总部"
|
||||
},
|
||||
"roles": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "超级管理员",
|
||||
"slug": "super-admin"
|
||||
}
|
||||
],
|
||||
"permissions": ["admin.index", "admin.store", "..."],
|
||||
"menus": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 修改密码
|
||||
|
||||
```http
|
||||
PUT /password
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| old_password | string | 是 | 原密码 |
|
||||
| password | string | 是 | 新密码 |
|
||||
| password_confirmation | string | 是 | 确认新密码 |
|
||||
|
||||
---
|
||||
|
||||
## 管理员管理
|
||||
|
||||
### 获取管理员列表
|
||||
|
||||
```http
|
||||
GET /admins
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| page | int | 页码,默认 1 |
|
||||
| page_size | int | 每页条数,默认 10 |
|
||||
| username | string | 用户名(模糊搜索) |
|
||||
| nickname | string | 昵称(模糊搜索) |
|
||||
| status | int | 状态:0-禁用,1-启用 |
|
||||
| department_id | int | 部门ID |
|
||||
| order_by | string | 排序,如 `id:desc` |
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"username": "admin",
|
||||
"nickname": "超级管理员",
|
||||
"email": "admin@example.com",
|
||||
"phone": "",
|
||||
"avatar": "",
|
||||
"status": 1,
|
||||
"department_id": 1,
|
||||
"department": {"id": 1, "name": "总部"},
|
||||
"roles": [{"id": 1, "name": "超级管理员"}],
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"last_login_at": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"total": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建管理员
|
||||
|
||||
```http
|
||||
POST /admins
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| username | string | 是 | 用户名(唯一) |
|
||||
| password | string | 是 | 密码 |
|
||||
| nickname | string | 是 | 昵称 |
|
||||
| email | string | 否 | 邮箱 |
|
||||
| phone | string | 否 | 手机号 |
|
||||
| avatar | string | 否 | 头像URL |
|
||||
| department_id | int | 否 | 部门ID |
|
||||
| role_ids | []int | 否 | 角色ID数组 |
|
||||
| status | int | 否 | 状态,默认 1 |
|
||||
|
||||
### 获取管理员详情
|
||||
|
||||
```http
|
||||
GET /admins/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 更新管理员
|
||||
|
||||
```http
|
||||
PUT /admins/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 删除管理员
|
||||
|
||||
```http
|
||||
DELETE /admins/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 重置密码
|
||||
|
||||
```http
|
||||
PUT /admins/{id}/reset-password
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| password | string | 是 | 新密码 |
|
||||
|
||||
---
|
||||
|
||||
## 角色管理
|
||||
|
||||
### 获取角色列表
|
||||
|
||||
```http
|
||||
GET /roles
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| page | int | 页码 |
|
||||
| page_size | int | 每页条数 |
|
||||
| name | string | 角色名称(模糊搜索) |
|
||||
| status | int | 状态 |
|
||||
|
||||
### 创建角色
|
||||
|
||||
```http
|
||||
POST /roles
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| name | string | 是 | 角色名称 |
|
||||
| slug | string | 是 | 角色标识(唯一) |
|
||||
| description | string | 否 | 描述 |
|
||||
| permission_ids | []int | 否 | 权限ID数组 |
|
||||
| menu_ids | []int | 否 | 菜单ID数组 |
|
||||
| status | int | 否 | 状态 |
|
||||
| sort | int | 否 | 排序 |
|
||||
|
||||
### 获取角色详情
|
||||
|
||||
```http
|
||||
GET /roles/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 更新角色
|
||||
|
||||
```http
|
||||
PUT /roles/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 删除角色
|
||||
|
||||
```http
|
||||
DELETE /roles/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 菜单管理
|
||||
|
||||
### 获取菜单列表(树形)
|
||||
|
||||
```http
|
||||
GET /menus
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"parent_id": 0,
|
||||
"title": "系统管理",
|
||||
"slug": "system",
|
||||
"icon": "Setting",
|
||||
"path": "/system",
|
||||
"component": "",
|
||||
"type": 1,
|
||||
"status": 1,
|
||||
"sort": 1,
|
||||
"children": [
|
||||
{
|
||||
"id": 2,
|
||||
"parent_id": 1,
|
||||
"title": "管理员管理",
|
||||
"slug": "admin",
|
||||
"path": "/system/admin",
|
||||
"component": "admin/AdminList",
|
||||
"type": 2,
|
||||
"status": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 创建菜单
|
||||
|
||||
```http
|
||||
POST /menus
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| parent_id | int | 否 | 父级ID,默认 0 |
|
||||
| title | string | 是 | 菜单标题 |
|
||||
| slug | string | 是 | 菜单标识 |
|
||||
| icon | string | 否 | 图标名称 |
|
||||
| path | string | 否 | 路由路径 |
|
||||
| component | string | 否 | 组件路径 |
|
||||
| type | int | 是 | 类型:1-目录,2-菜单,3-按钮 |
|
||||
| status | int | 否 | 状态 |
|
||||
| sort | int | 否 | 排序 |
|
||||
|
||||
### 获取菜单详情
|
||||
|
||||
```http
|
||||
GET /menus/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 更新菜单
|
||||
|
||||
```http
|
||||
PUT /menus/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 删除菜单
|
||||
|
||||
```http
|
||||
DELETE /menus/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 权限管理
|
||||
|
||||
### 获取权限列表
|
||||
|
||||
```http
|
||||
GET /permissions
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 创建权限
|
||||
|
||||
```http
|
||||
POST /permissions
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| name | string | 是 | 权限名称 |
|
||||
| slug | string | 是 | 权限标识(唯一) |
|
||||
| method | string | 是 | HTTP 方法(GET/POST/PUT/DELETE) |
|
||||
| path | string | 是 | API 路径 |
|
||||
| menu_id | int | 否 | 关联菜单ID |
|
||||
| status | int | 否 | 状态 |
|
||||
|
||||
### 更新权限
|
||||
|
||||
```http
|
||||
PUT /permissions/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 删除权限
|
||||
|
||||
```http
|
||||
DELETE /permissions/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 部门管理
|
||||
|
||||
### 获取部门列表(树形)
|
||||
|
||||
```http
|
||||
GET /departments
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 创建部门
|
||||
|
||||
```http
|
||||
POST /departments
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| name | string | 是 | 部门名称 |
|
||||
| parent_id | int | 否 | 父部门ID |
|
||||
| leader | string | 否 | 负责人 |
|
||||
| phone | string | 否 | 联系电话 |
|
||||
| email | string | 否 | 邮箱 |
|
||||
| status | int | 否 | 状态 |
|
||||
| sort | int | 否 | 排序 |
|
||||
|
||||
### 更新部门
|
||||
|
||||
```http
|
||||
PUT /departments/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 删除部门
|
||||
|
||||
```http
|
||||
DELETE /departments/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 字典管理
|
||||
|
||||
### 获取字典列表
|
||||
|
||||
```http
|
||||
GET /dictionaries
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 创建字典
|
||||
|
||||
```http
|
||||
POST /dictionaries
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| parent_id | int | 否 | 父级ID(0为字典类型) |
|
||||
| name | string | 是 | 名称 |
|
||||
| code | string | 是 | 编码 |
|
||||
| value | string | 否 | 值 |
|
||||
| description | string | 否 | 描述 |
|
||||
| status | int | 否 | 状态 |
|
||||
| sort | int | 否 | 排序 |
|
||||
|
||||
### 更新字典
|
||||
|
||||
```http
|
||||
PUT /dictionaries/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 删除字典
|
||||
|
||||
```http
|
||||
DELETE /dictionaries/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 黑名单管理
|
||||
|
||||
### 获取黑名单列表
|
||||
|
||||
```http
|
||||
GET /blacklists
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 创建黑名单
|
||||
|
||||
```http
|
||||
POST /blacklists
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| ip | string | 是 | IP地址(支持单IP、CIDR、范围) |
|
||||
| remark | string | 否 | 备注 |
|
||||
| status | int | 否 | 状态 |
|
||||
|
||||
**IP 格式示例:**
|
||||
|
||||
- 单个 IP: `192.168.1.100`
|
||||
- CIDR 格式: `192.168.1.0/24`
|
||||
- IP 范围: `192.168.1.1-192.168.1.100`
|
||||
|
||||
### 更新黑名单
|
||||
|
||||
```http
|
||||
PUT /blacklists/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 删除黑名单
|
||||
|
||||
```http
|
||||
DELETE /blacklists/{id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 批量删除黑名单
|
||||
|
||||
```http
|
||||
DELETE /blacklists/batch
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
```json
|
||||
{
|
||||
"ids": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 日志管理
|
||||
|
||||
### 操作日志
|
||||
|
||||
```http
|
||||
GET /logs/operation
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| admin_id | int | 管理员ID |
|
||||
| module | string | 模块 |
|
||||
| action | string | 操作 |
|
||||
| start_date | string | 开始日期 |
|
||||
| end_date | string | 结束日期 |
|
||||
|
||||
### 登录日志
|
||||
|
||||
```http
|
||||
GET /logs/login
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| admin_id | int | 管理员ID |
|
||||
| ip | string | IP地址 |
|
||||
| status | int | 状态:0-失败,1-成功 |
|
||||
| start_date | string | 开始日期 |
|
||||
| end_date | string | 结束日期 |
|
||||
|
||||
### 系统日志
|
||||
|
||||
```http
|
||||
GET /logs/system
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| level | string | 日志级别(error/warning/info/debug) |
|
||||
| trace_id | string | 追踪ID |
|
||||
| start_date | string | 开始日期 |
|
||||
| end_date | string | 结束日期 |
|
||||
|
||||
---
|
||||
|
||||
## 仪表盘
|
||||
|
||||
### 获取统计数据
|
||||
|
||||
```http
|
||||
GET /dashboard/stats
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"admin_count": 10,
|
||||
"role_count": 5,
|
||||
"menu_count": 30,
|
||||
"login_count_today": 15,
|
||||
"operation_count_today": 100,
|
||||
"recent_logins": [],
|
||||
"recent_operations": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 在线管理员
|
||||
|
||||
### 获取在线管理员列表
|
||||
|
||||
```http
|
||||
GET /online-admins
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 强制下线
|
||||
|
||||
```http
|
||||
DELETE /online-admins/{token_id}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 批量强制下线
|
||||
|
||||
```http
|
||||
POST /online-admins/batch-kick-out
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
```json
|
||||
{
|
||||
"token_ids": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 服务监控
|
||||
|
||||
### 获取系统信息
|
||||
|
||||
```http
|
||||
GET /monitor/system
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"cpu": {
|
||||
"cores": 8,
|
||||
"usage": 25.5
|
||||
},
|
||||
"memory": {
|
||||
"total": 16384,
|
||||
"used": 8192,
|
||||
"usage": 50.0
|
||||
},
|
||||
"disk": {
|
||||
"total": 512000,
|
||||
"used": 256000,
|
||||
"usage": 50.0
|
||||
},
|
||||
"go": {
|
||||
"version": "go1.21",
|
||||
"goroutines": 50,
|
||||
"gc_pause": "1.2ms"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件上传
|
||||
|
||||
### 上传文件
|
||||
|
||||
```http
|
||||
POST /attachments/upload
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file | file | 是 | 文件 |
|
||||
| type | string | 否 | 类型(image/document/video) |
|
||||
|
||||
### 分片上传
|
||||
|
||||
```http
|
||||
POST /attachments/chunk
|
||||
Authorization: Bearer {token}
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| file | file | 是 | 分片文件 |
|
||||
| chunk_id | string | 是 | 分片标识 |
|
||||
| chunk_index | int | 是 | 分片索引 |
|
||||
| total_chunks | int | 是 | 总分片数 |
|
||||
| filename | string | 是 | 原始文件名 |
|
||||
|
||||
### 合并分片
|
||||
|
||||
```http
|
||||
POST /attachments/merge
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**请求参数:**
|
||||
|
||||
```json
|
||||
{
|
||||
"chunk_id": "abc123",
|
||||
"filename": "large-file.zip",
|
||||
"total_chunks": 10
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 通知中心
|
||||
|
||||
### WebSocket 连接
|
||||
|
||||
```
|
||||
ws://localhost:3000/api/admin/ws/notifications
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**消息格式:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "notification",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"title": "新通知",
|
||||
"content": "通知内容",
|
||||
"created_at": "2024-01-01T12:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取通知列表
|
||||
|
||||
```http
|
||||
GET /notifications
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 标记已读
|
||||
|
||||
```http
|
||||
PUT /notifications/{id}/read
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
### 全部标记已读
|
||||
|
||||
```http
|
||||
PUT /notifications/read-all
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
错误码定义在 `app/errors/codes.go`,格式为 `XXYYYY`:
|
||||
- `XX`: 模块 (10-认证, 20-权限, 30-验证, 40-业务, 50-系统)
|
||||
- `YYYY`: 具体错误
|
||||
|
||||
### 认证模块 (10xxx)
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 10001 | 用户名或密码错误 |
|
||||
| 10002 | 账号已被禁用 |
|
||||
| 10003 | Token 已过期 |
|
||||
| 10004 | Token 已被撤销 |
|
||||
| 10005 | 验证码错误 |
|
||||
| 10006 | 登录尝试次数超限 |
|
||||
| 10007 | Token 无效 |
|
||||
|
||||
### 权限模块 (20xxx)
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 20001 | 无权限访问 |
|
||||
| 20002 | 资源不存在 |
|
||||
| 20003 | 访问被拒绝 |
|
||||
|
||||
### 验证模块 (30xxx)
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 30001 | 参数验证失败 |
|
||||
| 30002 | 数据已存在 |
|
||||
| 30003 | 数据不存在 |
|
||||
| 30004 | 格式无效 |
|
||||
|
||||
### 业务模块 (40xxx)
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 40001 | 操作失败 |
|
||||
| 40002 | 删除失败 |
|
||||
| 40003 | 更新失败 |
|
||||
| 40004 | 创建失败 |
|
||||
| 40005 | 上传失败 |
|
||||
| 40006 | 导出失败 |
|
||||
|
||||
### 系统模块 (50xxx)
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 50001 | 服务器内部错误 |
|
||||
| 50002 | 数据库错误 |
|
||||
| 50003 | 缓存错误 |
|
||||
| 50004 | 队列错误 |
|
||||
| 50005 | 第三方服务错误 |
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### 认证头格式
|
||||
|
||||
```http
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
```
|
||||
|
||||
### 分页参数
|
||||
|
||||
所有列表接口支持以下分页参数:
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| page | int | 1 | 页码 |
|
||||
| page_size | int | 10 | 每页条数(最大 100) |
|
||||
| order_by | string | id:desc | 排序(字段:方向) |
|
||||
|
||||
### 时间格式
|
||||
|
||||
所有时间字段使用 ISO 8601 格式:`2024-01-01T12:00:00Z`
|
||||
|
||||
### 多语言支持
|
||||
|
||||
通过请求头指定语言:
|
||||
|
||||
```http
|
||||
Accept-Language: zh-CN
|
||||
```
|
||||
|
||||
支持的语言:
|
||||
- `zh-CN` - 简体中文
|
||||
- `en-US` - English
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
# 系统架构文档
|
||||
|
||||
本文档描述 Goravel Admin 后台管理系统的整体架构设计。
|
||||
|
||||
## 目录
|
||||
|
||||
- [系统概览](#系统概览)
|
||||
- [技术架构](#技术架构)
|
||||
- [后端架构](#后端架构)
|
||||
- [前端架构](#前端架构)
|
||||
- [数据库设计](#数据库设计)
|
||||
- [安全架构](#安全架构)
|
||||
- [部署架构](#部署架构)
|
||||
|
||||
---
|
||||
|
||||
## 系统概览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 客户端 (Browser) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Vue 3 SPA (Element Plus) │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Nginx / CDN │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Goravel API Server │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ MySQL / PostgreSQL │ Redis │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 层级 | 技术 | 版本 |
|
||||
|------|------|------|
|
||||
| **后端框架** | Goravel | v1.14+ |
|
||||
| **编程语言** | Go | 1.21+ |
|
||||
| **前端框架** | Vue | 3.4+ |
|
||||
| **UI 组件** | Element Plus | 2.4+ |
|
||||
| **表格组件** | VXE-Table | 4.7+ |
|
||||
| **状态管理** | Pinia | 2.1+ |
|
||||
| **数据库** | MySQL/PostgreSQL | 8.0+ / 15+ |
|
||||
| **缓存** | Redis | 7.0+ |
|
||||
| **认证** | JWT | - |
|
||||
|
||||
### 架构模式
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 前端 (Vue 3) │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
||||
│ │ Views │ │Components│ │Composables│ │ Store (Pinia) │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬──────┘ └────────┬─────────┘ │
|
||||
│ └─────────────┴─────────────┴─────────────────┘ │
|
||||
│ │ API │
|
||||
├──────────────────────────────┼──────────────────────────────────┤
|
||||
│ 后端 (Goravel) │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
||||
│ │ Controllers │ │ Middleware │ │ Routes │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ ┌──────▼───────┐ ┌──────▼───────┐ │
|
||||
│ │ Services │ │ Helpers │ │
|
||||
│ └──────┬───────┘ └──────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼───────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
||||
│ │ Models │ │ Events │ │ Jobs │ │
|
||||
│ └──────┬───────┘ └──────────────┘ └──────────────────────┘ │
|
||||
├─────────┼───────────────────────────────────────────────────────┤
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Database │ │ Redis │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端架构
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
app/
|
||||
├── console/ # 命令行
|
||||
│ └── commands/ # 自定义命令
|
||||
├── http/
|
||||
│ ├── controllers/ # 控制器
|
||||
│ │ └── admin/ # 后台管理控制器
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── requests/ # 请求验证
|
||||
│ ├── helpers/ # 辅助函数
|
||||
│ ├── response/ # 统一响应
|
||||
│ └── trans/ # 翻译工具
|
||||
├── models/ # 数据模型
|
||||
├── services/ # 业务逻辑
|
||||
├── utils/ # 工具函数
|
||||
│ ├── logger/ # 日志工具
|
||||
│ ├── traceid/ # 链路追踪
|
||||
│ └── errorlog/ # 错误日志
|
||||
├── events/ # 事件
|
||||
├── listeners/ # 事件监听器
|
||||
├── jobs/ # 队列任务
|
||||
├── providers/ # 服务提供者
|
||||
├── rules/ # 自定义验证规则
|
||||
└── websocket/ # WebSocket
|
||||
└── notifications/ # 通知推送
|
||||
```
|
||||
|
||||
### 分层架构
|
||||
|
||||
#### 1. 控制器层 (Controllers)
|
||||
|
||||
负责接收请求、验证参数、调用服务、返回响应。
|
||||
|
||||
```go
|
||||
// app/http/controllers/admin/admin_controller.go
|
||||
func (a *AdminController) Index(ctx http.Context) http.Response {
|
||||
// 1. 获取查询参数
|
||||
// 2. 调用 Service 获取数据
|
||||
// 3. 返回统一响应
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 服务层 (Services)
|
||||
|
||||
封装业务逻辑,可被多个控制器复用。
|
||||
|
||||
```go
|
||||
// app/services/admin_service.go
|
||||
type AdminService struct{}
|
||||
|
||||
func (s *AdminService) GetAdminWithRoles(id uint) (*models.Admin, error) {
|
||||
// 业务逻辑处理
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 模型层 (Models)
|
||||
|
||||
定义数据结构和数据库映射。
|
||||
|
||||
```go
|
||||
// app/models/admin.go
|
||||
type Admin struct {
|
||||
orm.Model
|
||||
Username string `gorm:"size:50;uniqueIndex" json:"username"`
|
||||
Password string `gorm:"size:255" json:"-"`
|
||||
Roles []*Role `gorm:"many2many:admin_role" json:"roles,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 中间件层 (Middleware)
|
||||
|
||||
处理认证、权限、日志等横切关注点。
|
||||
|
||||
```go
|
||||
// 中间件执行顺序
|
||||
Request → TraceID → JWT Auth → Permission → Controller → OperationLog → Response
|
||||
```
|
||||
|
||||
**核心中间件:**
|
||||
|
||||
| 中间件 | 功能 |
|
||||
|--------|------|
|
||||
| `jwt.go` | JWT 认证 |
|
||||
| `permission.go` | 权限验证 |
|
||||
| `operation_log.go` | 操作日志记录 |
|
||||
| `blacklist.go` | IP 黑名单检查 |
|
||||
| `rate_limiter.go` | 请求限流 |
|
||||
| `trace_id.go` | 链路追踪 |
|
||||
|
||||
### 统一响应
|
||||
|
||||
```go
|
||||
// 成功响应
|
||||
response.Success(ctx, data)
|
||||
|
||||
// 错误响应
|
||||
response.Error(ctx, http.StatusBadRequest, "错误信息")
|
||||
|
||||
// 泛型查找
|
||||
admin, resp := response.FindByID[models.Admin](ctx, id, nil)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端架构
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
html/src/
|
||||
├── api/ # API 请求
|
||||
├── components/ # 通用组件
|
||||
│ ├── SearchForm.vue # 搜索表单
|
||||
│ ├── Pagination.vue # 分页组件
|
||||
│ ├── ErrorBoundary.vue # 错误边界
|
||||
│ └── ColumnSettingDialog.vue # 列设置
|
||||
├── composables/ # 可复用逻辑 (TypeScript)
|
||||
│ ├── useCrud.ts # CRUD 操作
|
||||
│ ├── useDebounce.ts # 防抖
|
||||
│ ├── useTableSort.ts # 表格排序
|
||||
│ ├── usePermission.ts # 权限检查
|
||||
│ ├── useListPage.js # 列表页面
|
||||
│ └── useColumnSetting.js # 列设置
|
||||
├── i18n/ # 国际化
|
||||
│ └── locales/
|
||||
│ ├── zh-CN.json
|
||||
│ └── en-US.json
|
||||
├── layouts/ # 布局组件
|
||||
├── router/ # 路由配置
|
||||
├── store/ # 状态管理 (Pinia)
|
||||
│ ├── user.js # 用户状态
|
||||
│ ├── app.js # 应用状态
|
||||
│ └── tabs.js # 标签页状态
|
||||
├── types/ # TypeScript 类型
|
||||
│ ├── index.d.ts # 实体类型
|
||||
│ └── composables.d.ts # Composable 类型
|
||||
├── utils/ # 工具函数
|
||||
│ ├── request.js # Axios 封装
|
||||
│ ├── storage.js # 存储工具
|
||||
│ └── validation.js # 验证器
|
||||
└── views/ # 页面组件
|
||||
├── admin/
|
||||
├── role/
|
||||
├── menu/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Composables 设计
|
||||
|
||||
核心 Composables 采用 TypeScript 编写,提供完整类型支持:
|
||||
|
||||
```typescript
|
||||
// useCrud.ts - CRUD 操作封装
|
||||
const {
|
||||
dialogVisible, // 对话框状态
|
||||
editId, // 编辑ID
|
||||
handleAdd, // 添加
|
||||
handleEdit, // 编辑
|
||||
handleDelete, // 删除
|
||||
handleBatchDelete // 批量删除
|
||||
} = useCrud({ deleteApi, batchDeleteApi })
|
||||
```
|
||||
|
||||
```typescript
|
||||
// usePermission.ts - 权限检查
|
||||
const {
|
||||
hasPermission, // 检查权限
|
||||
shouldShowButton, // 是否显示按钮
|
||||
getButtonState // 获取按钮状态
|
||||
} = usePermission()
|
||||
```
|
||||
|
||||
### 状态管理
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Pinia Store │
|
||||
├─────────────┬─────────────┬─────────────┤
|
||||
│ userStore │ appStore │ tabsStore │
|
||||
├─────────────┼─────────────┼─────────────┤
|
||||
│ - userInfo │ - sidebar │ - tabs │
|
||||
│ - token │ - fullscreen│ - activeTab │
|
||||
│ - menus │ - language │ │
|
||||
│ - permissions│ │ │
|
||||
└─────────────┴─────────────┴─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### ER 图
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ admins │─────│ admin_role │─────│ roles │
|
||||
├─────────────┤ ├─────────────┤ ├─────────────┤
|
||||
│ id │ │ admin_id │ │ id │
|
||||
│ username │ │ role_id │ │ name │
|
||||
│ password │ └─────────────┘ │ slug │
|
||||
│ department_id│ │ status │
|
||||
└──────┬──────┘ └──────┬──────┘
|
||||
│ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ departments │ │
|
||||
└────┤ │ ┌─────────────┴─────────────┐
|
||||
│ id │ │ role_permission │
|
||||
│ name │ ├───────────────────────────┤
|
||||
│ parent_id │ │ role_id │
|
||||
└─────────────┘ │ permission_id │
|
||||
└─────────────┬─────────────┘
|
||||
│
|
||||
┌─────────────▼─────────────┐
|
||||
│ permissions │
|
||||
├───────────────────────────┤
|
||||
│ id │
|
||||
│ name │
|
||||
│ slug │
|
||||
│ method │
|
||||
│ path │
|
||||
│ menu_id │
|
||||
└───────────────────────────┘
|
||||
```
|
||||
|
||||
### 核心表结构
|
||||
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `admins` | 管理员 |
|
||||
| `roles` | 角色 |
|
||||
| `permissions` | 权限 |
|
||||
| `menus` | 菜单 |
|
||||
| `departments` | 部门 |
|
||||
| `dictionaries` | 字典 |
|
||||
| `blacklists` | 黑名单 |
|
||||
| `operation_logs` | 操作日志 |
|
||||
| `login_logs` | 登录日志 |
|
||||
| `system_logs` | 系统日志 |
|
||||
| `personal_access_tokens` | Token 管理 |
|
||||
| `notifications` | 通知 |
|
||||
| `attachments` | 附件 |
|
||||
| `exports` | 导出任务 |
|
||||
|
||||
---
|
||||
|
||||
## 安全架构
|
||||
|
||||
### 认证流程
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Login │───▶│ Validate │───▶│ Generate │───▶│ Return │
|
||||
│ Request │ │ Credentials │ │ JWT │ │ Token │
|
||||
└─────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Rate Limit │
|
||||
│ Check │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 权限验证流程
|
||||
|
||||
```
|
||||
Request → JWT Middleware → Permission Middleware → Controller
|
||||
│ │
|
||||
▼ ▼
|
||||
验证 Token 检查权限
|
||||
│ │
|
||||
▼ ▼
|
||||
解析用户信息 匹配路由权限
|
||||
│ │
|
||||
▼ ▼
|
||||
写入 Context 允许/拒绝
|
||||
```
|
||||
|
||||
### 安全特性
|
||||
|
||||
| 特性 | 实现方式 |
|
||||
|------|----------|
|
||||
| **认证** | JWT Token + 滑动过期 |
|
||||
| **授权** | RBAC 权限模型 |
|
||||
| **限流** | Token Bucket 算法 |
|
||||
| **黑名单** | IP/Token 双重黑名单 |
|
||||
| **日志** | 操作/登录日志全记录 |
|
||||
| **追踪** | TraceID 链路追踪 |
|
||||
| **敏感字段** | 密码等字段 `json:"-"` |
|
||||
|
||||
---
|
||||
|
||||
## 部署架构
|
||||
|
||||
### 单机部署
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ Server │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ Nginx │──│ Goravel │──│ MySQL │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ │
|
||||
│ │ │
|
||||
│ ┌────▼────┐ │
|
||||
│ │ Redis │ │
|
||||
│ └─────────┘ │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 高可用部署
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Client │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ CDN │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ Load Balancer│
|
||||
└──────┬──────┘
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ Goravel │ │ Goravel │ │ Goravel │
|
||||
│ Server 1 │ │ Server 2 │ │ Server 3 │
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
└───────────────┼───────────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ MySQL Master│──────────│ Redis Cluster│
|
||||
└──────┬──────┘ └──────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ MySQL Slave │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 后端优化
|
||||
|
||||
| 优化项 | 实现 |
|
||||
|--------|------|
|
||||
| 数据库连接池 | 配置合理的连接数 |
|
||||
| 查询优化 | 预加载关联、索引优化 |
|
||||
| 缓存策略 | Redis 缓存热点数据 |
|
||||
| 日志分级 | Debug/Info 分离 |
|
||||
|
||||
### 前端优化
|
||||
|
||||
| 优化项 | 实现 |
|
||||
|--------|------|
|
||||
| 虚拟滚动 | VXE-Table 大数据渲染 |
|
||||
| 代码分割 | 路由懒加载 |
|
||||
| 状态管理 | Pinia 响应式优化 |
|
||||
| 防抖节流 | useDebounce composable |
|
||||
|
||||
---
|
||||
|
||||
## 扩展指南
|
||||
|
||||
### 添加新模块
|
||||
|
||||
1. **后端**
|
||||
- 创建 Model: `app/models/xxx.go`
|
||||
- 创建 Controller: `app/http/controllers/admin/xxx_controller.go`
|
||||
- 创建 Request: `app/http/requests/admin/xxx_request.go`
|
||||
- 注册路由: `routes/admin.go`
|
||||
|
||||
2. **前端**
|
||||
- 创建 API: `html/src/api/xxx.js`
|
||||
- 创建页面: `html/src/views/xxx/XxxList.vue`
|
||||
- 创建表单: `html/src/views/xxx/XxxForm.vue`
|
||||
- 注册路由: `html/src/router/index.js`
|
||||
|
||||
### 添加新权限
|
||||
|
||||
1. 数据库添加权限记录 (`permissions` 表)
|
||||
2. 关联到菜单 (`menu_id`)
|
||||
3. 分配给角色 (`role_permission` 表)
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Goravel 官方文档](https://www.goravel.dev)
|
||||
- [Vue 3 文档](https://vuejs.org)
|
||||
- [Element Plus 文档](https://element-plus.org)
|
||||
- [VXE-Table 文档](https://vxetable.cn)
|
||||
|
||||
+599
@@ -0,0 +1,599 @@
|
||||
# 部署文档
|
||||
|
||||
## 编译
|
||||
|
||||
### 本地编译(当前平台)
|
||||
|
||||
```bash
|
||||
# 生产环境推荐使用静态编译
|
||||
go build --ldflags "-extldflags -static -s -w" -o main .
|
||||
```
|
||||
|
||||
### Linux 服务器交叉编译(Windows/Mac 编译 Linux 版本)
|
||||
|
||||
```bash
|
||||
# Windows PowerShell:
|
||||
$env:CGO_ENABLED="0"; $env:GOOS="linux"; $env:GOARCH="amd64"; go build --ldflags "-extldflags -static -s -w" -o main .
|
||||
|
||||
# Windows CMD (需要分开执行):
|
||||
set GOOS=linux
|
||||
set GOARCH=amd64
|
||||
go build --ldflags "-extldflags -static -s -w" -o main .
|
||||
SET GOOS=windows
|
||||
SET GOARCH=amd64
|
||||
|
||||
# Linux/Mac:
|
||||
GOOS=linux GOARCH=amd64 go build --ldflags "-extldflags -static -s -w" -o main .
|
||||
```
|
||||
|
||||
**重要提示:**
|
||||
- 如果在 Windows 上编译,但要在 Linux 服务器上运行,必须使用交叉编译
|
||||
- 使用 `GOOS=linux GOARCH=amd64` 指定目标平台
|
||||
- 如果服务器是 ARM 架构,使用 `GOARCH=arm64`
|
||||
- **推荐使用静态编译**:添加 `--ldflags "-extldflags -static -s -w"` 参数,生成独立可执行文件
|
||||
|
||||
---
|
||||
|
||||
## 方案一:单服务部署(端口 3000)
|
||||
|
||||
### 步骤 1:准备部署文件
|
||||
|
||||
在服务器上创建应用目录并上传必要文件:
|
||||
|
||||
```bash
|
||||
# 在服务器上创建应用目录
|
||||
sudo mkdir -p /www/goravel-admin
|
||||
sudo chown -R www-data:www-data /www/goravel-admin
|
||||
|
||||
# 上传二进制文件
|
||||
scp main user@server:/www/goravel-admin/
|
||||
|
||||
# 上传配置文件和其他必要文件
|
||||
scp .env user@server:/www/goravel-admin/.env
|
||||
scp -r storage/ user@server:/www/goravel-admin/
|
||||
scp -r resources/ user@server:/www/goravel-admin/
|
||||
scp -r public/ user@server:/www/goravel-admin/
|
||||
```
|
||||
|
||||
### 步骤 2:配置 systemd 服务
|
||||
|
||||
```bash
|
||||
# 上传服务文件
|
||||
scp scripts/systemd/goravel-admin-3000.service user@server:/tmp/
|
||||
|
||||
# 在服务器上安装服务文件
|
||||
sudo cp /tmp/goravel-admin-3000.service /etc/systemd/system/goravel-admin.service
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
编辑服务文件,根据实际情况修改路径和用户:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/goravel-admin.service
|
||||
```
|
||||
|
||||
主要配置项:
|
||||
- `User` 和 `Group`:运行服务的用户和组(如 `www-data`)
|
||||
- `WorkingDirectory`:应用工作目录(如 `/www/goravel-admin`)
|
||||
- `ExecStart`:二进制文件路径(如 `/www/goravel-admin/main`)
|
||||
- `ReadWritePaths`:需要写入权限的目录(如 `/www/goravel-admin/storage`)
|
||||
|
||||
|
||||
### 步骤 3:设置文件权限
|
||||
|
||||
```bash
|
||||
# 设置二进制文件权限
|
||||
sudo chmod +x /www/goravel-admin/main
|
||||
|
||||
# 设置存储目录权限
|
||||
sudo chmod -R 775 /www/goravel-admin/storage
|
||||
sudo chown -R www-data:www-data /www/goravel-admin/storage
|
||||
|
||||
# 设置配置文件权限(保护敏感信息)
|
||||
sudo chmod 600 /www/goravel-admin/.env
|
||||
sudo chown www-data:www-data /www/goravel-admin/.env
|
||||
```
|
||||
|
||||
### 步骤 4:初始化应用
|
||||
|
||||
```bash
|
||||
# 生成应用密钥
|
||||
cd /www/goravel-admin
|
||||
./main artisan key:generate
|
||||
|
||||
# 数据库迁移
|
||||
./main artisan migrate
|
||||
|
||||
# 数据库填充(可选)
|
||||
./main artisan db:seed
|
||||
```
|
||||
|
||||
### 步骤 5:启动和管理服务
|
||||
|
||||
```bash
|
||||
# 启动服务
|
||||
sudo systemctl start goravel-admin
|
||||
|
||||
# 设置开机自启
|
||||
sudo systemctl enable goravel-admin
|
||||
|
||||
# 查看服务状态
|
||||
sudo systemctl status goravel-admin
|
||||
|
||||
# 停止服务
|
||||
sudo systemctl stop goravel-admin
|
||||
|
||||
# 重启服务(⚠️ 会有短暂中断 1-3 秒)
|
||||
sudo systemctl restart goravel-admin
|
||||
```
|
||||
|
||||
### 步骤 6:查看日志
|
||||
|
||||
```bash
|
||||
# 实时查看日志
|
||||
sudo journalctl -u goravel-admin -f
|
||||
|
||||
# 查看最近 100 行日志
|
||||
sudo journalctl -u goravel-admin -n 100
|
||||
|
||||
# 查看今天的日志
|
||||
sudo journalctl -u goravel-admin --since today
|
||||
```
|
||||
|
||||
### 步骤 7:验证部署
|
||||
|
||||
```bash
|
||||
# 检查服务是否运行
|
||||
sudo systemctl is-active goravel-admin
|
||||
|
||||
# 检查端口是否监听
|
||||
sudo netstat -tlnp | grep 3000
|
||||
# 或使用 ss 命令
|
||||
sudo ss -tlnp | grep 3000
|
||||
|
||||
```
|
||||
|
||||
### 配置外部访问
|
||||
|
||||
如果无法从外部 IP 访问,需要修改 `.env` 文件:
|
||||
|
||||
```env
|
||||
# 修改为 0.0.0.0 允许所有网络接口访问
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=3000
|
||||
```
|
||||
|
||||
然后重启服务:
|
||||
```bash
|
||||
sudo systemctl restart goravel-admin
|
||||
```
|
||||
|
||||
**检查防火墙设置:**
|
||||
|
||||
```bash
|
||||
# CentOS/RHEL 系统
|
||||
sudo firewall-cmd --list-ports
|
||||
sudo firewall-cmd --permanent --add-port=3000/tcp
|
||||
sudo firewall-cmd --reload
|
||||
|
||||
# Ubuntu/Debian 系统
|
||||
sudo ufw status
|
||||
sudo ufw allow 3000/tcp
|
||||
sudo ufw reload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方案二:双目录双端口零停机部署(蓝绿部署 + Nginx 切换)⭐ 推荐
|
||||
|
||||
这是生产环境最可靠的零停机部署方案,通过双目录双端口 + Nginx 切换实现真正的零停机。
|
||||
|
||||
### 工作原理
|
||||
|
||||
- 使用两个版本目录:`v1`(端口 3000)和 `v2`(端口 3001)
|
||||
- 两个实例可以同时运行
|
||||
- Nginx 负载均衡在两个端口之间切换
|
||||
- 部署新版本时:
|
||||
1. 部署新版本到备用目录(如 `v2`,端口 3001)
|
||||
2. 启动新版本服务
|
||||
3. 健康检查通过后,Nginx 切换流量到新端口
|
||||
4. 停止旧版本服务
|
||||
5. 实现真正的零停机,且可以快速回滚
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
/www/goravel-admin/
|
||||
├── v1/ # 版本 1 目录(端口 3000)
|
||||
│ ├── main # 可执行文件
|
||||
│ ├── .env # 配置文件(APP_PORT=3000)
|
||||
│ ├── storage/ # 存储目录
|
||||
│ └── ...
|
||||
└── v2/ # 版本 2 目录(端口 3001)
|
||||
├── main # 可执行文件
|
||||
├── .env # 配置文件(APP_PORT=3001)
|
||||
├── storage/ # 存储目录
|
||||
└── ...
|
||||
```
|
||||
|
||||
|
||||
### 步骤 1:准备部署文件
|
||||
|
||||
在服务器上创建应用目录结构:
|
||||
|
||||
```bash
|
||||
# 在服务器上创建应用目录
|
||||
sudo mkdir -p /www/goravel-admin/{v1,v2}
|
||||
sudo chown -R www-data:www-data /www/goravel-admin
|
||||
|
||||
# 上传第一个版本到 v1 目录
|
||||
scp main user@server:/www/goravel-admin/v1/
|
||||
scp .env user@server:/www/goravel-admin/v1/.env
|
||||
scp -r storage/ user@server:/www/goravel-admin/v1/
|
||||
scp -r resources/ user@server:/www/goravel-admin/v1/
|
||||
scp -r public/ user@server:/www/goravel-admin/v1/
|
||||
```
|
||||
|
||||
**重要提示:.env 文件配置**
|
||||
- 每个版本目录(v1、v2)都有独立的 `.env` 文件
|
||||
- **v1 目录的 `.env` 文件中配置 `APP_PORT=3000`**
|
||||
- **v2 目录的 `.env` 文件中配置 `APP_PORT=3001`**
|
||||
- 其他配置(如数据库连接)可以不同版本不同配置
|
||||
|
||||
### 步骤 2:配置双实例 systemd 服务
|
||||
|
||||
项目已提供两个服务文件:
|
||||
- `scripts/systemd/goravel-admin-v1.service` - v1 目录,端口 3000
|
||||
- `scripts/systemd/goravel-admin-v2.service` - v2 目录,端口 3001
|
||||
|
||||
**服务文件特点:**
|
||||
- `WorkingDirectory=/www/goravel-admin/v1` 或 `/www/goravel-admin/v2`
|
||||
- `ExecStart=/www/goravel-admin/v1/main` 或 `/www/goravel-admin/v2/main`
|
||||
- 端口由各自目录的 `.env` 文件中的 `APP_PORT` 配置管理
|
||||
|
||||
在服务器上安装:
|
||||
|
||||
```bash
|
||||
# 上传服务文件
|
||||
scp scripts/systemd/goravel-admin-v1.service user@server:/tmp/
|
||||
scp scripts/systemd/goravel-admin-v2.service user@server:/tmp/
|
||||
|
||||
# 在服务器上安装
|
||||
sudo cp /tmp/goravel-admin-v1.service /etc/systemd/system/
|
||||
sudo cp /tmp/goravel-admin-v2.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
编辑服务文件,根据实际情况修改路径和用户:
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/goravel-admin-v1.service
|
||||
sudo nano /etc/systemd/system/goravel-admin-v2.service
|
||||
```
|
||||
|
||||
主要配置项:
|
||||
- `User` 和 `Group`:运行服务的用户和组(如 `www-data`)
|
||||
- `WorkingDirectory`:应用工作目录(如 `/www/goravel-admin/v1` 或 `/www/goravel-admin/v2`)
|
||||
- `ExecStart`:二进制文件路径(如 `/www/goravel-admin/v1/main` 或 `/www/goravel-admin/v2/main`)
|
||||
- `ReadWritePaths`:需要写入权限的目录(如 `/www/goravel-admin/v1/storage` 或 `/www/goravel-admin/v2/storage`)
|
||||
|
||||
### 步骤 3:设置文件权限
|
||||
|
||||
```bash
|
||||
# 设置 v1 目录权限
|
||||
sudo chmod +x /www/goravel-admin/v1/main
|
||||
sudo chmod -R 775 /www/goravel-admin/v1/storage
|
||||
sudo chown -R www-data:www-data /www/goravel-admin/v1/storage
|
||||
sudo chmod 600 /www/goravel-admin/v1/.env
|
||||
sudo chown www-data:www-data /www/goravel-admin/v1/.env
|
||||
|
||||
# 设置 v2 目录权限(如果已上传)
|
||||
sudo chmod +x /www/goravel-admin/v2/main 2>/dev/null || true
|
||||
sudo chmod -R 775 /www/goravel-admin/v2/storage 2>/dev/null || true
|
||||
sudo chown -R www-data:www-data /www/goravel-admin/v2/storage 2>/dev/null || true
|
||||
sudo chmod 600 /www/goravel-admin/v2/.env 2>/dev/null || true
|
||||
sudo chown www-data:www-data /www/goravel-admin/v2/.env 2>/dev/null || true
|
||||
```
|
||||
|
||||
### 步骤 4:初始化应用
|
||||
|
||||
```bash
|
||||
# 生成应用密钥(在 v1 目录)
|
||||
cd /www/goravel-admin/v1
|
||||
./main artisan key:generate
|
||||
|
||||
# 数据库迁移
|
||||
./main artisan migrate
|
||||
|
||||
# 数据库填充(可选)
|
||||
./main artisan db:seed
|
||||
```
|
||||
|
||||
```bash
|
||||
# 试运行
|
||||
./main
|
||||
|
||||
```
|
||||
### 步骤 5:配置 Nginx 负载均衡
|
||||
|
||||
项目已提供 Nginx 配置文件:`scripts/nginx/goravel-admin.conf`
|
||||
|
||||
在服务器上安装:
|
||||
|
||||
```bash
|
||||
# 上传 Nginx 配置
|
||||
scp scripts/nginx/goravel-admin.conf user@server:/tmp/
|
||||
|
||||
# 在服务器上安装
|
||||
sudo cp /tmp/goravel-admin.conf /etc/nginx/sites-available/goravel-admin
|
||||
sudo ln -s /etc/nginx/sites-available/goravel-admin /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo nginx -s reload
|
||||
```
|
||||
|
||||
**Nginx 配置说明:**
|
||||
nginx.conf 参考
|
||||
编辑 `/etc/nginx/sites-available/goravel-admin`,修改 `server_name` 为你的域名:
|
||||
|
||||
|
||||
|
||||
### 步骤 6:启动第一个实例
|
||||
|
||||
```bash
|
||||
# 启动 v1 实例(端口 3000)
|
||||
sudo systemctl start goravel-admin-v1
|
||||
# 开机自启
|
||||
sudo systemctl enable goravel-admin-v1
|
||||
|
||||
# 检查状态
|
||||
sudo systemctl status goravel-admin-v1
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 查看日志
|
||||
|
||||
```bash
|
||||
# 查看 v1 服务日志
|
||||
sudo journalctl -u goravel-admin-v1 -f
|
||||
|
||||
# 查看 v2 服务日志
|
||||
sudo journalctl -u goravel-admin-v2 -f
|
||||
|
||||
# 查看最近 100 行日志
|
||||
sudo journalctl -u goravel-admin-v1 -n 100
|
||||
sudo journalctl -u goravel-admin-v2 -n 100
|
||||
```
|
||||
|
||||
### 验证部署
|
||||
|
||||
```bash
|
||||
# 检查服务状态
|
||||
sudo systemctl status goravel-admin-v1
|
||||
sudo systemctl status goravel-admin-v2
|
||||
|
||||
# 检查端口监听
|
||||
sudo ss -tlnp | grep 3000
|
||||
sudo ss -tlnp | grep 3001
|
||||
|
||||
```
|
||||
|
||||
|
||||
### (蓝绿部署 + Nginx 切换)
|
||||
|
||||
```bash
|
||||
# 启动服务
|
||||
sudo systemctl start goravel-admin-v1
|
||||
sudo systemctl start goravel-admin-v2
|
||||
|
||||
# 停止服务
|
||||
sudo systemctl stop goravel-admin-v1
|
||||
sudo systemctl stop goravel-admin-v2
|
||||
|
||||
# 重启服务
|
||||
sudo systemctl restart goravel-admin-v1
|
||||
sudo systemctl restart goravel-admin-v2
|
||||
|
||||
|
||||
# 查看状态
|
||||
sudo systemctl status goravel-admin-v1
|
||||
sudo systemctl status goravel-admin-v2
|
||||
|
||||
# 查看日志
|
||||
sudo journalctl -u goravel-admin-v1 -f
|
||||
sudo journalctl -u goravel-admin-v2 -f
|
||||
|
||||
# 设置开机自启
|
||||
sudo systemctl enable goravel-admin-v1
|
||||
sudo systemctl enable goravel-admin-v2
|
||||
|
||||
# 检查端口监听
|
||||
sudo ss -tlnp | grep 3000
|
||||
sudo ss -tlnp | grep 3001
|
||||
```
|
||||
|
||||
### 清理所有服务
|
||||
|
||||
```bash
|
||||
|
||||
# 停止所有服务
|
||||
sudo systemctl stop goravel-admin-v1 goravel-admin-v2 2>/dev/null
|
||||
|
||||
# 禁用所有服务
|
||||
sudo systemctl disable goravel-admin-v1 goravel-admin-v2 2>/dev/null
|
||||
|
||||
# 删除服务文件
|
||||
sudo rm -f /etc/systemd/system/goravel-admin*.service
|
||||
|
||||
# 重新加载 systemd
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# 重置失败状态
|
||||
sudo systemctl reset-failed goravel-admin* 2>/dev/null
|
||||
|
||||
# 如果还有进程在运行,强制停止
|
||||
sudo pkill -f 'goravel-admin' || true
|
||||
sudo pkill -f '/www/goravel-admin' || true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 方案三:Docker Compose 蓝绿部署(零停机)⭐ 推荐容器化方案
|
||||
|
||||
这是使用 Docker 容器化的零停机部署方案,适合本地没有 Docker 环境,但服务器有 Docker 的场景。
|
||||
|
||||
### 工作原理
|
||||
|
||||
- 使用两个 Docker Compose 配置:`blue`(端口 3000)和 `green`(端口 3001)
|
||||
- 两个容器可以同时运行
|
||||
- 部署新版本时:
|
||||
1. 在备用环境(如 `green`)构建并启动新版本
|
||||
2. 健康检查通过后,切换流量到新版本
|
||||
3. 停止旧版本容器
|
||||
4. 实现真正的零停机,且可以快速回滚
|
||||
|
||||
### 前置要求
|
||||
|
||||
- 服务器已安装 Docker 和 Docker Compose
|
||||
- 服务器可以访问 Git 仓库(或手动上传代码)
|
||||
- 已配置 `.env` 文件
|
||||
|
||||
### 步骤 1:在服务器上初始化
|
||||
|
||||
```bash
|
||||
# SSH 登录服务器
|
||||
ssh user@your-server.com
|
||||
|
||||
# 创建部署目录
|
||||
sudo mkdir -p /www/goravel-admin
|
||||
sudo chown -R $USER:$USER /www/goravel-admin
|
||||
cd /www/goravel-admin
|
||||
|
||||
# 克隆仓库(首次)
|
||||
git clone https://github.com/your-username/goravel-admin.git .
|
||||
|
||||
# 或者如果已经克隆过
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
### 步骤 2:配置环境变量
|
||||
|
||||
```bash
|
||||
# 确保 .env 文件存在
|
||||
cd /www/goravel-admin
|
||||
cp .env.example .env # 如果存在
|
||||
# 编辑 .env 文件,设置数据库等配置
|
||||
vim .env
|
||||
```
|
||||
|
||||
### 步骤 3:执行部署
|
||||
|
||||
#### 方式一:从 Git 拉取并部署(推荐)
|
||||
|
||||
```bash
|
||||
# 设置环境变量(可选)
|
||||
export GIT_REPO_URL="https://github.com/your-username/goravel-admin.git"
|
||||
export GIT_BRANCH="main"
|
||||
export DEPLOY_DIR="/www/goravel-admin"
|
||||
|
||||
# 执行部署脚本
|
||||
chmod +x scripts/deploy/git-deploy.sh
|
||||
./scripts/deploy/git-deploy.sh
|
||||
```
|
||||
|
||||
#### 方式二:手动部署(已拉取代码)
|
||||
|
||||
```bash
|
||||
cd /www/goravel-admin
|
||||
git pull origin main # 拉取最新代码
|
||||
|
||||
# 执行部署脚本
|
||||
chmod +x scripts/deploy/docker-blue-green.sh
|
||||
./scripts/deploy/docker-blue-green.sh
|
||||
```
|
||||
|
||||
### 部署流程说明
|
||||
|
||||
脚本会自动执行以下步骤:
|
||||
|
||||
1. **检测当前版本** - 自动检测运行的是 `blue` 还是 `green`
|
||||
2. **构建新版本** - 在备用环境构建新 Docker 镜像
|
||||
3. **启动新版本** - 启动新版本容器(使用不同端口)
|
||||
4. **健康检查** - 等待新版本通过健康检查(最多 30 次,每次 2 秒)
|
||||
5. **切换 Nginx 流量** - 如果存在 Nginx 配置,自动更新并重载
|
||||
6. **停止旧版本** - 停止旧版本容器
|
||||
|
||||
### 查看部署状态
|
||||
|
||||
```bash
|
||||
# 查看运行中的容器
|
||||
docker ps | grep goravel-admin
|
||||
|
||||
# 查看容器日志
|
||||
docker logs -f goravel-admin-blue
|
||||
docker logs -f goravel-admin-green
|
||||
|
||||
# 查看容器健康状态
|
||||
docker inspect --format='{{.State.Health.Status}}' goravel-admin-blue
|
||||
docker inspect --format='{{.State.Health.Status}}' goravel-admin-green
|
||||
```
|
||||
|
||||
### 回滚方案
|
||||
|
||||
如果需要回滚到上一个版本:
|
||||
|
||||
```bash
|
||||
cd /www/goravel-admin
|
||||
|
||||
# 方式一:手动切换
|
||||
# 如果当前运行的是 green,切换到 blue
|
||||
docker-compose -f docker-compose.blue.yml up -d
|
||||
# 然后停止 green
|
||||
docker-compose -f docker-compose.green.yml down
|
||||
|
||||
# 方式二:使用 Git 回滚代码后重新部署
|
||||
git checkout <previous-commit>
|
||||
./scripts/deploy/docker-blue-green.sh
|
||||
```
|
||||
|
||||
### 配置文件说明
|
||||
|
||||
- `docker-compose.blue.yml` - 蓝环境配置(端口 3000)
|
||||
- `docker-compose.green.yml` - 绿环境配置(端口 3001)
|
||||
- `scripts/deploy/docker-blue-green.sh` - 蓝绿部署主脚本
|
||||
- `scripts/deploy/git-deploy.sh` - 从 Git 拉取并部署的脚本
|
||||
|
||||
### 健康检查
|
||||
|
||||
应用已配置 `/health` 端点(在 `routes/web.go` 中),用于部署时的健康检查。
|
||||
|
||||
### 故障处理
|
||||
|
||||
如果部署过程中健康检查失败,脚本会自动:
|
||||
- 停止新版本容器
|
||||
- 保持旧版本继续运行
|
||||
- 退出并报告错误
|
||||
|
||||
### 本地开发流程
|
||||
|
||||
1. **本地开发** - 在本地修改代码(无需 Docker)
|
||||
2. **提交代码** - `git add . && git commit -m "更新" && git push`
|
||||
3. **服务器部署** - SSH 到服务器执行 `./scripts/deploy/git-deploy.sh`
|
||||
|
||||
### 优势
|
||||
|
||||
- ✅ **零停机部署** - 新版本就绪后再切换流量
|
||||
- ✅ **快速回滚** - 可以快速切换回旧版本
|
||||
- ✅ **本地无需 Docker** - 本地开发环境简单
|
||||
- ✅ **自动化** - 一键部署脚本
|
||||
- ✅ **健康检查** - 自动验证新版本是否正常
|
||||
|
||||
---
|
||||
|
||||
## 部署方案对比
|
||||
|
||||
| 方案 | 复杂度 | 零停机 | 适用场景 | 推荐度 |
|
||||
|------|--------|--------|----------|--------|
|
||||
| 单服务部署 | ⭐ | ❌ | 开发/测试环境 | ⭐⭐ |
|
||||
| 蓝绿部署 + Nginx | ⭐⭐⭐ | ✅ | 生产环境(非容器) | ⭐⭐⭐⭐ |
|
||||
| Docker Compose 蓝绿 | ⭐⭐ | ✅ | 生产环境(容器化) | ⭐⭐⭐⭐⭐ |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
||||
# 数据库迁移策略
|
||||
|
||||
## 问题背景
|
||||
|
||||
在容器化部署中,如果应用在数据库迁移完成之前启动,新版本的代码可能会尝试访问不存在的数据库字段或表,导致运行时错误。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 启动脚本自动迁移
|
||||
|
||||
我们使用 Docker 启动脚本 (`docker-entrypoint.sh`) 在应用启动前自动执行数据库迁移:
|
||||
|
||||
```
|
||||
容器启动 → 执行迁移 → 迁移成功 → 启动应用
|
||||
↓
|
||||
迁移失败 → 容器退出 → 部署脚本检测到并回滚
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
1. **容器启动时**:`docker-entrypoint.sh` 脚本首先执行
|
||||
2. **执行迁移**:运行 `artisan migrate` 命令
|
||||
3. **迁移成功**:继续启动应用
|
||||
4. **迁移失败**:容器退出(exit 1),部署脚本检测到并回滚
|
||||
|
||||
### 优势
|
||||
|
||||
- ✅ **避免字段不存在错误** - 迁移在应用启动前完成
|
||||
- ✅ **自动执行** - 无需手动操作
|
||||
- ✅ **失败自动回滚** - 迁移失败时容器退出,部署脚本自动回滚
|
||||
- ✅ **幂等操作** - Goravel 的 migrate 是安全的,可以重复执行
|
||||
|
||||
### 配置选项
|
||||
|
||||
#### 跳过迁移(不推荐)
|
||||
|
||||
如果需要在某些情况下跳过迁移:
|
||||
|
||||
```bash
|
||||
# 在 docker-compose.yml 中设置环境变量
|
||||
environment:
|
||||
- SKIP_MIGRATE=true
|
||||
```
|
||||
|
||||
**注意:** 跳过迁移可能导致应用启动后访问不存在的字段而报错。
|
||||
|
||||
### 部署脚本中的处理
|
||||
|
||||
部署脚本 (`docker-blue-green.sh`) 会:
|
||||
|
||||
1. 启动新版本容器
|
||||
2. 等待容器启动(包括迁移完成)
|
||||
3. 验证应用是否已启动(如果迁移失败,应用不会启动)
|
||||
4. 如果容器退出,自动检测并回滚
|
||||
|
||||
### 健康检查
|
||||
|
||||
健康检查的 `start_period` 设置为 60 秒,给迁移足够的时间完成:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
start_period: 60s # 等待迁移和应用启动
|
||||
```
|
||||
|
||||
### 单独执行迁移
|
||||
|
||||
如果需要单独执行迁移(不重启容器):
|
||||
|
||||
```bash
|
||||
# 在运行中的容器中执行迁移
|
||||
./scripts/deploy/migrate.sh
|
||||
```
|
||||
|
||||
### 故障排查
|
||||
|
||||
如果迁移失败:
|
||||
|
||||
1. **查看容器日志**:
|
||||
```bash
|
||||
docker logs goravel-admin-blue
|
||||
```
|
||||
|
||||
2. **检查迁移状态**:
|
||||
```bash
|
||||
docker exec goravel-admin-blue /www/main artisan migrate:status
|
||||
```
|
||||
|
||||
3. **手动执行迁移**:
|
||||
```bash
|
||||
docker exec goravel-admin-blue /www/main artisan migrate
|
||||
```
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **迁移应该是幂等的** - 确保迁移可以安全地重复执行
|
||||
2. **测试迁移** - 在测试环境先测试迁移
|
||||
3. **备份数据库** - 在生产环境执行迁移前备份数据库
|
||||
4. **监控迁移** - 关注容器日志,确保迁移成功
|
||||
|
||||
### 相关文件
|
||||
|
||||
- `docker-entrypoint.sh` - 容器启动脚本
|
||||
- `Dockerfile` - Docker 镜像构建文件
|
||||
- `scripts/deploy/docker-blue-green.sh` - 部署脚本
|
||||
- `scripts/deploy/migrate.sh` - 单独执行迁移脚本
|
||||
|
||||
+618
@@ -0,0 +1,618 @@
|
||||
# MySQL 常用命令
|
||||
|
||||
## 连接数据库
|
||||
|
||||
```bash
|
||||
# 查看版本
|
||||
mysql --version
|
||||
# 或
|
||||
mysql -V
|
||||
|
||||
# 连接数据库
|
||||
mysql -u root -p -h 127.0.0.1 -P 3306
|
||||
|
||||
# 连接指定数据库
|
||||
mysql -u root -p -h 127.0.0.1 -P 3306 database_name
|
||||
|
||||
# 使用密码连接(不安全,仅用于脚本)
|
||||
mysql -u root -p'password' -h 127.0.0.1 -P 3306
|
||||
|
||||
# 执行 SQL 文件
|
||||
mysql -u root -p -h 127.0.0.1 -P 3306 database_name < query.sql
|
||||
|
||||
# 执行 SQL 命令
|
||||
mysql -u root -p -h 127.0.0.1 -P 3306 -e "SHOW DATABASES;"
|
||||
|
||||
# 退出交互界面
|
||||
exit
|
||||
# 或
|
||||
\q
|
||||
# 或
|
||||
quit
|
||||
```
|
||||
|
||||
## 数据库管理
|
||||
|
||||
```sql
|
||||
-- 列出所有数据库
|
||||
SHOW DATABASES;
|
||||
|
||||
-- 创建数据库
|
||||
CREATE DATABASE database_name;
|
||||
CREATE DATABASE IF NOT EXISTS database_name;
|
||||
|
||||
-- 创建数据库并指定字符集
|
||||
CREATE DATABASE database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 删除数据库
|
||||
DROP DATABASE database_name;
|
||||
DROP DATABASE IF EXISTS database_name;
|
||||
|
||||
-- 切换数据库
|
||||
USE database_name;
|
||||
|
||||
-- 查看当前数据库
|
||||
SELECT DATABASE();
|
||||
|
||||
-- 查看数据库字符集
|
||||
SHOW CREATE DATABASE database_name;
|
||||
|
||||
-- 修改数据库字符集
|
||||
ALTER DATABASE database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 查看数据库大小
|
||||
SELECT
|
||||
table_schema AS 'Database',
|
||||
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'database_name'
|
||||
GROUP BY table_schema;
|
||||
|
||||
-- 查看所有数据库大小
|
||||
SELECT
|
||||
table_schema AS 'Database',
|
||||
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
|
||||
FROM information_schema.tables
|
||||
GROUP BY table_schema
|
||||
ORDER BY SUM(data_length + index_length) DESC;
|
||||
```
|
||||
|
||||
## 用户和权限管理
|
||||
|
||||
```sql
|
||||
-- 列出所有用户
|
||||
SELECT user, host FROM mysql.user;
|
||||
|
||||
-- 查看当前用户
|
||||
SELECT USER();
|
||||
SELECT CURRENT_USER();
|
||||
|
||||
-- 创建用户
|
||||
CREATE USER 'username'@'localhost' IDENTIFIED BY 'password';
|
||||
CREATE USER 'username'@'%' IDENTIFIED BY 'password'; -- 允许任意主机
|
||||
|
||||
-- 修改用户密码
|
||||
ALTER USER 'username'@'localhost' IDENTIFIED BY 'new_password';
|
||||
SET PASSWORD FOR 'username'@'localhost' = PASSWORD('new_password'); -- MySQL 5.7
|
||||
SET PASSWORD FOR 'username'@'localhost' = 'new_password'; -- MySQL 8.0+
|
||||
|
||||
-- 删除用户
|
||||
DROP USER 'username'@'localhost';
|
||||
DROP USER IF EXISTS 'username'@'localhost';
|
||||
|
||||
-- 授予权限
|
||||
GRANT ALL PRIVILEGES ON database_name.* TO 'username'@'localhost';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON database_name.* TO 'username'@'localhost';
|
||||
GRANT ALL PRIVILEGES ON *.* TO 'username'@'localhost' WITH GRANT OPTION; -- 所有数据库
|
||||
|
||||
-- 授予特定表权限
|
||||
GRANT SELECT, INSERT ON database_name.table_name TO 'username'@'localhost';
|
||||
|
||||
-- 撤销权限
|
||||
REVOKE ALL PRIVILEGES ON database_name.* FROM 'username'@'localhost';
|
||||
|
||||
-- 查看用户权限
|
||||
SHOW GRANTS FOR 'username'@'localhost';
|
||||
|
||||
-- 刷新权限(修改权限后必须执行)
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
## 表管理
|
||||
|
||||
```sql
|
||||
-- 列出所有表
|
||||
SHOW TABLES;
|
||||
SHOW TABLES FROM database_name;
|
||||
|
||||
-- 查看表结构
|
||||
DESCRIBE table_name;
|
||||
DESC table_name;
|
||||
SHOW COLUMNS FROM table_name;
|
||||
SHOW CREATE TABLE table_name; -- 查看建表语句
|
||||
|
||||
-- 查看表状态
|
||||
SHOW TABLE STATUS LIKE 'table_name';
|
||||
SHOW TABLE STATUS FROM database_name;
|
||||
|
||||
-- 查看表大小
|
||||
SELECT
|
||||
table_name AS 'Table',
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)',
|
||||
table_rows AS 'Rows'
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'database_name'
|
||||
AND table_name = 'table_name';
|
||||
|
||||
-- 查看所有表大小
|
||||
SELECT
|
||||
table_schema AS 'Database',
|
||||
table_name AS 'Table',
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)',
|
||||
table_rows AS 'Rows'
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'database_name'
|
||||
ORDER BY (data_length + index_length) DESC;
|
||||
|
||||
-- 查看表行数
|
||||
SELECT COUNT(*) FROM table_name;
|
||||
|
||||
-- 查看表引擎
|
||||
SHOW TABLE STATUS WHERE Name = 'table_name';
|
||||
|
||||
-- 修改表引擎
|
||||
ALTER TABLE table_name ENGINE = InnoDB;
|
||||
|
||||
-- 修改表字符集
|
||||
ALTER TABLE table_name CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- 重命名表
|
||||
RENAME TABLE old_table_name TO new_table_name;
|
||||
ALTER TABLE old_table_name RENAME TO new_table_name;
|
||||
|
||||
-- 清空表数据
|
||||
TRUNCATE TABLE table_name;
|
||||
|
||||
-- 删除表
|
||||
DROP TABLE table_name;
|
||||
DROP TABLE IF EXISTS table_name;
|
||||
```
|
||||
|
||||
## 索引管理
|
||||
|
||||
```sql
|
||||
-- 列出所有索引
|
||||
SHOW INDEX FROM table_name;
|
||||
SHOW INDEXES FROM table_name;
|
||||
SHOW KEYS FROM table_name;
|
||||
|
||||
-- 查看索引详细信息
|
||||
SELECT
|
||||
TABLE_NAME,
|
||||
INDEX_NAME,
|
||||
COLUMN_NAME,
|
||||
SEQ_IN_INDEX,
|
||||
INDEX_TYPE
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'database_name'
|
||||
AND TABLE_NAME = 'table_name';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_name ON table_name(column_name);
|
||||
|
||||
-- 创建唯一索引
|
||||
CREATE UNIQUE INDEX idx_name ON table_name(column_name);
|
||||
|
||||
-- 创建复合索引
|
||||
CREATE INDEX idx_name ON table_name(column1, column2);
|
||||
|
||||
-- 创建全文索引(MyISAM 或 InnoDB 5.6+)
|
||||
CREATE FULLTEXT INDEX idx_name ON table_name(column_name);
|
||||
|
||||
-- 创建前缀索引
|
||||
CREATE INDEX idx_name ON table_name(column_name(10));
|
||||
|
||||
-- 删除索引
|
||||
DROP INDEX idx_name ON table_name;
|
||||
ALTER TABLE table_name DROP INDEX idx_name;
|
||||
|
||||
-- 查看索引使用情况(需要启用 performance_schema)
|
||||
SELECT
|
||||
object_schema,
|
||||
object_name,
|
||||
index_name,
|
||||
count_star,
|
||||
sum_timer_wait / 1000000000000 AS sum_timer_wait_sec
|
||||
FROM performance_schema.table_io_waits_summary_by_index_usage
|
||||
WHERE object_schema = 'database_name'
|
||||
ORDER BY sum_timer_wait DESC;
|
||||
```
|
||||
|
||||
## 创建全文索引 ngram
|
||||
```sql
|
||||
CREATE TABLE table_name(
|
||||
id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
|
||||
title VARCHAR (200),
|
||||
content TEXT,
|
||||
-- # 建立全文索引,同时使用ngram全文分析器
|
||||
FULLTEXT (title) WITH PARSER ngram
|
||||
) ENGINE = INNODB;
|
||||
|
||||
-- # 通过 alter table 的方式来添加
|
||||
ALTER TABLE table_name ADD FULLTEXT INDEX full_index_title(title) WITH PARSER ngram;
|
||||
|
||||
-- # 为title创建全文索引并且使用ngram全文解析器进行分词
|
||||
CREATE FULLTEXT INDEX full_index_title ON table_name(title) WITH PARSER `ngram`;
|
||||
|
||||
-- # 使用双引号把要搜素的词括起来,效果类似于like '%高性能%'
|
||||
select * ,(MATCH ( `title` ) AGAINST ( '"高性能"' ) )AS score
|
||||
from table_name
|
||||
where MATCH(title) AGAINST('"高性能"' IN BOOLEAN MODE) order by score Desc;
|
||||
|
||||
SELECT * FROM table_name WHERE MATCH(`title`) AGAINST('高性能' IN BOOLEAN MODE);
|
||||
|
||||
-- # 优化全文索引,减少索引碎片
|
||||
REPAIR TABLE table_name
|
||||
-- 或更轻量的优化(不锁表)
|
||||
OPTIMIZE TABLE table_name;
|
||||
|
||||
-- 若需匹配单字(如 “微”“信”)
|
||||
-- 临时设置(会话级)
|
||||
SET SESSION ngram_token_size = 1;
|
||||
-- 永久设置(修改 my.cnf 后重启 MySQL)
|
||||
-- ngram_token_size = 1
|
||||
```
|
||||
|
||||
## 查询和分析
|
||||
|
||||
```sql
|
||||
-- 查看执行计划
|
||||
EXPLAIN SELECT * FROM table_name WHERE condition;
|
||||
EXPLAIN FORMAT=JSON SELECT * FROM table_name WHERE condition;
|
||||
EXPLAIN FORMAT=TREE SELECT * FROM table_name WHERE condition; -- MySQL 8.0+
|
||||
|
||||
-- 分析表(更新统计信息)
|
||||
ANALYZE TABLE table_name;
|
||||
|
||||
-- 查看慢查询日志(需要启用)
|
||||
SHOW VARIABLES LIKE 'slow_query_log';
|
||||
SHOW VARIABLES LIKE 'long_query_time';
|
||||
|
||||
-- 查看当前正在执行的查询
|
||||
SHOW PROCESSLIST;
|
||||
SHOW FULL PROCESSLIST; -- 显示完整 SQL
|
||||
|
||||
-- 查看等待锁的查询
|
||||
SELECT
|
||||
r.trx_id waiting_trx_id,
|
||||
r.trx_mysql_thread_id waiting_thread,
|
||||
r.trx_query waiting_query,
|
||||
b.trx_id blocking_trx_id,
|
||||
b.trx_mysql_thread_id blocking_thread,
|
||||
b.trx_query blocking_query
|
||||
FROM information_schema.innodb_lock_waits w
|
||||
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
|
||||
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
|
||||
|
||||
-- 终止查询
|
||||
KILL process_id;
|
||||
KILL QUERY process_id; -- 只终止查询,不断开连接
|
||||
```
|
||||
|
||||
## 备份和恢复
|
||||
|
||||
```bash
|
||||
# 备份数据库(SQL格式)
|
||||
mysqldump -u root -p -h 127.0.0.1 -P 3306 database_name > backup.sql
|
||||
|
||||
# 备份所有数据库
|
||||
mysqldump -u root -p -h 127.0.0.1 -P 3306 --all-databases > all_databases.sql
|
||||
|
||||
# 备份单个表
|
||||
mysqldump -u root -p -h 127.0.0.1 -P 3306 database_name table_name > table_backup.sql
|
||||
|
||||
# 仅备份结构(不包含数据)
|
||||
mysqldump -u root -p -h 127.0.0.1 -P 3306 --no-data database_name > schema.sql
|
||||
|
||||
# 仅备份数据(不包含结构)
|
||||
mysqldump -u root -p -h 127.0.0.1 -P 3306 --no-create-info database_name > data.sql
|
||||
|
||||
# 备份并压缩
|
||||
mysqldump -u root -p -h 127.0.0.1 -P 3306 database_name | gzip > backup.sql.gz
|
||||
|
||||
# 恢复数据库(从SQL文件)
|
||||
mysql -u root -p -h 127.0.0.1 -P 3306 database_name < backup.sql
|
||||
|
||||
# 恢复压缩的备份
|
||||
gunzip < backup.sql.gz | mysql -u root -p -h 127.0.0.1 -P 3306 database_name
|
||||
|
||||
# 备份时排除某些表
|
||||
mysqldump -u root -p -h 127.0.0.1 -P 3306 database_name --ignore-table=database_name.table1 --ignore-table=database_name.table2 > backup.sql
|
||||
|
||||
# 备份时添加锁表选项
|
||||
mysqldump -u root -p -h 127.0.0.1 -P 3306 --single-transaction --routines --triggers database_name > backup.sql
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
```sql
|
||||
-- 查看数据库连接数
|
||||
SHOW STATUS LIKE 'Threads_connected';
|
||||
SHOW STATUS LIKE 'Max_used_connections';
|
||||
|
||||
-- 查看最大连接数
|
||||
SHOW VARIABLES LIKE 'max_connections';
|
||||
|
||||
-- 查看当前配置
|
||||
SHOW VARIABLES;
|
||||
|
||||
-- 查看服务器状态
|
||||
SHOW STATUS;
|
||||
|
||||
-- 查看 InnoDB 状态
|
||||
SHOW ENGINE INNODB STATUS;
|
||||
|
||||
-- 查看表统计信息
|
||||
SELECT
|
||||
table_schema,
|
||||
table_name,
|
||||
table_rows,
|
||||
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'database_name'
|
||||
ORDER BY table_rows DESC;
|
||||
|
||||
-- 查看缓存命中率
|
||||
SHOW STATUS LIKE 'Qcache%';
|
||||
SHOW VARIABLES LIKE 'query_cache%';
|
||||
|
||||
-- 查看慢查询统计
|
||||
SHOW STATUS LIKE 'Slow_queries';
|
||||
|
||||
-- 查看当前连接信息
|
||||
SHOW STATUS LIKE 'Threads%';
|
||||
|
||||
-- 查看 InnoDB 缓冲池状态
|
||||
SHOW STATUS LIKE 'Innodb_buffer_pool%';
|
||||
|
||||
-- 查看锁等待情况
|
||||
SELECT
|
||||
r.trx_id waiting_trx_id,
|
||||
r.trx_mysql_thread_id waiting_thread,
|
||||
TIMESTAMPDIFF(SECOND, r.trx_wait_started, NOW()) wait_time,
|
||||
r.trx_query waiting_query,
|
||||
l.lock_table,
|
||||
l.lock_mode,
|
||||
l.lock_type,
|
||||
b.trx_id blocking_trx_id,
|
||||
b.trx_mysql_thread_id blocking_thread,
|
||||
b.trx_query blocking_query
|
||||
FROM information_schema.innodb_lock_waits w
|
||||
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
|
||||
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
|
||||
INNER JOIN information_schema.innodb_locks l ON l.lock_id = w.requested_lock_id;
|
||||
```
|
||||
|
||||
## 维护操作
|
||||
|
||||
```sql
|
||||
-- 分析表(更新统计信息)
|
||||
ANALYZE TABLE table_name;
|
||||
|
||||
-- 优化表(整理碎片,回收空间)
|
||||
OPTIMIZE TABLE table_name;
|
||||
|
||||
-- 检查表
|
||||
CHECK TABLE table_name;
|
||||
|
||||
-- 修复表(仅 MyISAM)
|
||||
REPAIR TABLE table_name;
|
||||
|
||||
-- 查看表碎片情况
|
||||
SELECT
|
||||
table_schema,
|
||||
table_name,
|
||||
ROUND(data_free / 1024 / 1024, 2) AS data_free_mb,
|
||||
ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_size_mb,
|
||||
ROUND(data_free / (data_length + index_length) * 100, 2) AS fragmentation_percent
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'database_name'
|
||||
AND data_free > 0
|
||||
ORDER BY data_free DESC;
|
||||
|
||||
-- 刷新表缓存
|
||||
FLUSH TABLES;
|
||||
|
||||
-- 刷新日志
|
||||
FLUSH LOGS;
|
||||
```
|
||||
|
||||
## 实用命令
|
||||
|
||||
```sql
|
||||
-- 查看当前时间
|
||||
SELECT NOW();
|
||||
SELECT CURRENT_TIMESTAMP();
|
||||
|
||||
-- 查看时区
|
||||
SELECT @@global.time_zone, @@session.time_zone;
|
||||
SHOW VARIABLES LIKE '%time_zone%';
|
||||
|
||||
-- 设置时区
|
||||
SET time_zone = '+08:00';
|
||||
SET GLOBAL time_zone = '+08:00';
|
||||
|
||||
-- 查看当前用户
|
||||
SELECT USER();
|
||||
SELECT CURRENT_USER();
|
||||
|
||||
-- 查看当前数据库
|
||||
SELECT DATABASE();
|
||||
|
||||
-- 查看 MySQL 版本
|
||||
SELECT VERSION();
|
||||
SHOW VARIABLES LIKE 'version%';
|
||||
|
||||
-- 查看服务器配置
|
||||
SHOW VARIABLES LIKE 'datadir'; -- 数据目录
|
||||
SHOW VARIABLES LIKE 'basedir'; -- 安装目录
|
||||
|
||||
-- 复制表结构
|
||||
CREATE TABLE new_table LIKE old_table;
|
||||
|
||||
-- 复制表数据
|
||||
INSERT INTO new_table SELECT * FROM old_table;
|
||||
|
||||
-- 复制表结构和数据
|
||||
CREATE TABLE new_table AS SELECT * FROM old_table;
|
||||
|
||||
-- 查看自增 ID 当前值
|
||||
SHOW TABLE STATUS WHERE Name = 'table_name';
|
||||
SELECT AUTO_INCREMENT FROM information_schema.tables WHERE table_schema = 'database_name' AND table_name = 'table_name';
|
||||
|
||||
-- 重置自增 ID
|
||||
ALTER TABLE table_name AUTO_INCREMENT = 1;
|
||||
|
||||
-- 查看字符集和排序规则
|
||||
SHOW VARIABLES LIKE 'character_set%';
|
||||
SHOW VARIABLES LIKE 'collation%';
|
||||
```
|
||||
|
||||
## 事务管理
|
||||
|
||||
```sql
|
||||
-- 开启事务
|
||||
START TRANSACTION;
|
||||
BEGIN;
|
||||
|
||||
-- 提交事务
|
||||
COMMIT;
|
||||
|
||||
-- 回滚事务
|
||||
ROLLBACK;
|
||||
|
||||
-- 设置自动提交
|
||||
SET autocommit = 0; -- 关闭自动提交
|
||||
SET autocommit = 1; -- 开启自动提交
|
||||
|
||||
-- 查看事务隔离级别
|
||||
SELECT @@transaction_isolation; -- MySQL 8.0+
|
||||
SELECT @@tx_isolation; -- MySQL 5.7
|
||||
|
||||
-- 设置事务隔离级别
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
|
||||
-- 查看当前事务状态
|
||||
SELECT * FROM information_schema.innodb_trx;
|
||||
```
|
||||
|
||||
## 常用 mysql 命令
|
||||
|
||||
```bash
|
||||
# 在 mysql 交互界面中
|
||||
SHOW DATABASES; # 列出所有数据库
|
||||
USE dbname; # 切换到数据库
|
||||
SHOW TABLES; # 列出当前数据库的所有表
|
||||
DESC table; # 显示表结构
|
||||
SHOW CREATE TABLE table; # 显示建表语句
|
||||
SHOW INDEX FROM table; # 显示索引
|
||||
SHOW PROCESSLIST; # 显示当前连接
|
||||
SHOW STATUS; # 显示服务器状态
|
||||
SHOW VARIABLES; # 显示系统变量
|
||||
SOURCE file.sql; # 执行 SQL 文件
|
||||
\. file.sql # 执行 SQL 文件(另一种方式)
|
||||
exit # 退出
|
||||
quit # 退出
|
||||
\q # 退出
|
||||
\c # 清除当前输入
|
||||
\G # 垂直显示结果
|
||||
\g # 执行命令(等同于 ;)
|
||||
\s # 显示状态信息
|
||||
\T # 开始记录日志
|
||||
\t # 停止记录日志
|
||||
\W # 显示警告
|
||||
\w # 不显示警告
|
||||
```
|
||||
|
||||
## 导入/导出
|
||||
|
||||
```sql
|
||||
-- 导出查询结果到文件(需要 FILE 权限)
|
||||
SELECT * FROM table_name
|
||||
INTO OUTFILE '/path/to/file.csv'
|
||||
FIELDS TERMINATED BY ','
|
||||
ENCLOSED BY '"'
|
||||
LINES TERMINATED BY '\n';
|
||||
|
||||
-- 从文件导入数据(需要 FILE 权限)
|
||||
LOAD DATA INFILE '/path/to/file.csv'
|
||||
INTO TABLE table_name
|
||||
FIELDS TERMINATED BY ','
|
||||
ENCLOSED BY '"'
|
||||
LINES TERMINATED BY '\n'
|
||||
IGNORE 1 ROWS; -- 忽略第一行(标题行)
|
||||
```
|
||||
|
||||
```bash
|
||||
# 使用 mysql 命令导出 CSV
|
||||
mysql -u root -p -h 127.0.0.1 -P 3306 database_name -e "SELECT * FROM table_name" > output.csv
|
||||
|
||||
# 使用 mysql 命令导出 CSV(带表头)
|
||||
mysql -u root -p -h 127.0.0.1 -P 3306 database_name -e "SELECT * FROM table_name" | sed 's/\t/,/g' > output.csv
|
||||
|
||||
# 使用 mysql 命令导入 CSV
|
||||
mysql -u root -p -h 127.0.0.1 -P 3306 database_name -e "LOAD DATA LOCAL INFILE '/path/to/file.csv' INTO TABLE table_name FIELDS TERMINATED BY ',' LINES TERMINATED BY '\n' IGNORE 1 ROWS"
|
||||
```
|
||||
|
||||
## 日志管理
|
||||
|
||||
```sql
|
||||
-- 查看日志相关配置
|
||||
SHOW VARIABLES LIKE 'log%';
|
||||
|
||||
-- 查看二进制日志
|
||||
SHOW BINARY LOGS;
|
||||
SHOW MASTER STATUS;
|
||||
|
||||
-- 查看错误日志位置
|
||||
SHOW VARIABLES LIKE 'log_error';
|
||||
|
||||
-- 查看慢查询日志配置
|
||||
SHOW VARIABLES LIKE 'slow_query%';
|
||||
SHOW VARIABLES LIKE 'long_query_time';
|
||||
|
||||
-- 启用慢查询日志
|
||||
SET GLOBAL slow_query_log = 'ON';
|
||||
SET GLOBAL long_query_time = 2; -- 设置慢查询阈值(秒)
|
||||
|
||||
-- 查看通用查询日志
|
||||
SHOW VARIABLES LIKE 'general_log%';
|
||||
SET GLOBAL general_log = 'ON';
|
||||
```
|
||||
|
||||
## 主从复制
|
||||
|
||||
```sql
|
||||
-- 查看主从状态
|
||||
SHOW MASTER STATUS;
|
||||
SHOW SLAVE STATUS; -- MySQL 8.0+ 使用 SHOW REPLICA STATUS
|
||||
|
||||
-- 查看从库状态
|
||||
SHOW REPLICA STATUS; -- MySQL 8.0+
|
||||
SHOW SLAVE STATUS\G; -- MySQL 5.7
|
||||
|
||||
-- 停止从库复制
|
||||
STOP SLAVE; -- MySQL 5.7
|
||||
STOP REPLICA; -- MySQL 8.0+
|
||||
|
||||
-- 启动从库复制
|
||||
START SLAVE; -- MySQL 5.7
|
||||
START REPLICA; -- MySQL 8.0+
|
||||
|
||||
-- 跳过错误(谨慎使用)
|
||||
SET GLOBAL sql_slave_skip_counter = 1; -- MySQL 5.7
|
||||
SET GLOBAL sql_replica_skip_counter = 1; -- MySQL 8.0+
|
||||
```
|
||||
@@ -0,0 +1,185 @@
|
||||
# 权限按钮显示配置说明
|
||||
|
||||
## 功能说明
|
||||
|
||||
此配置用于控制操作按钮(如添加、编辑、删除)在没有权限时是否显示在页面上。
|
||||
|
||||
## 配置项
|
||||
|
||||
在 `.env` 文件中添加以下配置:
|
||||
|
||||
```env
|
||||
# 是否显示无权限的操作按钮
|
||||
# false: 不显示(默认)- 用户没有权限时,按钮完全隐藏
|
||||
# true: 显示但禁用 - 用户没有权限时,按钮显示但处于禁用状态
|
||||
ADMIN_SHOW_BUTTONS_WITHOUT_PERMISSION=false
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
- **默认值**: `false`(不显示)
|
||||
- **类型**: 布尔值(`true` 或 `false`)
|
||||
- **作用范围**: 全局配置,影响所有页面的操作按钮
|
||||
|
||||
### 配置效果
|
||||
|
||||
#### `ADMIN_SHOW_BUTTONS_WITHOUT_PERMISSION=false`(默认)
|
||||
- 用户没有权限时,按钮**完全隐藏**
|
||||
- 页面更加简洁,用户看不到无法使用的功能
|
||||
|
||||
#### `ADMIN_SHOW_BUTTONS_WITHOUT_PERMISSION=true`
|
||||
- 用户没有权限时,按钮**显示但禁用**
|
||||
- 用户可以知道有哪些功能,但无法使用
|
||||
- 适合需要让用户了解系统完整功能的场景
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 在 Vue 组件中使用
|
||||
|
||||
#### 方法一:使用 `getButtonState`(推荐,一个方法搞定)
|
||||
|
||||
**方式一:同时控制显示和禁用(根据配置决定是否隐藏)**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- 根据配置和权限决定是否显示按钮 -->
|
||||
<el-button
|
||||
v-if="getButtonState('admin.store').show"
|
||||
:disabled="getButtonState('admin.store').disabled"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="getButtonState('admin.update').show"
|
||||
:disabled="getButtonState('admin.update').disabled"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="getButtonState('admin.destroy').show"
|
||||
:disabled="getButtonState('admin.destroy').disabled"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
|
||||
const { getButtonState } = usePermission()
|
||||
</script>
|
||||
```
|
||||
|
||||
**方式二:只控制禁用(按钮始终显示,无权限时禁用)**
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- 按钮始终显示,无权限时禁用 -->
|
||||
<el-button
|
||||
:disabled="getButtonState('admin.store').disabled"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
:disabled="getButtonState('admin.update').disabled"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
:disabled="getButtonState('admin.destroy').disabled"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
|
||||
const { getButtonState } = usePermission()
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 方法二:使用两个方法(兼容旧代码)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="shouldShowButton('admin.store')"
|
||||
:disabled="isButtonDisabled('admin.store')"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
|
||||
const { shouldShowButton, isButtonDisabled } = usePermission()
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 方法三:直接使用 store
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="userStore.shouldShowButton('admin.store')"
|
||||
:disabled="!userStore.hasPermission('admin.store')"
|
||||
@click="handleAdd"
|
||||
>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
</script>
|
||||
```
|
||||
|
||||
## 权限标识说明
|
||||
|
||||
权限标识格式通常为:`模块.操作`
|
||||
|
||||
常见示例:
|
||||
- `admin.store` - 添加管理员
|
||||
- `admin.update` - 编辑管理员
|
||||
- `admin.destroy` - 删除管理员
|
||||
- `role.store` - 添加角色
|
||||
- `role.update` - 编辑角色
|
||||
- `permission.index` - 查看权限列表
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 配置修改后需要重启后端服务才能生效
|
||||
2. 前端会在用户登录时自动获取配置,无需刷新页面
|
||||
3. 如果用户有权限,按钮总是会显示(不受此配置影响)
|
||||
4. 此配置只影响按钮的显示/隐藏,不影响后端权限验证
|
||||
|
||||
## 相关文件
|
||||
|
||||
- 后端配置:`config/admin.go`
|
||||
- 后端 API:`app/http/controllers/admin/auth_controller.go`
|
||||
- 前端 Store:`html/src/store/user.js`
|
||||
- 前端工具:`html/src/composables/usePermission.js`
|
||||
|
||||
+452
@@ -0,0 +1,452 @@
|
||||
# PostgreSQL 常用命令
|
||||
|
||||
## 连接数据库
|
||||
|
||||
```bash
|
||||
# 查看版本
|
||||
psql -V
|
||||
|
||||
# 连接数据库
|
||||
psql -U postgres -h 127.0.0.1 -p 5432
|
||||
|
||||
# 连接指定数据库
|
||||
psql -U postgres -d database_name -h 127.0.0.1 -p 5432
|
||||
|
||||
# 使用密码文件连接
|
||||
psql -U postgres -h 127.0.0.1 -p 5432 -d database_name -f query.sql
|
||||
|
||||
# 退出交互界面
|
||||
\q
|
||||
```
|
||||
|
||||
## 宝塔 aapanel 安装扩展
|
||||
```bash
|
||||
ss -tlnp | grep postgres
|
||||
|
||||
cd /usr/local/pgsql
|
||||
make -C contrib -j"$(nproc)" PG_CONFIG=/www/server/pgsql/bin/pg_config
|
||||
make -C contrib install PG_CONFIG=/www/server/pgsql/bin/pg_config
|
||||
|
||||
/www/server/pgsql/bin/psql -U postgres -d postgres -c "SELECT name FROM pg_available_extensions ORDER BY name;"
|
||||
|
||||
sudo su - postgres
|
||||
/www/server/pgsql/bin/pg_ctl restart -D /www/server/pgsql/data
|
||||
|
||||
# 重新创建扩展并验证
|
||||
/www/server/pgsql/bin/psql -U postgres -d database_name -p 5432 -c "CREATE EXTENSION IF NOT EXISTS citext;"
|
||||
/www/server/pgsql/bin/psql -U postgres -d database_name -p 5432 -c "\dx"
|
||||
|
||||
# 可选(最常用的扩展)
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE EXTENSION IF NOT EXISTS unaccent;
|
||||
CREATE EXTENSION IF NOT EXISTS citext;
|
||||
CREATE EXTENSION IF NOT EXISTS hstore;
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
```
|
||||
|
||||
## 数据库管理
|
||||
|
||||
```sql
|
||||
-- 列出所有数据库
|
||||
\l
|
||||
-- 或
|
||||
SELECT datname FROM pg_database;
|
||||
|
||||
-- 创建数据库
|
||||
CREATE DATABASE database_name;
|
||||
|
||||
-- 删除数据库
|
||||
DROP DATABASE database_name;
|
||||
|
||||
-- 切换数据库
|
||||
\c database_name
|
||||
|
||||
-- 查看当前数据库
|
||||
SELECT current_database();
|
||||
|
||||
-- 查看数据库大小
|
||||
SELECT pg_size_pretty(pg_database_size('database_name'));
|
||||
|
||||
-- 查看所有数据库大小
|
||||
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size
|
||||
FROM pg_database
|
||||
ORDER BY pg_database_size(datname) DESC;
|
||||
```
|
||||
|
||||
## 用户和权限管理
|
||||
|
||||
```sql
|
||||
-- 列出所有用户
|
||||
\du
|
||||
-- 或
|
||||
SELECT usename FROM pg_user;
|
||||
|
||||
-- 创建用户
|
||||
CREATE USER username WITH PASSWORD 'password';
|
||||
|
||||
-- 创建超级用户
|
||||
CREATE USER username WITH SUPERUSER PASSWORD 'password';
|
||||
|
||||
-- 修改用户密码
|
||||
ALTER USER username WITH PASSWORD 'new_password';
|
||||
|
||||
-- 删除用户
|
||||
DROP USER username;
|
||||
|
||||
-- 授予权限
|
||||
GRANT ALL PRIVILEGES ON DATABASE database_name TO username;
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO username;
|
||||
|
||||
-- 撤销权限
|
||||
REVOKE ALL PRIVILEGES ON DATABASE database_name FROM username;
|
||||
|
||||
-- 查看用户权限
|
||||
\du username
|
||||
```
|
||||
|
||||
## 表管理
|
||||
|
||||
```sql
|
||||
-- 列出所有表
|
||||
\dt
|
||||
-- 或
|
||||
SELECT tablename FROM pg_tables WHERE schemaname = 'public';
|
||||
|
||||
-- 列出所有表(包括系统表)
|
||||
\dt+
|
||||
|
||||
-- 查看表结构
|
||||
\d table_name
|
||||
\d+ table_name -- 详细信息
|
||||
|
||||
-- 查看表大小
|
||||
SELECT pg_size_pretty(pg_total_relation_size('table_name')) AS total_size,
|
||||
pg_size_pretty(pg_relation_size('table_name')) AS table_size,
|
||||
pg_size_pretty(pg_indexes_size('table_name')) AS indexes_size;
|
||||
|
||||
-- 查看所有表大小
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||
|
||||
-- 查看表行数
|
||||
SELECT COUNT(*) FROM table_name;
|
||||
|
||||
-- 查看表统计信息
|
||||
SELECT * FROM pg_stat_user_tables WHERE relname = 'table_name';
|
||||
```
|
||||
|
||||
## 索引管理
|
||||
|
||||
```sql
|
||||
-- 列出所有索引
|
||||
\di
|
||||
\di table_name*
|
||||
-- 或
|
||||
SELECT indexname FROM pg_indexes WHERE tablename = 'table_name';
|
||||
|
||||
-- 查看索引详细信息
|
||||
\d+ index_name
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_name ON table_name(column_name);
|
||||
|
||||
-- 创建唯一索引
|
||||
CREATE UNIQUE INDEX idx_name ON table_name(column_name);
|
||||
|
||||
-- 创建复合索引
|
||||
CREATE INDEX idx_name ON table_name(column1, column2);
|
||||
|
||||
-- 创建 GIN 索引(用于全文搜索、数组等)
|
||||
CREATE INDEX idx_name ON table_name USING GIN(column_name gin_trgm_ops);
|
||||
|
||||
-- 创建 GIST 索引(用于几何数据、范围等)
|
||||
CREATE INDEX idx_name ON table_name USING GIST(column_name);
|
||||
|
||||
-- 删除索引
|
||||
DROP INDEX index_name;
|
||||
|
||||
-- 重建索引(解决索引碎片)
|
||||
REINDEX INDEX index_name;
|
||||
REINDEX TABLE table_name;
|
||||
REINDEX DATABASE database_name;
|
||||
|
||||
-- 查看索引使用情况
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
indexname,
|
||||
idx_scan,
|
||||
idx_tup_read,
|
||||
idx_tup_fetch
|
||||
FROM pg_stat_user_indexes
|
||||
ORDER BY idx_scan DESC;
|
||||
```
|
||||
|
||||
## 查询和分析
|
||||
|
||||
```sql
|
||||
-- 查看执行计划
|
||||
EXPLAIN SELECT * FROM table_name WHERE condition;
|
||||
EXPLAIN ANALYZE SELECT * FROM table_name WHERE condition;
|
||||
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT * FROM table_name WHERE condition;
|
||||
|
||||
-- 查看慢查询(需要启用 pg_stat_statements)
|
||||
SELECT
|
||||
query,
|
||||
calls,
|
||||
total_exec_time,
|
||||
mean_exec_time,
|
||||
max_exec_time
|
||||
FROM pg_stat_statements
|
||||
ORDER BY total_exec_time DESC
|
||||
LIMIT 10;
|
||||
|
||||
-- 查看当前正在执行的查询
|
||||
SELECT pid, usename, application_name, state, query, query_start
|
||||
FROM pg_stat_activity
|
||||
WHERE state = 'active';
|
||||
|
||||
-- 查看等待锁的查询
|
||||
SELECT
|
||||
blocked_locks.pid AS blocked_pid,
|
||||
blocking_locks.pid AS blocking_pid,
|
||||
blocked_activity.usename AS blocked_user,
|
||||
blocking_activity.usename AS blocking_user,
|
||||
blocked_activity.query AS blocked_statement,
|
||||
blocking_activity.query AS blocking_statement
|
||||
FROM pg_catalog.pg_locks blocked_locks
|
||||
JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
|
||||
JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
|
||||
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
|
||||
WHERE NOT blocked_locks.granted AND blocking_locks.granted;
|
||||
|
||||
-- 终止查询
|
||||
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid = <pid>;
|
||||
```
|
||||
|
||||
## 扩展管理
|
||||
|
||||
```sql
|
||||
-- 列出所有已安装的扩展
|
||||
\dx
|
||||
-- 或
|
||||
SELECT extname, extversion FROM pg_extension;
|
||||
|
||||
-- 查看可用扩展
|
||||
SELECT * FROM pg_available_extensions;
|
||||
|
||||
-- 安装扩展
|
||||
CREATE EXTENSION extension_name;
|
||||
|
||||
-- 安装扩展(如果不存在)
|
||||
CREATE EXTENSION IF NOT EXISTS extension_name;
|
||||
|
||||
-- 卸载扩展
|
||||
DROP EXTENSION extension_name;
|
||||
|
||||
-- pg_trgm 扩展(用于相似度搜索)
|
||||
-- 查看已安装的扩展,确认包含 pg_trgm
|
||||
SELECT extname, extversion FROM pg_extension WHERE extname = 'pg_trgm';
|
||||
|
||||
-- 启用 pg_trgm 扩展(数据库级生效,超级用户权限)
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
|
||||
-- 降低阈值(默认0.3),匹配更多相似结果(测试用)
|
||||
SET pg_trgm.similarity_threshold = 0.2;
|
||||
|
||||
-- 执行相似匹配查询
|
||||
SELECT * FROM test_trgm WHERE similarity(content, '三四五') > 0.2;
|
||||
|
||||
-- 重建GIN索引(定期执行,比如每月1次) 解决索引碎片
|
||||
REINDEX INDEX idx_test_trgm_content;
|
||||
```
|
||||
|
||||
## 备份和恢复
|
||||
|
||||
```bash
|
||||
# 备份数据库(自定义格式,支持压缩)
|
||||
pg_dump -U postgres -h 127.0.0.1 -d database_name -F c -f backup.dump
|
||||
|
||||
# 备份数据库(SQL格式)
|
||||
pg_dump -U postgres -h 127.0.0.1 -d database_name -f backup.sql
|
||||
|
||||
# 备份单个表
|
||||
pg_dump -U postgres -h 127.0.0.1 -d database_name -t table_name -f table_backup.sql
|
||||
|
||||
# 仅备份结构(不包含数据)
|
||||
pg_dump -U postgres -h 127.0.0.1 -d database_name -s -f schema.sql
|
||||
|
||||
# 仅备份数据(不包含结构)
|
||||
pg_dump -U postgres -h 127.0.0.1 -d database_name -a -f data.sql
|
||||
|
||||
# 恢复数据库(从自定义格式)
|
||||
pg_restore -U postgres -h 127.0.0.1 -d database_name backup.dump
|
||||
|
||||
# 恢复数据库(从SQL文件)
|
||||
psql -U postgres -h 127.0.0.1 -d database_name -f backup.sql
|
||||
|
||||
# 备份所有数据库
|
||||
pg_dumpall -U postgres -h 127.0.0.1 -f all_databases.sql
|
||||
|
||||
# 恢复所有数据库
|
||||
psql -U postgres -h 127.0.0.1 -f all_databases.sql
|
||||
```
|
||||
|
||||
## 性能监控
|
||||
|
||||
```sql
|
||||
-- 查看数据库连接数
|
||||
SELECT count(*) FROM pg_stat_activity;
|
||||
|
||||
-- 查看最大连接数
|
||||
SHOW max_connections;
|
||||
|
||||
-- 查看当前配置
|
||||
SHOW ALL;
|
||||
|
||||
-- 查看共享缓冲区使用情况
|
||||
SELECT * FROM pg_stat_bgwriter;
|
||||
|
||||
-- 查看表统计信息
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
n_tup_ins AS inserts,
|
||||
n_tup_upd AS updates,
|
||||
n_tup_del AS deletes,
|
||||
n_live_tup AS live_rows,
|
||||
n_dead_tup AS dead_rows,
|
||||
last_vacuum,
|
||||
last_autovacuum,
|
||||
last_analyze,
|
||||
last_autoanalyze
|
||||
FROM pg_stat_user_tables
|
||||
ORDER BY n_live_tup DESC;
|
||||
|
||||
-- 查看缓存命中率
|
||||
SELECT
|
||||
sum(heap_blks_read) as heap_read,
|
||||
sum(heap_blks_hit) as heap_hit,
|
||||
(sum(heap_blks_hit) - sum(heap_blks_read)) / sum(heap_blks_hit) as ratio
|
||||
FROM pg_statio_user_tables;
|
||||
```
|
||||
|
||||
## 维护操作
|
||||
|
||||
```sql
|
||||
-- 分析表(更新统计信息)
|
||||
ANALYZE table_name;
|
||||
ANALYZE; -- 分析所有表
|
||||
|
||||
-- 清理表(VACUUM)
|
||||
VACUUM table_name;
|
||||
VACUUM FULL table_name; -- 完全清理,会锁表
|
||||
VACUUM ANALYZE table_name; -- 清理并分析
|
||||
|
||||
-- 自动清理(后台进程)
|
||||
-- 查看自动清理配置
|
||||
SHOW autovacuum;
|
||||
|
||||
-- 查看需要清理的表
|
||||
SELECT
|
||||
schemaname,
|
||||
tablename,
|
||||
n_dead_tup,
|
||||
n_live_tup,
|
||||
round(n_dead_tup * 100.0 / NULLIF(n_live_tup, 0), 2) AS dead_ratio
|
||||
FROM pg_stat_user_tables
|
||||
WHERE n_dead_tup > 0
|
||||
ORDER BY dead_ratio DESC;
|
||||
```
|
||||
|
||||
## 实用命令
|
||||
|
||||
```sql
|
||||
-- 查看当前时间
|
||||
SELECT NOW();
|
||||
|
||||
-- 查看时区
|
||||
SHOW timezone;
|
||||
SELECT current_setting('timezone');
|
||||
|
||||
-- 设置时区
|
||||
SET timezone = 'Asia/Shanghai';
|
||||
|
||||
-- 查看当前用户
|
||||
SELECT current_user;
|
||||
|
||||
-- 查看当前数据库
|
||||
SELECT current_database();
|
||||
|
||||
-- 查看 PostgreSQL 版本
|
||||
SELECT version();
|
||||
|
||||
-- 查看服务器配置
|
||||
SHOW config_file; -- 配置文件路径
|
||||
SHOW data_directory; -- 数据目录
|
||||
|
||||
-- 复制表结构
|
||||
CREATE TABLE new_table (LIKE old_table INCLUDING ALL);
|
||||
|
||||
-- 复制表数据
|
||||
INSERT INTO new_table SELECT * FROM old_table;
|
||||
|
||||
-- 查看序列
|
||||
\ds
|
||||
SELECT sequence_name FROM information_schema.sequences;
|
||||
|
||||
-- 重置序列
|
||||
ALTER SEQUENCE sequence_name RESTART WITH 1;
|
||||
```
|
||||
|
||||
## 安装扩展(Ubuntu/Debian)
|
||||
|
||||
```bash
|
||||
# 替换版本号(如 14, 15, 16)为你的 PostgreSQL 版本
|
||||
apt-get update
|
||||
apt-get install -y postgresql-contrib-16
|
||||
|
||||
# 安装其他常用扩展
|
||||
apt-get install -y postgresql-16-pg-stat-statements
|
||||
```
|
||||
|
||||
## 常用 psql 命令
|
||||
|
||||
```bash
|
||||
# 在 psql 交互界面中
|
||||
\l # 列出所有数据库
|
||||
\c dbname # 连接到数据库
|
||||
\dt # 列出当前数据库的所有表
|
||||
\d table # 显示表结构
|
||||
\di # 列出索引
|
||||
\du # 列出用户
|
||||
\dx # 列出扩展
|
||||
\df # 列出函数
|
||||
\dn # 列出模式
|
||||
\q # 退出
|
||||
\? # 帮助
|
||||
\h # SQL 命令帮助
|
||||
\timing # 显示查询执行时间
|
||||
\x # 扩展显示模式(列转行)
|
||||
\copy # 导入/导出 CSV
|
||||
```
|
||||
|
||||
## 导入/导出 CSV
|
||||
|
||||
```sql
|
||||
-- 导出表到 CSV
|
||||
\copy table_name TO '/path/to/file.csv' WITH CSV HEADER;
|
||||
|
||||
-- 从 CSV 导入
|
||||
\copy table_name FROM '/path/to/file.csv' WITH CSV HEADER;
|
||||
|
||||
-- 使用 COPY 命令(需要超级用户权限)
|
||||
COPY table_name TO '/path/to/file.csv' WITH CSV HEADER;
|
||||
COPY table_name FROM '/path/to/file.csv' WITH CSV HEADER;
|
||||
```
|
||||
@@ -0,0 +1,481 @@
|
||||
# 数据库分表指南
|
||||
|
||||
项目支持两种分表策略:**时间分表**(按月分表)和**哈希分表**(按ID哈希分表)。本文档包含分表功能的完整说明,包括如何创建分表、如何使用分表,以及如何为新的表添加分表支持。
|
||||
|
||||
## 目录
|
||||
|
||||
- [分表策略概述](#分表策略概述)
|
||||
- [时间分表(按月分表)](#时间分表按月分表)
|
||||
- [哈希分表(按ID哈希分表)](#哈希分表按id哈希分表)
|
||||
- [为新的表添加分表支持](#为新的表添加分表支持)
|
||||
- [分表字段修改](#分表字段修改)
|
||||
- [工具函数](#工具函数)
|
||||
|
||||
## 分表策略概述
|
||||
|
||||
### 已实现的分表
|
||||
|
||||
#### 时间分表(按月分表)
|
||||
- `orders` - 订单主表
|
||||
- `order_details` - 订单详情表
|
||||
|
||||
#### 哈希分表(按ID哈希分表)
|
||||
- `user_balance_logs` - 用户余额变动记录表(按 `user_id` 哈希分表,4个分表)
|
||||
|
||||
### 分表策略对比
|
||||
|
||||
| 特性 | 时间分表 | 哈希分表 |
|
||||
|------|---------|---------|
|
||||
| **分表键** | `created_at` (时间) | 业务ID(如 `user_id`) |
|
||||
| **分表数量** | 动态增长(按月) | 固定数量(可配置) |
|
||||
| **适用场景** | 数据按时间分布,查询通常有时间范围 | 数据按业务ID分布,查询通常按ID |
|
||||
| **查询特点** | 可能需要跨多个分表查询 | 通常只查询单个分表 |
|
||||
| **分表命名** | `{table}_YYYYMM` | `{table}_{shard_index}` |
|
||||
| **示例** | `orders_202501` | `user_balance_logs_0` |
|
||||
|
||||
## 时间分表(按月分表)
|
||||
|
||||
### 分表策略
|
||||
|
||||
- **分表方式**:按月分表
|
||||
- **分表键字段**:使用 `created_at`(由 `orm.Model` 自动提供),类型为 `time.Time`
|
||||
- **分表名称格式**:`{base_table_name}_{YYYYMM}`,例如 `orders_202501`
|
||||
- **表结构定义**:统一在 migrations 中,便于维护和版本控制
|
||||
- **查询跨月数据**:需要查询多个分表并合并结果(已在 `OrderService` 中实现)
|
||||
|
||||
### 创建时间分表
|
||||
|
||||
#### 1. 在 Migration 中定义表创建函数
|
||||
|
||||
在对应的 migration 文件中添加创建分表的函数,例如 `database/migrations/20250128000001_create_orders_table.go`:
|
||||
|
||||
```go
|
||||
// CreateOrdersShardingTable 创建订单主表分表(供服务层和命令层调用)
|
||||
func CreateOrdersShardingTable(tableName string) error {
|
||||
return facades.Schema().Create(tableName, func(table schema.Blueprint) {
|
||||
table.BigIncrements("id")
|
||||
table.String("order_no", 50).Comment("订单号")
|
||||
// ... 其他字段定义
|
||||
table.Index("order_no")
|
||||
table.Comment(fmt.Sprintf("订单主表 - %s", tableName))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 在 ShardingService 中注册表创建函数
|
||||
|
||||
在 `app/services/sharding_service.go` 中注册表创建函数:
|
||||
|
||||
```go
|
||||
// registerOrderTables 注册订单表的创建函数
|
||||
func (s *ShardingServiceImpl) registerOrderTables() {
|
||||
// 注册订单主表(调用 migrations 中的函数)
|
||||
s.RegisterTableCreator("orders", migrations.CreateOrdersShardingTable)
|
||||
|
||||
// 注册订单详情表(调用 migrations 中的函数)
|
||||
s.RegisterTableCreator("order_details", migrations.CreateOrderDetailsShardingTable)
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 创建分表命令(可选)
|
||||
|
||||
如需手动创建分表,可以参考 `app/console/commands/create_order_sharding_tables.go` 创建类似的命令。
|
||||
|
||||
**命令使用示例:**
|
||||
|
||||
```bash
|
||||
# 创建分表(默认创建上个月、当前月份及未来2个月,共4个月)
|
||||
go run . artisan order:create-sharding-tables
|
||||
|
||||
# 创建指定月份的分表
|
||||
go run . artisan order:create-sharding-tables --month=202512
|
||||
|
||||
# 创建上个月、当前月份及未来N个月的分表(--months 参数包括上个月和当前月)
|
||||
# 例如:--months=6 会创建上个月、当前月及未来4个月,共6个月
|
||||
go run . artisan order:create-sharding-tables --months=6
|
||||
```
|
||||
|
||||
#### 4. 定时任务(可选)
|
||||
|
||||
在 `app/console/kernel.go` 中添加定时任务,自动创建未来的分表:
|
||||
|
||||
```go
|
||||
// 每月1号凌晨1点执行(UTC时间),创建下个月的订单分表
|
||||
facades.Schedule().Command("order:create-sharding-tables").Monthly().OnOneServer()
|
||||
```
|
||||
|
||||
### 使用时间分表
|
||||
|
||||
#### 在服务层使用分表
|
||||
|
||||
```go
|
||||
// 确保分表存在(使用订单的创建时间)
|
||||
now := time.Now().UTC()
|
||||
tableName := utils.GetShardingTableName("orders", now)
|
||||
if err := s.shardingService.EnsureShardingTable(tableName, "orders"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用分表进行查询
|
||||
facades.Orm().Query().Table(tableName).Where("id", orderID).First(&order)
|
||||
```
|
||||
|
||||
#### 查询跨月数据
|
||||
|
||||
查询跨月数据时,需要使用 `utils.GetShardingTableNames()` 获取所有相关的分表,然后分别查询并合并结果:
|
||||
|
||||
```go
|
||||
// 获取时间范围内的所有分表
|
||||
tableNames := utils.GetShardingTableNames("orders", startTime, endTime)
|
||||
|
||||
// 分别查询每个分表
|
||||
for _, tableName := range tableNames {
|
||||
// 查询逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 哈希分表(按ID哈希分表)
|
||||
|
||||
### 分表策略
|
||||
|
||||
- **分表方式**:按业务ID哈希分表
|
||||
- **分表键字段**:业务ID(如 `user_id`),类型为 `uint`
|
||||
- **分表名称格式**:`{base_table_name}_{shard_index}`,例如 `user_balance_logs_0`
|
||||
- **分表数量**:固定数量,建议为 2 的幂次(如 4, 8, 16, 32, 64 等)
|
||||
- **分表逻辑**:`shardingKey % numberOfShards`
|
||||
- **查询特点**:通常只查询单个分表,不支持跨分表查询
|
||||
|
||||
### 创建哈希分表
|
||||
|
||||
#### 1. 在 Migration 中定义表创建函数
|
||||
|
||||
在对应的 migration 文件中添加创建分表的函数,例如 `database/migrations/20250130000002_create_user_balance_logs_table.go`:
|
||||
|
||||
```go
|
||||
// CreateUserBalanceLogsShardingTable 创建用户余额变动记录分表(供服务层调用)
|
||||
func CreateUserBalanceLogsShardingTable(tableName string) error {
|
||||
return facades.Schema().Create(tableName, func(table schema.Blueprint) {
|
||||
table.BigIncrements("id")
|
||||
table.UnsignedBigInteger("user_id").Comment("用户ID")
|
||||
table.String("type", 20).Comment("变动类型:income收入,expense支出,refund退款")
|
||||
// ... 其他字段定义
|
||||
table.Index("user_id")
|
||||
table.Comment(fmt.Sprintf("用户余额变动记录表 - %s", tableName))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 在 ShardingService 中注册表创建函数
|
||||
|
||||
在 `app/services/sharding_service.go` 中注册表创建函数:
|
||||
|
||||
```go
|
||||
// registerUserBalanceLogTables 注册用户余额变动记录表的创建函数
|
||||
func (s *ShardingServiceImpl) registerUserBalanceLogTables() {
|
||||
// 注册用户余额变动记录表(调用 migrations 中的函数)
|
||||
s.RegisterTableCreator("user_balance_logs", migrations.CreateUserBalanceLogsShardingTable)
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 定义分表数量常量(可选)
|
||||
|
||||
在 `app/constants/sharding.go` 中定义分表数量:
|
||||
|
||||
```go
|
||||
// UserBalanceLogsShards 用户余额变动记录表的分表数量
|
||||
// 建议设置为 2 的幂次(如 4, 8, 16, 32, 64, 128 等)
|
||||
const UserBalanceLogsShards = 4
|
||||
```
|
||||
|
||||
### 使用哈希分表
|
||||
|
||||
#### 在服务层使用分表
|
||||
|
||||
```go
|
||||
// 根据 user_id 计算分表名称
|
||||
tableName := utils.GetHashShardingTableName("user_balance_logs", userID, constants.UserBalanceLogsShards)
|
||||
|
||||
// 确保分表存在
|
||||
if err := s.shardingService.EnsureShardingTable(tableName, "user_balance_logs"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用分表进行查询(必须包含分表键字段)
|
||||
facades.Orm().Query().Table(tableName).Where("user_id", userID).First(&log)
|
||||
```
|
||||
|
||||
#### 注意事项
|
||||
|
||||
1. **所有查询必须包含分表键字段**:哈希分表需要分表键来路由到正确的分表
|
||||
2. **不支持跨分表查询**:如果需要查询多个ID的数据,需要分别查询后合并
|
||||
3. **分表自动创建**:首次插入数据时,会自动创建对应的分表(通过 `EnsureShardingTable`)
|
||||
|
||||
## 为新的表添加分表支持
|
||||
|
||||
### 添加时间分表支持
|
||||
|
||||
参考 [时间分表(按月分表)](#时间分表按月分表) 章节的步骤。
|
||||
|
||||
### 添加哈希分表支持
|
||||
|
||||
#### 步骤 1: 创建 Migration 文件
|
||||
|
||||
创建 migration 文件,定义表创建函数:
|
||||
|
||||
```go
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goravel/framework/contracts/database/schema"
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
type M20250101000001CreateExampleTable struct {
|
||||
}
|
||||
|
||||
func (r *M20250101000001CreateExampleTable) Signature() string {
|
||||
return "20250101000001_create_example_table"
|
||||
}
|
||||
|
||||
func (r *M20250101000001CreateExampleTable) Up() error {
|
||||
// 使用哈希分表,不创建基础表
|
||||
// 分表通过 CreateExampleShardingTable 函数创建
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *M20250101000001CreateExampleTable) Down() error {
|
||||
// 哈希分表,不删除基础表(因为基础表不存在)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateExampleShardingTable 创建示例表分表(供服务层调用)
|
||||
func CreateExampleShardingTable(tableName string) error {
|
||||
return facades.Schema().Create(tableName, func(table schema.Blueprint) {
|
||||
table.BigIncrements("id")
|
||||
table.UnsignedBigInteger("entity_id").Comment("实体ID(分表键)")
|
||||
// ... 其他字段定义
|
||||
table.Index("entity_id")
|
||||
table.Comment(fmt.Sprintf("示例表 - %s", tableName))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 2: 定义分表数量常量
|
||||
|
||||
在 `app/constants/sharding.go` 中添加:
|
||||
|
||||
```go
|
||||
// ExampleShards 示例表的分表数量
|
||||
// 建议设置为 2 的幂次(如 4, 8, 16, 32, 64, 128 等)
|
||||
const ExampleShards = 8
|
||||
```
|
||||
|
||||
#### 步骤 3: 在 ShardingService 中注册
|
||||
|
||||
在 `app/services/sharding_service.go` 的 `NewShardingService()` 方法中注册:
|
||||
|
||||
```go
|
||||
func NewShardingService() ShardingService {
|
||||
service := &ShardingServiceImpl{
|
||||
creators: make(map[string]TableCreator),
|
||||
}
|
||||
|
||||
// 注册订单相关表的创建函数
|
||||
service.registerOrderTables()
|
||||
|
||||
// 注册用户余额变动记录表的创建函数
|
||||
service.registerUserBalanceLogTables()
|
||||
|
||||
// 注册示例表的创建函数
|
||||
service.RegisterTableCreator("example_table", migrations.CreateExampleShardingTable)
|
||||
|
||||
return service
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤 4: 在服务层使用
|
||||
|
||||
```go
|
||||
// 根据 entity_id 计算分表名称
|
||||
tableName := utils.GetHashShardingTableName("example_table", entityID, constants.ExampleShards)
|
||||
|
||||
// 确保分表存在
|
||||
if err := s.shardingService.EnsureShardingTable(tableName, "example_table"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用分表进行查询
|
||||
facades.Orm().Query().Table(tableName).Where("entity_id", entityID).Create(&example)
|
||||
```
|
||||
|
||||
## 分表字段修改
|
||||
|
||||
当表使用分表策略时,如果需要添加或修改字段,需要同时更新:
|
||||
|
||||
1. **表创建函数**(用于新创建的分表)
|
||||
2. **所有已存在的分表**(通过 migration)
|
||||
|
||||
### 修改表创建函数
|
||||
|
||||
在对应的 migration 文件中修改表创建函数,添加新字段。
|
||||
|
||||
### 创建 Migration 修改已存在的分表
|
||||
|
||||
创建一个新的 migration 文件,使用 `utils.GetAllExistingShardingTables()` 或 `utils.GetAllExistingShardingTablesByPattern()` 获取所有已存在的分表,然后逐个修改:
|
||||
|
||||
```go
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goravel/framework/contracts/database/schema"
|
||||
"github.com/goravel/framework/facades"
|
||||
"goravel/app/utils"
|
||||
)
|
||||
|
||||
type M20251228000001AddFieldToExampleTable struct {
|
||||
}
|
||||
|
||||
func (r *M20251228000001AddFieldToExampleTable) Signature() string {
|
||||
return "20251228000001_add_field_to_example_table"
|
||||
}
|
||||
|
||||
func (r *M20251228000001AddFieldToExampleTable) Up() error {
|
||||
// 获取所有已存在的分表(哈希分表使用模式匹配)
|
||||
exampleTables, err := utils.GetAllExistingShardingTablesByPattern("example_table_%")
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取示例表分表列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 遍历所有分表,添加字段
|
||||
for _, tableName := range exampleTables {
|
||||
if !facades.Schema().HasTable(tableName) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查字段是否已存在(避免重复添加)
|
||||
if !facades.Schema().HasColumn(tableName, "new_field") {
|
||||
if err := facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.String("new_field", 100).Nullable().Comment("新字段").After("existing_field")
|
||||
}); err != nil {
|
||||
return fmt.Errorf("修改分表 %s 失败: %v", tableName, err)
|
||||
}
|
||||
facades.Log().Infof("✓ 已为分表 %s 添加字段 new_field", tableName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *M20251228000001AddFieldToExampleTable) Down() error {
|
||||
// 回滚操作:删除添加的字段
|
||||
exampleTables, err := utils.GetAllExistingShardingTablesByPattern("example_table_%")
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取示例表分表列表失败: %v", err)
|
||||
}
|
||||
|
||||
for _, tableName := range exampleTables {
|
||||
if !facades.Schema().HasTable(tableName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if facades.Schema().HasColumn(tableName, "new_field") {
|
||||
if err := facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.DropColumn("new_field")
|
||||
}); err != nil {
|
||||
return fmt.Errorf("回滚分表 %s 失败: %v", tableName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. **字段检查**:在添加字段前,务必检查字段是否已存在,避免重复添加导致错误
|
||||
2. **表检查**:在操作前检查表是否存在
|
||||
3. **数据备份**:修改字段类型或删除字段前,请先备份数据
|
||||
4. **新分表**:修改表创建函数后,新创建的分表会自动包含新字段
|
||||
5. **已存在分表**:需要通过 migration 手动修改所有已存在的分表
|
||||
6. **回滚支持**:务必实现 `Down()` 方法,支持 migration 回滚
|
||||
|
||||
## 工具函数
|
||||
|
||||
`app/utils/sharding_helper.go` 提供了以下工具函数:
|
||||
|
||||
### 时间分表工具函数
|
||||
|
||||
- `GetShardingTableName(baseTableName string, orderTime time.Time)`: 根据时间获取分表名称
|
||||
- `GetShardingTableNames(baseTableName string, startTime, endTime time.Time)`: 获取时间范围内的所有分表名称
|
||||
- `ValidateTimeRange(startTime, endTime time.Time, maxMonths ...int)`: 验证时间范围是否超过指定月数
|
||||
|
||||
### 哈希分表工具函数
|
||||
|
||||
- `GetHashShardingTableName(baseTableName string, shardingKey uint, numberOfShards int)`: 通用的哈希分表名称生成函数
|
||||
- `GetUserBalanceLogsShardingTableName(userID uint)`: 用户余额变动记录表的分表名称(特定实现)
|
||||
|
||||
### 通用工具函数
|
||||
|
||||
- `GetAllExistingShardingTables(baseTableName string)`: 获取所有已存在的时间分表名称(格式:`{table}_YYYYMM`)
|
||||
- `GetAllExistingShardingTablesByPattern(pattern string)`: 通过表名模式获取所有已存在的分表(适用于哈希分表)
|
||||
|
||||
### 使用示例
|
||||
|
||||
#### 时间分表
|
||||
|
||||
```go
|
||||
// 根据时间获取分表名称
|
||||
tableName := utils.GetShardingTableName("orders", time.Now())
|
||||
|
||||
// 获取时间范围内的所有分表
|
||||
tableNames := utils.GetShardingTableNames("orders", startTime, endTime)
|
||||
```
|
||||
|
||||
#### 哈希分表
|
||||
|
||||
```go
|
||||
// 通用的哈希分表名称生成
|
||||
tableName := utils.GetHashShardingTableName("example_table", entityID, 8)
|
||||
|
||||
// 特定表的便捷函数(内部调用通用函数)
|
||||
tableName := utils.GetUserBalanceLogsShardingTableName(userID)
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
### 时间分表示例
|
||||
|
||||
参考 `database/migrations/20250128000001_create_orders_table.go` 和 `app/services/order_service.go`。
|
||||
|
||||
### 哈希分表示例
|
||||
|
||||
参考 `database/migrations/20250130000002_create_user_balance_logs_table.go` 和 `app/services/user_balance_log_service.go`。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何选择分表策略?
|
||||
|
||||
**A:**
|
||||
- **时间分表**:适用于数据按时间分布,查询通常有时间范围,如订单、日志等
|
||||
- **哈希分表**:适用于数据按业务ID分布,查询通常按ID,如用户相关数据、余额记录等
|
||||
|
||||
### Q: 哈希分表数量如何确定?
|
||||
|
||||
**A:**
|
||||
- 建议设置为 2 的幂次(如 4, 8, 16, 32, 64, 128 等)
|
||||
- 根据数据量和查询负载确定,一般 4-16 个分表即可
|
||||
- 分表数量一旦确定,不建议频繁修改(需要数据迁移)
|
||||
|
||||
### Q: 如何查询跨分表数据?
|
||||
|
||||
**A:**
|
||||
- **时间分表**:使用 `GetShardingTableNames()` 获取所有相关分表,分别查询后合并结果
|
||||
- **哈希分表**:不支持跨分表查询,需要分别查询每个分表后合并结果
|
||||
|
||||
### Q: 分表字段修改后,已存在的分表怎么办?
|
||||
|
||||
**A:** 创建 migration,使用 `GetAllExistingShardingTables()` 或 `GetAllExistingShardingTablesByPattern()` 获取所有已存在的分表,然后逐个修改。
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
# 数据库分表迁移指南
|
||||
|
||||
> **注意**:本文档主要介绍分表字段修改的详细步骤。如需了解完整的分表功能说明,请参考 [分表指南 (SHARDING_GUIDE.md)](SHARDING_GUIDE.md)。
|
||||
|
||||
项目支持两种分表策略:**时间分表**(按月分表)和**哈希分表**(按ID哈希分表)。本文档主要介绍如何修改分表字段。
|
||||
|
||||
## 目录
|
||||
|
||||
- [分表功能概述](#分表功能概述)
|
||||
- [创建分表](#创建分表)
|
||||
- [使用分表](#使用分表)
|
||||
- [分表字段修改](#分表字段修改)
|
||||
|
||||
## 分表功能概述
|
||||
|
||||
### 分表策略
|
||||
|
||||
- **分表方式**:按月分表
|
||||
- **分表键字段**:使用 `created_at`(由 `orm.Model` 自动提供),类型为 `time.Time`
|
||||
- **分表名称格式**:`{base_table_name}_{YYYYMM}`,例如 `orders_202501`
|
||||
- **表结构定义**:统一在 migrations 中,便于维护和版本控制
|
||||
- **查询跨月数据**:需要查询多个分表并合并结果(已在 `OrderService` 中实现)
|
||||
|
||||
### 已实现的分表
|
||||
|
||||
- `orders` - 订单主表
|
||||
- `order_details` - 订单详情表
|
||||
|
||||
## 创建分表
|
||||
|
||||
### 1. 在 Migration 中定义表创建函数
|
||||
|
||||
在对应的 migration 文件中添加创建分表的函数,例如 `database/migrations/20250128000001_create_orders_table.go`:
|
||||
|
||||
```go
|
||||
// CreateOrdersShardingTable 创建订单主表分表(供服务层和命令层调用)
|
||||
func CreateOrdersShardingTable(tableName string) error {
|
||||
return facades.Schema().Create(tableName, func(table schema.Blueprint) {
|
||||
table.BigIncrements("id")
|
||||
table.String("order_no", 50).Comment("订单号")
|
||||
// ... 其他字段定义
|
||||
table.Index("order_no")
|
||||
table.Comment(fmt.Sprintf("订单主表 - %s", tableName))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在 ShardingService 中注册表创建函数
|
||||
|
||||
在 `app/services/sharding_service.go` 的 `registerOrderTables` 方法中(或创建新的注册方法)注册表创建函数:
|
||||
|
||||
```go
|
||||
// registerOrderTables 注册订单表的创建函数
|
||||
func (s *ShardingServiceImpl) registerOrderTables() {
|
||||
// 注册订单主表(调用 migrations 中的函数)
|
||||
s.RegisterTableCreator("orders", migrations.CreateOrdersShardingTable)
|
||||
|
||||
// 注册订单详情表(调用 migrations 中的函数)
|
||||
s.RegisterTableCreator("order_details", migrations.CreateOrderDetailsShardingTable)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 创建分表命令(可选)
|
||||
|
||||
如需手动创建分表,可以参考 `app/console/commands/create_order_sharding_tables.go` 创建类似的命令。
|
||||
|
||||
**命令使用示例:**
|
||||
|
||||
```bash
|
||||
# 创建分表(默认创建上个月、当前月份及未来2个月,共4个月)
|
||||
go run . artisan order:create-sharding-tables
|
||||
|
||||
# 创建指定月份的分表
|
||||
go run . artisan order:create-sharding-tables --month=202512
|
||||
|
||||
# 创建上个月、当前月份及未来N个月的分表(--months 参数包括上个月和当前月)
|
||||
# 例如:--months=6 会创建上个月、当前月及未来4个月,共6个月
|
||||
go run . artisan order:create-sharding-tables --months=6
|
||||
```
|
||||
|
||||
### 4. 定时任务(可选)
|
||||
|
||||
在 `app/console/kernel.go` 中添加定时任务,自动创建未来的分表:
|
||||
|
||||
```go
|
||||
// 每月1号凌晨1点执行(UTC时间),创建下个月的订单分表
|
||||
facades.Schedule().Command("order:create-sharding-tables").Monthly().OnOneServer()
|
||||
```
|
||||
|
||||
## 使用分表
|
||||
|
||||
### 在服务层使用分表
|
||||
|
||||
在服务层使用 `ShardingService` 确保分表存在:
|
||||
|
||||
```go
|
||||
// 确保分表存在(使用订单的创建时间)
|
||||
now := time.Now().UTC()
|
||||
tableName := utils.GetShardingTableName("orders", now)
|
||||
if err := s.shardingService.EnsureShardingTable(tableName, "orders"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用分表进行查询
|
||||
facades.Orm().Query().Table(tableName).Where("id", orderID).First(&order)
|
||||
```
|
||||
|
||||
### 查询跨月数据
|
||||
|
||||
查询跨月数据时,需要使用 `utils.GetShardingTableNames()` 获取所有相关的分表,然后分别查询并合并结果:
|
||||
|
||||
```go
|
||||
// 获取时间范围内的所有分表
|
||||
tableNames := utils.GetShardingTableNames("orders", startTime, endTime)
|
||||
|
||||
// 分别查询每个分表
|
||||
for _, tableName := range tableNames {
|
||||
// 查询逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 分表字段修改
|
||||
|
||||
当订单表使用分表策略(按月分表)时,如果需要添加或修改字段,需要同时更新:
|
||||
|
||||
1. **表创建函数**(用于新创建的分表)
|
||||
2. **所有已存在的分表**(通过 migration)
|
||||
|
||||
## 步骤说明
|
||||
|
||||
### 1. 修改表创建函数
|
||||
|
||||
在 `database/migrations/20250128000001_create_orders_table.go` 中修改 `CreateOrdersShardingTable` 和/或 `CreateOrderDetailsShardingTable` 函数,添加新字段:
|
||||
|
||||
```go
|
||||
// CreateOrdersShardingTable 创建订单主表分表(供服务层和命令层调用)
|
||||
func CreateOrdersShardingTable(tableName string) error {
|
||||
return facades.Schema().Create(tableName, func(table schema.Blueprint) {
|
||||
table.BigIncrements("id")
|
||||
table.String("order_no", 50).Comment("订单号")
|
||||
table.UnsignedBigInteger("user_id").Comment("用户ID")
|
||||
table.Decimal("amount").Comment("订单金额(10,2)")
|
||||
table.String("status", 20).Default("pending").Comment("订单状态 pending:待支付 paid:已支付 cancelled:已取消")
|
||||
table.Text("remark").Nullable().Comment("备注")
|
||||
|
||||
// 新添加的字段
|
||||
table.String("payment_method", 50).Nullable().Comment("支付方式: alipay, wechat, bank")
|
||||
|
||||
table.Timestamps()
|
||||
table.SoftDeletes()
|
||||
table.Unique("order_no")
|
||||
table.Index("user_id")
|
||||
table.Index("created_at")
|
||||
table.Comment(fmt.Sprintf("订单主表 - %s", tableName))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建 Migration 修改已存在的分表
|
||||
|
||||
创建一个新的 migration 文件,根据分表类型选择合适的方法获取所有已存在的分表:
|
||||
|
||||
#### 时间分表(按月分表)
|
||||
|
||||
使用 `utils.GetAllExistingShardingTables()` 获取所有已存在的时间分表:
|
||||
|
||||
```go
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/goravel/framework/contracts/database/schema"
|
||||
"github.com/goravel/framework/facades"
|
||||
"goravel/app/utils"
|
||||
)
|
||||
|
||||
type M20251228000001AddPaymentMethodToOrders struct {
|
||||
}
|
||||
|
||||
func (r *M20251228000001AddPaymentMethodToOrders) Signature() string {
|
||||
return "20251228000001_add_payment_method_to_orders"
|
||||
}
|
||||
|
||||
func (r *M20251228000001AddPaymentMethodToOrders) Up() error {
|
||||
// 获取所有已存在的订单主表分表(时间分表)
|
||||
ordersTables, err := utils.GetAllExistingShardingTables("orders")
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取订单分表列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 遍历所有分表,添加字段
|
||||
for _, tableName := range ordersTables {
|
||||
if !facades.Schema().HasTable(tableName) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查字段是否已存在(避免重复添加)
|
||||
if !facades.Schema().HasColumn(tableName, "payment_method") {
|
||||
if err := facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.String("payment_method", 50).Nullable().Comment("支付方式: alipay, wechat, bank").After("status")
|
||||
}); err != nil {
|
||||
return fmt.Errorf("修改分表 %s 失败: %v", tableName, err)
|
||||
}
|
||||
facades.Log().Infof("✓ 已为分表 %s 添加字段 payment_method", tableName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *M20251228000001AddPaymentMethodToOrders) Down() error {
|
||||
// 回滚操作:删除添加的字段
|
||||
ordersTables, err := utils.GetAllExistingShardingTables("orders")
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取订单分表列表失败: %v", err)
|
||||
}
|
||||
|
||||
for _, tableName := range ordersTables {
|
||||
if !facades.Schema().HasTable(tableName) {
|
||||
continue
|
||||
}
|
||||
|
||||
if facades.Schema().HasColumn(tableName, "payment_method") {
|
||||
if err := facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.DropColumn("payment_method")
|
||||
}); err != nil {
|
||||
return fmt.Errorf("回滚分表 %s 失败: %v", tableName, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 注册 Migration
|
||||
|
||||
在 `database/kernel.go` 的 `Migrations()` 方法中注册新的 migration:
|
||||
|
||||
```go
|
||||
func (kernel Kernel) Migrations() []schema.Migration {
|
||||
return []schema.Migration{
|
||||
// ... 其他 migrations
|
||||
&migrations.M20250128000001CreateOrdersTable{},
|
||||
&migrations.M20251228000001AddPaymentMethodToOrders{}, // 新添加的 migration
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 执行 Migration
|
||||
|
||||
运行 migration:
|
||||
|
||||
```bash
|
||||
go run . artisan migrate
|
||||
```
|
||||
|
||||
## 常用操作示例
|
||||
|
||||
### 添加字段
|
||||
|
||||
```go
|
||||
if !facades.Schema().HasColumn(tableName, "new_field") {
|
||||
facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.String("new_field", 100).Nullable().Comment("新字段").After("existing_field")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 修改字段类型
|
||||
|
||||
注意:某些数据库可能不支持直接修改字段类型,需要先删除再添加(会丢失数据,请谨慎操作):
|
||||
|
||||
```go
|
||||
// 方式1: 直接修改(如果数据库支持)
|
||||
facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.String("field_name", 200).Change() // 修改字段长度
|
||||
})
|
||||
|
||||
// 方式2: 删除后重新添加(会丢失数据)
|
||||
if facades.Schema().HasColumn(tableName, "field_name") {
|
||||
facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.DropColumn("field_name")
|
||||
})
|
||||
facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.String("field_name", 200).Comment("新字段")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 删除字段
|
||||
|
||||
```go
|
||||
if facades.Schema().HasColumn(tableName, "field_name") {
|
||||
facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.DropColumn("field_name")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 添加索引
|
||||
|
||||
```go
|
||||
indexes, _ := facades.Schema().GetIndexes(tableName)
|
||||
hasIndex := false
|
||||
for _, index := range indexes {
|
||||
if index.Name == "index_name" {
|
||||
hasIndex = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasIndex {
|
||||
facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.Index("field_name", "index_name")
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 删除索引
|
||||
|
||||
```go
|
||||
facades.Schema().Table(tableName, func(table schema.Blueprint) {
|
||||
table.DropIndex("index_name")
|
||||
})
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **字段检查**:在添加字段前,务必检查字段是否已存在,避免重复添加导致错误
|
||||
2. **表检查**:在操作前检查表是否存在
|
||||
3. **数据备份**:修改字段类型或删除字段前,请先备份数据
|
||||
4. **新分表**:修改表创建函数后,新创建的分表会自动包含新字段
|
||||
5. **已存在分表**:需要通过 migration 手动修改所有已存在的分表
|
||||
6. **回滚支持**:务必实现 `Down()` 方法,支持 migration 回滚
|
||||
|
||||
## 工具函数
|
||||
|
||||
`app/utils/sharding_helper.go` 提供了以下工具函数:
|
||||
|
||||
### 时间分表工具函数
|
||||
|
||||
- `GetAllExistingShardingTables(baseTableName string)`: 获取所有已存在的时间分表名称(格式:`{table}_YYYYMM`)
|
||||
- `GetShardingTableName(baseTableName string, orderTime time.Time)`: 根据时间获取分表名称
|
||||
- `GetShardingTableNames(baseTableName string, startTime, endTime time.Time)`: 获取时间范围内的所有分表名称
|
||||
|
||||
### 哈希分表工具函数
|
||||
|
||||
- `GetAllExistingShardingTablesByPattern(pattern string)`: 通过表名模式获取所有已存在的分表(适用于哈希分表,如 `"user_balance_logs_%"`)
|
||||
- `GetHashShardingTableName(baseTableName string, shardingKey uint, numberOfShards int)`: 通用的哈希分表名称生成函数
|
||||
|
||||
### 使用建议
|
||||
|
||||
- **时间分表**:使用 `GetAllExistingShardingTables("orders")` 获取所有分表
|
||||
- **哈希分表**:使用 `GetAllExistingShardingTablesByPattern("user_balance_logs_%")` 获取所有分表
|
||||
|
||||
## 完整示例
|
||||
|
||||
参考 `database/migrations/20251228001858_example_modify_orders_sharding_tables.go` 查看完整示例。
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
# 分表查询服务使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
`ShardingQueryService` 是一个通用的分表查询服务,用于简化多分表查询的实现。它封装了 UNION ALL 查询、分页、排序等通用逻辑,让开发者只需要关注业务特定的筛选条件和列定义。
|
||||
|
||||
## 核心优势
|
||||
|
||||
1. **代码复用**:避免为每个分表重复编写 UNION ALL 查询逻辑
|
||||
2. **统一优化**:所有分表查询都使用相同的优化策略(表存在性检查、列名明确指定等)
|
||||
3. **易于维护**:通用逻辑集中管理,修改一处即可影响所有使用该服务的表
|
||||
4. **类型安全**:通过接口和回调函数保证类型安全
|
||||
|
||||
## 使用步骤
|
||||
|
||||
### 1. 定义筛选条件结构体
|
||||
|
||||
```go
|
||||
// 例如:日志表的筛选条件
|
||||
type LogFilters struct {
|
||||
UserID uint // 用户ID
|
||||
Level string // 日志级别
|
||||
Keyword string // 关键词搜索
|
||||
StartTime time.Time // 开始时间
|
||||
EndTime time.Time // 结束时间
|
||||
OrderBy string // 排序字段(格式:字段:asc/desc)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实现 WHERE 条件构建函数
|
||||
|
||||
```go
|
||||
// buildLogWhereClause 构建日志查询的 WHERE 条件
|
||||
func (s *LogServiceImpl) buildLogWhereClause(filters any) (string, []any) {
|
||||
logFilters, ok := filters.(LogFilters)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var conditions []string
|
||||
var args []any
|
||||
|
||||
// 时间范围
|
||||
if !logFilters.StartTime.IsZero() {
|
||||
conditions = append(conditions, "created_at >= ?")
|
||||
args = append(args, logFilters.StartTime)
|
||||
}
|
||||
if !logFilters.EndTime.IsZero() {
|
||||
conditions = append(conditions, "created_at <= ?")
|
||||
args = append(args, logFilters.EndTime)
|
||||
}
|
||||
|
||||
// 用户ID筛选
|
||||
if logFilters.UserID > 0 {
|
||||
conditions = append(conditions, "user_id = ?")
|
||||
args = append(args, logFilters.UserID)
|
||||
}
|
||||
|
||||
// 日志级别筛选
|
||||
if logFilters.Level != "" {
|
||||
conditions = append(conditions, "level = ?")
|
||||
args = append(args, logFilters.Level)
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if logFilters.Keyword != "" {
|
||||
conditions = append(conditions, "(message LIKE ? OR context LIKE ?)")
|
||||
keyword := "%" + logFilters.Keyword + "%"
|
||||
args = append(args, keyword, keyword)
|
||||
}
|
||||
|
||||
return strings.Join(conditions, " AND "), args
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 实现列名获取函数
|
||||
|
||||
```go
|
||||
// getLogTableColumns 获取日志表的所有列名
|
||||
func (s *LogServiceImpl) getLogTableColumns() string {
|
||||
// 只包含模型中定义的字段,确保所有分表都有这些字段
|
||||
columns := []string{
|
||||
"id",
|
||||
"user_id",
|
||||
"level",
|
||||
"message",
|
||||
"context",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"deleted_at",
|
||||
}
|
||||
return strings.Join(columns, ", ")
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 初始化分表查询服务
|
||||
|
||||
```go
|
||||
type LogServiceImpl struct {
|
||||
shardingService ShardingService
|
||||
shardingQueryService ShardingQueryService
|
||||
}
|
||||
|
||||
func NewLogService() *LogServiceImpl {
|
||||
service := &LogServiceImpl{
|
||||
shardingService: NewShardingService(),
|
||||
}
|
||||
|
||||
// 初始化分表查询服务
|
||||
service.shardingQueryService = NewShardingQueryService(ShardingQueryConfig{
|
||||
BaseTableName: "logs",
|
||||
GetColumns: func() string {
|
||||
return service.getLogTableColumns()
|
||||
},
|
||||
BuildWhereClause: func(filters any) (string, []any) {
|
||||
return service.buildLogWhereClause(filters)
|
||||
},
|
||||
GetAllowedOrderFields: func() map[string]bool {
|
||||
return map[string]bool{
|
||||
"id": true,
|
||||
"user_id": true,
|
||||
"level": true,
|
||||
"created_at": true,
|
||||
"updated_at": true,
|
||||
}
|
||||
},
|
||||
DefaultOrderBy: "created_at:desc",
|
||||
ModuleName: "log",
|
||||
})
|
||||
|
||||
return service
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 使用分表查询服务
|
||||
|
||||
```go
|
||||
// GetLogs 查询日志列表(支持多分表)
|
||||
func (s *LogServiceImpl) GetLogs(filters LogFilters, page, pageSize int) ([]models.Log, int64, error) {
|
||||
// 验证时间范围
|
||||
valid, err := utils.ValidateTimeRange(filters.StartTime, filters.EndTime)
|
||||
if !valid {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取需要查询的所有分表
|
||||
tableNames := utils.GetShardingTableNames("logs", filters.StartTime, filters.EndTime)
|
||||
if len(tableNames) == 0 {
|
||||
return []models.Log{}, 0, nil
|
||||
}
|
||||
|
||||
// 如果只有一个分表,直接查询(使用 ORM)
|
||||
if len(tableNames) == 1 {
|
||||
return s.querySingleTable(tableNames[0], filters, page, pageSize)
|
||||
}
|
||||
|
||||
// 多个分表:使用通用分表查询服务
|
||||
var logs []models.Log
|
||||
total, err := s.shardingQueryService.QueryMultipleTables(tableNames, filters, page, pageSize, &logs)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return logs, total, nil
|
||||
}
|
||||
|
||||
// GetAllLogsForExport 获取所有日志用于导出
|
||||
func (s *LogServiceImpl) GetAllLogsForExport(filters LogFilters) ([]models.Log, error) {
|
||||
// 验证时间范围
|
||||
valid, err := utils.ValidateTimeRange(filters.StartTime, filters.EndTime)
|
||||
if !valid {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取需要查询的所有分表
|
||||
tableNames := utils.GetShardingTableNames("logs", filters.StartTime, filters.EndTime)
|
||||
if len(tableNames) == 0 {
|
||||
return []models.Log{}, nil
|
||||
}
|
||||
|
||||
// 如果只有一个分表,直接查询
|
||||
if len(tableNames) == 1 {
|
||||
query := s.buildShardingQuery(tableNames[0], filters)
|
||||
orderBy := filters.OrderBy
|
||||
if orderBy == "" {
|
||||
orderBy = "created_at:desc"
|
||||
}
|
||||
query = s.applyOrderBy(query, orderBy)
|
||||
|
||||
var logs []models.Log
|
||||
if err := query.Find(&logs); err != nil {
|
||||
return nil, apperrors.ErrQueryFailed.WithError(err)
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// 多个分表:使用通用分表查询服务
|
||||
var logs []models.Log
|
||||
err = s.shardingQueryService.QueryMultipleTablesForExport(tableNames, filters, &logs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### ShardingQueryConfig 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 | 必填 |
|
||||
|------|------|------|------|
|
||||
| `BaseTableName` | `string` | 基础表名,如 "orders" | 是 |
|
||||
| `GetColumns` | `func() string` | 获取表的所有列名(用于 UNION ALL 查询) | 是 |
|
||||
| `BuildWhereClause` | `func(any) (string, []any)` | 构建 WHERE 条件,返回条件字符串和参数列表 | 是 |
|
||||
| `GetAllowedOrderFields` | `func() map[string]bool` | 获取允许排序的字段列表 | 是 |
|
||||
| `DefaultOrderBy` | `string` | 默认排序,格式:字段:方向,如 "created_at:desc" | 否(默认:created_at:desc) |
|
||||
| `ModuleName` | `string` | 模块名称,用于日志记录 | 否(默认:sharding) |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **列名必须明确指定**:不要使用 `SELECT *`,必须明确列出所有列名,确保不同分表的列数一致
|
||||
2. **只包含模型中的字段**:列名列表应该只包含模型中定义的字段,避免查询不存在的字段
|
||||
3. **WHERE 条件格式**:`BuildWhereClause` 返回的条件字符串不应该包含 `WHERE` 关键字,只返回条件部分(如 "user_id = ? AND status = ?")
|
||||
4. **时间范围处理**:如果筛选条件包含时间范围,应该在调用 `QueryMultipleTables` 之前验证时间范围
|
||||
5. **单表优化**:如果只有一个分表,建议直接使用 ORM 查询,而不是使用 UNION ALL(性能更好)
|
||||
|
||||
## 完整示例
|
||||
|
||||
参考 `app/services/order_service.go` 中的实现,这是使用通用分表查询服务的完整示例。
|
||||
|
||||
## 优势对比
|
||||
|
||||
### 使用通用服务前(每个表都需要重复实现)
|
||||
|
||||
```go
|
||||
// 每个表都需要实现这些方法
|
||||
func (s *OrderServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
|
||||
func (s *LogServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
|
||||
func (s *PaymentServiceImpl) queryMultipleTablesWithUnion(...) { /* 100+ 行代码 */ }
|
||||
// ... 每个表都重复
|
||||
```
|
||||
|
||||
### 使用通用服务后(只需配置)
|
||||
|
||||
```go
|
||||
// 每个表只需要配置一次
|
||||
service.shardingQueryService = NewShardingQueryService(ShardingQueryConfig{
|
||||
// ... 配置
|
||||
})
|
||||
|
||||
// 使用时只需一行代码
|
||||
total, err := s.shardingQueryService.QueryMultipleTables(tableNames, filters, page, pageSize, &results)
|
||||
```
|
||||
|
||||
## 扩展性
|
||||
|
||||
如果需要添加新的通用功能(如缓存、性能监控等),只需要在 `ShardingQueryService` 中修改一次,所有使用该服务的表都会自动获得这些功能。
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# 系统日志记录指南
|
||||
|
||||
## 日志级别记录策略
|
||||
|
||||
### ✅ 需要记录到系统日志的级别
|
||||
|
||||
#### 1. **error(错误)级别** - **必须记录**
|
||||
- **数据库操作失败** - 创建、更新、删除数据失败
|
||||
- **文件操作失败** - 保存、删除文件失败
|
||||
- **系统服务异常** - 缓存、队列、分片表等基础设施错误
|
||||
- **关键业务逻辑错误** - 订单创建失败、支付失败等影响业务流程的错误
|
||||
- **外部服务调用失败** - 第三方API调用失败
|
||||
|
||||
#### 2. **warning(警告)级别** - **应该记录**
|
||||
- **非关键操作失败但可继续** - 如分片删除失败、分片读取失败(不影响主流程)
|
||||
- **资源清理失败** - 临时文件清理失败、缓存清理失败等
|
||||
- **性能警告** - 操作耗时过长、资源使用率高等
|
||||
- **配置问题** - 配置缺失或无效,但系统仍可运行
|
||||
|
||||
### ❌ 不需要记录到系统日志的级别
|
||||
|
||||
#### 3. **info(信息)级别** - **不记录到数据库**
|
||||
- **正常业务流程** - 订单创建成功、支付成功等
|
||||
- **操作统计** - 用户登录、数据导出等
|
||||
- **系统状态** - 服务启动、配置加载等
|
||||
- **说明**:这些信息只在文件日志中记录,不写入数据库,避免产生大量日志
|
||||
|
||||
#### 4. **debug(调试)级别** - **不记录到数据库**
|
||||
- **调试信息** - 变量值、函数调用栈等
|
||||
- **详细执行流程** - 仅在开发环境需要
|
||||
- **说明**:只在开发环境的文件日志中记录,生产环境通常不记录
|
||||
|
||||
## 原则总结
|
||||
|
||||
**需要记录到系统日志的错误:**
|
||||
1. **数据库操作失败** - 创建、更新、删除数据失败
|
||||
2. **文件操作失败** - 保存、删除文件失败
|
||||
3. **系统服务异常** - 缓存、队列、分片表等基础设施错误
|
||||
4. **关键业务逻辑错误** - 订单创建失败、支付失败等影响业务流程的错误
|
||||
5. **外部服务调用失败** - 第三方API调用失败
|
||||
|
||||
**需要记录到系统日志的警告:**
|
||||
1. **非关键操作失败** - 不影响主流程但需要关注的问题
|
||||
2. **资源清理失败** - 临时文件、缓存清理失败等
|
||||
3. **性能警告** - 需要监控的性能问题
|
||||
|
||||
**不需要记录到系统日志的错误:**
|
||||
1. **参数验证错误** - 如"订单ID不能为空"、"user_id不能为空"
|
||||
2. **业务逻辑的正常错误** - 如"订单不存在"、"用户不存在"、"记录不存在"
|
||||
3. **查询不存在的记录** - 这些是正常的业务场景,不是系统错误
|
||||
|
||||
## 需要记录的错误清单
|
||||
|
||||
### 1. order_service.go
|
||||
|
||||
#### ✅ 需要记录
|
||||
- `fmt.Errorf("获取锁失败: %v", err)` - 系统服务异常
|
||||
- `fmt.Errorf("生成唯一订单号失败,请重试")` - 关键业务逻辑错误
|
||||
- `fmt.Errorf("创建订单失败: %v", err)` - 数据库操作失败
|
||||
- `fmt.Errorf("创建订单详情失败: %v", err)` - 数据库操作失败
|
||||
- `fmt.Errorf("删除订单详情失败: %v", err)` - 数据库操作失败
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `fmt.Errorf("订单ID不能为空")` - 参数验证错误
|
||||
- `fmt.Errorf("订单不存在")` - 业务逻辑的正常错误(多次出现)
|
||||
|
||||
### 2. attachment_service.go
|
||||
|
||||
#### ✅ 需要记录
|
||||
- `fmt.Errorf("保存分片失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("创建目标目录失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("创建目标文件失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("写入分片 %d 失败: %w", i, err)` - 文件操作失败
|
||||
- `fmt.Errorf("关闭目标文件失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("创建附件记录失败: %w", err)` - 数据库操作失败
|
||||
- `fmt.Errorf("保存文件失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("更新附件显示名称失败: %v", err)` - 数据库操作失败
|
||||
- `fmt.Errorf("删除文件失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("删除附件记录失败: %w", err)` - 数据库操作失败
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `fmt.Errorf("分片索引无效")` - 参数验证错误
|
||||
- `fmt.Errorf("分片 %d 不存在", i)` - 业务逻辑的正常错误
|
||||
- `fmt.Errorf("附件不存在: %v", err)` - 业务逻辑的正常错误(多次出现)
|
||||
- `fmt.Errorf("查询附件失败: %v", err)` - 查询错误,但可能是正常的"不存在"情况
|
||||
|
||||
### 3. user_service.go
|
||||
|
||||
#### ✅ 需要记录
|
||||
- `fmt.Errorf("更新用户余额失败: %v", err)` - 数据库操作失败
|
||||
- `fmt.Errorf("创建余额变动记录失败: %v", err)` - 数据库操作失败
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `fmt.Errorf("用户不存在: %v", err)` - 业务逻辑的正常错误
|
||||
- `fmt.Errorf("余额不足,当前余额: %.2f", user.Balance)` - 业务逻辑的正常错误
|
||||
- `fmt.Errorf("无效的变动类型: %s", logType)` - 参数验证错误
|
||||
|
||||
### 4. user_balance_log_service.go
|
||||
|
||||
#### ✅ 需要记录
|
||||
- `fmt.Errorf("创建余额变动记录失败: %v", err)` - 数据库操作失败
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `fmt.Errorf("user_id 不能为空,GORM Sharding 需要 ShardingKey")` - 参数验证错误(多次出现)
|
||||
- `fmt.Errorf("用户不存在: %v", err)` - 业务逻辑的正常错误
|
||||
|
||||
### 5. export_service.go
|
||||
|
||||
#### ✅ 需要记录
|
||||
- `fmt.Errorf("写入CSV表头失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("写入CSV数据失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("CSV写入失败: %w", err)` - 文件操作失败
|
||||
- `fmt.Errorf("保存文件失败: %w", err)` - 文件操作失败
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `fmt.Errorf("Excel导出功能暂未实现,请使用CSV格式")` - 功能未实现,不是错误
|
||||
|
||||
### 6. export_record_service.go
|
||||
|
||||
#### ✅ 需要记录
|
||||
- `fmt.Errorf("删除导出记录失败: %v", err)` - 数据库操作失败
|
||||
- `fmt.Errorf("批量删除导出记录失败: %v", err)` - 数据库操作失败
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `fmt.Errorf("导出记录不存在: %v", err)` - 业务逻辑的正常错误(多次出现)
|
||||
- `fmt.Errorf("查询导出记录失败: %v", err)` - 查询错误
|
||||
|
||||
### 7. 各种 service 的 GetByID 方法
|
||||
|
||||
#### ❌ 不需要记录(统一处理)
|
||||
以下错误都是查询不存在的记录,属于正常业务场景:
|
||||
- `fmt.Errorf("系统日志不存在: %v", err)`
|
||||
- `fmt.Errorf("操作日志不存在: %v", err)`
|
||||
- `fmt.Errorf("登录日志不存在: %v", err)`
|
||||
- `fmt.Errorf("部门不存在: %v", err)`
|
||||
- `fmt.Errorf("权限不存在: %v", err)`
|
||||
- `fmt.Errorf("角色不存在: %v", err)`
|
||||
- `fmt.Errorf("黑名单不存在: %v", err)`
|
||||
- `fmt.Errorf("字典不存在: %v", err)`
|
||||
- `fmt.Errorf("管理员不存在: %v", err)`
|
||||
|
||||
### 8. 基础设施相关
|
||||
|
||||
#### ✅ 需要记录
|
||||
- `providers/database_service_provider.go`: `fmt.Errorf("获取 GORM DB 实例失败: %v", err)` - 系统服务异常
|
||||
- `providers/database_service_provider.go`: `fmt.Errorf("注册 GORM Sharding 插件失败: %v", err)` - 系统服务异常
|
||||
- `utils/gorm_sharding.go`: `fmt.Errorf("ORM 未初始化")` - 系统服务异常
|
||||
- `utils/gorm_sharding.go`: `fmt.Errorf("无法通过反射获取原生 GORM DB 实例...")` - 系统服务异常
|
||||
- `utils/sharding_helper.go`: `fmt.Errorf("查询分表失败: %v", err)` - 系统服务异常(多次出现)
|
||||
- `services/sharding_service.go`: `fmt.Errorf("创建分表 %s 失败: %v", tableName, err)` - 系统服务异常
|
||||
- `console/commands/create_order_sharding_tables.go`: `fmt.Errorf("创建分表 %s 失败: %v", tableName, err)` - 系统服务异常(多次出现)
|
||||
- `console/commands/queue_clear.go`: 所有 Redis 相关错误 - 系统服务异常
|
||||
- `console/commands/queue_stats.go`: 所有 Redis 相关错误 - 系统服务异常
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `console/commands/create_order_sharding_tables.go`: `fmt.Errorf("月份格式错误...")` - 参数验证错误
|
||||
|
||||
### 9. 控制器相关
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `order_controller.go`: 时间格式验证错误 - 参数验证错误
|
||||
|
||||
### 10. 其他服务
|
||||
|
||||
#### ✅ 需要记录
|
||||
- `google_authenticator_service.go`: 所有错误 - 外部服务调用失败
|
||||
|
||||
#### ❌ 不需要记录
|
||||
- `admin_service.go`: `fmt.Errorf("查询管理员失败: %v", err)` - 查询错误,可能是正常的"不存在"
|
||||
|
||||
## 实现建议
|
||||
|
||||
### 方式1:使用 errorlog.RecordHTTP(推荐)
|
||||
|
||||
```go
|
||||
import "goravel/app/utils/errorlog"
|
||||
|
||||
// 在 HTTP 请求处理中
|
||||
if err != nil {
|
||||
errorlog.RecordHTTP(ctx, "order", "创建订单失败", map[string]any{
|
||||
"error": err.Error(),
|
||||
"user_id": userID,
|
||||
"amount": amount,
|
||||
}, "创建订单失败: %v", err)
|
||||
return nil, nil, fmt.Errorf("创建订单失败: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 方式2:使用 response.ErrorWithLog
|
||||
|
||||
```go
|
||||
import "goravel/app/http/response"
|
||||
|
||||
// 在控制器中
|
||||
if err != nil {
|
||||
return response.ErrorWithLog(ctx, http.StatusInternalServerError, "create_failed", "order", "创建订单失败", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 方式3:使用 systemLogService.RecordHTTP
|
||||
|
||||
```go
|
||||
import "goravel/app/services"
|
||||
|
||||
systemLogService := services.NewSystemLogService()
|
||||
_ = systemLogService.RecordHTTP(ctx, "error", "order", "创建订单失败", map[string]any{
|
||||
"error": err.Error(),
|
||||
"user_id": userID,
|
||||
})
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **避免重复记录**:如果已经在 controller 层记录了日志,service 层就不需要再记录
|
||||
2. **记录上下文信息**:尽量记录相关的业务数据(如订单ID、用户ID等),方便排查问题
|
||||
3. **日志级别**:大部分错误使用 "error" 级别,警告使用 "warning" 级别
|
||||
4. **性能考虑**:系统日志记录是异步的,但也要避免在高频操作中记录过多日志
|
||||
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
# 单元测试指南
|
||||
|
||||
本项目已添加前后端单元测试支持,以提升代码质量和可维护性。
|
||||
|
||||
## 后端测试 (Go - Goravel)
|
||||
|
||||
基于 [Goravel 测试框架](https://www.goravel.dev/zh_CN/testing/getting-started.html),使用 `testify/suite` 组织测试。
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── test_case.go # 测试基类
|
||||
├── feature/ # 功能测试(集成测试)
|
||||
│ ├── main_test.go # 测试入口
|
||||
│ ├── example_test.go
|
||||
│ └── ...
|
||||
├── unit/ # 单元测试
|
||||
│ ├── main_test.go # 测试入口
|
||||
│ ├── token_service_test.go
|
||||
│ ├── tree_service_test.go
|
||||
│ ├── ip_matcher_test.go
|
||||
│ └── pagination_test.go
|
||||
└── services/ # 服务层测试
|
||||
└── main_test.go
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
go test ./tests/...
|
||||
|
||||
# 运行单元测试
|
||||
go test ./tests/unit/...
|
||||
|
||||
# 运行功能测试
|
||||
go test ./tests/feature/...
|
||||
|
||||
# 显示详细输出
|
||||
go test -v ./tests/...
|
||||
|
||||
# 运行特定测试
|
||||
go test -v -run TestTokenService ./tests/unit/...
|
||||
|
||||
# 生成覆盖率报告
|
||||
go test -coverprofile=coverage.out ./tests/...
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
```
|
||||
|
||||
### 创建测试
|
||||
|
||||
使用 Artisan 命令创建测试:
|
||||
|
||||
```bash
|
||||
go run . artisan make:test unit/MyServiceTest
|
||||
go run . artisan make:test feature/MyFeatureTest
|
||||
```
|
||||
|
||||
### 测试示例
|
||||
|
||||
```go
|
||||
// tests/unit/token_service_test.go
|
||||
package unit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"goravel/tests"
|
||||
)
|
||||
|
||||
type TokenServiceTestSuite struct {
|
||||
suite.Suite
|
||||
tests.TestCase
|
||||
}
|
||||
|
||||
func TestTokenServiceTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(TokenServiceTestSuite))
|
||||
}
|
||||
|
||||
func (s *TokenServiceTestSuite) SetupTest() {
|
||||
// 每个测试前执行
|
||||
}
|
||||
|
||||
func (s *TokenServiceTestSuite) TearDownTest() {
|
||||
// 每个测试后执行
|
||||
}
|
||||
|
||||
func (s *TokenServiceTestSuite) TestHashToken() {
|
||||
// 测试 token 哈希
|
||||
s.Equal(64, len(hashToken("test")))
|
||||
}
|
||||
|
||||
func (s *TokenServiceTestSuite) TestGenerateRandomToken() {
|
||||
token1 := generateRandomToken()
|
||||
token2 := generateRandomToken()
|
||||
|
||||
s.Len(token1, 40)
|
||||
s.NotEqual(token1, token2)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端测试 (Vitest)
|
||||
|
||||
### 目录结构
|
||||
|
||||
前端测试支持两种组织方式:
|
||||
|
||||
#### 方式一:共置模式(当前使用)✅ 推荐
|
||||
|
||||
测试文件放在被测模块旁边的 `__tests__` 目录:
|
||||
|
||||
```
|
||||
html/src/
|
||||
├── composables/
|
||||
│ ├── useCrud.js
|
||||
│ ├── useDebounce.js
|
||||
│ └── __tests__/ # 测试放在模块旁边
|
||||
│ ├── useCrud.test.js
|
||||
│ └── useDebounce.test.js
|
||||
├── utils/
|
||||
│ ├── validation.js
|
||||
│ ├── storage.js
|
||||
│ └── __tests__/
|
||||
│ ├── validation.test.js
|
||||
│ └── storage.test.js
|
||||
└── components/
|
||||
└── __tests__/
|
||||
└── ...
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 测试文件与源码紧密关联,便于查找和维护
|
||||
- 符合 Vue/React 社区最佳实践
|
||||
- 模块独立性强,便于移动或删除
|
||||
|
||||
#### 方式二:集中模式
|
||||
|
||||
所有测试放在根目录的 `tests` 目录:
|
||||
|
||||
```
|
||||
html/
|
||||
├── src/
|
||||
│ ├── composables/
|
||||
│ ├── utils/
|
||||
│ └── components/
|
||||
└── tests/ # 所有测试集中存放
|
||||
├── unit/
|
||||
│ ├── composables/
|
||||
│ │ ├── useCrud.test.js
|
||||
│ │ └── useDebounce.test.js
|
||||
│ └── utils/
|
||||
│ ├── validation.test.js
|
||||
│ └── storage.test.js
|
||||
└── integration/
|
||||
└── ...
|
||||
```
|
||||
|
||||
**优点:**
|
||||
- 测试代码与源码分离
|
||||
- 与后端测试结构一致
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
cd html
|
||||
|
||||
# 交互式监听模式
|
||||
npm test
|
||||
|
||||
# 单次运行
|
||||
npm run test:run
|
||||
|
||||
# 生成覆盖率报告
|
||||
npm run test:coverage
|
||||
```
|
||||
|
||||
### 当前测试文件
|
||||
|
||||
| 测试文件 | 覆盖内容 | 测试数量 |
|
||||
|----------|----------|----------|
|
||||
| `composables/__tests__/useDebounce.test.js` | 防抖功能 | 12 |
|
||||
| `composables/__tests__/useCrud.test.js` | CRUD 操作 | 19 |
|
||||
| `utils/__tests__/validation.test.js` | 验证器函数 | 41 |
|
||||
| `utils/__tests__/storage.test.js` | Storage 工具 | 13 |
|
||||
|
||||
### 测试示例
|
||||
|
||||
```javascript
|
||||
// src/utils/__tests__/validation.test.js
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { validators } from '../validation'
|
||||
|
||||
describe('validators', () => {
|
||||
describe('required', () => {
|
||||
it('应该拒绝空字符串', () => {
|
||||
expect(validators.required('')).not.toBe(true)
|
||||
})
|
||||
|
||||
it('应该接受有效字符串', () => {
|
||||
expect(validators.required('hello')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('email', () => {
|
||||
it('应该接受有效邮箱', () => {
|
||||
expect(validators.email('test@example.com')).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试最佳实践
|
||||
|
||||
### 1. 命名约定
|
||||
|
||||
```go
|
||||
// Go: TestSuiteName + TestMethodName
|
||||
func (s *TokenServiceTestSuite) TestHashToken_ValidInput()
|
||||
|
||||
// JavaScript: describe + it
|
||||
describe('validators', () => {
|
||||
describe('required', () => {
|
||||
it('应该拒绝空字符串', () => {})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 表格驱动测试 (Go)
|
||||
|
||||
```go
|
||||
func (s *TokenServiceTestSuite) TestHashToken() {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{"正常 token", "test-token", 64},
|
||||
{"空 token", "", 64},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
s.Run(tt.name, func() {
|
||||
got := hashToken(tt.input)
|
||||
s.Len(got, tt.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Mock 使用 (JavaScript)
|
||||
|
||||
```javascript
|
||||
import { vi } from 'vitest'
|
||||
|
||||
// Mock 模块
|
||||
vi.mock('element-plus', () => ({
|
||||
ElMessage: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// 验证调用
|
||||
expect(ElMessage.success).toHaveBeenCalledWith('操作成功')
|
||||
```
|
||||
|
||||
### 4. 异步测试
|
||||
|
||||
```javascript
|
||||
// async/await
|
||||
it('应该处理异步操作', async () => {
|
||||
const result = await asyncFunction()
|
||||
expect(result).toBe('expected')
|
||||
})
|
||||
|
||||
// Fake timers
|
||||
beforeEach(() => vi.useFakeTimers())
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD 集成
|
||||
|
||||
### GitHub Actions 示例
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.24'
|
||||
- run: go test -v ./tests/...
|
||||
|
||||
frontend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: cd html && npm install
|
||||
- run: cd html && npm run test:run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试覆盖率目标
|
||||
|
||||
| 模块类型 | 当前状态 | 目标覆盖率 |
|
||||
|----------|----------|------------|
|
||||
| 工具函数 | ✅ 已添加 | 80%+ |
|
||||
| Composables | ✅ 已添加 (TypeScript) | 70%+ |
|
||||
| Services | ✅ 已添加 | 60%+ |
|
||||
| Controllers | ✅ 已添加(集成测试) | 40%+ |
|
||||
|
||||
---
|
||||
|
||||
## Controller 集成测试
|
||||
|
||||
后端 Controller 集成测试位于 `tests/feature/` 目录:
|
||||
|
||||
### 测试文件
|
||||
|
||||
| 文件 | 覆盖内容 |
|
||||
|------|----------|
|
||||
| `admin_api_test.go` | 管理员登录、信息、列表、角色、菜单、部门、日志等 |
|
||||
| `blacklist_api_test.go` | 黑名单 CRUD、IP格式验证、批量删除 |
|
||||
| `permission_test.go` | 权限列表、角色权限绑定 |
|
||||
|
||||
### 运行集成测试
|
||||
|
||||
```bash
|
||||
# 运行所有集成测试(需要 Docker)
|
||||
go test -v ./tests/feature/...
|
||||
|
||||
# 运行特定测试
|
||||
go test -v -run TestAdminApiTestSuite ./tests/feature/...
|
||||
go test -v -run TestBlacklistApiTestSuite ./tests/feature/...
|
||||
```
|
||||
|
||||
### 测试示例
|
||||
|
||||
```go
|
||||
func (s *AdminApiTestSuite) TestLogin_Success() {
|
||||
body := strings.NewReader(`{"username":"admin","password":"admin123"}`)
|
||||
resp, err := s.Http(s.T()).
|
||||
WithHeader("Content-Type", "application/json").
|
||||
Post("/api/admin/login", body)
|
||||
|
||||
s.Require().NoError(err)
|
||||
resp.AssertSuccessful()
|
||||
|
||||
content, err := resp.Content()
|
||||
s.Require().NoError(err)
|
||||
|
||||
var result map[string]any
|
||||
json.Unmarshal([]byte(content), &result)
|
||||
s.Equal(float64(200), result["code"])
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 集成测试需要 Docker 环境(自动创建测试数据库和 Redis)
|
||||
- 每个测试会执行 `RefreshDatabase()` 重置数据库
|
||||
- 使用 `Seed()` 填充测试数据
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 前端测试应该放在哪里?
|
||||
|
||||
**A:** 推荐使用共置模式(`__tests__` 目录),这是 Vue/React 社区的最佳实践。
|
||||
|
||||
### Q: 后端测试为什么放在 tests 目录?
|
||||
|
||||
**A:** 这是 Goravel 框架的推荐做法,参考 [官方文档](https://www.goravel.dev/zh_CN/testing/getting-started.html)。
|
||||
|
||||
### Q: 如何添加新测试?
|
||||
|
||||
**A:**
|
||||
- 后端:`go run . artisan make:test unit/MyTest`
|
||||
- 前端:在对应模块的 `__tests__` 目录创建 `.test.js` 文件
|
||||
+2137
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,226 @@
|
||||
# 生产环境使用 pprof 性能分析
|
||||
|
||||
## 概述
|
||||
|
||||
pprof 是 Go 语言内置的性能分析工具,可以帮助你诊断生产环境中的性能问题。本指南介绍如何在生产环境中安全地使用 pprof。
|
||||
|
||||
## 安全配置
|
||||
|
||||
### 1. 启用 pprof
|
||||
|
||||
在 `.env` 文件中添加以下配置:
|
||||
|
||||
```env
|
||||
# 启用 pprof(生产环境需要显式设置)
|
||||
PPROF_ENABLED=true
|
||||
|
||||
# IP 白名单(推荐使用)
|
||||
# 支持单个 IP 和 CIDR 格式,多个 IP 用逗号分隔
|
||||
PPROF_ALLOWED_IPS=127.0.0.1,192.168.1.100,10.0.0.0/8
|
||||
|
||||
# 访问 token(可选,但强烈推荐)
|
||||
# 如果设置,需要在请求头或查询参数中提供
|
||||
PPROF_TOKEN=your-secret-token-here
|
||||
```
|
||||
|
||||
### 2. 安全建议
|
||||
|
||||
**强烈推荐同时使用 IP 白名单和 token:**
|
||||
- IP 白名单:限制只有特定 IP 可以访问
|
||||
- Token 认证:即使 IP 泄露,也需要 token 才能访问
|
||||
|
||||
**不推荐在生产环境不设置任何限制:**
|
||||
- 如果 `PPROF_ALLOWED_IPS` 为空且 `PPROF_TOKEN` 为空,任何人都可以访问性能数据
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 方法一:使用 go tool pprof(推荐)
|
||||
|
||||
即使生产服务器没有 Go 环境,你也可以在本地机器上使用 `go tool pprof` 连接到远程服务器。
|
||||
|
||||
#### 1. 安装 Go 工具链
|
||||
|
||||
在你的本地机器上安装 Go(如果还没有安装):
|
||||
- 下载地址:https://golang.org/dl/
|
||||
- 或者使用包管理器:`brew install go` (macOS) / `apt-get install golang-go` (Linux)
|
||||
|
||||
#### 2. 连接到远程服务器
|
||||
|
||||
```bash
|
||||
# CPU 性能分析(30秒采样)
|
||||
go tool pprof http://your-server:3008/debug/pprof/profile?token=your-secret-token-here
|
||||
|
||||
# 堆内存分析
|
||||
go tool pprof http://your-server:3008/debug/pprof/heap?token=your-secret-token-here
|
||||
|
||||
# 协程分析
|
||||
go tool pprof http://your-server:3008/debug/pprof/goroutine?token=your-secret-token-here
|
||||
|
||||
# 线程创建分析
|
||||
go tool pprof http://your-server:3008/debug/pprof/threadcreate?token=your-secret-token-here
|
||||
```
|
||||
|
||||
#### 3. 使用请求头传递 token
|
||||
|
||||
```bash
|
||||
# 使用 curl 设置请求头
|
||||
curl -H "X-Pprof-Token: your-secret-token-here" \
|
||||
http://your-server:3008/debug/pprof/heap > heap.prof
|
||||
|
||||
# 然后使用 go tool pprof 分析
|
||||
go tool pprof heap.prof
|
||||
```
|
||||
|
||||
### 方法二:导出数据文件
|
||||
|
||||
如果无法直接连接,可以先导出数据文件,然后在有 Go 环境的机器上分析。
|
||||
|
||||
#### 1. 导出 pprof 数据
|
||||
|
||||
```bash
|
||||
# 使用 curl 导出数据(需要 token)
|
||||
curl -H "X-Pprof-Token: your-secret-token-here" \
|
||||
http://your-server:3008/debug/pprof/heap > heap.prof
|
||||
|
||||
curl -H "X-Pprof-Token: your-secret-token-here" \
|
||||
http://your-server:3008/debug/pprof/profile?seconds=30 > cpu.prof
|
||||
```
|
||||
|
||||
#### 2. 传输到本地机器
|
||||
|
||||
```bash
|
||||
# 使用 scp 传输文件
|
||||
scp user@your-server:/path/to/heap.prof ./
|
||||
```
|
||||
|
||||
#### 3. 在本地分析
|
||||
|
||||
```bash
|
||||
# 分析堆内存
|
||||
go tool pprof heap.prof
|
||||
|
||||
# 分析 CPU
|
||||
go tool pprof cpu.prof
|
||||
```
|
||||
|
||||
### 方法三:使用 Web 界面
|
||||
|
||||
直接在浏览器中访问(需要 token):
|
||||
|
||||
```
|
||||
http://your-server:3008/debug/pprof/?token=your-secret-token-here
|
||||
```
|
||||
|
||||
或者使用请求头:
|
||||
|
||||
```bash
|
||||
# 使用 curl 访问主页
|
||||
curl -H "X-Pprof-Token: your-secret-token-here" \
|
||||
http://your-server:3008/debug/pprof/
|
||||
```
|
||||
|
||||
## 常用分析命令
|
||||
|
||||
### CPU 性能分析
|
||||
|
||||
```bash
|
||||
# 30秒 CPU 采样
|
||||
go tool pprof http://your-server:3008/debug/pprof/profile?token=your-token
|
||||
|
||||
# 交互式命令
|
||||
(pprof) top10 # 查看占用 CPU 最多的 10 个函数
|
||||
(pprof) list 函数名 # 查看函数详细代码
|
||||
(pprof) web # 生成 SVG 图表(需要安装 graphviz)
|
||||
(pprof) png # 生成 PNG 图表
|
||||
```
|
||||
|
||||
### 内存分析
|
||||
|
||||
```bash
|
||||
# 堆内存分析
|
||||
go tool pprof http://your-server:3008/debug/pprof/heap?token=your-token
|
||||
|
||||
# 交互式命令
|
||||
(pprof) top10 # 查看占用内存最多的 10 个函数
|
||||
(pprof) alloc_space # 查看累计分配的内存
|
||||
(pprof) inuse_space # 查看当前使用的内存
|
||||
```
|
||||
|
||||
### 协程分析
|
||||
|
||||
```bash
|
||||
# 协程分析(排查 goroutine 泄漏)
|
||||
go tool pprof http://your-server:3008/debug/pprof/goroutine?token=your-token
|
||||
|
||||
# 交互式命令
|
||||
(pprof) top10 # 查看协程数量最多的 10 个函数
|
||||
```
|
||||
|
||||
## 常见问题排查
|
||||
|
||||
### 1. CPU 使用率过高
|
||||
|
||||
```bash
|
||||
# 分析 CPU profile
|
||||
go tool pprof http://your-server:3008/debug/pprof/profile?token=your-token
|
||||
|
||||
# 查看 top 函数
|
||||
(pprof) top20
|
||||
```
|
||||
|
||||
### 2. 内存泄漏
|
||||
|
||||
```bash
|
||||
# 分析堆内存
|
||||
go tool pprof http://your-server:3008/debug/pprof/heap?token=your-token
|
||||
|
||||
# 查看内存分配
|
||||
(pprof) top20 -cum
|
||||
```
|
||||
|
||||
### 3. 协程泄漏
|
||||
|
||||
```bash
|
||||
# 分析协程
|
||||
go tool pprof http://your-server:3008/debug/pprof/goroutine?token=your-token
|
||||
|
||||
# 查看协程堆栈
|
||||
(pprof) top20
|
||||
```
|
||||
|
||||
### 4. 阻塞问题
|
||||
|
||||
```bash
|
||||
# 分析阻塞
|
||||
go tool pprof http://your-server:3008/debug/pprof/block?token=your-token
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **性能影响**:pprof 采样会对性能产生一定影响,建议在排查问题时临时启用
|
||||
2. **数据安全**:pprof 数据可能包含敏感信息,务必使用 IP 白名单和 token 保护
|
||||
3. **网络访问**:确保防火墙规则允许访问 pprof 端口
|
||||
4. **定期清理**:排查完成后,建议关闭 pprof 或加强安全限制
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 404 错误
|
||||
|
||||
- 检查 `PPROF_ENABLED=true` 是否设置
|
||||
- 检查应用是否重启以加载新配置
|
||||
|
||||
### 403 错误(IP 不允许)
|
||||
|
||||
- 检查 `PPROF_ALLOWED_IPS` 是否包含你的 IP
|
||||
- 如果使用代理,检查真实 IP 获取是否正确
|
||||
|
||||
### 401 错误(Token 无效)
|
||||
|
||||
- 检查 `PPROF_TOKEN` 是否设置
|
||||
- 检查请求头或查询参数中的 token 是否正确
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Go pprof 官方文档](https://pkg.go.dev/net/http/pprof)
|
||||
- [Go 性能分析指南](https://github.com/golang/go/wiki/Performance)
|
||||
|
||||
+2113
File diff suppressed because it is too large
Load Diff
+1423
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user