Files
hotgo/AGENTS.md

34 KiB
Raw Permalink Blame History

HotGo 项目开发规范

生成或修改任何代码前,必须完整阅读本文档。
本规范是项目唯一事实来源,所有新增代码必须严格对齐现有模块风格,不得自创新的代码模式。


技术栈

技术
后端语言 Gomodule 名 hotgo
后端框架 GoFrame v2gdb、ghttp、gcron、gerror、gctx、gerror、gjson、gvar
前端框架 Vue 3 + Vite + TypeScript
UI 组件库 Naive UI(优先使用,未经允许不得引入其他 UI 库)
状态管理 Pinia
HTTP 客户端 项目封装 @/utils/http/axioshttp.request),禁止直接使用 axios

后端目录结构

server/
├── addons/                       # 插件模块(每个插件独立微架构,见"插件开发"章节)
│   ├── modules/                  # 隐式注册文件(每个插件一个 .go仅含 import
│   └── <addonName>/              # 插件目录,与主模块结构镜像
├── api/admin/<module>/           # 主模块 HTTP 契约Req/Res + g.Meta 路由声明
├── internal/
│   ├── consts/                   # 全局常量与字典选项(枚举在此定义并注册)
│   ├── global/                   # 全局公用变量(谨慎添加,尽量少用)
│   ├── controller/admin/sys/     # 薄控制器,只做参数透传,不含业务逻辑
│   ├── crons/                    # 主模块定时任务注册与处理
│   ├── queues/                   # 主模块消息队列消费者注册与处理
│   ├── logic/sys/                # 业务实现sSysXxxinit() 注册到 service
│   ├── library/                  # 内部基础库addons、dict、hgorm、cache、queue、cron 等)
│   ├── service/                  # 接口声明ISysXxx+ 注册函数
│   ├── dao/                      # 对外 DAOinternal/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 等

后端分层约定

分层职责(禁止跨层调用)

apiHTTP契约 → controller参数透传 → service接口 → logic业务实现 → dao数据访问
  • controller 层:不写任何业务逻辑,只转调 service
  • logic 层:不出现 HTTP 相关代码,所有校验通过 Filter()g.Validator() 完成
  • dao 层internal/dao/internal/ 由 CLI 自动生成,禁止手改

1. API 层 api/admin/<module>/<module>.go

  • 每个接口一对 XxxReq / XxxResg.Meta 声明路由
  • GET 接口用 method:"get",写操作用 method:"post"
  • 请求体嵌入 sysin.XxxInp,响应体嵌入对应 Model 或空 struct{}
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/<module>.go

  • 变量:var XxxModule = cXxxModule{},类型:type cXxxModule struct{}
  • 只做参数转发和结果组装,禁止直接操作 dao 或写业务逻辑
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/<module>.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
  • 字段校验在 sysinFilter(ctx) 方法完成logic 层不重复校验

5. Model 层 internal/model/input/sysin/<module>.go

类型 用途
XxxUpdateFields 更新时字段白名单
XxxInsertFields 新增时字段白名单
XxxEditInp 嵌入 entity.XxxTable,含 Filter() 校验方法
XxxListInp 列表查询入参,嵌入 form.PageInp
XxxListModel 列表返回字段
XxxViewInp / XxxViewModel 详情入参/出参
XxxDeleteInp 删除入参(通常含 Ids []int64
XxxStatusInpXxxMaxSortInp 其他操作

6. 路由注册 internal/router/genrouter/<module>.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/ 下对应文件)

// 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 层中使用字典

// 获取标签文本
label := dict.GetOptionLabel(consts.XxxStatusOptions, in.Status)

// 校验是否为合法值
if !dict.HasOptionKey(consts.XxxStatusOptions, in.Status) {
    return gerror.New("状态值不合法")
}

前端:加载与使用字典

加载(在 model.tsloadOptions() 中):

export function loadOptions() {
    dict.loadOptions(['XxxStatusOptions', 'sys_normal_disable']);
}

model.ts 的 columns 中渲染字典标签:

import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();

// columns 中
{
    title: '状态',
    key: 'status',
    render(row) {
        return dict.getLabel('XxxStatusOptions', row.status);
    }
}

