This commit is contained in:
Joe
2026-01-16 15:49:34 +08:00
commit 550d3e1f42
380 changed files with 62024 additions and 0 deletions
+690
View File
@@ -0,0 +1,690 @@
# AI 模块开发提示词
## 角色定义
你是一位经验丰富的全栈开发工程师,精通 Go 语言(Goravel 框架)和 Vue 3Element 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
View File
@@ -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 | 否 | 父级ID0为字典类型) |
| 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
+509
View File
@@ -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
View File
@@ -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
+106
View File
@@ -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
View File
@@ -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+
```
+185
View File
@@ -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
View File
@@ -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;
```
+481
View File
@@ -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()` 获取所有已存在的分表,然后逐个修改。
+360
View File
@@ -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` 查看完整示例。
+258
View File
@@ -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` 中修改一次,所有使用该服务的表都会自动获得这些功能。
+218
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+226
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1423
View File
File diff suppressed because it is too large Load Diff