# 开发指南:留言板模块 CRUD 完整示例 本文档以**留言板模块**为例,详细说明如何开发一个完整的增删改查功能,包括后端接口和前端页面。 ## 目录 - [概述](#概述) - [后端开发](#后端开发) - [1. 数据库迁移](#1-数据库迁移) - [2. 创建模型](#2-创建模型) - [3. 创建服务层](#3-创建服务层) - [4. 创建请求验证](#4-创建请求验证) - [5. 创建控制器](#5-创建控制器) - [6. 注册路由](#6-注册路由) - [7. 运行迁移](#7-运行迁移) - [前端开发](#前端开发) - [1. 创建 API 客户端](#1-创建-api-客户端) - [2. 创建列表页面](#2-创建列表页面) - [3. 创建表单组件](#3-创建表单组件) - [4. 注册路由](#4-注册路由) - [测试验证](#测试验证) - [总结](#总结) --- ## 概述 留言板模块功能需求: - 留言列表(支持分页、搜索、筛选) - 创建留言 - 编辑留言 - 删除留言 - 回复留言(可选) 数据库表结构: - `guestbooks` 表:留言主表 - 字段:id, name, email, content, reply, status, ip, user_agent, created_at, updated_at, deleted_at --- ## 后端开发 ### 1. 数据库迁移 创建迁移文件:`database/migrations/20250101000029_create_guestbooks_table.go` ```go package migrations import ( "github.com/goravel/framework/contracts/database/schema" "github.com/goravel/framework/facades" ) type M20250101000029CreateGuestbooksTable struct{} func (r *M20250101000029CreateGuestbooksTable) Signature() string { return "20250101000029_create_guestbooks_table" } func (r *M20250101000029CreateGuestbooksTable) Up() error { if !facades.Schema().HasTable("guestbooks") { return facades.Schema().Create("guestbooks", func(table schema.Blueprint) { table.BigIncrements("id") table.String("name", 50).Comment("留言人姓名") table.String("email", 100).Comment("留言人邮箱") table.Text("content").Comment("留言内容") table.Text("reply").Nullable().Comment("回复内容") table.UnsignedTinyInteger("status").Default(1).Comment("状态:1-已审核,0-待审核") table.String("ip", 50).Nullable().Comment("IP地址") table.String("user_agent", 500).Nullable().Comment("用户代理") table.Timestamps() table.SoftDeletes() table.Comment("留言板表") // 索引 table.Index("status") table.Index("created_at") }) } return nil } func (r *M20250101000029CreateGuestbooksTable) Down() error { return facades.Schema().DropIfExists("guestbooks") } ``` ### 2. 创建模型 创建模型文件:`app/models/guestbook.go` ```go package models import ( "github.com/goravel/framework/database/orm" ) type Guestbook struct { orm.Model Name string `gorm:"size:50;comment:留言人姓名" json:"name"` Email string `gorm:"size:100;comment:留言人邮箱" json:"email"` Content string `gorm:"type:text;comment:留言内容" json:"content"` Reply string `gorm:"type:text;comment:回复内容" json:"reply"` Status uint8 `gorm:"default:1;comment:状态 1:已审核 0:待审核" json:"status"` IP string `gorm:"size:50;comment:IP地址" json:"ip"` UserAgent string `gorm:"size:500;comment:用户代理" json:"user_agent"` orm.SoftDeletes } ``` ### 3. 创建服务层 创建服务接口和实现:`app/services/guestbook_service.go` ```go package services import ( "github.com/goravel/framework/contracts/database/orm" "github.com/goravel/framework/facades" "github.com/spf13/cast" apperrors "goravel/app/errors" "goravel/app/http/helpers" "goravel/app/models" ) type GuestbookService interface { GetByID(id uint) (*models.Guestbook, error) GetList(filters GuestbookFilters, page, pageSize int) ([]models.Guestbook, int64, error) Create(data map[string]any) (*models.Guestbook, error) Update(id uint, data map[string]any) error Delete(id uint) error Reply(id uint, replyContent string) error } type GuestbookFilters struct { Name string Email string Status string StartTime string EndTime string OrderBy string } type GuestbookServiceImpl struct{} func NewGuestbookServiceImpl() *GuestbookServiceImpl { return &GuestbookServiceImpl{} } func (s *GuestbookServiceImpl) GetByID(id uint) (*models.Guestbook, error) { var guestbook models.Guestbook if err := facades.Orm().Query().Where("id", id).First(&guestbook); err != nil { return nil, apperrors.ErrNotFound.WithError(err) } return &guestbook, nil } func (s *GuestbookServiceImpl) buildQuery(filters GuestbookFilters) orm.Query { query := facades.Orm().Query().Model(&models.Guestbook{}) if filters.Name != "" { query = query.Where("name LIKE ?", "%"+filters.Name+"%") } if filters.Email != "" { query = query.Where("email LIKE ?", "%"+filters.Email+"%") } if filters.Status != "" { query = query.Where("status", filters.Status) } if filters.StartTime != "" { query = query.Where("created_at >= ?", filters.StartTime) } if filters.EndTime != "" { query = query.Where("created_at <= ?", filters.EndTime) } return query } func (s *GuestbookServiceImpl) GetList(filters GuestbookFilters, page, pageSize int) ([]models.Guestbook, 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 guestbooks []models.Guestbook err = query.Offset((page-1)*pageSize).Limit(pageSize).Find(&guestbooks) if err != nil { return nil, 0, err } return guestbooks, total, nil } func (s *GuestbookServiceImpl) Create(data map[string]any) (*models.Guestbook, error) { var guestbook models.Guestbook if err := facades.Orm().Query().Create(&guestbook, data); err != nil { return nil, err } return &guestbook, nil } func (s *GuestbookServiceImpl) Update(id uint, data map[string]any) error { var guestbook models.Guestbook if err := facades.Orm().Query().Where("id", id).First(&guestbook); err != nil { return apperrors.ErrNotFound.WithError(err) } return facades.Orm().Query().Save(&guestbook, data) } func (s *GuestbookServiceImpl) Delete(id uint) error { var guestbook models.Guestbook if err := facades.Orm().Query().Where("id", id).First(&guestbook); err != nil { return apperrors.ErrNotFound.WithError(err) } _, err := facades.Orm().Query().Delete(&guestbook) return err } func (s *GuestbookServiceImpl) Reply(id uint, replyContent string) error { var guestbook models.Guestbook if err := facades.Orm().Query().Where("id", id).First(&guestbook); err != nil { return apperrors.ErrNotFound.WithError(err) } guestbook.Reply = replyContent guestbook.Status = 1 // 回复后自动审核通过 return facades.Orm().Query().Save(&guestbook) } ``` ### 4. 创建请求验证 创建创建请求验证:`app/http/requests/admin/guestbook_create.go` ```go package admin import ( "goravel/app/http/trans" "github.com/goravel/framework/contracts/http" ) type GuestbookCreate struct { Name string `form:"name" json:"name"` Email string `form:"email" json:"email"` Content string `form:"content" json:"content"` Status uint8 `form:"status" json:"status"` } func (r *GuestbookCreate) Authorize(ctx http.Context) error { return nil } func (r *GuestbookCreate) Rules(ctx http.Context) map[string]string { return map[string]string{ "name": "required|max_len:50", "email": "required|email|max_len:100", "content": "required|min_len:5|max_len:2000", "status": "in:0,1", } } func (r *GuestbookCreate) Messages(ctx http.Context) map[string]string { return map[string]string{ "name.required": trans.Get(ctx, "validation_name_required"), "name.max_len": trans.Get(ctx, "validation_name_max"), "email.required": trans.Get(ctx, "validation_email_required"), "email.email": trans.Get(ctx, "validation_email_format"), "content.required": trans.Get(ctx, "validation_content_required"), "content.min_len": trans.Get(ctx, "validation_content_min"), "content.max_len": trans.Get(ctx, "validation_content_max"), "status.in": trans.Get(ctx, "validation_status_in"), } } func (r *GuestbookCreate) Attributes(ctx http.Context) map[string]string { return map[string]string{ "name": trans.Get(ctx, "validation_name"), "email": trans.Get(ctx, "validation_email"), "content": trans.Get(ctx, "validation_content"), "status": trans.Get(ctx, "validation_status"), } } ``` 创建更新请求验证:`app/http/requests/admin/guestbook_update.go` ```go package admin import ( "goravel/app/http/trans" "github.com/goravel/framework/contracts/http" ) type GuestbookUpdate struct { Name string `form:"name" json:"name"` Email string `form:"email" json:"email"` Content string `form:"content" json:"content"` Reply string `form:"reply" json:"reply"` Status uint8 `form:"status" json:"status"` } func (r *GuestbookUpdate) Authorize(ctx http.Context) error { return nil } func (r *GuestbookUpdate) Rules(ctx http.Context) map[string]string { return map[string]string{ "name": "max_len:50", "email": "email|max_len:100", "content": "min_len:5|max_len:2000", "reply": "max_len:2000", "status": "in:0,1", } } func (r *GuestbookUpdate) Messages(ctx http.Context) map[string]string { return map[string]string{ "name.max_len": trans.Get(ctx, "validation_name_max"), "email.email": trans.Get(ctx, "validation_email_format"), "content.min_len": trans.Get(ctx, "validation_content_min"), "content.max_len": trans.Get(ctx, "validation_content_max"), "reply.max_len": trans.Get(ctx, "validation_reply_max"), "status.in": trans.Get(ctx, "validation_status_in"), } } func (r *GuestbookUpdate) Attributes(ctx http.Context) map[string]string { return map[string]string{ "name": trans.Get(ctx, "validation_name"), "email": trans.Get(ctx, "validation_email"), "content": trans.Get(ctx, "validation_content"), "reply": trans.Get(ctx, "validation_reply"), "status": trans.Get(ctx, "validation_status"), } } ``` 创建回复请求验证:`app/http/requests/admin/guestbook_reply.go` ```go package admin import ( "goravel/app/http/trans" "github.com/goravel/framework/contracts/http" ) type GuestbookReply struct { Reply string `form:"reply" json:"reply"` } func (r *GuestbookReply) Authorize(ctx http.Context) error { return nil } func (r *GuestbookReply) Rules(ctx http.Context) map[string]string { return map[string]string{ "reply": "required|min_len:5|max_len:2000", } } func (r *GuestbookReply) Messages(ctx http.Context) map[string]string { return map[string]string{ "reply.required": trans.Get(ctx, "validation_reply_required"), "reply.min_len": trans.Get(ctx, "validation_reply_min"), "reply.max_len": trans.Get(ctx, "validation_reply_max"), } } func (r *GuestbookReply) Attributes(ctx http.Context) map[string]string { return map[string]string{ "reply": trans.Get(ctx, "validation_reply"), } } ``` ### 5. 创建控制器 创建控制器文件:`app/http/controllers/admin/guestbook_controller.go` ```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 GuestbookController struct { guestbookService services.GuestbookService } func NewGuestbookController() *GuestbookController { return &GuestbookController{ guestbookService: services.NewGuestbookServiceImpl(), } } // Index 留言列表 // @Summary 获取留言列表 // @Description 分页获取留言列表,支持按姓名、邮箱、状态等条件筛选 // @Tags 留言板管理 // @Accept json // @Produce json // @Param page query int false "页码" default(1) // @Param page_size query int false "每页数量" default(20) // @Param name query string false "姓名(模糊搜索)" // @Param email query string false "邮箱(模糊搜索)" // @Param status query string false "状态:1-已审核,0-待审核" // @Param start_time query string false "开始时间(格式:YYYY-MM-DD HH:mm:ss)" // @Param end_time query string false "结束时间(格式:YYYY-MM-DD HH:mm:ss)" // @Param order_by query string false "排序(格式:字段:asc/desc,如:created_at:desc)" // @Success 200 {object} map[string]any // @Failure 400 {object} map[string]any "参数错误" // @Failure 401 {object} map[string]any "未登录" // @Failure 403 {object} map[string]any "无权限" // @Failure 500 {object} map[string]any "服务器错误" // @Router /api/admin/guestbooks [get] // @Security BearerAuth func (r *GuestbookController) Index(ctx http.Context) http.Response { page := cast.ToInt(ctx.Request().Query("page", "1")) pageSize := cast.ToInt(ctx.Request().Query("page_size", "20")) startTimeStr := ctx.Request().Query("start_time", "") endTimeStr := ctx.Request().Query("end_time", "") startTime := "" endTime := "" if startTimeStr != "" { startTime = helpers.ConvertTimeToUTC(ctx, startTimeStr) } if endTimeStr != "" { endTime = helpers.ConvertTimeToUTC(ctx, endTimeStr) } filters := services.GuestbookFilters{ Name: ctx.Request().Query("name", ""), Email: ctx.Request().Query("email", ""), Status: ctx.Request().Query("status", ""), StartTime: startTime, EndTime: endTime, OrderBy: ctx.Request().Query("order_by", ""), } guestbooks, total, err := r.guestbookService.GetList(filters, page, pageSize) if err != nil { return response.Error(ctx, http.StatusInternalServerError, err.Error()) } return response.Success(ctx, http.Json{ "list": guestbooks, "total": total, "page": page, "page_size": pageSize, }) } // Show 留言详情 // @Summary 获取留言详情 // @Description 根据ID获取留言详细信息 // @Tags 留言板管理 // @Accept json // @Produce json // @Param id path int true "留言ID" // @Success 200 {object} map[string]any // @Failure 404 {object} map[string]any "留言不存在" // @Router /api/admin/guestbooks/{id} [get] // @Security BearerAuth func (r *GuestbookController) Show(ctx http.Context) http.Response { id := cast.ToUint(ctx.Request().Route("id")) guestbook, err := r.guestbookService.GetByID(id) if err != nil { return response.Error(ctx, http.StatusNotFound, apperrors.ErrNotFound.Code) } return response.Success(ctx, http.Json{ "guestbook": guestbook, }) } // Store 创建留言 // @Summary 创建留言 // @Description 创建新的留言 // @Tags 留言板管理 // @Accept json // @Produce json // @Param name body string true "留言人姓名" // @Param email body string true "留言人邮箱" // @Param content body string true "留言内容" // @Param status body int false "状态:1-已审核,0-待审核" default(0) // @Success 200 {object} map[string]any // @Failure 400 {object} map[string]any "参数错误" // @Router /api/admin/guestbooks [post] // @Security BearerAuth func (r *GuestbookController) Store(ctx http.Context) http.Response { var req adminrequests.GuestbookCreate errors, err := ctx.Request().ValidateRequest(&req) if err != nil { return response.Error(ctx, http.StatusBadRequest, err.Error()) } if errors != nil { return response.ValidationError(ctx, http.StatusBadRequest, "validation_failed", errors.All()) } // 获取客户端IP和User-Agent ip := ctx.Request().Ip() userAgent := ctx.Request().Header("User-Agent", "") data := map[string]any{ "name": req.Name, "email": req.Email, "content": req.Content, "status": req.Status, "ip": ip, "user_agent": userAgent, } guestbook, err := r.guestbookService.Create(data) if err != nil { return response.Error(ctx, http.StatusInternalServerError, err.Error()) } return response.Success(ctx, http.Json{ "guestbook": guestbook, }) } // Update 更新留言 // @Summary 更新留言 // @Description 更新留言信息,包括回复内容 // @Tags 留言板管理 // @Accept json // @Produce json // @Param id path int true "留言ID" // @Param name body string false "留言人姓名" // @Param email body string false "留言人邮箱" // @Param content body string false "留言内容" // @Param reply body string false "回复内容" // @Param status body int false "状态:1-已审核,0-待审核" // @Success 200 {object} map[string]any // @Failure 404 {object} map[string]any "留言不存在" // @Router /api/admin/guestbooks/{id} [put] // @Security BearerAuth func (r *GuestbookController) Update(ctx http.Context) http.Response { id := cast.ToUint(ctx.Request().Route("id")) var req adminrequests.GuestbookUpdate errors, err := ctx.Request().ValidateRequest(&req) if err != nil { return response.Error(ctx, http.StatusBadRequest, err.Error()) } if errors != nil { return response.ValidationError(ctx, http.StatusBadRequest, "validation_failed", errors.All()) } data := map[string]any{} if req.Name != "" { data["name"] = req.Name } if req.Email != "" { data["email"] = req.Email } if req.Content != "" { data["content"] = req.Content } if req.Reply != "" { data["reply"] = req.Reply } if ctx.Request().Input("status") != "" { data["status"] = req.Status } if err := r.guestbookService.Update(id, data); err != nil { return response.Error(ctx, http.StatusInternalServerError, err.Error()) } guestbook, _ := r.guestbookService.GetByID(id) return response.Success(ctx, http.Json{ "guestbook": guestbook, }) } // Destroy 删除留言 // @Summary 删除留言 // @Description 删除指定的留言 // @Tags 留言板管理 // @Accept json // @Produce json // @Param id path int true "留言ID" // @Success 200 {object} map[string]any // @Failure 404 {object} map[string]any "留言不存在" // @Router /api/admin/guestbooks/{id} [delete] // @Security BearerAuth func (r *GuestbookController) Destroy(ctx http.Context) http.Response { id := cast.ToUint(ctx.Request().Route("id")) if err := r.guestbookService.Delete(id); err != nil { return response.Error(ctx, http.StatusInternalServerError, err.Error()) } return response.Success(ctx) } // Reply 回复留言 // @Summary 回复留言 // @Description 对留言进行回复,回复后自动审核通过 // @Tags 留言板管理 // @Accept json // @Produce json // @Param id path string true "留言ID" // @Param reply body string true "回复内容" // @Success 200 {object} map[string]any // @Failure 404 {object} map[string]any "留言不存在" // @Router /api/admin/guestbooks/{id}/reply [post] // @Security BearerAuth func (r *GuestbookController) Reply(ctx http.Context) http.Response { id := cast.ToUint(ctx.Request().Route("id")) var req adminrequests.GuestbookReply errors, err := ctx.Request().ValidateRequest(&req) if err != nil { return response.Error(ctx, http.StatusBadRequest, err.Error()) } if errors != nil { return response.ValidationError(ctx, http.StatusBadRequest, "validation_failed", errors.All()) } if err := r.guestbookService.Reply(id, req.Reply); err != nil { return response.Error(ctx, http.StatusInternalServerError, err.Error()) } guestbook, _ := r.guestbookService.GetByID(id) return response.Success(ctx, http.Json{ "guestbook": guestbook, }) } ``` ### 6. 注册路由 在 `routes/admin.go` 文件中添加路由: ```go // 在 Admin() 函数中添加控制器实例 guestbookController := admin.NewGuestbookController() // 在需要认证、权限验证的路由组中添加(约第73行) router.Resource("guestbooks", guestbookController) router.Post("guestbooks/{id}/reply", guestbookController.Reply) // 回复留言接口 ``` ### 7. 运行迁移 ```bash # 运行迁移 go run . migrate # 或者使用 artisan ./artisan migrate ``` --- ## 前端开发 ### 1. 创建 API 客户端 创建 API 文件:`html/src/api/guestbook.js` ```javascript import request from '../utils/request' import { createCRUDApi, extendApi } from '../utils/apiFactory' // 创建基础 CRUD API const baseGuestbookApi = createCRUDApi('guestbooks') // 扩展 API,添加自定义方法 const guestbookApi = extendApi(baseGuestbookApi, { // 回复留言 reply: (id, data) => { return request({ url: `/guestbooks/${id}/reply`, method: 'post', data }) } }) // 导出所有方法 export const { list: getGuestbookList, detail: getGuestbookDetail, create: createGuestbook, update: updateGuestbook, delete: deleteGuestbook, reply: replyGuestbook } = guestbookApi ``` ### 2. 创建列表页面 创建列表页面:`html/src/views/guestbook/GuestbookList.vue` ```vue {{ $t('menu.guestbook') }} {{ $t('guestbook.add_guestbook') }} {{ row.status === 1 ? $t('guestbook.status_approved') : $t('guestbook.status_pending') }} {{ row.content && row.content.length > 50 ? row.content.substring(0, 50) + '...' : row.content }} {{ row.reply.length > 50 ? row.reply.substring(0, 50) + '...' : row.reply }} - ``` ### 3. 创建表单组件 创建表单组件:`html/src/views/guestbook/GuestbookForm.vue` ```vue {{ $t('guestbook.status_approved') }} {{ $t('guestbook.status_pending') }} {{ $t('common.cancel') }} {{ $t('common.confirm') }} ``` ### 4. 注册路由 在 `html/src/router/index.js` 中添加路由: ```javascript { path: '/guestbook', name: 'GuestbookList', component: () => import('../views/guestbook/GuestbookList.vue'), meta: { title: 'menu.guestbook', permission: 'guestbook.index' } } ``` ### 5. 添加国际化文本 在 `lang/cn.json` 和 `lang/en.json` 中添加相关翻译: **cn.json:** ```json { "menu": { "guestbook": "留言板" }, "guestbook": { "add_guestbook": "添加留言", "edit_guestbook": "编辑留言", "name": "姓名", "name_placeholder": "请输入姓名", "email": "邮箱", "email_placeholder": "请输入邮箱", "content": "留言内容", "content_placeholder": "请输入留言内容(5-2000字符)", "reply": "回复", "reply_placeholder": "请输入回复内容(5-2000字符)", "status": "状态", "status_all": "全部", "status_approved": "已审核", "status_pending": "待审核", "ip": "IP地址", "create_success": "留言创建成功", "update_success": "留言更新成功", "reply_success": "回复成功" } } ``` --- ## 测试验证 ### 后端接口测试 1. **测试列表接口** ```bash curl -X GET "http://localhost:3000/api/admin/guestbooks?page=1&page_size=20" \ -H "Authorization: Bearer YOUR_TOKEN" ``` 2. **测试创建接口** ```bash curl -X POST "http://localhost:3000/api/admin/guestbooks" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "张三", "email": "zhangsan@example.com", "content": "这是一条测试留言", "status": 0 }' ``` 3. **测试回复接口** ```bash curl -X POST "http://localhost:3000/api/admin/guestbooks/1/reply" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "reply": "感谢您的留言,我们会尽快处理。" }' ``` ### 前端功能测试 1. 访问留言列表页面,验证数据加载 2. 测试搜索和筛选功能 3. 测试添加留言功能 4. 测试编辑留言功能 5. 测试删除留言功能 6. 测试回复留言功能 --- ## 总结 通过以上步骤,我们完成了一个完整的留言板模块开发,包括: ### 后端部分 - ✅ 数据库迁移文件 - ✅ 模型定义 - ✅ 服务层业务逻辑 - ✅ 请求验证 - ✅ 控制器处理 - ✅ 路由注册 ### 前端部分 - ✅ API 客户端封装 - ✅ 列表页面(搜索、分页、操作) - ✅ 表单组件(创建/编辑) - ✅ 路由配置 - ✅ 国际化文本 ### 开发要点 1. **遵循项目规范**:代码风格、目录结构、命名规范 2. **统一响应格式**:使用 `response.Success()` 和 `response.Error()` 3. **权限控制**:使用中间件和前端权限检查 4. **错误处理**:统一的错误处理和提示 5. **代码复用**:使用组合式函数和工具函数 6. **用户体验**:加载状态、表单验证、操作确认 按照这个文档的步骤,你可以快速开发其他类似的 CRUD 模块。