在搜索表单 schemas 中使用字典下拉:

{
    field: 'status',
    component: 'NSelect',
    label: '状态',
    componentProps: {
        options: computed(() => dict.getOptions('XxxStatusOptions')),
        placeholder: '请选择状态',
    },
}

插件Addon开发规范

定位:独立、临时性、工具类型的功能推荐插件化开发,例如小游戏、广告管理、文章管理、小程序、微商城等。插件之间完全隔离,方便多项目复用。

插件目录结构

每个插件拥有完整的独立微架构,与主模块结构镜像,但 包路径前缀为 hotgo/addons/<addonName>/

server/addons/<addonName>/
├── 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   # 插件版 genrouterRegister() 使用插件前缀
│       └── *.go      # 每个生成模块一个文件,追加到插件 LoginRequiredRouter
├── consts/           # 插件内常量与字典(同主模块 consts 写法)
├── crons/            # 插件定时任务(可选)
├── queues/           # 插件消息队列消费者(可选)
└── resource/         # 插件静态资源与模板(可选)

对应前端目录:

web/src/
├── api/addons/<addonName>/       # 插件前端 API 封装
└── views/addons/<addonName>/     # 插件前端页面

插件入口 main.go

package <addonName>

import (
    "context"
    "github.com/gogf/gf/v2/net/ghttp"
    _ "hotgo/addons/<addonName>/crons"
    "hotgo/addons/<addonName>/global"
    _ "hotgo/addons/<addonName>/logic"
    _ "hotgo/addons/<addonName>/queues"
    "hotgo/addons/<addonName>/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:        "<addonName>",  // 与目录名一致
            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/<addonName>/接口路径    → 对应 controller/admin/sys/
前台:/api/<addonName>/接口路径      → 对应 controller/api/
首页:/home/<addonName>/接口路径     → 对应 controller/home/
WebSocket/socket/<addonName>/接口路径

路由前缀通过 addons.RouterPrefix(ctx, consts.AppAdmin, global.GetSkeleton().Name) 自动生成,禁止在路由文件里硬编码路径前缀

插件 genrouter 与主模块的区别

插件 genrouter/init.goRegister() 函数需要传入 ctxgroup,并使用插件路由前缀:

// 插件 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/<module>.go
func init() {
    LoginRequiredRouter = append(LoginRequiredRouter, sys.XxxModule)
}

参考实现:server/addons/hgexample/router/genrouter/

插件调用主模块服务

在插件 input 层继承主模块 input 结构,解耦参数依赖,避免 import cycle

// 插件 model/input/sysin/config.go
package sysin

import "hotgo/internal/model/input/sysin"

type UpdateConfigInp struct {
    sysin.UpdateAddonsConfigInp
}
// 插件 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)
}

插件获取自身信息

// 插件内部
global.GetSkeleton()

// 任意位置判断当前请求是否为插件请求
contexts.IsAddonRequest(ctx)
contexts.GetAddonName(ctx)

插件隐式注册

server/addons/modules/ 下新建 <addonName>.go,内容仅含一行匿名导入:

package modules

import _ "hotgo/addons/<addonName>"

新增插件检查清单

后端:

  • addons/<addonName>/main.go — 插件入口,实现 Module 接口
  • addons/<addonName>/global/ — global.go + init.go
  • addons/modules/<addonName>.go — 隐式注册
  • addons/<addonName>/service/ — 插件内接口声明(独立包)
  • addons/<addonName>/logic/logic.go — 匿名导入触发 init()
  • addons/<addonName>/logic/sys/<module>.go — 业务实现
  • addons/<addonName>/model/input/sysin/<module>.go — 入参/出参
  • addons/<addonName>/api/admin/<module>/<module>.go — API 契约
  • addons/<addonName>/controller/admin/sys/<module>.go — 薄控制器
  • addons/<addonName>/router/admin.go — 路由注册
  • addons/<addonName>/router/genrouter/init.go — 插件 genrouter
  • addons/<addonName>/router/genrouter/<module>.go — 模块路由追加

