鱼皮AI导航
鱼皮AI导航
  • 主页
  • 社区
  • AI工具
  • AI知识库
  • AI提示词
  • MCP
  • Skills
  • AI学习资源
  • AI资讯
  • GitHub开源
登录/注册
站长:程序员鱼皮 老鱼简历 商务合作 增值电信业务经营许可证
2026 鱼皮AI导航 | 沪ICP备19026706号-6
4
5
2

易扣AI (Go + CloudWeGo) 企业级AI智能体项目教程 第4章:后端项目应用模块搭建

绯雾sama
绯雾sama
2026-05-31 12:52
阅读 1303
绯雾sama
2026-05-31 12:52
阅读 1303

第4章:后端项目应用模块搭建

本章将讲解如何开发应用模块的基础部分和如何将第3章实现的 AI 代码生成核心功能集成到完整的后端应用模块中。

知识点清单

一、方案设计

业务需求描述

在前面的章节,我们已经封装好了代码生成智能体。在接下来的章节,我们将进一步构建一个 AI 代码生成平台,用户可以通过自然语言描述需求提示词,AI 智能体 自动生成对应的代码文件。为了满足需求,我们需要实现以下核心功能:

核心业务功能:

功能模块功能描述技术实现
应用管理创建、编辑、删除、查询应用CRUD 操作
代码生成根据用户描述生成代码AI 模型 + Eino 框架
代码部署将生成的代码部署到服务器静态文件服务
应用展示展示用户创建的应用列表分页查询、排序

数据库表设计

执行以下sql语句,并且执行gorm实体结构体的生成脚本,直接生成应用表的dao文件

应用表(app)

应用表存储用户创建的应用信息,包括应用名称、封面、初始 Prompt、代码生成类型、部署信息等。

表结构:

