# HotGo 项目开发规范 **生成或修改任何代码前,必须完整阅读本文档。** 本规范是项目唯一事实来源,所有新增代码必须严格对齐现有模块风格,不得自创新的代码模式。 --- ## 技术栈 | 层 | 技术 | |----|----------------------------------------------------------------| | 后端语言 | Go,module 名 `hotgo` | | 后端框架 | GoFrame v2(gdb、ghttp、gcron、gerror、gctx、gerror、gjson、gvar) | | 前端框架 | Vue 3 + Vite + TypeScript | | UI 组件库 | **Naive UI**(优先使用,未经允许不得引入其他 UI 库) | | 状态管理 | Pinia | | HTTP 客户端 | 项目封装 `@/utils/http/axios`(`http.request`),禁止直接使用 axios | --- ## 后端目录结构 ``` server/ ├── addons/ # 插件模块(每个插件独立微架构,见"插件开发"章节) │ ├── modules/ # 隐式注册文件(每个插件一个 .go,仅含 import) │ └── / # 插件目录,与主模块结构镜像 ├── api/admin// # 主模块 HTTP 契约:Req/Res + g.Meta 路由声明 ├── internal/ │ ├── consts/ # 全局常量与字典选项(枚举在此定义并注册) │ ├── global/ # 全局公用变量(谨慎添加,尽量少用) │ ├── controller/admin/sys/ # 薄控制器,只做参数透传,不含业务逻辑 │ ├── crons/ # 主模块定时任务注册与处理 │ ├── queues/ # 主模块消息队列消费者注册与处理 │ ├── logic/sys/ # 业务实现(sSysXxx),init() 注册到 service │ ├── library/ # 内部基础库(addons、dict、hgorm、cache、queue、cron 等) │ ├── service/ # 接口声明(ISysXxx)+ 注册函数 │ ├── dao/ # 对外 DAO;internal/dao/internal/ 为 CLI 生成,禁止手改 │ ├── model/ │ │ ├── entity/ # 表结构体(CLI 生成,禁止手改) │ │ ├── do/ # ORM 写入结构(CLI 生成,禁止手改) │ │ └── input/sysin/ # 业务入参、出参、过滤结构 │ └── router/ │ ├── admin.go # 主路由,末尾调用 genrouter.Register() │ └── genrouter/ # 每个生成模块一个文件,init() 追加到 LoginRequiredRouter ├── resource/ │ ├── generate/default/curd/ # 主模块 CRUD 代码生成模板(参考,禁止直接修改) │ └── generate/default/addon/ # 插件脚手架代码生成模板(参考,禁止直接修改) └── utility/ # 通用工具:convert、excel、validate 等 ``` --- ## 后端分层约定 ### 分层职责(禁止跨层调用) ``` api(HTTP契约) → controller(参数透传) → service(接口) → logic(业务实现) → dao(数据访问) ``` - **controller 层**:不写任何业务逻辑,只转调 service - **logic 层**:不出现 HTTP 相关代码,所有校验通过 `Filter()` 或 `g.Validator()` 完成 - **dao 层**:`internal/dao/internal/` 由 CLI 自动生成,禁止手改 --- ### 1. API 层 `api/admin//.go` - 每个接口一对 `XxxReq` / `XxxRes`,`g.Meta` 声明路由 - GET 接口用 `method:"get"`,写操作用 `method:"post"` - 请求体嵌入 `sysin.XxxInp`,响应体嵌入对应 Model 或空 `struct{}` ```go type ListReq struct { g.Meta `path:"/xxxModule/list" method:"get" tags:"XXX模块" summary:"获取XXX列表"` sysin.XxxListInp } type ListRes struct { form.PageRes List []*sysin.XxxListModel `json:"list" dc:"数据列表"` } ``` ### 2. Controller 层 `internal/controller/admin/sys/.go` - 变量:`var XxxModule = cXxxModule{}`,类型:`type cXxxModule struct{}` - 只做参数转发和结果组装,禁止直接操作 dao 或写业务逻辑 ```go func (c *cXxxModule) List(ctx context.Context, req *xxxmodule.ListReq) (res *xxxmodule.ListRes, err error) { list, totalCount, err := service.SysXxxModule().List(ctx, &req.XxxListInp) if err != nil { return } if list == nil { list = []*sysin.XxxListModel{} } res = new(xxxmodule.ListRes) res.List = list res.PageRes.Pack(req, totalCount) return } ``` ### 3. Service 层 `internal/service/sys.go` - 在已有文件中**追加**接口声明和注册函数,禁止新建文件 - 声明 `ISysXxxModule` 接口,提供 `SysXxxModule()` 获取函数和 `RegisterSysXxxModule()` 注册函数 ### 4. Logic 层 `internal/logic/sys/.go` - 结构体:`type sSysXxxModule struct{}` - `init()` 里调用 `service.RegisterSysXxxModule(NewSysXxxModule())` - `Model()` 方法:`return handler.Model(dao.XxxTable.Ctx(ctx), option...)` - 列名引用必须用 `dao.XxxTable.Columns().FieldName`,禁止硬编码字符串字段名 - `Edit` 方法以 `in.Id > 0` 区分更新/新增,更新用 `XxxUpdateFields`,新增用 `XxxInsertFields` - 字段校验在 `sysin` 的 `Filter(ctx)` 方法完成,logic 层不重复校验 ### 5. Model 层 `internal/model/input/sysin/.go` | 类型 | 用途 | |------|------| | `XxxUpdateFields` | 更新时字段白名单 | | `XxxInsertFields` | 新增时字段白名单 | | `XxxEditInp` | 嵌入 `entity.XxxTable`,含 `Filter()` 校验方法 | | `XxxListInp` | 列表查询入参,嵌入 `form.PageInp` | | `XxxListModel` | 列表返回字段 | | `XxxViewInp` / `XxxViewModel` | 详情入参/出参 | | `XxxDeleteInp` | 删除入参(通常含 `Ids []int64`) | | `XxxStatusInp`、`XxxMaxSortInp` 等 | 其他操作 | ### 6. 路由注册 `internal/router/genrouter/.go` ```go package genrouter import "hotgo/internal/controller/admin/sys" func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.XxxModule) // XXX模块 } ``` 新建此文件即可,`genrouter/init.go` 通过 `init()` 链自动加载,无需修改 `admin.go`。 --- ## 字典与枚举规范 **字典由后端统一维护**,前端通过固定接口拉取,禁止在前端硬编码枚举值。 ### 后端:定义与注册(`internal/consts/` 下对应文件) ```go // 1. 定义常量 const ( XxxStatusEnabled = 1 // 启用 XxxStatusDisable = 2 // 禁用 ) // 2. 定义选项(选择合适的样式函数) var XxxStatusOptions = []*model.Option{ dict.GenSuccessOption(XxxStatusEnabled, "启用"), dict.GenWarningOption(XxxStatusDisable, "禁用"), } // 3. 在 init() 中注册(同文件) func init() { dict.RegisterEnums("XxxStatusOptions", "XXX状态选项", XxxStatusOptions) } ``` **样式函数选择规则:** | 函数 | 适用场景 | |------|---------| | `dict.GenSuccessOption` | 正常、启用、成功 | | `dict.GenWarningOption` | 待处理、禁用、警告 | | `dict.GenErrorOption` | 失败、封禁、危险 | | `dict.GenInfoOption` | 信息类、中性状态 | | `dict.GenPrimaryOption` | 主要操作、强调 | | `dict.GenDefaultOption` | 默认、无特殊含义 | | `dict.GenHashOption` | 不确定数量的动态枚举 | ### 后端:Logic 层中使用字典 ```go // 获取标签文本 label := dict.GetOptionLabel(consts.XxxStatusOptions, in.Status) // 校验是否为合法值 if !dict.HasOptionKey(consts.XxxStatusOptions, in.Status) { return gerror.New("状态值不合法") } ``` ### 前端:加载与使用字典 **加载**(在 `model.ts` 的 `loadOptions()` 中): ```ts export function loadOptions() { dict.loadOptions(['XxxStatusOptions', 'sys_normal_disable']); } ``` **在 `model.ts` 的 columns 中渲染字典标签:** ```ts import { useDictStore } from '@/store/modules/dict'; const dict = useDictStore(); // columns 中 { title: '状态', key: 'status', render(row) { return dict.getLabel('XxxStatusOptions', row.status); } } ``` **在搜索表单 schemas 中使用字典下拉:** ```ts { field: 'status', component: 'NSelect', label: '状态', componentProps: { options: computed(() => dict.getOptions('XxxStatusOptions')), placeholder: '请选择状态', }, } ``` --- ## 插件(Addon)开发规范 > 定位:独立、临时性、工具类型的功能推荐插件化开发,例如小游戏、广告管理、文章管理、小程序、微商城等。插件之间完全隔离,方便多项目复用。 ### 插件目录结构 每个插件拥有完整的独立微架构,与主模块结构镜像,但 **包路径前缀为 `hotgo/addons//`**: ``` server/addons// ├── main.go # 插件入口:定义 module 结构体,实现 Module 接口,init() 注册 ├── global/ │ ├── global.go # 插件级全局变量 │ └── init.go # Init(ctx, skeleton),GetSkeleton() 方法 ├── api/ │ ├── admin/ # 后台接口契约(同主模块 api 写法) │ ├── api/ # 前台 API 接口契约 │ └── home/ # 首页接口契约(如有) ├── controller/ │ ├── admin/sys/ # 后台薄控制器 │ ├── api/ # 前台控制器 │ └── home/ # 首页控制器(如有) ├── logic/ │ ├── logic.go # 匿名导入各 logic 子包触发 init() │ └── sys/ # 业务实现(同主模块 logic 写法) ├── service/ # 插件内接口声明与注册(独立,不与主模块 service 混用) ├── model/ │ └── input/sysin/ # 插件内入参/出参(调用主模块服务时继承主模块 input 结构) ├── router/ │ ├── admin.go # 注册后台路由,使用 addons.RouterPrefix() 获取路由前缀 │ ├── api.go # 注册前台路由 │ ├── home.go # 注册首页路由(如有) │ ├── websocket.go # 注册 WebSocket 路由(如有) │ └── genrouter/ │ ├── init.go # 插件版 genrouter:Register() 使用插件前缀 │ └── *.go # 每个生成模块一个文件,追加到插件 LoginRequiredRouter ├── consts/ # 插件内常量与字典(同主模块 consts 写法) ├── crons/ # 插件定时任务(可选) ├── queues/ # 插件消息队列消费者(可选) └── resource/ # 插件静态资源与模板(可选) ``` **对应前端目录:** ``` web/src/ ├── api/addons// # 插件前端 API 封装 └── views/addons// # 插件前端页面 ``` ### 插件入口 `main.go` ```go package import ( "context" "github.com/gogf/gf/v2/net/ghttp" _ "hotgo/addons//crons" "hotgo/addons//global" _ "hotgo/addons//logic" _ "hotgo/addons//queues" "hotgo/addons//router" "hotgo/internal/library/addons" "hotgo/internal/service" "sync" ) type module struct { skeleton *addons.Skeleton ctx context.Context sync.Mutex } func init() { newModule() } func newModule() { m := &module{ skeleton: &addons.Skeleton{ Label: "插件显示名", Name: "", // 与目录名一致 Group: 1, Brief: "简介", Description: "详细描述", Author: "作者", Version: "v1.0.0", }, ctx: gctx.New(), } addons.RegisterModule(m) } func (m *module) Start(option *addons.Option) (err error) { global.Init(m.ctx, m.skeleton) option.Server.Group("/", func(group *ghttp.RouterGroup) { group.Middleware(service.Middleware().Addon) router.Admin(m.ctx, group) router.Api(m.ctx, group) }) return } // 其余方法:Stop、Ctx、GetSkeleton、Install、Upgrade、UnInstall(默认留空) ``` ### 插件路由前缀规则 ``` 后台:/admin//接口路径 → 对应 controller/admin/sys/ 前台:/api//接口路径 → 对应 controller/api/ 首页:/home//接口路径 → 对应 controller/home/ WebSocket:/socket//接口路径 ``` 路由前缀通过 `addons.RouterPrefix(ctx, consts.AppAdmin, global.GetSkeleton().Name)` 自动生成,**禁止在路由文件里硬编码路径前缀**。 ### 插件 genrouter 与主模块的区别 插件 `genrouter/init.go` 的 `Register()` 函数需要传入 `ctx` 和 `group`,并使用插件路由前缀: ```go // 插件 genrouter/init.go func Register(ctx context.Context, group *ghttp.RouterGroup) { prefix := addons.RouterPrefix(ctx, consts.AppAdmin, global.GetSkeleton().Name) group.Group(prefix, func(group *ghttp.RouterGroup) { group.Middleware(service.Middleware().AdminAuth) if len(LoginRequiredRouter) > 0 { group.Bind(LoginRequiredRouter...) } }) } // 插件 genrouter/.go func init() { LoginRequiredRouter = append(LoginRequiredRouter, sys.XxxModule) } ``` 参考实现:`server/addons/hgexample/router/genrouter/` ### 插件调用主模块服务 **在插件 input 层继承主模块 input 结构,解耦参数依赖**,避免 import cycle: ```go // 插件 model/input/sysin/config.go package sysin import "hotgo/internal/model/input/sysin" type UpdateConfigInp struct { sysin.UpdateAddonsConfigInp } ``` ```go // 插件 logic 中调用主模块服务 import isc "hotgo/internal/service" func (s *sSysConfig) UpdateConfigByGroup(ctx context.Context, in sysin.UpdateConfigInp) error { in.UpdateAddonsConfigInp.AddonName = global.GetSkeleton().Name return isc.SysAddonsConfig().UpdateConfigByGroup(ctx, in.UpdateAddonsConfigInp) } ``` ### 插件获取自身信息 ```go // 插件内部 global.GetSkeleton() // 任意位置判断当前请求是否为插件请求 contexts.IsAddonRequest(ctx) contexts.GetAddonName(ctx) ``` ### 插件隐式注册 在 `server/addons/modules/` 下新建 `.go`,内容仅含一行匿名导入: ```go package modules import _ "hotgo/addons/" ``` ### 新增插件检查清单 **后端:** - [ ] `addons//main.go` — 插件入口,实现 Module 接口 - [ ] `addons//global/` — global.go + init.go - [ ] `addons/modules/.go` — 隐式注册 - [ ] `addons//service/` — 插件内接口声明(独立包) - [ ] `addons//logic/logic.go` — 匿名导入触发 init() - [ ] `addons//logic/sys/.go` — 业务实现 - [ ] `addons//model/input/sysin/.go` — 入参/出参 - [ ] `addons//api/admin//.go` — API 契约 - [ ] `addons//controller/admin/sys/.go` — 薄控制器 - [ ] `addons//router/admin.go` — 路由注册 - [ ] `addons//router/genrouter/init.go` — 插件 genrouter - [ ] `addons//router/genrouter/.go` — 模块路由追加 **前端:** - [ ] `web/src/api/addons//index.ts` — API 封装 - [ ] `web/src/views/addons//model.ts` — State、schemas、columns - [ ] `web/src/views/addons//index.vue` — 列表页 - [ ] `web/src/views/addons//edit.vue` — 编辑页 参考实现:`server/addons/hgexample/`(完整插件示例)、`server/resource/generate/default/addon/`(生成模板) --- ## 标准 CRUD 接口清单 | 接口 | method | 说明 | |------|--------|------| | `/xxx/list` | GET | 分页列表 | | `/xxx/view` | GET | 详情 | | `/xxx/edit` | POST | 新增/编辑(id>0 为编辑) | | `/xxx/delete` | POST | 删除(支持批量,传 ids) | | `/xxx/status` | POST | 启用/禁用状态切换 | | `/xxx/switch` | POST | 开关字段切换 | | `/xxx/maxSort` | GET | 获取最大排序值 | | `/xxx/export` | GET | 导出列表 | 按实际需求裁剪,不强制全部实现。 --- ## 前端目录结构 ``` web/src/ ├── api//index.ts # HTTP 请求封装 ├── views// │ ├── index.vue # 列表页 │ ├── edit.vue # 新增/编辑弹窗 │ └── model.ts # State 类、rules、schemas、columns、loadOptions ├── components/ # 通用组件(BasicTable、BasicForm、BasicModal 等) ├── store/modules/dict.ts # 字典 store(loadOptions、getLabel、getOptions) └── utils/ ├── http/axios.ts # HTTP 封装(http.request / jumpExport) └── dateUtil.ts # 日期工具(defRangeShortcuts 等) ``` --- ## 前端编码约定 ### API 文件 `api//index.ts` - 使用 `http.request`,禁止直接使用 axios - 函数名与接口语义一致:`List`、`Edit`、`Delete`、`View`、`Status`、`Switch`、`Export`、`MaxSort` - 导出接口用 `jumpExport('/xxx/export', params)` ```ts import { http, jumpExport } from '@/utils/http/axios'; export function List(params) { return http.request({ url: '/xxxModule/list', method: 'get', params }); } export function Edit(params) { return http.request({ url: '/xxxModule/edit', method: 'POST', params }); } export function Export(params) { jumpExport('/xxxModule/export', params); } ``` ### model.ts 结构 - `class State`:属性与后端 entity 字段对应,提供合理默认值 - `newState()`:工厂函数,支持深拷贝(`cloneDeep`) - `rules`:Naive UI 表单校验规则 - `schemas`:搜索栏 `FormSchema[]`(`ref` 包装) - `columns`:表格列定义,含权限守卫的操作用 `hasPermission(['/xxx/edit'])` - `loadOptions()`:调用 `dict.loadOptions([...])`,预加载字典数据 ### 组件使用规范 **必须优先使用 Naive UI 原生组件**,能用 Naive UI 实现的禁止自行封装替代组件: | 场景 | 必须使用的组件 | |------|--------------| | 表格 | 项目封装 `BasicTable`(基于 Naive UI) | | 表单 | 项目封装 `BasicForm`(基于 Naive UI) | | 弹窗 | `n-modal` 或项目封装 `BasicModal` | | 按钮 | `n-button`,类型用 `primary`/`default`/`error` 等语义值 | | 选择器 | `n-select`(字典下拉)、`n-tree-select`(树形) | | 开关 | `n-switch`,配合 Switch 接口 | | 输入框 | `n-input` | | 日期选择 | `n-date-picker`,范围选择配合 `defRangeShortcuts()` | | 图片渲染 | `renderImage(url)` | | 文件渲染 | `renderFile(url)` | | 成员信息 | `renderPopoverMemberSumma(summa)` | **禁止行为:** - 禁止引入 Element Plus、Ant Design Vue 等其他 UI 库 - 禁止使用内联 style(使用 Naive UI 的 props 或 class) - 禁止在模板里硬编码字典值文本,必须通过 `dict.getLabel()` 渲染 ### 权限控制 ```ts import { usePermission } from '@/hooks/web/usePermission'; const { hasPermission } = usePermission(); // 模板中 v-if="hasPermission(['/xxxModule/edit'])" // columns 中开关操作 disabled: !hasPermission(['/xxxModule/switch']) ``` 权限标识与后端路由路径保持一致,**路由由后端菜单数据驱动,禁止在源码里硬编码路由路径**。 ### Vue 3 代码规范 - 使用 Composition API(`