前端:

  • web/src/api/addons/<addonName>/index.ts — API 封装
  • web/src/views/addons/<addonName>/model.ts — State、schemas、columns
  • web/src/views/addons/<addonName>/index.vue — 列表页
  • web/src/views/addons/<addonName>/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/<module>/index.ts         # HTTP 请求封装
├── views/<module>/
│   ├── index.vue                 # 列表页
│   ├── edit.vue                  # 新增/编辑弹窗
│   └── model.ts                  # State 类、rules、schemas、columns、loadOptions
├── components/                   # 通用组件BasicTable、BasicForm、BasicModal 等)
├── store/modules/dict.ts         # 字典 storeloadOptions、getLabel、getOptions
└── utils/
    ├── http/axios.ts             # HTTP 封装http.request / jumpExport
    └── dateUtil.ts               # 日期工具defRangeShortcuts 等)

前端编码约定

API 文件 api/<module>/index.ts

  • 使用 http.request,禁止直接使用 axios
  • 函数名与接口语义一致:ListEditDeleteViewStatusSwitchExportMaxSort
  • 导出接口用 jumpExport('/xxx/export', params)
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
  • rulesNaive 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() 渲染

权限控制

import { usePermission } from '@/hooks/web/usePermission';
const { hasPermission } = usePermission();

// 模板中
v-if="hasPermission(['/xxxModule/edit'])"

// columns 中开关操作
disabled: !hasPermission(['/xxxModule/switch'])

权限标识与后端路由路径保持一致,路由由后端菜单数据驱动,禁止在源码里硬编码路由路径

Vue 3 代码规范

  • 使用 Composition API<script setup>
  • 响应式数据用 ref / reactive,计算属性用 computed
  • 字典选项传给组件时用 computed(() => dict.getOptions('XxxOptions')),保持响应式

新增 CRUD 模块检查清单

后端(必须全部完成,顺序建议从下往上):

  • internal/consts/<module>.go — 常量定义、选项变量、init() 注册字典
  • 使用 goframe CLI 生成:entitydodao/internaldao(禁止手写)
  • internal/model/input/sysin/<module>.go — 入参/出参/过滤模型
  • internal/service/sys.go — 追加接口声明与注册函数
  • internal/logic/sys/<module>.go — 业务实现
  • internal/controller/admin/sys/<module>.go — 薄控制器
  • api/admin/<module>/<module>.go — API 契约
  • internal/router/genrouter/<module>.go — 路由注册(LoginRequiredRouter 追加)

前端(必须全部完成):

  • web/src/api/<module>/index.ts — HTTP 封装
  • web/src/views/<module>/model.ts — State、schemas、columns、rules、loadOptions
  • web/src/views/<module>/index.vue — 列表页
  • web/src/views/<module>/edit.vue — 编辑页

参考实现

所有新模块必须严格对齐以下文件的风格,生成前请逐一阅读:

主模块 CRUD 参考:

文件 说明
server/internal/consts/status.go 字典常量定义与注册标准写法
server/api/admin/curddemo/curddemo.go API 契约标准写法
server/internal/controller/admin/sys/curd_demo.go 薄控制器标准写法
server/internal/logic/sys/curd_demo.go Logic 标准写法(含关联查询、导出、字段过滤)
server/internal/model/input/sysin/curd_demo.go 入参/出参模型标准写法
server/internal/router/genrouter/curd_demo.go 主模块路由注册标准写法
web/src/api/curdDemo/index.ts 前端 API 封装标准写法
web/src/views/curdDemo/model.ts 前端 model 标准写法(含字典加载)
web/src/views/curdDemo/index.vue 列表页标准写法
web/src/views/curdDemo/edit.vue 编辑页标准写法

插件参考(以 hgexample 为标准):

文件 说明
server/addons/hgexample/main.go 插件入口标准写法
server/addons/hgexample/global/init.go 插件 global 标准写法
server/addons/hgexample/router/admin.go 插件路由注册标准写法
server/addons/hgexample/router/genrouter/init.go 插件 genrouter 标准写法
server/addons/hgexample/router/genrouter/tenant_order.go 插件模块路由追加标准写法
server/addons/hgexample/logic/sys/tenant_order.go 插件业务逻辑标准写法
server/addons/hgexample/logic/sys/config.go 插件调用主模块服务标准写法