▼
sql
复制代码
create table app ( id bigint auto_increment comment 'id' primary key, appName varchar(256) null comment '应用名称', cover varchar(512) null comment '应用封面', initPrompt text null comment '应用初始化的 prompt', codeGenType varchar(64) null comment '代码生成类型(枚举)', deployKey varchar(64) null comment '部署标识', deployedTime datetime null comment '部署时间', priority int default 0 not null comment '优先级', userId bigint not null comment '创建用户id', editTime datetime default CURRENT_TIMESTAMP not null comment '编辑时间', createTime datetime default CURRENT_TIMESTAMP not null comment '创建时间', updateTime datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间', isDelete tinyint default 0 not null comment '是否删除', UNIQUE KEY uk_deployKey (deployKey), INDEX idx_appName (appName), INDEX idx_userId (userId) ) comment '应用' collate = utf8mb4_unicode_ci;

字段说明:

字段名类型说明约束业务含义
idbigint应用 ID,自增主键PRIMARY KEY唯一标识一个应用
appNamevarchar(256)应用名称NULL用户定义的应用名称,如"个人博客"
covervarchar(512)应用封面图片 URLNULL应用展示的封面图片
initPrompttext应用初始化的 PromptNULLAI 生成代码的系统提示词,定义应用的基本功能和样式
codeGenTypevarchar(64)代码生成类型NULL枚举值:html(单文件)、multi_file(多文件)
deployKeyvarchar(64)部署标识UNIQUE唯一的部署标识,用于生成访问链接,如 "my-blog-20240101"
deployedTimedatetime部署时间NULL应用最后一次部署的时间
priorityint优先级NOT NULL, DEFAULT 0应用展示的优先级,数值越大越靠前
userIdbigint创建用户 IDNOT NULL关联用户表,标识应用的创建者
editTimedatetime最后编辑时间NOT NULL用户最后一次编辑应用的时间
createTimedatetime创建时间NOT NULL应用创建的时间
updateTimedatetime更新时间NOT NULL数据库记录更新的时间
isDeletetinyint是否删除NOT NULL, DEFAULT 0软删除标记,0:未删除, 1:已删除

索引说明:

索引名索引类型字段说明
PRIMARY主键索引id主键
uk_deployKey唯一索引deployKey保证部署标识唯一性,用于生成唯一的访问链接
idx_appName普通索引appName提升按应用名称搜索的性能
idx_userId普通索引userId提升按用户 ID 查询应用的性能

二、实现应用模块基础接口

应用模块是本项目的核心模块之一,提供应用的创建、查询、更新、删除等功能。本节将按照每个接口的完整流程,从 API 层、Handler 层到 Service 层,详细讲解每个接口的实现。

路由配置

文件位置: internal/router/router.go

应用模块路由分组

▼
go
复制代码
appRoute := h.Group("/app") { // 公开接口(无需登录) appRoute.POST("/good/list/page/vo", appHandler.ListGoodApp) appRoute.GET("/get/vo", middleware.AuthMiddleware(enum.UserRole, db), appHandler.GetAppVo) // 用户接口(需要登录) appRoute.POST("/my/list/page/vo", middleware.AuthMiddleware(enum.UserRole, db), appHandler.ListMyApp) appRoute.POST("/add", middleware.AuthMiddleware(enum.UserRole, db), appHandler.AddApp) appRoute.POST("/update", middleware.AuthMiddleware(enum.UserRole, db), appHandler.UpdateApp) appRoute.POST("/delete", middleware.AuthMiddleware(enum.UserRole, db), appHandler.DeleteApp) // 管理员接口(需要管理员权限) appRoute.POST("/admin/update", middleware.AuthMiddleware(enum.AdminRole, db), appHandler.AdminUpdateApp) appRoute.POST("/admin/delete", middleware.AuthMiddleware(enum.AdminRole, db), appHandler.AdminDeleteApp) appRoute.GET("/admin/get/vo", middleware.AuthMiddleware(enum.AdminRole, db), appHandler.AdminGetAppVo) appRoute.POST("/admin/list/page/vo", middleware.AuthMiddleware(enum.AdminRole, db), appHandler.AdminListApp) }

接口实现详解

新增应用接口

接口路径: POST /app/add 功能说明: 用户创建新应用,填写初始 Prompt,系统自动生成应用名称和 ID。

API 层

文件位置: internal/api/app.go

请求结构体:

▼
go
复制代码
type YiKouAppAddRequest struct { InitPrompt string `json:"initPrompt"` }

请求字段说明:

字段名类型JSON 标签说明必填
InitPromptstringinitPrompt应用初始化 Prompt,AI 根据此生成代码是

响应结构体:

▼
go
复制代码
type YiKouAppAddResponse response.BaseResponse[string]

响应数据说明:

  • 返回新创建的应用 ID(string 类型)
Handler 层

文件位置: internal/handler/app_handler.go

控制器结构体:

▼
go
复制代码
type AppHandler struct { appService service.IAppService // 应用服务接口 userService service.IUserService // 用户服务接口 } func NewAppHandler( appService service.IAppService, userService service.IUserService, ) *AppHandler { return &AppHandler{ appService: appService, userService: userService, } }

接口实现:

▼
go
复制代码
// AddApp 新增应用 // @Summary 新增应用 // @Description 新增应用 // @Tags 应用模块 // @Accept json // @Produce json // @Param req body api.YiKouAppAddRequest true "新增应用请求" // @Success 200 {object} api.YiKouAppAddResponse "应用ID" // @Router /app/add [post] func (a *AppHandler) AddApp(ctx context.Context, c *app.RequestContext) { // 1. 绑定和验证请求参数 req := &api.YiKouAppAddRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 2. 获取当前登录用户 userVo, err := a.userService.GetLoginUserVo(ctx, c) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 3. 调用服务层创建应用 appId, err := a.appService.AddApp(ctx, req, userVo.ID) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 4. 返回成功响应 c.JSON(consts.StatusOK, response.NewSuccessResponse[string](strconv.Itoa(int(appId)))) }
Service 层

文件位置: internal/logic/app_logic.go

服务结构体:

▼
go
复制代码
type AppService struct { aiCodeGenFacade *core.YiKouAiCodegenFacade // AI 代码生成门面 userService service.IUserService // 用户服务接口 db *gorm.DB // 数据库连接 } func NewAppService( aiCodeGenFacade *core.YiKouAiCodegenFacade, userService service.IUserService, db *gorm.DB, ) *AppService { return &AppService{ aiCodeGenFacade: aiCodeGenFacade, userService: userService, db: db, } }

业务逻辑实现:

▼
go
复制代码
func (s *AppService) AddApp(ctx context.Context, req *api.YiKouAppAddRequest, userId int64) (int64, error) { // 1. 参数校验 if req.InitPrompt == "" { return 0, errorutil.ParamsError.WithMessage("初始化prompt不能为空") } // 2. 生成应用名称(截取前12个字符) appName := req.InitPrompt count := 0 for i := range appName { if count >= 12 { appName = appName[:i] break } count++ } // 3. 生成应用 ID(雪花算法) appId, err := snowflake.GenerateSnowFlakeId() if err != nil { return 0, err } // 4. 构建应用实体 newApp := &model.App{ ID: appId, AppName: appName, InitPrompt: req.InitPrompt, UserID: userId, CodeGenType: string(enum.HtmlCodeGen), Priority: 0, } // 5. 保存到数据库 err = query.Use(s.db).App. Select(query.App.ID, query.App.AppName, query.App.InitPrompt, query.App.UserID, query.App.Priority, query.App.CodeGenType). Create(newApp) if err != nil { return 0, err } logger.Infof("应用创建成功,ID: %d, 类型: %s", appId, enum.HtmlCodeGen) return newApp.ID, nil }
更新应用接口

接口路径: POST /app/update 功能说明: 用户更新自己的应用信息,只能更新应用名称。

API 层

请求结构体:

▼
go
复制代码
type YiKouAppUpdateRequest struct { request.DeleteRequest AppName string `json:"appName"` }

请求字段说明:

字段名类型JSON 标签说明必填
Idintid应用 ID(继承自 DeleteRequest)是
AppNamestringappName应用名称否

响应结构体:

▼
go
复制代码
type YiKouAppUpdateResponse response.BaseResponse[bool]

响应数据说明:

  • 返回是否更新成功(bool 类型)
Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) UpdateApp(ctx context.Context, c *app.RequestContext) { req := &api.YiKouAppUpdateRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } userVo, err := a.userService.GetLoginUserVo(ctx, c) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } success, err := a.appService.UpdateApp(ctx, req, userVo.ID) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[bool](success)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) UpdateApp(ctx context.Context, req *api.YiKouAppUpdateRequest, userId int64) (bool, error) { // 1. 参数校验 if req.Id == 0 { return false, errorutil.ParamsError.WithMessage("应用ID不能为空") } // 2. 查询应用 app, err := query.Use(s.db).App.Where(query.App.ID.Eq(int64(req.Id))).First() if err != nil { return false, err } // 3. 权限校验 if app.UserID != userId { return false, errorutil.ParamsError.WithMessage("无权修改该应用") } // 4. 构建更新字段 updateMap := make(map[string]interface{}) if req.AppName != "" { updateMap["appName"] = req.AppName } // 5. 执行更新 _, err = query.Use(s.db).App.Where(query.App.ID.Eq(int64(req.Id))).Updates(updateMap) if err != nil { return false, err } return true, nil }
删除应用接口

接口路径: POST /app/delete 功能说明: 用户删除自己的应用,使用逻辑删除(软删除)。

API 层

请求结构体:

▼
go
复制代码
type DeleteRequest struct { Id int `json:"id"` }

请求字段说明:

字段名类型JSON 标签说明必填
Idintid应用 ID是

响应结构体:

▼
go
复制代码
type YiKouAppDeleteResponse response.BaseResponse[bool]

响应数据说明:

  • 返回是否删除成功(bool 类型)
Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) DeleteApp(ctx context.Context, c *app.RequestContext) { req := &request.DeleteRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } userVo, err := a.userService.GetLoginUserVo(ctx, c) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } success, err := a.appService.DeleteApp(ctx, int64(req.Id), userVo.ID) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[bool](success)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) DeleteApp(ctx context.Context, id int64, userId int64) (bool, error) { // 1. 查询应用 app, err := query.Use(s.db).App.Where(query.App.ID.Eq(id)).First() if err != nil { return false, err } // 2. 权限校验 if app.UserID != userId { return false, errorutil.ParamsError.WithMessage("无权删除该应用") } // 3. 逻辑删除应用 _, err = query.Use(s.db).App.Where(query.App.ID.Eq(id)).Update(query.App.IsDelete, 1) if err != nil { return false, err } return true, nil }
获取应用详情接口

接口路径: GET /app/get/vo 功能说明: 根据 ID 获取应用详情,返回应用 VO(包含用户信息)。

API 层

请求参数:

  • id(query 参数):应用 ID

响应结构体:

▼
go
复制代码
type YiKouAppGetVoResponse response.BaseResponse[vo.AppVo]

AppVo 结构体:

▼
go
复制代码
type AppVo struct { ID int64 `json:"id"` AppName string `json:"appName"` Cover string `json:"cover"` InitPrompt string `json:"initPrompt"` CodeGenType string `json:"codeGenType"` DeployKey string `json:"deployKey"` DeployedTime time.Time `json:"deployedTime"` Priority int32 `json:"priority"` UserID int64 `json:"userId"` User UserVo `json:"user"` CreateTime time.Time `json:"createTime"` UpdateTime time.Time `json:"updateTime"` }
Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) GetAppVo(ctx context.Context, c *app.RequestContext) { // 1. 获取查询参数 id := c.Query("id") if id == "" { c.JSON(consts.StatusOK, response.NewErrorResponse[any](errorutil.ParamsError)) return } idInt64, _ := strconv.ParseInt(id, 10, 64) // 2. 获取当前登录用户 userVo, err := a.userService.GetLoginUserVo(ctx, c) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 3. 调用服务层获取应用详情 appVo, err := a.appService.GetAppVo(ctx, idInt64, userVo.ID) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } // 4. 返回应用详情 c.JSON(consts.StatusOK, response.NewSuccessResponse[vo.AppVo](appVo)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) GetAppVo(ctx context.Context, id int64, userId int64) (vo.AppVo, error) { // 1. 获取应用实体 app, err := s.GetApp(ctx, id, userId) if err != nil { return vo.AppVo{}, err } // 2. 获取用户信息 userVo, err := s.userService.GetUserVo(ctx, app.UserID) if err != nil { return vo.AppVo{}, err } // 3. 构建应用 VO appVo := vo.AppVo{ ID: app.ID, AppName: app.AppName, Cover: app.Cover, InitPrompt: app.InitPrompt, CodeGenType: app.CodeGenType, DeployKey: app.DeployKey, DeployedTime: app.DeployedTime, Priority: app.Priority, UserID: app.UserID, User: userVo, CreateTime: app.CreateTime, UpdateTime: app.UpdateTime, } return appVo, nil }

GetApp 方法:

▼
go
复制代码
func (s *AppService) GetApp(ctx context.Context, id int64, userId int64) (*model.App, error) { // 1. 查询应用 app, err := query.Use(s.db).App.Where(query.App.ID.Eq(id)).First() if err != nil { return nil, err } // 2. 权限校验 if app.UserID != userId { return nil, errorutil.ParamsError.WithMessage("无权查看该应用") } return app, nil }
我的应用列表接口

接口路径: POST /app/my/list/page/vo 功能说明: 分页获取当前用户的应用列表,支持按应用名称模糊查询和排序。

API 层

请求结构体:

▼
go
复制代码
type YiKouAppMyListRequest struct { request.PageRequest AppName string `json:"appName"` }

PageRequest 基础结构体:

▼
go
复制代码
type PageRequest struct { PageNum int `json:"pageNum"` PageSize int `json:"pageSize"` SortField string `json:"sortField"` SortOrder string `json:"sortOrder"` }

请求字段说明:

字段名类型JSON 标签说明必填
PageNumintpageNum页码,默认 1否
PageSizeintpageSize每页大小,默认 20,最大 20否
SortFieldstringsortField排序字段否
SortOrderstringsortOrder排序方式(asc/desc)否
AppNamestringappName应用名称(模糊查询)否

响应结构体:

▼
go
复制代码
type YiKouAppMyListResponse response.BaseResponse[response.PageResponse[vo.AppVo]]

PageResponse 结构体:

▼
go
复制代码
type PageResponse[T any] struct { Records []T `json:"records"` PageNum int `json:"pageNum"` PageSize int `json:"pageSize"` TotalPage int `json:"totalPage"` TotalRow int `json:"totalRow"` OptimizeCountQuery bool `json:"optimizeCountQuery"` }
Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) ListMyApp(ctx context.Context, c *app.RequestContext) { req := &api.YiKouAppMyListRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } userVo, err := a.userService.GetLoginUserVo(ctx, c) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } pageResponse, err := a.appService.ListMyApp(ctx, req, userVo.ID) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[*response.PageResponse[vo.AppVo]](pageResponse)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) ListMyApp(ctx context.Context, req *api.YiKouAppMyListRequest, userId int64) (*response.PageResponse[vo.AppVo], error) { // 1. 参数校验和默认值设置 if req.PageNum <= 0 { req.PageNum = 1 } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageSize > 20 { req.PageSize = 20 } // 2. 构建查询条件 queryBuilder := query.Use(s.db).App.Where(query.App.IsDelete.Eq(0), query.App.UserID.Eq(userId)) if req.AppName != "" { queryBuilder = queryBuilder.Where(query.App.AppName.Like("%" + req.AppName + "%")) } // 3. 查询总数 totalCount, err := queryBuilder.Count() if err != nil { return nil, err } // 4. 计算分页信息 totalPage := int((totalCount + int64(req.PageSize) - 1) / int64(req.PageSize)) offset := (req.PageNum - 1) * req.PageSize // 5. 设置排序 if req.SortField != "" { if orderExpr, ok := query.App.GetFieldByName(req.SortField); ok { if req.SortOrder == "desc" { queryBuilder = queryBuilder.Order(orderExpr.Desc()) } else { queryBuilder = queryBuilder.Order(orderExpr) } } else { queryBuilder = queryBuilder.Order(query.App.CreateTime.Desc()) } } else { queryBuilder = queryBuilder.Order(query.App.CreateTime.Desc()) } // 6. 执行分页查询 appList, err := queryBuilder.Offset(offset).Limit(req.PageSize).Find() if err != nil { return nil, err } // 7. 转换为AppVo列表 appVoList, err := s.GetAppVoList(ctx, appList) if err != nil { return nil, err } // 8. 构建分页响应 pageResponse := &response.PageResponse[vo.AppVo]{ Records: appVoList, PageNum: req.PageNum, PageSize: req.PageSize, TotalPage: totalPage, TotalRow: int(totalCount), OptimizeCountQuery: false, } return pageResponse, nil }
精选应用列表接口

接口路径: POST /app/good/list/page/vo 功能说明: 分页获取精选应用列表(priority > 0),无需登录,支持多条件查询。

API 层

请求结构体:

▼
go
复制代码
type YiKouAppFeaturedListRequest struct { request.PageRequest AppName string `json:"appName"` CodeGenType string `json:"codeGenType"` InitPrompt string `json:"initPrompt"` Priority int32 `json:"priority"` }

请求字段说明:

字段名类型JSON 标签说明必填
PageNumintpageNum页码,默认 1否
PageSizeintpageSize每页大小,默认 20,最大 20否
SortFieldstringsortField排序字段否
SortOrderstringsortOrder排序方式(asc/desc)否
AppNamestringappName应用名称(模糊查询)否
CodeGenTypestringcodeGenType代码生成类型否
InitPromptstringinitPrompt初始化 Prompt(模糊查询)否
Priorityint32priority优先级否

响应结构体:

▼
go
复制代码
type YiKouAppFeaturedListResponse response.BaseResponse[response.PageResponse[vo.AppVo]]
Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) ListGoodApp(ctx context.Context, c *app.RequestContext) { req := &api.YiKouAppFeaturedListRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } pageResponse, err := a.appService.ListGoodApp(ctx, req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[*response.PageResponse[vo.AppVo]](pageResponse)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) ListGoodApp(ctx context.Context, req *api.YiKouAppFeaturedListRequest) (*response.PageResponse[vo.AppVo], error) { // 1. 参数校验和默认值设置 if req.PageNum <= 0 { req.PageNum = 1 } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageSize > 20 { req.PageSize = 20 } // 2. 构建查询条件(精选应用:priority > 0) queryBuilder := query.Use(s.db).App.Where(query.App.IsDelete.Eq(0), query.App.Priority.Gt(0)) // 3. 添加查询条件 if req.AppName != "" { queryBuilder = queryBuilder.Where(query.App.AppName.Like("%" + req.AppName + "%")) } if req.CodeGenType != "" { queryBuilder = queryBuilder.Where(query.App.CodeGenType.Eq(req.CodeGenType)) } if req.InitPrompt != "" { queryBuilder = queryBuilder.Where(query.App.InitPrompt.Like("%" + req.InitPrompt + "%")) } if req.Priority != 0 { queryBuilder = queryBuilder.Where(query.App.Priority.Eq(req.Priority)) } // 4. 查询总数 totalCount, err := queryBuilder.Count() if err != nil { return nil, err } // 5. 计算分页信息 totalPage := int((totalCount + int64(req.PageSize) - 1) / int64(req.PageSize)) offset := (req.PageNum - 1) * req.PageSize // 6. 设置排序(默认按优先级降序、创建时间降序) if req.SortField != "" { if orderExpr, ok := query.App.GetFieldByName(req.SortField); ok { if req.SortOrder == "desc" { queryBuilder = queryBuilder.Order(orderExpr.Desc()) } else { queryBuilder = queryBuilder.Order(orderExpr) } } else { queryBuilder = queryBuilder.Order(query.App.Priority.Desc(), query.App.CreateTime.Desc()) } } else { queryBuilder = queryBuilder.Order(query.App.Priority.Desc(), query.App.CreateTime.Desc()) } // 7. 执行分页查询 appList, err := queryBuilder.Offset(offset).Limit(req.PageSize).Find() if err != nil { return nil, err } // 8. 转换为AppVo列表 appVoList, err := s.GetAppVoList(ctx, appList) if err != nil { return nil, err } // 9. 构建分页响应 pageResponse := &response.PageResponse[vo.AppVo]{ Records: appVoList, PageNum: req.PageNum, PageSize: req.PageSize, TotalPage: totalPage, TotalRow: int(totalCount), } return pageResponse, nil }
管理员更新应用接口

接口路径: POST /app/admin/update 功能说明: 管理员更新应用信息,可更新应用名称、封面和优先级,无需权限校验。

API 层

请求结构体:

▼
go
复制代码
type YiKouAppAdminUpdateRequest struct { Id string `json:"id"` AppName string `json:"appName"` Cover string `json:"cover"` Priority int32 `json:"priority"` }

响应结构体:

▼
go
复制代码
type YiKouAppAdminUpdateResponse response.BaseResponse[bool]
Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) AdminUpdateApp(ctx context.Context, c *app.RequestContext) { req := &api.YiKouAppAdminUpdateRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } success, err := a.appService.AdminUpdateApp(ctx, req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[bool](success)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) AdminUpdateApp(ctx context.Context, req *api.YiKouAppAdminUpdateRequest) (bool, error) { // 1. 参数校验 if req.Id == "" { return false, errorutil.ParamsError.WithMessage("应用ID不能为空") } appId, err := strconv.Atoi(req.Id) if err != nil { return false, err } // 2. 查询应用 _, err = query.Use(s.db).App.Where(query.App.ID.Eq(int64(appId))).First() if err != nil { return false, err } // 3. 构建更新字段 updateMap := make(map[string]interface{}) if req.AppName != "" { updateMap["appName"] = req.AppName } if req.Cover != "" { updateMap["cover"] = req.Cover } updateMap["priority"] = req.Priority // 4. 执行更新 _, err = query.Use(s.db).App.Where(query.App.ID.Eq(int64(appId))).Updates(updateMap) if err != nil { return false, err } return true, nil }
管理员删除应用接口

接口路径: POST /app/admin/delete 功能说明: 管理员删除应用,使用逻辑删除,无需权限校验。

API 层

请求结构体:

▼
go
复制代码
type DeleteRequest struct { Id int `json:"id"` }

响应结构体:

▼
go
复制代码
type YiKouAppAdminDeleteResponse response.BaseResponse[bool]
Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) AdminDeleteApp(ctx context.Context, c *app.RequestContext) { req := &request.DeleteRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } success, err := a.appService.AdminDeleteApp(ctx, int64(req.Id)) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[bool](success)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) AdminDeleteApp(ctx context.Context, id int64) (bool, error) { // 逻辑删除应用 _, err := query.Use(s.db).App.Where(query.App.ID.Eq(id)).Update(query.App.IsDelete, 1) if err != nil { return false, err } return true, nil }
管理员获取应用详情接口

接口路径: GET /app/admin/get/vo 功能说明: 管理员根据 ID 获取应用详情,无需权限校验。

API 层

请求参数:

  • id(query 参数):应用 ID

响应结构体:

▼
go
复制代码
type YiKouAppAdminGetResponse response.BaseResponse[vo.AppVo]
Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) AdminGetAppVo(ctx context.Context, c *app.RequestContext) { id := c.Query("id") if id == "" { c.JSON(consts.StatusOK, response.NewErrorResponse[any](errorutil.ParamsError)) return } idInt64, _ := strconv.ParseInt(id, 10, 64) appVo, err := a.appService.AdminGetAppVo(ctx, idInt64) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[vo.AppVo](appVo)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) AdminGetAppVo(ctx context.Context, id int64) (vo.AppVo, error) { // 1. 查询应用 app, err := query.Use(s.db).App.Where(query.App.ID.Eq(id)).First() if err != nil { return vo.AppVo{}, err } // 2. 获取用户信息 userVo, err := s.userService.GetUserVo(ctx, app.UserID) if err != nil { return vo.AppVo{}, err } // 3. 构建应用 VO appVo := vo.AppVo{ ID: app.ID, AppName: app.AppName, Cover: app.Cover, InitPrompt: app.InitPrompt, CodeGenType: app.CodeGenType, DeployKey: app.DeployKey, DeployedTime: app.DeployedTime, Priority: app.Priority, UserID: app.UserID, User: userVo, CreateTime: app.CreateTime, UpdateTime: app.UpdateTime, } return appVo, nil }
管理员应用列表接口

接口路径: POST /app/admin/list/page/vo 功能说明: 管理员分页获取所有应用列表,支持多条件查询,无需权限校验。

API 层

请求结构体:

▼
go
复制代码
type YiKouAppAdminListRequest struct { request.PageRequest ID string `json:"id"` AppName string `json:"appName"` Cover string `json:"cover"` InitPrompt string `json:"initPrompt"` CodeGenType string `json:"codeGenType"` DeployKey string `json:"deployKey"` DeployedTime string `json:"deployedTime"` Priority int32 `json:"priority"` UserID int64 `json:"userId"` }

响应结构体:

▼
go
复制代码
type YiKouAppAdminListResponse response.BaseResponse[response.PageResponse[model.App]]

注意: 管理员列表返回的是 model.App 实体,而不是 vo.AppVo。

Handler 层

接口实现:

▼
go
复制代码
func (a *AppHandler) AdminListApp(ctx context.Context, c *app.RequestContext) { req := &api.YiKouAppAdminListRequest{} err := c.BindAndValidate(req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } pageResponse, err := a.appService.AdminListApp(ctx, req) if err != nil { c.JSON(consts.StatusOK, response.NewErrorResponse[any](err)) return } c.JSON(consts.StatusOK, response.NewSuccessResponse[*response.PageResponse[*model.App]](pageResponse)) }
Service 层

业务逻辑实现:

▼
go
复制代码
func (s *AppService) AdminListApp(ctx context.Context, req *api.YiKouAppAdminListRequest) (*response.PageResponse[*model.App], error) { // 1. 参数校验和默认值设置 if req.PageNum <= 0 { req.PageNum = 1 } if req.PageSize <= 0 { req.PageSize = 20 } if req.PageSize > 20 { req.PageSize = 20 } // 2. 构建查询条件 queryBuilder := query.Use(s.db).App.Where(query.App.IsDelete.Eq(0)) // 3. 添加查询条件 if req.ID != "" { id, _ := strconv.ParseInt(req.ID, 10, 64) queryBuilder = queryBuilder.Where(query.App.ID.Eq(id)) } if req.AppName != "" { queryBuilder = queryBuilder.Where(query.App.AppName.Like("%" + req.AppName + "%")) } if req.Cover != "" { queryBuilder = queryBuilder.Where(query.App.Cover.Like("%" + req.Cover + "%")) } if req.InitPrompt != "" { queryBuilder = queryBuilder.Where(query.App.InitPrompt.Like("%" + req.InitPrompt + "%")) } if req.CodeGenType != "" { queryBuilder = queryBuilder.Where(query.App.CodeGenType.Eq(req.CodeGenType)) } if req.DeployKey != "" { queryBuilder = queryBuilder.Where(query.App.DeployKey.Like("%" + req.DeployKey + "%")) } if req.Priority != 0 { queryBuilder = queryBuilder.Where(query.App.Priority.Eq(req.Priority)) } if req.UserID != 0 { queryBuilder = queryBuilder.Where(query.App.UserID.Eq(req.UserID)) } // 4. 查询总数 totalCount, err := queryBuilder.Count() if err != nil { return nil, err } // 5. 计算分页信息 totalPage := int((totalCount + int64(req.PageSize) - 1) / int64(req.PageSize)) offset := (req.PageNum - 1) * req.PageSize // 6. 设置排序 if req.SortField != "" { if orderExpr, ok := query.App.GetFieldByName(req.SortField); ok { if req.SortOrder == "desc" { queryBuilder = queryBuilder.Order(orderExpr.Desc()) } else { queryBuilder = queryBuilder.Order(orderExpr) } } else { queryBuilder = queryBuilder.Order(query.App.CreateTime.Desc()) } } else { queryBuilder = queryBuilder.Order(query.App.CreateTime.Desc()) } // 7. 执行分页查询 appList, err := queryBuilder.Offset(offset).Limit(req.PageSize).Find() if err != nil { return nil, err } // 8. 构建分页响应 pageResponse := &response.PageResponse[*model.App]{ Records: appList, PageNum: req.PageNum, PageSize: req.PageSize, TotalPage: totalPage, TotalRow: int(totalCount), } return pageResponse, nil }

三、实现应用生成接口

应用生成接口是本项目的核心功能,实现了用户与应用的AI对话,实时生成代码并保存。本节我将详细讲解应用生成接口的实现,包括流式响应、代码解析、代码保存等关键流程。

接口实现详解

Handler 层实现

文件位置: internal/handler/app_handler.go

接口实现:

▼
go
复制代码
// ChatToGenCode 应用聊天生成代码(流式) // @Summary 应用聊天生成代码(流式) // @Description 应用聊天生成代码(流式) // @Tags 应用模块 // @Accept json // @Produce json // @Param appId query string true "应用ID" // @Param message query string true "消息" // @Router /app/chat/gen/code [get] func (a *AppHandler) ChatToGenCode(ctx context.Context, c *app.RequestContext) { // 1. 设置 SSE 响应头 c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no") // 2. 获取请求参数 appIdStr := c.Query("appId") w := sse.NewWriter(c) lastEventID := sse.GetLastEventID(&c.Request) if appIdStr == "" { c.JSON(consts.StatusOK, response.NewErrorResponse[any](errorutil.ParamsError.WithMessage("应用ID不能为空"))) return } message := c.Query("message") if message == "" { _ = w.WriteEvent(lastEventID, "error", []byte("消息不能为空")) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return } // 3. 获取当前登录用户 userVo, err := a.userService.GetLoginUserVo(ctx, c) if err != nil { _ = w.WriteEvent(lastEventID, "error", []byte(fmt.Sprintf("%v", err))) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return } // 4. 转换应用ID appId, err := strconv.ParseInt(appIdStr, 10, 64) if err != nil { _ = w.WriteEvent(lastEventID, "error", []byte(fmt.Sprintf("%v", err))) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return } // 5. 获取流数据 streamResp, err := a.appService.ChatToGenCode(ctx, appId, message, &userVo) if err != nil { _ = w.WriteEvent(lastEventID, "error", []byte(fmt.Sprintf("%v", err))) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return } defer streamResp.Close() // 6. 流式返回数据 var aiResponseBuilder strings.Builder for { select { case <-ctx.Done(): logger.Info("连接中断") _ = w.WriteEvent(lastEventID, "done", []byte{1}) return default: } chunk, err := streamResp.Recv() if err == io.EOF || errors.Is(err, context.Canceled) { break } if err != nil { _ = w.WriteEvent(lastEventID, "error", []byte(fmt.Sprintf("%v", err))) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return } aiResponseBuilder.WriteString(chunk.Content) // 7. 发送SSE事件 wrapper := &map[string]string{ "d": chunk.Content, } data, err := json.Marshal(wrapper) if err != nil { logger.Errorf("序列化数据失败: %v\n", err) continue } err = w.WriteEvent(lastEventID, "message", data) if err != nil { _ = w.WriteEvent(lastEventID, "error", []byte(fmt.Sprintf("%v", err))) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return } } // 8. 发送完成事件 _ = w.WriteEvent(lastEventID, "done", []byte{1}) }

以下是我将会对某些步骤进行详解,因为我自己在当初在开发这个代码生成接口时踩了不少坑,所以我现在通过我踩过的坑给你们讲解一些重点步骤

设置 SSE 响应头

代码:

▼
go
复制代码
c.Header("Content-Type", "text/event-stream") c.Header("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") c.Header("X-Accel-Buffering", "no")

详细说明:

响应头值说明
Content-Typetext/event-streamSSE协议要求的MIME类型,告诉浏览器这是流式事件
Cache-Controlno-cache禁止缓存,确保实时接收数据
Connectionkeep-alive保持长连接,不断开TCP连接
X-Accel-Bufferingno禁用Nginx缓冲,确保数据实时传输
获取流数据

代码:

▼
go
复制代码
streamResp, err := a.appService.ChatToGenCode(ctx, appId, message, &userVo) if err != nil { _ = w.WriteEvent(lastEventID, "error", []byte(fmt.Sprintf("%v", err))) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return } defer streamResp.Close()

流式响应说明:

  • streamResp是 *schema.StreamReader[*schema.Message]类型
  • 使用 Recv()方法接收流数据
  • 使用 Close()方法关闭流
  • 必须使用defer确保流关闭,避免资源泄漏
循环读取流式数据

代码:

▼
go
复制代码
var aiResponseBuilder strings.Builder for { select { case <-ctx.Done(): logger.Info("连接中断") _ = w.WriteEvent(lastEventID, "done", []byte{1}) return default: } chunk, err := streamResp.Recv() if err == io.EOF || errors.Is(err, context.Canceled) { break } if err != nil { _ = w.WriteEvent(lastEventID, "error", []byte(fmt.Sprintf("%v", err))) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return } aiResponseBuilder.WriteString(chunk.Content) // ... 发送SSE事件 }

详细说明:

操作说明技术点
strings.Builder构建完整响应用于收集所有流数据
for循环持续接收流数据直到EOF或错误
select监听上下文取消处理连接中断
Recv()接收流数据块返回Message结构体
io.EOF流结束标志正常结束
context.Canceled上下文取消用户取消或超时
发送SSE事件

代码:

▼
go
复制代码
wrapper := &map[string]string{ "d": chunk.Content, } data, err := json.Marshal(wrapper) if err != nil { logger.Errorf("序列化数据失败: %v\n", err) continue } err = w.WriteEvent(lastEventID, "message", data) if err != nil { _ = w.WriteEvent(lastEventID, "error", []byte(fmt.Sprintf("%v", err))) _ = w.WriteEvent(lastEventID, "done", []byte{1}) return }

原生的data数据在传输数据的时候会丢失空格,影响了原本的内容格式。这里我们可以包装成json格式发送给前端,由前端再去解析json格式,这样就保证了格式的一致性了

SSE事件格式:

在这里插入图片描述

发送完成事件

代码:

▼
go
复制代码
_ = w.WriteEvent(lastEventID, "done", []byte{1})

完成事件说明:

  • 流正常结束后发送
  • 前端收到此事件后关闭SSE连接
  • 数据为 []byte{1},表示成功完成

这里我也是踩过一个非常致命的坑,我在前端调试的过程中,发现每次后端结束流的时候都没有发送完成事件。后来我发现,原来是因为hertz的sse库发送事件必须得夹带数据,不然就不会发送,当场整个人都红温了

在这里插入图片描述

Service 层实现

文件位置: internal/logic/app_logic.go

业务逻辑实现:

▼
go
复制代码
func (s *AppService) ChatToGenCode(ctx context.Context, appId int64, message string, loginUser *vo.UserVo) (*schema.StreamReader[*schema.Message], error) { // 1. 校验参数 if message == "" { return nil, errorutil.ParamsError.WithMessage("消息不能为空") } if appId == 0 || appId < 0 { return nil, errorutil.ParamsError.WithMessage("应用ID不能为空") } // 2. 校验应用是否存在 app, err := query.Use(s.db).App.Where(query.App.ID.Eq(appId), query.App.IsDelete.Eq(0)).First() if err != nil { return nil, err } // 3. 校验用户是否有权限使用该应用 if app.UserID != loginUser.ID { return nil, errorutil.NotAuthError.WithMessage("无权使用该应用") } // 4. 获取代码生成类型 if enum.CodeGenTypeTextMap[enum.CodeGenTypeEnum(app.CodeGenType)] == "" { return nil, errorutil.ParamsError.WithMessage("应用代码生成类型不支持") } // 5. 调用代码生成服务 return s.aiCodeGenFacade.GenCodeStreamAndSave(ctx, message, enum.CodeGenTypeEnum(app.CodeGenType), appId) }
修改 AI 代码生成门面结构体

文件位置: internal/core/ai_codegen_facade.go

流式生成并保存代码方法增加appId参数:

▼
go
复制代码
func (y *YiKouAiCodegenFacade) GenCodeStreamAndSave(ctx context.Context, userMessage string, typeStr enum.CodeGenTypeEnum, appId int64) (*schema.StreamReader[*schema.Message], error) { switch typeStr { case enum.HtmlCodeGen: streamResp, err := y.codegenService.GenerateHtmlCodeStream(ctx, userMessage) if err != nil { return nil, err } return y.processCodeStream(streamResp, typeStr, appId) case enum.MultiFileGen: streamResp, err := y.codegenService.GenerateMultiFileCodeStream(ctx, userMessage) if err != nil { return nil, err } return y.processCodeStream(streamResp, typeStr, appId) default: return nil, fmt.Errorf("不支持的代码生成类型: %s", typeStr) } }

处理代码流方法也一样:

▼
go
复制代码
func (y *YiKouAiCodegenFacade) processCodeStream(respStream *schema.StreamReader[*schema.Message], typeStr enum.CodeGenTypeEnum, appId int64) (*schema.StreamReader[*schema.Message], error) { // 1. 复制流,一个用于处理,一个返回给上游 streams := respStream.Copy(2) processingStream := streams[0] returnStream := streams[1] // 2. 在 goroutine 中处理流数据,不阻塞返回 go func() { var builder strings.Builder defer processingStream.Close() // 3. 接收完整的流数据 for { chunk, err := processingStream.Recv() if err == io.EOF { break } if err != nil { return } builder.WriteString(chunk.Content) } // 4. 解析代码 parsedResp, err := y.codeParserExecutor.ExecuteParser(builder.String(), typeStr) if err != nil { return } // 5. 保存代码(传入appId) dirPath, err := y.codeFileSaverExecutor.ExecuteSaver(parsedResp, typeStr, appId) if err != nil { return } logger.Info("代码已保存到目录: %s", dirPath) }() return returnStream, nil }
代码保存器实现

文件位置: internal/core/saver/codefile_saver.go

代码保存执行器的执行方法增加appId参数:

▼
go
复制代码
func (e *CodeFileSaverExecutor) ExecuteSaver(content interface{}, saveType enum.CodeGenTypeEnum, appId int64) (string, error) { switch saveType { case enum.HtmlCodeGen: return e.htmlCodeFileSaver.saveCode(content.(*aimodel.HtmlCodeResponse), appId) case enum.MultiFileGen: return e.multiFileCodeFileSaver.saveCode(content.(*aimodel.MultiFileCodeResponse), appId) default: return "", fmt.Errorf("不支持的代码文件类型: %s", saveType) } }

代码保存模板的两个方法也一样:

▼
go
复制代码
type CodeFileSaverTemplate[T any] struct { CodeFileSaver[T] } func (d *CodeFileSaverTemplate[T]) saveCode(response T, appId int64) (string, error) { err := d.validateInput(response) if err != nil { return "", err } dirPath, err := d.buildUniqueDir(appId) if err != nil { return "", err } return dirPath, d.saveFiles(response, dirPath) } // buildUniqueDir 构建唯一的目录名 // 目录名格式: {代码生成类型}_{唯一ID} func (d *CodeFileSaverTemplate[T]) buildUniqueDir(appId int64) (string, error) { if appId == 0 { return "", fmt.Errorf("应用id不能为空") } //构建唯一目录名 fileSaveDir, err := myfile.GetCodeOutputRoot() uniqueDirName := fmt.Sprintf("%s_%s", d.getCodeType(), strconv.FormatUint(uint64(appId), 20)) dirPath := filepath.Join(fileSaveDir, uniqueDirName) // 创建目录 err = os.MkdirAll(dirPath, os.ModePerm) if err != nil { return "", err } return dirPath, nil }
修改测试方法增加appId

文件位置: internal/core/ai_codegen_facade_test.go

流式生成测试:

▼
go
复制代码
func TestYiKouAiCodegenFacade_GenCodeStreamAndSave(t *testing.T) { config.SetEnvFlag("local") // 解析命令行参数 initConfig := config.InitConfig() chatModel := llm.NewChatModel(initConfig) codeGenAgent := agent.NewCodeGenAgent(chatModel, enum.MultiFileGen) parserExecutor := parser.NewCodeParserExecutor() fileSaverExecutor := saver.NewCodeFileSaverExecutor() aiCodegenFacade := NewYiKouAiCodegenFacade(codeGenAgent, parserExecutor, fileSaverExecutor) // 调用流式生成方法(传入appId) resp, err := aiCodegenFacade.GenCodeStreamAndSave(context.Background(), "帮我生成一个日常记录网站", enum.MultiFileGen, 1) if err != nil { panic(err) } var builder strings.Builder for { message, err := resp.Recv() if err != nil { break } builder.WriteString(message.Content) } assert.NotNil(t, builder.String()) }
修改路由配置

文件位置: internal/router/router.go

增加接口声明:

▼
go
复制代码
appRoute := h.Group("/app") { // ... 其他路由 // 需要登录的接口 appRoute.GET("/chat/gen/code", middleware.AuthMiddleware(enum.UserRole, db), appHandler.ChatToGenCode) // ... 其他路由 }
修改依赖注入配置

文件位置: wire/wire.go

修改服务依赖注入(记得把包引入修改成自己的包):

▼
go
复制代码
//go:build wireinject package wire import ( "fmt" "github.com/cloudwego/hertz/pkg/app/server" "github.com/google/wire" "github.com/hertz-contrib/swagger" "gorm.io/gorm" "strconv" "yikou-ai-go-teach/config" "yikou-ai-go-teach/docs" "yikou-ai-go-teach/internal/ai" "yikou-ai-go-teach/internal/ai/agent" "yikou-ai-go-teach/internal/ai/llm" "yikou-ai-go-teach/internal/core" "yikou-ai-go-teach/internal/core/parser" "yikou-ai-go-teach/internal/core/saver" "yikou-ai-go-teach/internal/dal" "yikou-ai-go-teach/internal/handler" "yikou-ai-go-teach/internal/logic" "yikou-ai-go-teach/internal/router" "yikou-ai-go-teach/internal/service" ) // 配置依赖 var configSet = wire.NewSet( config.InitConfig, ) var llmSet = wire.NewSet(llm.NewChatModel) // 数据库依赖 var dbSet = wire.NewSet( dal.InitDB, ) // Service依赖 var serviceSet = wire.NewSet( core.NewYiKouAiCodegenFacade, logic.NewAppService, wire.Bind(new(service.IAppService), new(*logic.AppService)), logic.NewUserService, wire.Bind(new(service.IUserService), new(*logic.UserService)), agent.NewTestCodeGenAgent, wire.Bind(new(ai.IYiKouAiCodegenService), new(*agent.CodeGenAgent)), ) // Handler依赖 var handlerSet = wire.NewSet( handler.NewUserHandler, handler.NewAppHandler, ) // initServer 初始化 Web 服务器 func initServer(cfg *config.Config, userHandler *handler.UserHandler, appHandler *handler.AppHandler, db *gorm.DB) *server.Hertz { // 动态设置 Swagger 信息 docs.SwaggerInfo.Host = fmt.Sprintf("localhost:%d", cfg.Server.Port) docs.SwaggerInfo.BasePath = cfg.Server.ContextPath // 初始化swagger路径 swaggerPath := fmt.Sprintf("http://localhost:%d%s/swagger/doc.json", cfg.Server.Port, cfg.Server.ContextPath) url := swagger.URL(swaggerPath) // 创建 Hertz 服务器 h := server.Default( server.WithHostPorts(":"+strconv.Itoa(cfg.Server.Port)), server.WithBasePath(cfg.Server.ContextPath), ) // 注册路由 router.RegisterRoutes(h, url, db, userHandler, appHandler) return h } // InitializeApp 初始化所有依赖(依赖图) func InitializeApp() (*server.Hertz, error) { panic(wire.Build( initServer, configSet, dbSet, serviceSet, handlerSet, llmSet, parser.NewCodeParserExecutor, saver.NewCodeFileSaverExecutor, )) }

在项目的根目录下执行wire生成命令,生成注入文件

▼
bash
复制代码
cd ./wire wire

在ide的命令台执行swagger的api文档生成命令

▼
bash
复制代码
swag init

四、测试sse接口

由于sse接口不能像之前一样在swagger文档上测试,所以我们直接在前端页面上测试,尽管现在没有开发完所有的后端接口,但是我们仍然能看到接口的效果。这里你们直接到我的GitHub教学仓库https://github.com/FeiWuSama/yikou-ai-go-teach克隆或者复制仓库下载前端源码就行了。获得前端源码直接进入前端源码的根目录,打开该目录的cmd窗口,输入以下前端运行命令即可:npm run dev

▼
bash
复制代码
npm run dev

在这里插入图片描述

在这里插入图片描述

然后再启动后端服务,但是一定要记住后端的服务启动端口要和前端的反向代理的配置端口一致。这里我就不教大家怎样修改前端配置了,直接修改后端的配置文件保持和前端的配置文件一样就行了

在这里插入图片描述

我们启动完前端和后端,直接打开浏览器在正上方访问 http://localhost:5173/ 就可以测试了

我们先在右上方登录之前注册过的账号,这里记得先提前按f12打开前端开发控制台实时观察流式输出过程,然后在对话框输入提示词:

▼
markdown
复制代码
请帮我生成一个简单的任务记录工具网站

然后点击发送按钮

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可以看到,前端的效果是正常的,到这里我们的应用模块的已经基本完成了。在下一章,我们将会进一步拓展代码生成智能体,赋予其记忆能力,并且开发出对话记忆模块,请大家尽情期待。要是对该教程感兴趣的,可以star一下仓库 https://github.com/FeiWuSama/yikou-ai-go 给予博主更多支持哦,谢谢各位看到这里的读者!

GitHub
Go
AI
AI 零代码应用生成平台
4
2
分享
操作

相关专栏

Go + CloudWeGo 零代码 AI 应用平台

Go + CloudWeGo 零代码 AI 应用平台

Go + CloudWeGo 零代码 AI 应用平台项目实战教程

评论
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片

目录

🔥 热门文章
1
手把手教你设计一个成熟的 Agent Harness
2510
2
我用 REA 逆向了付费颜色 APP,结果很意外!
570
3
离谱,DeepSeek 崩了 5 小时,大家却喊:梁圣恩情还不完??
420
4
DeepSeek 全新模型发布,性价比最高的一集!
380
5
B 站上 DeepSeek 闹鱼灾了,我发起《大肥鱼清除计划》!
370
6
自己学习ai,直接买cursor 的套餐,还是买gpt 或者claude 的套餐?
360
7
又一个国产版 Codex 神器,也来卷办公了!
330
8
day3-如何写出一个高质量的 Skill(组内分享) 高质量 Skill 不是“一个调得很顺的巨型 Prompt”,而是一个可验证、可回归、可维护的能力包。 先定义“什么算好”,再实现“怎么做好”。评估体系先于 Skill 本身成型。 下面用“故障诊断 Markdown 转 YAML”作为例子,但方法是通用的。 --- 一、先想清楚:Skill 到底是什么 一个 Skill 至少包含四部分: 部分 作用 接口 输入什么、输出什么、边界在哪 流程 分几步做,每步的产物是什么 规则 硬约束、经验规则、失败模式 降级策略 不确定时怎么办,而不是硬编一个看似合理的结果 高质量 Skill 的共同点:职责单一、接口清晰、规则可定位、输出可验证。 反例:把几十条经验全塞进一个主 Prompt,改一条规则要重调整个 Prompt,评估时也不知道是哪条规则退化。 --- 二、核心方法:先建评估,再写 Skill 不要先写转换器再补测试。正确顺序是: 1. 定义输出契约:什么算合法、什么算合格; 2. 建金标准集:一小批人工认可“可放行”的样本; 3. 先写评估 Skill:让它在金标准集上跑出基线; 4. 再迭代生成 Skill:每次修改跑同一套评估,无退化且目标维度提升才算正向。 这就是评估驱动开发:评估是 Skill 的单元测试,金标准集是回归锚点。 --- 三、评估 Skill:三层门禁,逐层递进 前一层不过,后一层不跑。每层留可追溯证据。 第 1 层:确定性硬门禁 用解析器、Schema、校验器完成,不消耗模型成本。 · 输出可解析; · 必填字段齐全,类型/枚举合法; · 引用目标存在,无悬空引用、无不可达节点; · 关键路径至少有一条能到达预期出口。 不通过直接打回。 第 2 层:语义断言 用脚本 + 结构遍历检查“逻辑对不对”,仍然保持确定性。 通用检查项: · 覆盖率:源材料里的关键步骤、分支、条件,输出里是否都有对应? · 完整性:每个判断节点是否都有明确出口?有没有死胡同? · 可达性:每条路径是否都能到达预期终点?有没有无出口循环? · 顺序与依赖:有先后依赖的操作,是否保留了顺序?有没有被拍平成无序列表? 例子:故障诊断转换中,检查“每个判断项是否有是/否/异常出口”“每条路径是否能到根因”。 第 3 层:LLM-as-Judge 只处理解析器表达不了的维度:忠实度、完整性、可执行性。 但 Judge 会漂移,不能单独放行,必须建立在第 1、2 层之上。Judge 的 Prompt、rubric、模型版本都要版本化,换模型必须用金标准集重新校准。 通用 Judge 维度: 维度 问什么 忠实度 输出是否忠实源材料意图?有没有自行发明内容? 完整性 关键步骤、条件、证据是否保留?隐式知识有没有被压成空话? 收敛性 输出是否仍指向原目标,而不是发散? 可执行性 下游不依赖源材料能否直接使用? --- 四、金标准集:回归测试的锚点 不需要一开始就标注几百份。用已认可的样本当种子,扩到 15~30 份,覆盖失败模式而非随机抽样: · 完整正例:典型流程; · 边界/异常分支:异常路径、循环、前置失败回退; · 已知易错模式:实际踩过的坑; · 留出集:不参与调优,只用于最终验收,防过拟合。 金标准不需要完美,只需要当前标准下认可可放行。 回归指标:关键路径用 pass^k(k 次全过),稳定性用 pass@k。 --- 五、工程约束:让批量产出可重放 · 幂等:同输入 + 同 Skill 版本 + 同规则 → 输出一致,否则无法判断变好是 Skill 改了还是随机波动; · 隔离不阻断:单份失败进隔离队列,记录失败层和原因,不阻塞其余; · 版本绑定:规则、rubric、金标准集进 Git;每份报告绑定“源版本 + 生成 Skill 版本 + 评估 Skill 版本 + Judge 模型版本”; · 抽样人工复核:Judge 临界样本 + 易错模式,人工抽检结果反过来校准 rubric。 --- 六、Skill 本身怎么写 · SKILL.md 控制篇幅,写清:输入约定、输出契约、分步流程、硬规则、降级策略; · 经验规则放 references/ 或 rules/,按失败模式分类,不要全塞进主 Prompt; · 不确定时标 needs_review: true,不要硬编看似合理的结果; · 迭代由评估报告驱动:报告告诉你哪层挂了、哪类失败模式,你改对应规则文件,而不是漫无目的调 Prompt。 --- 七、落地顺序(不要反过来) 1. 固化输出契约:Schema + 结构约束; 2. 建金标准集:从已认可的 + 最易出错的样本开始; 3. 先写评估 Skill:跑出基线报告; 4. 再迭代生成 Skill:每次修改对比逐项通过率,无退化且目标提升才算正向; 5. 批量运行 + 隔离回放:失败进队列,结束后人工抽检; 6. 评估 Skill 自身版本化:换 Judge 模型必须重新校准。 --- 八、给组员的三条硬要求 1. 不许凭感觉说“变好了”:每次修改必须附评估报告对比,逐项通过率说话。 2. 不许跳过确定性门禁直接看 Judge 分数:前两层挂了,Judge 分数没有意义。 3. 不许把规则堆进主 Prompt:规则按失败模式分文件,评估报告要能定位到具体文件。 --- 一句话收尾 生成 Skill 决定“能不能做”,评估 Skill 决定“敢不敢用”。 先让评估体系站稳,再让生成器迭代。这样交付的不是一堆“看起来还行”的产出,而是一套可证明、可回归、可交接的 Skill 质量体系。
310
9
AI 越强,被它拒绝的代价就越大 我有时候会想:AI 越来越厉害之后,有些事情,会不会反而越来越难做? 这个想法看起来有点矛盾。AI 明明在降低门槛,让不会写代码的人做出软件,让不会画画的人生成图片,让一个人完成过去需要整支团队才能完成的工作。但今天用 Codex 的时候,AI 拒绝帮我编写 SQL 注入的请求体,而那还是我有源码、在本地跑测试的情况。那么,往后是否只会更难? 我们假设某一天,AI 说:「这个请求违规,我不能帮助你。」到那个时候,你还有多少能力,能绕过它,把事情做完? 这里的「违规」需要打个引号。平台的规则、模型的判断、法律的要求,以及一件事实际有没有造成伤害,彼此之间并不总是画得上等号。一个请求可能确实危险,也可能只是缺少上下文被误判,或者恰好落在某家公司的限制范围里。 无论是哪种情况,随着我们越来越依赖 AI,这句拒绝都可能变得越来越贵。 今天,一个程序员让 AI 帮忙写代码,AI 拒绝了,他大概率还有别的办法:查文档、找开源项目、问同行,或者自己写。麻烦一点,慢一点,但路还在。 可如果十年之后,大部分程序都由 AI 完成,情况就不一样了。 很多人从接触编程的第一天起,就习惯了描述需求、等待生成、运行结果、继续迭代。他们能做出相当复杂的产品,却未必经历过独立处理底层问题的训练。平时,这种能力结构完全够用,效率甚至更高。只有 AI 拒绝的时候,他们才会发现:平常被省掉的那段学习过程,恰好是此刻最需要的东西。 到那时,一次拒绝的成本,可能就从「今晚多花两个小时」,变成「先花半年学会这门技术」。 拿 SQL 注入举个稍显极端的例子:往后,连真正理解这类攻击如何发生、能够独立构造测试的人,会不会也越来越少? 这里也包括那些有正当需求的人。一个开发者想检查自己的旧系统,一个经过授权的安全研究员想复现漏洞,假如 AI 识别到危险模式就拒绝,而使用者又只熟悉「让 AI 帮我检查」,这项原本可以独立完成的工作,就这样卡住了。 但是安全专家不会因此消失,但这类技能可能越来越集中在少数人手里。你仍然可以找专家,只是要付出更多的钱、更多的时间,以及更高的沟通成本。 类似的困境,未必只发生在网络安全领域。 你想从一个停止维护的软件里取回自己的数据,需要解析它的旧文件格式。AI 判断这涉及规避保护措施,拒绝继续。你拥有这些数据,却没有能力把它们读出来。 你买了一台设备,厂商停止提供服务。你想修改程序让它继续运转,过程中需要处理某种访问限制。AI 不愿协助,而你所掌握的维修方式,早已只剩下「把报错发给 AI」。 你想给常用的网站写一个辅助工具,减少重复操作。平台将此归入自动化限制,AI 也据此拒绝。过去会写脚本的人可以自己判断、自己处理;未来的使用者,可能连从哪里开始都不知道。 这些例子有一个共同点:当工具同时承担了「执行工作」和「传授知识」的角色,它的拒绝就不只是挡住了一次任务,也挡住了获得这种能力的机会。 更麻烦的是,这种依赖往往是我们主动选择的,而且每一步都很合理。 既然 AI 几秒钟就能做好,为什么还要花几个月学习?既然公司可以采购成熟的 AI 服务,为什么还要养一支负责基础工作的团队?既然市场上已经很少有人手写这些东西,学校为什么还要花大量时间教? 对个人、公司和学校来说,这些选择都有现实依据。但叠加起来,整个社会保留的独立能力就可能越来越少。等到某项能力突然受到限制,我们才发现,备用方案早已在追求效率的过程中被一点点削掉了。 所以,「AI 越强,做事越便宜」可能只描述了未来的一半。 另一半是:被允许的事情越来越便宜,被拒绝的事情依然需要人工完成,却要面对更少的人才、更高的报价,以及越来越难获得的知识。两边的成本差距,会越拉越大。 有人会说,可以换一个模型,或者用本地模型。这确实是重要的选项。只要工具之间还有充分竞争,开放模型还足够可用,限制就未必能形成真正的门槛。 但备用方案也需要维护。它需要硬件、知识、软件生态,以及一群仍然理解这些系统的人。如果大多数人已经习惯了最方便的商业服务,那么「理论上还有别的办法」和「普通人实际用得起、用得明白」,之间仍然可能隔着很远。 我担心的,正是这段距离。 我不指望 AI 对任何请求都照单全收。对明显会伤害他人的行为设置限制,有充分的理由。但我们也需要关心:限制由谁定义,误判有没有申诉渠道,用户能不能换工具,以及脱离这个工具之后,还剩下多少独立行动的能力。 保留这些能力,平时看起来甚至有点低效,明明可以自动生成,还要理解原理;明明可以一键完成,还要知道手动怎么做。 但这些看似多余的知识,会在工具拒绝、失效、涨价或改变规则的那一刻,成为真正的选择权。 AI 越强,我们可能越需要留意它说「不」的那一刻。 因为那句话的分量,最终取决于:它拒绝之后,我们还能做什么。
310
10
什么是LLM
300
🔥热门工具
朱雀AI检测朱雀AI检测
朱雀AI检测助手朱雀AI检测助手
LovartLovart
老鱼简历老鱼简历
WritePass官网WritePass官网
面多多面多多
CursorCursor
星月写作星月写作
零感AI零感AI
墨得问题墨得问题