代码生成模板:

  • 主模块 CRUDserver/resource/generate/default/curd/
  • 插件脚手架:server/resource/generate/default/addon/

以上模板可参考逻辑,禁止直接修改模板文件


错误处理规范

基本原则

  • 业务错误直接用 gerror.New("描述清晰的中文信息") 返回GoFrame 框架会统一处理响应码
  • 错误信息要面向用户,清晰说明发生了什么,而不是堆栈路径

错误创建方式

import "github.com/gogf/gf/v2/errors/gerror"

// 普通业务错误(最常用)
return gerror.New("用户名已存在")

// 包装底层错误,追加上下文(调用第三方/dao 层出错时使用)
if err := dao.User.Ctx(ctx).Data(data).Insert(); err != nil {
    return gerror.Wrap(err, "创建用户失败")
}

// 带格式化参数
return gerror.Newf("订单 %d 状态不合法,当前状态:%d", in.Id, order.Status)

错误传递原则

  • logic 层:遇到底层错误用 gerror.Wrap 追加上下文后返回;纯业务校验失败用 gerror.New 直接返回
  • controller 层:直接 return 透传 logic 的 error不再包装,不打印日志(框架统一处理)
  • 禁止在 controller 层 fmt.Println(err)g.Log().Error 打印已经会被框架记录的错误

缓存使用规范

统一使用 cache.Instance()

禁止直接调用 g.Redis().Do(),项目统一通过 cache.Instance() 操作缓存底层适配器Memory / Redis / File由配置决定业务层无需关心。

import (
    "hotgo/internal/library/cache"
    "github.com/gogf/gf/v2/os/gcache"
    "time"
)

// 设置缓存(带过期时间)
err := cache.Instance().Set(ctx, "user:info:1", userInfo, 30*time.Minute)

// 获取缓存
val, err := cache.Instance().Get(ctx, "user:info:1")
if err != nil || val.IsNil() {
    // 缓存未命中,查数据库
}

// 删除缓存
_, err = cache.Instance().Remove(ctx, "user:info:1")

// GetOrSet缓存不存在时自动执行函数并写入推荐用于只读类缓存
val, err := cache.Instance().GetOrSet(ctx, "user:info:1", func(ctx context.Context) (interface{}, error) {
    return dao.AdminMember.Ctx(ctx).Where(...).One()
}, 30*time.Minute)

缓存 Key 命名规范

  • 格式:模块:实体:唯一标识,例如:user:info:1config:site:basicdict:options:XxxStatus
  • 同一模块的 Key 统一在 internal/consts/cache.go(或对应模块的 consts 文件)中定义为常量,禁止散落在 logic 里硬编码
  • 插件缓存 Key 加插件名前缀:hgexample:order:1

缓存使用原则

  • 更新或删除数据后,主动清除相关缓存,不依赖过期自动失效
  • 缓存时间频繁更新的数据配置、状态用较短时间530 分钟);基本不变的数据(字典、枚举)可用更长时间
  • 缓存穿透场景:查询结果为空时也写入短时缓存(如 1 分钟)防止击穿

日志规范

日志级别选择

级别 方法 适用场景
Info g.Log().Info / g.Log().Infof 正常业务流程的关键节点(登录成功、订单创建、支付回调等)
Warning g.Log().Warning / g.Log().Warningf 遇到错误但不影响主流程(重试、降级、外部服务异常、非致命校验失败)
Panic g.Log().Panic / g.Log().Panicf 极其严重、必须立即终止的情况(配置缺失、核心依赖不可用)

使用示例

// Info记录正常流程关键事件
g.Log().Infof(ctx, "用户登录成功 uid:%d ip:%s", member.Id, ip)

// Info记录外部调用结果
g.Log().Info(ctx, "支付回调处理完成", g.Map{"orderId": orderId, "status": status})

// Warning外部接口失败但已降级处理
g.Log().Warningf(ctx, "短信发送失败,已跳过 mobile:%s err:%+v", mobile, err)

// Warning业务异常但不中断流程
g.Log().Warning(ctx, "用户余额不足,跳过自动扣款", g.Map{"uid": uid, "balance": balance})

// Panic启动时核心配置缺失
g.Log().Panicf(ctx, "缓存未初始化,无法启动: %+v", err)

日志上下文原则

  • 日志必须传入 ctx(第一个参数),便于链路追踪
  • 关键日志需携带足够的业务字段uid、orderId、requestId 等),用 g.Map{} 附加结构化信息
  • 禁止fmt.Println 替代日志,禁止在 logic/controller 层裸打印调试信息

数据库建表约定

建表时遵循以下字段命名约定,代码生成器和 ORM hook 会自动识别并处理:

字段名 类型 说明
id bigint(20) NOT NULL AUTO_INCREMENT 主键,必须
created_at datetime 创建时间,自动写入,禁止手动赋值
updated_at datetime 更新时间,自动维护,禁止手动赋值
deleted_at datetime 软删除时间,存在时查询自动加 IS NULL,删除只更新此字段
created_by bigint(20) 创建者 ID自动写入
updated_by bigint(20) 更新者 ID自动维护
deleted_by bigint(20) 删除者 ID自动写入
status tinyint 状态字段,默认使用系统字典 sys_normal_disable
sort int 排序字段,存在时编辑表单自动获取最大值+1填充
pid bigint(20) 树表上级ID树表必须
level int 树等级(树表必须,自动维护)
tree varchar(512) 关系树(树表必须,自动维护)
tenant_id bigint(20) 租户ID多租户场景按需加

特殊字段查询方式(代码生成器默认生成):

  • string 类型字段 → LIKE 模糊查询
  • int/uint/int64= 精确查询
  • created_by/updated_by/deleted_by → 关键词查询ID/用户名/手机号匹配),调用 service.AdminMember().GetIdsByKeyword()

数据类型 → Go 类型映射(影响生成代码的字段类型):

  • tinyint/int/smallintint;加 unsigneduint
  • bigintint64;加 unsigneduint64
  • varchar/text/charstring
  • datetime/timestamp*gtime.Time
  • json/jsonb*gjson.Json
  • decimal/float/doublefloat64

数据权限(权限过滤)

业务查询中如需过滤数据权限(仅自己/所属部门/下级等),在 ORM 链式调用中加入 handler

import "hotgo/internal/library/hgorm/handler"

// 按登录用户的数据权限范围过滤(表需含 created_by 或 member_id 字段)
dao.XxxTable.Ctx(ctx).Handler(handler.FilterAuth).Scan(&res)

// 表中无标准字段时,指定自定义字段
dao.XxxTable.Ctx(ctx).Handler(handler.FilterAuthWithField("custom_field")).Scan(&res)

// 多租户数据权限过滤(表需含 tenant_id 字段)
// 在 Model() 方法中通过 handler.Option 开启
func (s *sSysXxx) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
    if len(option) == 0 {
        option = append(option, &handler.Option{FilterTenant: true})
    }
    return handler.Model(dao.XxxTable.Ctx(ctx), option...)
}

多租户增改时自动维护租户关系,在 ORM 操作后加 .Hook(hook.SaveTenant)

import "hotgo/internal/library/hgorm/hook"

// 新增时自动写入 tenant_id/merchant_id/user_id
dao.XxxTable.Ctx(ctx).Fields(sysin.XxxInsertFields{}).Hook(hook.SaveTenant).Data(in).Insert()

// 更新时自动维护
s.Model(ctx).Fields(sysin.XxxUpdateFields{}).WherePri(in.Id).Data(in).Hook(hook.SaveTenant).Update()

定时任务Crons

定时任务通过实现接口 + init() 注册,在后台「系统设置 → 定时任务」中配置执行策略:

主模块:新建 server/internal/crons/<name>.go
插件:新建 server/addons/<addonName>/crons/<name>.go

package crons

import (
    "context"
    "hotgo/internal/library/cron"
)

func init() {
    cron.Register(MyTask)
}

var MyTask = &cMyTask{name: "myTask"} // name 与后台配置的任务名称一致

type cMyTask struct{ name string }

func (c *cMyTask) GetName() string { return c.name }

func (c *cMyTask) Execute(ctx context.Context, parser *cron.Parser) (err error) {
    parser.Logger.Infof(ctx, "任务执行:%v", ...)
    return
}

消息队列Queues

队列采用接口 + init() 注册topic 统一定义在 internal/consts/queue.go(或对应模块 consts 文件)。

主模块:新建 server/internal/queues/<name>.go
插件:新建 server/addons/<addonName>/queues/<name>.go

package queues

import (
    "context"
    "encoding/json"
    "hotgo/internal/consts"
    "hotgo/internal/library/queue"
)

func init() {
    queue.RegisterConsumer(MyConsumer)
}

var MyConsumer = &qMyConsumer{}

type qMyConsumer struct{}

func (q *qMyConsumer) GetTopic() string { return consts.QueueMyTopic }

func (q *qMyConsumer) Handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
    var data MyData
    if err = json.Unmarshal(mqMsg.Body, &data); err != nil {
        return
    }
    // 处理消息...
    return
}

发送消息:

// 即时消息
queue.Push(consts.QueueMyTopic, data)

// 延迟消息(仅 redis/rocketmq 驱动支持)
queue.SendDelayMsg(consts.QueueMyTopic, data, 10) // redis: 延迟秒数rocketmq: 延迟级别

工具库使用规范

后端(server/utility/)优先使用已有工具,禁止重复造轮子:

用途
utility/convert 数据类型转换
utility/encrypt 加密/解密
utility/excel 电子表格导出/导入(导出接口用此包)
utility/validate 数据验证工具
utility/tree 树形结构处理
utility/simple 简捷函数集合
utility/format 数据格式化

前端(web/src/utils/)优先使用已有工具:

工具 用途
@/utils/http/axioshttp.request HTTP 请求,唯一入口
@/utils/dateUtildefRangeShortcuts 日期范围快捷选项
@/utilsrenderImagerenderFilerenderPopoverMemberSumma 表格渲染工具函数
@/store/modules/dictuseDictStore 字典数据加载与渲染
@/hooks/web/usePermissionhasPermission 权限判断

树表Tree CRUD特殊约定

树表在普通 CRUD 基础上有以下差异:

  • 表中必须含 pidleveltree 字段(参见建表约定)
  • pid/level/tree 字段由系统自动维护,禁止在编辑表单中手动设置这三个字段
  • 前端列表页使用树形表格(BasicTablechildrenKeytreeData 模式),而非普通分页列表
  • 参考实现:server/internal/logic/sys/tree_demo.goweb/src/views/develop/curd/treeDemo/

响应格式约定

所有接口统一响应格式(由框架中间件自动处理,无需在 controller 手动包装

{ "code": 0, "message": "操作成功", "timestamp": 1234567890, "traceID": "xxx", "data": {} }
  • 成功状态码:0;失败状态码:-1GoFrame 内置,无需手动设置)
  • gerror.Wrap(err, "用户友好的提示") → 客户端只看到最外层提示,开发者日志可见完整堆栈
  • 禁止在 controller 层手动 Write/WriteJSON 等直接写响应(破坏统一格式)

常见错误禁止清单

后端:

  • 禁止在 controller 层直接调用 dao
  • 禁止硬编码字段名字符串(用 dao.Table.Columns().Field
  • 禁止手改 entity/do/dao/internal/ 目录下的文件
  • 禁止在 logic 层重复做 Filter() 已完成的校验
  • 禁止在 internal/service/sys.go 之外新建主模块 service 文件(同文件追加)
  • 禁止插件 import 另一个插件的包(插件间完全隔离,通过主模块 service 通信)
  • 禁止在插件路由文件里硬编码路由前缀(用 addons.RouterPrefix() 生成)
  • 禁止在 internal/library/ 下修改基础库(如需扩展,提 issue 或在 utility/ 添加)

前端:

  • 禁止直接使用 axios必须用 http.request
  • 禁止硬编码字典值文本,必须用 dict.getLabel() 渲染
  • 禁止引入 Naive UI 以外的 UI 框架
  • 禁止内联 style
  • 禁止在源码里硬编码菜单路由路径