im 管理后台

This commit is contained in:
richjohnrambo
2026-08-11 23:13:51 +08:00
parent 0397fe5f22
commit 331a55751d
1407 changed files with 162069 additions and 2 deletions
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/im_manager.iml" filepath="$PROJECT_DIR$/.idea/im_manager.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+960
View File
@@ -0,0 +1,960 @@
# 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)
│ └── <addonName>/ # 插件目录,与主模块结构镜像
├── api/admin/<module>/ # 主模块 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/<module>/<module>.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/<module>.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/<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`
- 字段校验在 `sysin` 的 `Filter(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`) |
| `XxxStatusInp`、`XxxMaxSortInp` 等 | 其他操作 |
### 6. 路由注册 `internal/router/genrouter/<module>.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/<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 # 插件版 genrouter:Register() 使用插件前缀
│ └── *.go # 每个生成模块一个文件,追加到插件 LoginRequiredRouter
├── consts/ # 插件内常量与字典(同主模块 consts 写法)
├── crons/ # 插件定时任务(可选)
├── queues/ # 插件消息队列消费者(可选)
└── resource/ # 插件静态资源与模板(可选)
```
**对应前端目录:**
```
web/src/
├── api/addons/<addonName>/ # 插件前端 API 封装
└── views/addons/<addonName>/ # 插件前端页面
```
### 插件入口 `main.go`
```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.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/<module>.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/` 下新建 `<addonName>.go`,内容仅含一行匿名导入:
```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 # 字典 store(loadOptions、getLabel、getOptions)
└── utils/
├── http/axios.ts # HTTP 封装(http.request / jumpExport)
└── dateUtil.ts # 日期工具(defRangeShortcuts 等)
```
---
## 前端编码约定
### API 文件 `api/<module>/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(`<script setup>`)
- 响应式数据用 `ref` / `reactive`,计算属性用 `computed`
- 字典选项传给组件时用 `computed(() => dict.getOptions('XxxOptions'))`,保持响应式
---
## 新增 CRUD 模块检查清单
**后端(必须全部完成,顺序建议从下往上):**
- [ ] `internal/consts/<module>.go` — 常量定义、选项变量、`init()` 注册字典
- [ ] 使用 goframe CLI 生成:`entity`、`do`、`dao/internal`、`dao`(禁止手写)
- [ ] `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` | 插件调用主模块服务标准写法 |
代码生成模板:
- 主模块 CRUD:`server/resource/generate/default/curd/`
- 插件脚手架:`server/resource/generate/default/addon/`
以上模板可参考逻辑,**禁止直接修改模板文件**。
---
## 错误处理规范
### 基本原则
- 业务错误直接用 `gerror.New("描述清晰的中文信息")` 返回,GoFrame 框架会统一处理响应码
- 错误信息要面向用户,清晰说明**发生了什么**,而不是堆栈路径
### 错误创建方式
```go
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)由配置决定,业务层无需关心。
```go
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:1`、`config:site:basic`、`dict:options:XxxStatus`
- 同一模块的 Key 统一在 `internal/consts/cache.go`(或对应模块的 consts 文件)中定义为常量,禁止散落在 logic 里硬编码
- 插件缓存 Key 加插件名前缀:`hgexample:order:1`
### 缓存使用原则
- 更新或删除数据后,**主动清除**相关缓存,不依赖过期自动失效
- 缓存时间:频繁更新的数据(配置、状态)用较短时间(5~30 分钟);基本不变的数据(字典、枚举)可用更长时间
- 缓存穿透场景:查询结果为空时也写入短时缓存(如 1 分钟)防止击穿
---
## 日志规范
### 日志级别选择
| 级别 | 方法 | 适用场景 |
|------|------|---------|
| Info | `g.Log().Info` / `g.Log().Infof` | 正常业务流程的关键节点(登录成功、订单创建、支付回调等) |
| Warning | `g.Log().Warning` / `g.Log().Warningf` | **遇到错误但不影响主流程**(重试、降级、外部服务异常、非致命校验失败) |
| Panic | `g.Log().Panic` / `g.Log().Panicf` | 极其严重、必须立即终止的情况(配置缺失、核心依赖不可用) |
### 使用示例
```go
// 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/smallint` → `int`;加 `unsigned` → `uint`
- `bigint` → `int64`;加 `unsigned` → `uint64`
- `varchar/text/char` → `string`
- `datetime/timestamp` → `*gtime.Time`
- `json/jsonb` → `*gjson.Json`
- `decimal/float/double` → `float64`
---
## 数据权限(权限过滤)
业务查询中如需过滤数据权限(仅自己/所属部门/下级等),在 ORM 链式调用中加入 handler:
```go
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)`:
```go
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`
```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`
```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
}
```
**发送消息:**
```go
// 即时消息
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/axios`(`http.request`) | HTTP 请求,**唯一入口** |
| `@/utils/dateUtil`(`defRangeShortcuts`) | 日期范围快捷选项 |
| `@/utils`(`renderImage`、`renderFile`、`renderPopoverMemberSumma`) | 表格渲染工具函数 |
| `@/store/modules/dict`(`useDictStore`) | 字典数据加载与渲染 |
| `@/hooks/web/usePermission`(`hasPermission`) | 权限判断 |
---
## 树表(Tree CRUD)特殊约定
树表在普通 CRUD 基础上有以下差异:
- 表中必须含 `pid`、`level`、`tree` 字段(参见建表约定)
- `pid`/`level`/`tree` 字段**由系统自动维护**,禁止在编辑表单中手动设置这三个字段
- 前端列表页使用树形表格(`BasicTable` 的 `childrenKey` 或 `treeData` 模式),而非普通分页列表
- 参考实现:`server/internal/logic/sys/tree_demo.go`、`web/src/views/develop/curd/treeDemo/`
---
## 响应格式约定
所有接口统一响应格式(由框架中间件自动处理,**无需在 controller 手动包装**):
```json
{ "code": 0, "message": "操作成功", "timestamp": 1234567890, "traceID": "xxx", "data": {} }
```
- 成功状态码:`0`;失败状态码:`-1`(GoFrame 内置,无需手动设置)
- `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
- 禁止在源码里硬编码菜单路由路径
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021-present HotGo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+166 -2
View File
@@ -1,3 +1,167 @@
# im_manager
# HotGo-V2
<div align="center">
<img width="140px" src="https://gmycos.facms.cn/hotgo/logo.sig.png">
<p>
<h1>HotGo V2</h1>
</p>
<p align="center">
<a href="https://goframe.org/pages/viewpage.action?pageId=1114119" target="_blank">
<img src="https://img.shields.io/badge/goframe-2.10.0-green" alt="goframe">
</a>
<a href="https://v3.vuejs.org/" target="_blank">
<img src="https://img.shields.io/badge/vue.js-vue3.4-green" alt="vue">
</a>
<a href="https://www.naiveui.com" target="_blank">
<img src="https://img.shields.io/badge/naiveui-%3E2.44.1-blue" alt="naiveui">
</a>
<a href="https://www.tslang.cn/" target="_blank">
<img src="https://img.shields.io/badge/typescript-%3E4.0.0-blue" alt="typescript">
</a>
<a href="https://vitejs.dev/" target="_blank">
<img src="https://img.shields.io/badge/vite-%3E5.4.2-yellow" alt="vite">
</a>
<a href="https://github.com/bufanyun/hotgo/blob/v2.0/LICENSE" target="_blank">
<img src="https://img.shields.io/badge/license-MIT-success" alt="license">
</a>
</p>
</div>
## 平台简介
* 基于 GoFrame2 + Vue3 + NaiveUI + uniapp 的企业级全栈框架,为二次开发而生。
* 前端采用 Naive-Ui-Admin、Vue、Naive UI、uniapp。
* 企业级 AI 赋能基础开发平台,内置 AI 开发规范,适配主流 AI 开发工具,实现人机协同开发,大幅提升开发效率。
## 演示地址
- [https://hotgo.facms.cn/admin](https://hotgo.facms.cn/admin)
> 账号:admin 密码:123456
### 使用文档
[安装文档](docs/guide-zh-CN/start-installation.md) · [本地文档](docs/guide-zh-CN/README.md) · [更新历史](docs/guide-zh-CN/start-update-log.md) · [常见问题](docs/guide-zh-CN/start-issue.md)
## 特征
* 高生产率:极强的可扩展性,应用化、模块化、插件化机制敏捷开发,几分钟即可搭建一个应用开发骨架。
* 多应用入口:多入口分为 Admin (后台)、Home (前台页面)、Api (对外通用接口)、WebSocket (即时通讯接口),不同的业务,进入不同的应用入口。
* 极致的插件化: 微核架构,功能隔离,高可定制性,可以渐进式开发,亦可以多人协同开发。支持一键创建插件模板、一键安装、更新、卸载插件、可以非常方便的将插件迁移到新项目中。
* 快速生成代码:无需编写代码,只需创建表进行简单配置就能生成一个完善的 CURD、树表等常用的开发代码,其中所需表单控件也是勾选即可直接生成。
* 认证机制:采用 JWT 的用户状态认证及 casbin 的权限认证
* 路由模式:得益于 GoFrame 提供了规范化的路由注册方式,无需注解自动生成api文档
* 模块化设计,面向接口开发
## 内置功能
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 部门管理:配置系统组织机构(公司、部门、岗位),树结构展现支持数据权限。
3. 岗位管理:配置系统用户所属担任职务。
4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
5. 角色管理:角色菜单权限分配、设置角色按机构或按上下级关系进行数据范围权限划分。
6. 字典管理:对系统中经常使用的一些特定数据进行维护,支持枚举字典和自定义方法字典。
7. 配置管理:对系统动态配置常用参数。
8. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
9. 登录日志:系统登录日志记录查询包含登录异常。
10. 服务日志:服务端运行所产生的警告、异常、崩溃日志的详细数据和堆栈信息。
11. 支付网关:集成支付宝、微信支付、QQ支付等多种支付方式,只需简单配置即可使用。
12. 资金管理:支持在线充值、订单申请/原路退款、资金提现、资金/积分变动明细等通用模块。
13. 在线用户:当前系统中活跃用户状态监控。
14. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
15. 代码生成:支持自动化生成前后端代码。CURD关联表、树表、消息队列、定时任务一键生成等。
16. 插件应用:支持一键生成插件模板,每个插件之间开发隔离,拥有独立多应用入口、独立配置。完美支持多人协同开发、插件插拔不会对原系统产生影响等。
17. 服务监控:监视当前系统CPU、内存、磁盘、网络、堆栈等相关信息。
18. 附件管理:文件图片上传,大文件分片上传、断点续传,支持本地、阿里云oss、腾讯云cos、ucloud对象存储、七牛云对象存储、minio等多种上传驱动,后台一键切换配置,并集成了文件选择器。
19. TCP服务:基于gtcp的服务应用,支持长连接、断线重连、服务认证、路由分发、RPC消息、拦截器和数据绑定等。简化和规范了服务器开发流程。
20. 消息队列:同时兼容 kafka、redis、rocketmq、磁盘队列,一键配置切换到场景适用的MQ。
21. 通知公告:采用WebSocket实时推送在线用户最新通知、公告、私信消息。
22. 地区编码:整合国内通用省市区编码,运用于项目于一身,支持动态省市区选项。
23. 常用工具:集成常用的工具包和命令行工具,可以快速开发自定义命令行,多种启动入口。
> HotGo开源以来得到了大家的很多支持,本项目初衷只为互相学习交流,没有任何盈利性目的!欢迎为HotGo贡献代码或提供建议!
## 演示图
<table>
<tr>
<td><img src="./docs/guide-zh-CN/images/demo/1.png"/></td>
<td><img src="./docs/guide-zh-CN/images/demo/2.png"/></td>
</tr>
<tr>
<td><img src="./docs/guide-zh-CN/images/demo/3.png"/></td>
<td><img src="./docs/guide-zh-CN/images/demo/4.png"/></td>
</tr>
<tr>
<td><img src="./docs/guide-zh-CN/images/demo/5.png"/></td>
<td><img src="./docs/guide-zh-CN/images/demo/6.png"/></td>
</tr>
<tr>
<td><img src="./docs/guide-zh-CN/images/demo/7.png"/></td>
<td><img src="./docs/guide-zh-CN/images/demo/8.png"/></td>
</tr>
<tr>
<td><img src="./docs/guide-zh-CN/images/demo/9.png"/></td>
<td><img src="./docs/guide-zh-CN/images/demo/10.png"/></td>
</tr>
<tr>
<td><img src="./docs/guide-zh-CN/images/demo/11.png"/></td>
<td><img src="./docs/guide-zh-CN/images/demo/12.png"/></td>
</tr>
</table>
## 感谢(排名不分先后)
> gf框架 [https://github.com/gogf/gf](https://github.com/gogf/gf)
>
> naive-ui [https://www.naiveui.com](https://www.naiveui.com)
>
> naive-ui-admin [https://github.com/jekip/naive-ui-admin](https://github.com/jekip/naive-ui-admin)
>
> websocket [https://github.com/gorilla/websocket](github.com/gorilla/websocket)
>
> casbin [https://github.com/casbin/casbin](https://github.com/casbin/casbin)
>
> gopay [https://github.com/go-pay/gopay](https://github.com/go-pay/gopay)
## 交流QQ群
交流群①:190966648 <a target="_blank" href="https://qm.qq.com/cgi-bin/qm/qr?k=mJafkvme3VNyiQlCFIFNRtY8Xlr7pj9U&jump_from=webapi&authKey=jL10vIESr+vO8wpxwyd6DlChzkrbHpzN9uhAsIHgAinL/Vvd+nvuRyilf2UqUlCy"><img border="0" src="https://gmycos.facms.cn/hotgo/group.png" alt="HotGo框架交流1群" title="HotGo框架交流1群"></a>
> <img src="https://gmycos.facms.cn/hotgo/hotgo1qun.png" width="400px"/>
## 商用说明
> HotGo 是开源免费的,遵循 MIT 开源协议,意味着您无需支付任何费用,也无需授权,即可将它应用到您的产品中。
* 使用本项目必须保留所有版权信息。
* 本项目包含的第三方源码和二进制文件之版权信息另行标注。
* 版权所有Copyright © 2020-2026 by Ms (https://github.com/bufanyun/hotgo)
* All rights reserved。
## 免责声明:
* HotGo为开源学习项目,一切商业行为与HotGo无关。
* 用户不得利用HotGo从事非法行为,用户应当合法合规的使用,发现用户在使用产品时有任何的非法行为,HotGo有权配合有关机关进行调查或向政府部门举报,HotGo不承担用户因非法行为造成的任何法律责任,一切法律责任由用户自行承担,如因用户使用造成第三方损害的,用户应当依法予以赔偿。
* 所有与使用HotGo相关的资源直接风险均由用户承担。
#### 如果对您有帮助,您可以点右上角 💘Star💘支持
## [感谢JetBrains提供的免费GoLand](https://jb.gg/OpenSource)
[![avatar](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://jb.gg/OpenSource)
## License
[MIT © HotGo-2026](./LICENSE)
后台管理系统
+60
View File
@@ -0,0 +1,60 @@
## 目录
#### 介绍安装
- [系统介绍](../../README.md)
- [环境搭建](start-environment.md)
- [系统安装](start-installation.md)
- [生产部署](start-deploy.md)
- [如何提问](start-questions.md)
- [常见问题](start-issue.md)
- [更新历史](start-update-log.md)
#### 系统开发
- [目录结构](sys-catalog.md)
- [开发规范](sys-exploit.md)
- [控制台](sys-console.md)
- [中间件/拦截器](sys-middleware.md)
- [WebHook](sys-webhook.md)
- [权限控制](sys-auth.md)
- [支付网关](sys-payment.md)
- [定时任务](sys-cron.md)
- [消息队列](sys-queue.md)
- [功能扩展库](sys-library.md)
- [工具方法](sys-utility.md)
- [WebSocket服务器](sys-websocket-server.md)
- [TCP服务器](sys-tcp-server.md)
- [SaaS多租户](sys-tenant.md)
- [国际化](sys-i18n.md)
- [单元测试](sys-test.md)
#### 插件模块开发
- [模块介绍及目录](addon-introduce-catalog.md)
- [模块开发流程](addon-flow.md)
- [模块辅助说明](addon-helper.md)
#### 生成代码
- [使用前提](code-start.md)
- [数据库](sys-db.md)
- [生成配置](code-config.md)
- [生成CURD](code-curd.md)
- [生成关联表CURD](code-curd-join.md)
- [生成树型CURD](code-tree.md)
- [生成业务模板](code-business.md)
- [生成模板开发](code-template-dev.md)
- [生成常见问题](code-help.md)
### 前端开发
- [表单组件](web-form.md)
- [WebSocket客户端](sys-websocket-client.md)
- [独立部署](web-deploy.md)
#### 附录
- [网址收录](append-website.md)
+216
View File
@@ -0,0 +1,216 @@
## 模块开发流程
目录
- 创建新插件
- 开发
- 调用主模块服务接口
- 访问路径
- 数据迁移
### 创建新插件
1、HotGo 后台进入 开发工具->插件管理->找到创建新插件,根据引导进行创建即可。
```shell
创建成功后默认情况下会在以下目录中生成插件文件,假设新生成的插件名为:hgexample
1. /server/addons/hgexample/ # 插件模块目录
2. /server/addons/modules/hgexample.go # 隐式注册插件文件
3. /server/resource/addons/hgexample # 静态资源和页面模板目录,属于扩展功能选项,勾选对应选项后才会生成
4. /web/src/api/addons/hgexample # webApi目录
5. /web/src/views/addons/hgexample # web页面目录
# 默认情况下没有为web页面生成菜单权限,因为在实际场景中插件不一定需要用到web页面,所以如有需要请手动到后台 权限管理 -> 菜单权限->自行添加菜单和配置权限
```
2、创建插件完毕重启服务端后,插件管理中会出现你新创建的插件信息。操作栏有几个按钮,在此进行说明
- 安装:会自动执行 server/hgexample/main.go 文件中的Install方法,方法中的具体逻辑默认为空,可以根据实际情况自行配置。如生成后台菜单、生成插件配置表初始化数据、迁移home页面、web项目文件等。
```go
// Install 安装模块
func (m *module) Install(ctx context.Context) (err error) {
// ...
return
}
```
- 更新:会自动执行 server/hgexample/main.go 文件中的Upgrade方法,方法中的具体逻辑默认为空,可以根据实际情况自行配置。
```go
// Upgrade 更新模块
func (m *module) Upgrade(ctx context.Context) (err error) {
// ...
return
}
```
- 卸载:会自动执行 server/hgexample/main.go 文件中的UnInstall方法,方法中的具体逻辑默认为空,可以根据实际情况自行配置。如会清除所有的数据表和已安装的信息等。
```go
// UnInstall 卸载模块
func (m *module) UnInstall(ctx context.Context) (err error) {
// ...
return
}
```
### 开发
完全可以根据HotGo正常的开发流程去开发对应的API、控制器、业务逻辑、插件内的应用
### 调用主模块服务接口
这里推荐的方式是在插件input层新建一个结构,继承主模块中的input结构。这样做的目的是为了服务与服务之间的输入/输出关系解耦,便于参数扩展和避免插件模块下使用`gf gen service`时出现`import cycle not allowed`。
一个简单的例子:
> 假设hgexample插件模块要通过主模块的服务接口更新插件配置
插件模块input:\server\addons\hgexample\model\input\sysin\config.go
```go
package sysin
import (
"hotgo/internal/model/input/sysin"
)
// UpdateConfigInp 更新指定配置
type UpdateConfigInp struct {
sysin.UpdateAddonsConfigInp
}
```
插件模块业务逻辑:\server\addons\hgexample\logic\sys\config.go
```go
package sys
import (
"context"
"hotgo/addons/hgexample/global"
"hotgo/addons/hgexample/model/input/sysin"
"hotgo/addons/hgexample/service"
isc "hotgo/internal/service"
)
type sSysConfig struct{}
func NewSysConfig() *sSysConfig {
return &sSysConfig{}
}
func init() {
service.RegisterSysConfig(NewSysConfig())
}
// UpdateConfigByGroup 更新指定分组的配置
func (s *sSysConfig) UpdateConfigByGroup(ctx context.Context, in sysin.UpdateConfigInp) error {
in.UpdateAddonsConfigInp.AddonName = global.GetSkeleton().Name
return isc.SysAddonsConfig().UpdateConfigByGroup(ctx, in.UpdateAddonsConfigInp)
}
```
主模块input:\server\internal\model\input\sysin\addons_config.go
```go
package sysin
import (
"github.com/gogf/gf/v2/frame/g"
)
// UpdateAddonsConfigInp 更新指定插件的配置
type UpdateAddonsConfigInp struct {
AddonName string `json:"addonName"`
Group string `json:"group"`
List g.Map `json:"list"`
}
```
主模块业务逻辑:\server\internal\logic\sys\addons_config.go
```go
package sys
import (
"context"
"hotgo/internal/model/input/sysin"
"hotgo/internal/service"
)
type sSysAddonsConfig struct{}
func NewSysAddonsConfig() *sSysAddonsConfig {
return &sSysAddonsConfig{}
}
func init() {
service.RegisterSysAddonsConfig(NewSysAddonsConfig())
}
// UpdateConfigByGroup 更新指定分组的配置
func (s *sSysAddonsConfig) UpdateConfigByGroup(ctx context.Context, in sysin.UpdateAddonsConfigInp) error {
// ...
return nil
}
```
### 访问路径
#### 后台插件访问路径
```
// IP+端口或域名/admin/插件名称/API路径
如:127.0.0.1:8000/admin/hgexample/index/test
```
- 对应控制器路径:`server/addons/hgexample/controller/admin/sys/index.go`
#### 前端API插件访问路径
```
// IP+端口或域名/api/插件名称/API路径
如:127.0.0.1:8000/api/hgexample/index/test
```
- 对应控制器路径:`server/addons/hgexample/controller/api/index.go`
#### 前台页面插件访问路径
```
// IP+端口或域名/home/插件名称/API路径
如:127.0.0.1:8000/home/hgexample/index/test
```
- 对应控制器路径:`server/addons/hgexample/controller/home/index.go`
- 对应模板路径:`server/addons/hgexample/resource/public/template`
#### 静态资源插件访问路径
```
// IP+端口或域名/home/插件名称/API路径
如:127.0.0.1:8000/addons/hgexample/default
```
- 对应资源路径:`server/addons/hgexample/resource/public`
#### Websocket插件访问路径
```
// IP+端口或域名/socket/插件名称/API路径
如:127.0.0.1:8000/socket/hgexample/index/test
```
- 对应控制器路径:`server/addons/hgexample/controller/socket/index.go`
### 数据迁移
- 可以将数据迁移逻辑写进server/xxx插件/main.go 文件中的Install方法中,并遵循系统规范进行数据安装
+86
View File
@@ -0,0 +1,86 @@
## 模块辅助说明
目录
- 模块结构
- 获取模块信息
- 插件路由规则
#### 模块结构
- 文件路径:server/internal/library/addons/module.go
```go
// Skeleton 模块骨架
type Skeleton struct {
Label string `json:"label"` // 标识
Name string `json:"name"` // 名称
Group int `json:"group"` // 分组
Logo string `json:"logo"` // logo
Brief string `json:"brief"` // 简介
Description string `json:"description"` // 详细描述
Author string `json:"author"` // 作者
Version string `json:"version"` // 版本号
RootPath string `json:"rootPath"` // 根路径
View *gview.View `json:"view"` // 模板引擎
}
func (s *Skeleton) GetModule() Module {
return GetModule(s.Name)
}
// Module 插件模块
type Module interface {
Start(option *Option) (err error) // 启动模块
Stop() (err error) // 停止模块
Ctx() context.Context // 上下文
GetSkeleton() *Skeleton // 获取模块
Install(ctx context.Context) (err error) // 安装模块
Upgrade(ctx context.Context) (err error) // 更新模块
UnInstall(ctx context.Context) (err error) // 卸载模块
}
```
#### 获取模块信息
- 在插件模块内
```go
package main
import (
"fmt"
"hotgo/addons/hgexample/global"
)
func test() {
fmt.Printf("当前插件模块是:%+v", global.GetSkeleton())
}
```
- 在插件模块外
```go
package main
import (
"context"
"fmt"
"hotgo/internal/library/addons"
"hotgo/internal/library/contexts"
)
func test(ctx context.Context) {
fmt.Printf("当前是否为插件请求:%v", contexts.IsAddonRequest(ctx))
if contexts.IsAddonRequest(ctx) {
fmt.Printf("当前插件名称:%v", contexts.GetAddonName(ctx))
fmt.Printf("当前插件信息:%v", addons.GetModule(contexts.GetAddonName(ctx)))
}
}
```
- 更多辅助方法请参考插件功能库:server/internal/library/addons
#### 插件路由规则
- 如果你不喜欢现在的路由风格,可以自行调整。修改位置在:\server\internal\library\addons\addons.go的RouterPrefix方法。
- 注意调整后如web前端页面中如有之前的路由风格也需同步修改。
@@ -0,0 +1,40 @@
## 模块介绍及目录
目录
- 模块介绍
- 启动流程
- 目录结构
### 模块介绍
> 定位:开发独立、临时性、工具类型的功能时推荐使用插件化开发,例如:小游戏(大转盘/消消乐/抽奖/大屏互动/红包等)、小插件(广告管理/文章管理/友情链接等等)、小模块(报名/投票/签到)、小程序、大型插件微商城等等。
> 插件模块方便多项目复用,同时完美支持多人协同开发,每个插件模块都有独立的微架构目录结构,多插件之间完全隔离。
### 启动流程
HotGo 入口文件->隐式注入(hotgo/addons/modules)->注册所有插件->初始化已安装的插件->写入路由组->根据 HotGo 正常的开发和访问流程去开发访问插件
### 目录结构
- 详细介绍请参考:[目录结构](sys-catalog.md)
```
/server
├── addons
│ ├── modules
│ ├── xxx插件
│ | ├── api
│ | ├── consts
│ | ├── controller
│ | ├── crons
│ | ├── global
│ | ├── logic
│ | ├── model
│ | ├── queues
│ | ├── resource
│ | ├── router
│ | ├── service
│ | ├── main.go
│ | └── README.md
```
+21
View File
@@ -0,0 +1,21 @@
目录
- 框架文档
- [goframe](https://goframe.org/pages/viewpage.action?pageId=1114119)
- [naiveui](https://www.naiveui.com)
- [naive-ui-admin](https://docs.naiveadmin.com/)
- 常用组件
- [websocket](https://github.com/gorilla/websocket)
- [casbin](https://github.com/casbin/casbin)
- [tailwind](https://tailwind.docs.73zls.com/docs/flex-direction)
- 系统通用
- [redis](https://redis.io/)
- 其他
- [awesome-go](https://github.com/avelino/awesome-go)
+5
View File
@@ -0,0 +1,5 @@
## 生成业务模板
根据`api`接口文件一键生成业务模板:api、controller、logic、service
待写。
+172
View File
@@ -0,0 +1,172 @@
## 生成配置
目录
- 模板配置
- CLI配置
- 多数据库使用
### 模板配置
- 配置路径:server/manifest/config/config.yaml
```yaml
# 生成代码
hggen:
allowedIPs: [ "127.0.0.1", "*" ] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能
selectDbs: [ "default" ] # 可选生成表的数据库配置名称,支持多库
disableTables: [ "hg_sys_gen_codes","hg_admin_role_casbin" ] # 禁用的表,禁用以后将不会在选择表中看到
delimiters: [ "@{", "}" ] # 模板引擎变量分隔符号
# 生成应用模型,所有生成模板允许自定义,可以参考default模板进行改造
application:
# CRUD和关系树列表模板
crud:
templates:
# 默认的主包模板
- group: "default" # 分组名称
isAddon: false # 是否为插件模板 false|true
masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联
templatePath: "./resource/generate/default/curd" # 模板路径
apiPath: "./api/admin" # goApi生成路径
controllerPath: "./internal/controller/admin/sys" # 控制器生成路径
logicPath: "./internal/logic/sys" # 主要业务生成路径
inputPath: "./internal/model/input/sysin" # 表单过滤器生成路径
routerPath: "./internal/router/genrouter" # 生成路由表路径
sqlPath: "./storage/data/generate" # 生成sql语句路径
webApiPath: "../web/src/api" # webApi生成路径
webViewsPath: "../web/src/views" # web页面生成路径
# 默认的插件包模板,{$name}会自动替换成实际的插件名称
- group: "addon" # 分组名称
isAddon: true # 是否为插件模板 false|true
masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联
templatePath: "./resource/generate/default/curd" # 模板路径
apiPath: "./addons/{$name}/api/admin" # goApi生成路径
controllerPath: "./addons/{$name}/controller/admin/sys" # 控制器生成路径
logicPath: "./addons/{$name}/logic/sys" # 主要业务生成路径
inputPath: "./addons/{$name}/model/input/sysin" # 表单过滤器生成路径
routerPath: "./addons/{$name}/router/genrouter" # 生成路由表路径
sqlPath: "./storage/data/generate/addons" # 生成sql语句路径
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
# 消息队列模板
queue:
templates:
- group: "default"
templatePath: "./resource/generate/default/queue"
# 定时任务模板
cron:
templates:
- group: "default"
templatePath: "./resource/generate/default/cron"
# 生成插件模块,通过后台创建新插件时使用的模板,允许自定义,可以参考default模板进行改造
addon:
srcPath: "./resource/generate/default/addon" # 生成模板路径
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
```
### CLI配置
- hotgo在生成dao、service配置时,默认了和gf官方一致的配置方式和代码生成规则。所以无论你是通过hotgo亦或gf命令生成,最终代码格式完全一致,遵循一致的代码规范。
- 配置路径:[server/hack/config.yaml](../../server/hack/config.yaml)
```yaml
gfcli:
build:
name: "hotgo" # 编译后的可执行文件名称
# arch: "all" #不填默认当前系统架构,可选:386,amd64,arm,all
# system: "all" #不填默认当前系统平台,可选:linux,darwin,windows,all
mod: "none"
cgo: 0
packSrc: "resource" # 将resource目录打包进可执行文件,静态资源无需单独部署
packDst: "internal/packed/packed.go" # 打包后生成的Go文件路径,一般使用相对路径指定到本项目目录中
version: ""
output: "./temp/hotgo" # 可执行文件生成路径
extra: ""
gen:
dao:
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
group: "default" # 分组 使用hotgo代码生成功能时必须填
# tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
removePrefix: "hg_"
descriptionTag: true
noModelComment: true
jsonCase: "CamelLower"
gJsonSupport: true
clear: true
service: # 生成业务配置
srcFolder: "internal/logic"
dstFolder: "internal/service"
dstFileNameCase: "CamelLower"
clear: true
```
### 多数据库使用
- 假设我们要增加一个库名为`hotgo2`、分组为`default2`的数据库,并要为其生成代码
1. 配置[server/hack/config.yaml](../../server/hack/config.yaml) 如下:
```yaml
gen:
dao:
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
group: "default" # 分组 使用hotgo代码生成功能时必须填
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
removePrefix: "hg_"
descriptionTag: true
noModelComment: true
jsonCase: "CamelLower"
gJsonSupport: true
clear: false
- link: "mysql:hotgo2:hg123456.@tcp(127.0.0.1:3306)/hotgo2?loc=Local&parseTime=true"
group: "default2" # 分组 使用hotgo代码生成功能时必须填
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
removePrefix: ""
descriptionTag: true
noModelComment: true
jsonCase: "CamelLower"
gJsonSupport: true
clear: false
```
2. 配置`server/manifest/config/config.yaml`,
`database`配置如下:
```yaml
database:
logger:
level: "all"
stdout: true
default:
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
# link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
debug: true
Prefix: "hg_"
default2:
link: "mysql:hotgo2:hg123456.@tcp(127.0.0.1:3306)/hotgo2?loc=Local&parseTime=true"
debug: true
Prefix: ""
```
`hggen`配置如下:
```yaml
hggen:
allowedIPs: ["127.0.0.1", "*"] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能
selectDbs: [ "default", "default2" ] # 可选生成表的数据库配置名称,支持多库
disableTables : ["hg_sys_gen_codes","hg_admin_role_casbin"] # 禁用的表,禁用以后将不会在选择表中看到
delimiters: ["@{", "}"] # 模板引擎变量分隔符号
```
3. 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,就会发现`数据库`选项增加了一个`default2`,后续生成步骤和生成例子完全一样
> 注意:上述的配置中所有的`default2`名称必须保持一致
+115
View File
@@ -0,0 +1,115 @@
## 生成关联表CURD
### 热编译启动
- 推荐使用热编译方式启动HotGo,这样生成完成页面自动刷新即可看到新生成内容,无需手动重启
```shell
# 服务端
cd server
gf run main.go
# web端
cd web
pnpm run dev 或 npm run dev
```
以下是一个关联表的CURD生成流程
### 创建表结构
- 以下表结构和数据为了方便功能演示已经内置无需再次创建
- 创建主表:hg_sys_gen_curd_demo
```sql
CREATE TABLE `hg_sys_gen_curd_demo` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`category_id` bigint(20) DEFAULT '0' COMMENT '分类ID',
`title` varchar(64) NOT NULL COMMENT '标题',
`description` varchar(255) DEFAULT '' COMMENT '描述',
`content` text COMMENT '内容',
`image` varchar(255) DEFAULT NULL COMMENT '单图',
`attachfile` varchar(255) DEFAULT NULL COMMENT '附件',
`city_id` bigint(20) DEFAULT '0' COMMENT '所在城市',
`switch` int(11) DEFAULT '1' COMMENT '显示开关',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
`created_by` bigint(20) DEFAULT '0' COMMENT '创建者',
`updated_by` bigint(20) DEFAULT '0' COMMENT '更新者',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COMMENT='系统_生成curd演示';
```
创建关联表:hg_test_category
```sql
CREATE TABLE `hg_test_category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`name` varchar(255) NOT NULL COMMENT '分类名称',
`short_name` varchar(128) DEFAULT NULL COMMENT '简称',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`sort` int(11) NOT NULL COMMENT '排序',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='测试分类';
```
### 表字段要求
- 使用生成时,表中必须具有以下字段和属性
| 字段名称 | 字段含义 | 字段类型 | 可为空 |
|--------|----------------------|---------------------|-----|
| id | 主键ID | bigint(20) | 否 |
### 创建生成配置
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
![](images/code/join-add.png)
### 基本设置
- 确认无误后,点击生成配置会跳转到生成配置页面,如下:
- 你可以在该页面调整生成表格表头/表列的接口功能、菜单权限、高级设置和关联表设置
![](images/code/join-init.png)
### 主表字段设置
- 在该页面你可以调整生成表格字段名称、表单组件、表单编辑/验证项、列表展示/查询项、字段排序、重置和同步字段
![](images/code/join-fields.png)
### 关联表字段设置
- 在该页面你可以调整生成表格关联表字段名称、列表展示/查询项、字段排序、重置和同步字段
- 如果存在多个关联表,也可以对多个关联表字段进行设置
![](images/code/join-fields2.png)
### 预览并生成
点击`预览代码`查看生成的代码内容。如果无需继续调整直接点击`提交生成`即可,以下是预览代码效果:
![](images/code/join-preview.png)
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
### 生成完成
- 让我们看看生成的表格页面,效果如下:
![](images/code/join-list.png)
### 常见问题
- [生成常见问题](code-help.md)
+84
View File
@@ -0,0 +1,84 @@
## 生成CURD
### 热编译启动
- 推荐使用热编译方式启动HotGo,这样生成完成页面自动刷新即可看到新生成内容,无需手动重启
```shell
# 服务端
cd server
gf run main.go
# web端
cd web
pnpm run dev 或 npm run dev
```
以下是一个基本的CURD生成流程
### 创建表结构
- 以下表结构和数据为了方便功能演示已经内置无需再次创建
```sql
CREATE TABLE `hg_test_category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`name` varchar(255) NOT NULL COMMENT '分类名称',
`short_name` varchar(128) DEFAULT NULL COMMENT '简称',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`sort` int(11) NOT NULL COMMENT '排序',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='测试分类';
```
### 表字段要求
- 使用生成时,表中必须具有以下字段和属性
| 字段名称 | 字段含义 | 字段类型 | 可为空 |
|--------|----------------------|---------------------|-----|
| id | 主键ID | bigint(20) | 否 |
### 创建生成配置
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
![](images/code/curd-add.png)
### 基本设置
- 确认无误后,点击生成配置会跳转到生成配置页面,如下:
- 你可以在该页面调整生成表格表头/表列的接口功能、菜单权限、高级设置和关联表设置
![](images/code/curd-init.png)
### 表字段设置
- 在该页面你可以调整生成表格字段名称、表单组件、表单编辑/验证项、列表展示/查询项、字段排序、重置和同步字段
![](images/code/curd-fields.png)
### 预览并生成
点击`预览代码`查看生成的代码内容。如果无需继续调整直接点击`提交生成`即可,以下是预览代码效果:
![](images/code/curd-preview.png)
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
### 生成完成
- 让我们看看生成的表格页面,效果如下:
![](images/code/curd-list.png)
### 常见问题
- [生成常见问题](code-help.md)
+18
View File
@@ -0,0 +1,18 @@
## 生成常见问题
### 生成完后页面提示:服务器错误,请稍候重试!
- 热编译环境下,web端往往会快于服务端重启并加载完成,此时接口访问会出现`服务器错误,请稍候重试!`或`404`。这是服务端正在重启导致的,一般稍等几秒就好,如果不行就手动重启下服务端。
### fetching tables failed: SHOW TABLES: Error 1045 (28000): Access denied for user * (using password: YES)
- 请去确认`server/manifest/config/config.yaml`和`server/hack/config.yaml`下的数据库配置一致并且权限正确
- 参考:[生成配置](code-config.md)
### 为什么后台找不到开发工具菜单
- 请去确认`server/manifest/config/config.yaml`中的`system.mode`不为`product`。product模式下后台【开发工具】菜单自动隐藏
+20
View File
@@ -0,0 +1,20 @@
## 使用前提
在HotGo中可以通过后台开发工具快速的一键生成CRUD,自动生成Api、控制器、业务逻辑、Web页面、表单组件、菜单权限等。
- hotgo 版本号 >= 2.13.1
- 使用前必须配置 [生成配置](code-config.md)
- 使用前必须了解 [数据库](sys-db.md)
相关目录
- [生成配置](code-config.md)
- [生成CURD](code-curd.md)
- [生成关联表CURD](code-curd-join.md)
- [生成树型CURD](code-tree.md)
- [生成业务模板](code-business.md)
- [生成模板开发](code-template-dev.md)
- [生成常见问题](code-help.md)
+59
View File
@@ -0,0 +1,59 @@
## 生成模板开发
### 自定义生成模板
- HotGo允许你新建新的模板分组来满足你的需求,模板可根据现有模板基础拷贝一份出来做改造,默认模板目录:[server/resource/generate/default](../../server/resource/generate/default)
- 系统内置了两组CURD生成模板,请参考[生成模板配置](sys-code.md#生成模板配置)。default:是默认的生成到主模块下;addon:是默认生成到指定的插件下
### 内置gf-cli
- 为了确保生成代码的依赖稳定性,在面对`gf`版本更新可能导致向下不兼容情况时,HotGo将`gf-cli`工具内置到系统中并进行在线执行调整,从而提供更可靠和一致的生成代码功能。
- 后续我们也将开放在线运行`gf gen ...`功能。在做插件开发时也会支持到在线生成插件下的service接口,这将会使得插件开发更加方便
### 指定gf-cli版本
- HotGo多数情况下会和最新版本的gf-cli保持同步,如果更新不及时或你不想使用最新版本的gf-cli来生成代码,可以找到自己想要的版本进行替换即可。
- 下面大致做一些替换步骤说明:
1. 打开`github.com/gogf/gf` 找到你想要使用的版本`clone`下来
2. 将`clone`代码中`gf/cmd/gf/internal/`目录覆盖到`server/internal/library/hggen/internal`
3. 将覆盖过来的目录文件中引入包名`github.com/gogf/gf/cmd/gf/v2/`批量改为`hotgo/internal/library/hggen/`
4. 运行`go mod tidy`
5. 运行`go run main.go`,如果没有报错,那么恭喜你已经完成了。如果有报错一般都是版本差异带来的影响,需要根据情况自行调整
### 指定数据库驱动
> HotGo默认使用mysql驱动,如果你想用其他数据库驱动打开下方文件中注释即可,目前已支持mysql、pgsql
- 修改文件路径:[server/internal/library/hggen/internal/cmd/cmd_gen_dao.go](../../server/internal/library/hggen/internal/cmd/cmd_gen_dao.go)
```go
package cmd
import (
//_ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
//_ "github.com/gogf/gf/contrib/drivers/mssql/v2"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
//_ "github.com/gogf/gf/contrib/drivers/oracle/v2"
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
//_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"hotgo/internal/library/hggen/internal/cmd/gendao"
)
type (
cGenDao = gendao.CGenDao
)
```
修改完成后运行`go mod tidy`
+93
View File
@@ -0,0 +1,93 @@
## 生成树型CURD
### 热编译启动
- 推荐使用热编译方式启动HotGo,这样生成完成页面自动刷新即可看到新生成内容,无需手动重启
```shell
# 服务端
cd server
gf run main.go
# web端
cd web
pnpm run dev 或 npm run dev
```
以下是一个基本的树形CURD生成流程
### 创建表结构
- 以下表结构和数据为了方便功能演示已经内置无需再次创建
```sql
CREATE TABLE `hg_sys_gen_tree_demo` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`pid` bigint(20) DEFAULT NULL COMMENT '上级ID',
`level` int(11) DEFAULT '1' COMMENT '关系树级别',
`tree` varchar(512) COMMENT '关系树',
`category_id` bigint(20) DEFAULT '0' COMMENT '分类ID',
`title` varchar(64) NOT NULL COMMENT '标题',
`description` varchar(255) DEFAULT '' COMMENT '描述',
`sort` int(11) DEFAULT NULL COMMENT '排序',
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
`created_by` bigint(20) DEFAULT '0' COMMENT '创建者',
`updated_by` bigint(20) DEFAULT '0' COMMENT '更新者',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COMMENT='系统_生成树表演示';
```
### 表字段要求
- 使用生成时,表中必须具有以下字段和属性
| 字段名称 | 字段含义 | 字段类型 | 可为空 |
|--------|----------------------|---------------------|-----|
| id | 主键ID | bigint(20) | 否 |
| pid | 上级树ID | bigint(20) | 是 |
| level | 关系树级别 | int(11) | 否 |
| tree | 关系树 | varchar(512) | 是 |
### 创建生成配置
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
![](images/code/tree-add.png)
### 基本设置
- 确认无误后,点击生成配置会跳转到生成配置页面,如下:
- 你可以在该页面调整生成表格表头/表列的接口功能、菜单权限、高级设置和关联表设置
- 相比普通CURD,这里多了`树名称字段`和`树表格样式`选项
![](images/code/tree-init.png)
### 表字段设置
- 在该页面你可以调整生成表格字段名称、表单组件、表单编辑/验证项、列表展示/查询项、字段排序、重置和同步字段
- 树表中的pid、level、tree字段由系统维护,单独设置编辑表单功能无效!
![](images/code/tree-fields.png)
### 预览并生成
点击`预览代码`查看生成的代码内容。如果无需继续调整直接点击`提交生成`即可,以下是预览代码效果:
![](images/code/tree-preview.png)
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
### 生成完成
- 让我们看看生成的表格页面,效果如下:
- 普通树表
![](images/code/tree-list.png)
- 选项式树表
![](images/code/tree-list2.png)
### 常见问题
- [生成常见问题](code-help.md)
Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+108
View File
@@ -0,0 +1,108 @@
## 生产部署
目录
- 编译配置
- 编译
- 修改生产配置文件
- 启动服务
- Nginx配置
### 编译配置
- 配置文件:server/hack/config.yaml,以下是默认配置
```yaml
gfcli:
build:
name: "hotgo" # 编译后的可执行文件名称
# arch: "all" #不填默认当前系统架构,可选:386,amd64,arm,all
# system: "all" #不填默认当前系统平台,可选:linux,darwin,windows,all
mod: "none"
cgo: 0
packSrc: "resource" # 将resource目录打包进可执行文件,静态资源无需单独部署
packDst: "internal/packed/packed.go" # 打包后生成的Go文件路径,一般使用相对路径指定到本项目目录中
version: ""
output: "./temp/hotgo" # 可执行文件生成路径
extra: ""
```
### 编译
- 以下方式任选其一即可
1、 make一键编译 (确认已安装`make环境`)
```shell
cd server && make build
```
2、 按步骤手动编译
```shell
cd server # 切换到服务端目录下
rm -rf ./resource/public/admin/ # 删除之前的web资源
mkdir ./resource/public/admin/ # 重新创建web资源存放目录,除首次编译后续可以跳过执行此步骤
cd ../web && pnpm run build # 切换到web项目下,编译web项目
\cp -rf ./dist/* ../server/resource/public/admin/ # 将编译好的web资源复制到server对应的资源存放路径下
cd ../server # 切换回服务端目录下
echo "y" | gf build # 编译hotgo服务端
# 不出意外你已经编译好了hotgo可执行文件!
```
3、分端编译 (多人开发时推荐)
```shell
# 编译服务端
cd server # 切换到服务端目录下
rm -rf ./resource/public/admin/ # 删除之前的web资源,如果开发环境没有这个目录可以忽略此步骤
echo "y" | gf build # 编译hotgo服务端
# 编译web端
cd web
pnpm run build 或 npm run build
# web端编译完成后,将web/dist/*中的文件上传到`server`端线上运行目录:/resource/public/admin即可
# 至此,web端和server端都可以独立覆盖更新
```
### 修改生产配置文件
- 配置文件:server/manifest/config/config.yaml
> 如关闭debug、mode设为生产环境、修改数据库地址、缓存驱动、队列驱动、日志路径等
### 启动服务
> 这里可以接使用gf官方推荐的启动方式,请参考:https://goframe.org/pages/viewpage.action?pageId=1114403
### Nginx配置
```
# websocket
location = /socket {
proxy_pass http://127.0.0.1:8000/socket;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-NginX-Proxy true;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_connect_timeout 600s;
proxy_read_timeout 600;
proxy_send_timeout 600s;
}
# http
location ^~ / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:8000/;
proxy_redirect off;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection upgrade;
}
```
+31
View File
@@ -0,0 +1,31 @@
## 环境搭建
目录
- 前端环境
- 后端环境
- 使用说明
### 前端环境
1. 前往https://nodejs.org/zh-cn/下载当前版本node
2. 命令行运行 `node -v` 若控制台输出版本号则node安装成功
3. node 版本需大于等于 `16.0`
4. 安装pnpm:`npm install -g pnpm`
5. 命令行运行 `pnpm -v` 若控制台输出版本号则前端环境搭建成功
### 后端环境
1. 下载golang安装 版本号需>=1.23
2. 国际: https://golang.org/dl/
3. 国内: https://golang.google.cn/dl/
4. 命令行运行 go 若控制台输出各类提示命令 则安装成功 输入 `go version` 确认版本大于1.23
5. 开发工具推荐 [Goland](https://www.jetbrains.com/go/)
### 使用说明
> 需要本地具有 git node golang 环境
- node版本 >= 20.0.0
- golang版本 >= 1.23
- mysql版本 >= 5.7,引擎需要是 innoDB 或 postgresql版本 >=14
- IDE推荐:Goland
+107
View File
@@ -0,0 +1,107 @@
## 系统安装
目录
- 环境要求
- 安装
### 环境要求
- node版本 >= v20.0.0
- golang版本 >= v1.23
- goframe版本 >=v2.9.4
- mysql版本 >=5.7 或 postgresql版本 >=14
> 必须先看[环境搭建文档](start-environment.md),如果安装遇到问题务必先查看[常见问题文档](start-issue.md)
### 安装
一、克隆项目
```
git clone https://github.com/bufanyun/hotgo.git && cd hotgo
```
二、配置你的站点信息
1、服务端:
- 项目数据库文件 `storage/data/hotgo.sql` 创建数据库并导入
- 将配置文件 `manifest/config/config.example.yaml` 复制后改为`manifest/config/config.yaml`
- 将`manifest/config/config.yaml`中的`database.default.link`数据库配置改为你自己的:
```yaml
# Database. 配置参考:https://goframe.org/pages/viewpage.action?pageId=1114245
database:
logger:
path: "logs/database" # 日志文件路径。默认为空,表示关闭,仅输出到终端
<<: *defaultLogger
stdout: true
default:
# link: "mysql:账号:密码@tcp(127.0.0.1:3306)/数据库名?loc=Local&parseTime=true&charset=utf8mb4"
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
debug: true
Prefix: "hg_"
```
- 将`hack/config.yaml`中的`gfcli.gen.dao[0].link`数据库配置改为你自己的:
```yaml
gfcli:
gen:
dao:
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
group: "default" # 分组 使用hotgo代码生成功能时必须填
# tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
removePrefix: "hg_"
descriptionTag: true
noModelComment: true
jsonCase: "CamelLower"
gJsonSupport: true
clear: false
```
2、web前端:
- 配置服务端地址,包含在以下文件中:
* /hotgo/web/.env.development
* /hotgo/web/.env.production
* /hotgo/web/.env
三、 启动服务
1、服务端:
```shell
cd server
# 设置国内代理,如果已经设置好了代理可以跳过
go env -w GOPROXY=https://goproxy.io,direct
# 更新包
go mod tidy
# 查看命令行方法
go run main.go help
# 启动所有服务
go run main.go # 热编译启动: gf run main.go
```
2、web前端:
```shell
cd web
# 首先确定你以安装node16.0以上版本并安装了包[npm、pnpm],否则可能会出现一些未知报错
# 安装依赖
pnpm install
# 启动web项目
pnpm run dev
# 如果顺利,至此到浏览器打开:http://你的IP:8001/admin
# 登录账号:admin, 密码:123456
```
+93
View File
@@ -0,0 +1,93 @@
## 常见问题
目录
- 一、后台相关
- 二、数据库相关
- 三、环境相关
### 一、后台相关
#### 1、连接超时,请刷新重试。如仍未解决请检查websocket连接是否正确!
线上或非本地运行时,请到 系统设置 -> 配置管理 -> 基本设置 -> 找到网站域名和websocket地址,改成你自己实际的地址,保存刷新页面即可
#### 2、web页面菜单切换后页面出现白屏
请参考:https://github.com/jekip/naive-ui-admin/issues/183
### 二、数据库相关
#### 1、安装数据库出现 json 报错不支持
请安装 mysql5.7 及以上版本的数据库。如果你使用的是mariadb请确认版本号,mariadb从 10.2 版本开始支持 json 数据类型。
### 三、环境相关
#### 1、not found in resource manager or following system searching paths
> 报错信息:panic: possible config files "config" or "config.toml/yaml/yml/json/ini/xml/properties" not found in resource manager or following system searching paths:
系统没有找到配置文件,将配置文件 `manifest/config/config.yaml.bak` 复制后改为`manifest/config/config.yaml`
#### 2、net.DialTimeout failed with network
> 报错信息:connect to 127.0.0.1:8099 error: net.DialTimeout failed with network "tcp", address "127.0.0.1:8099", timeout "10s": dial tcp
- http服务没有启动或正在启动
- 通过一键启动所有服务运行时属正常情况,多服务启动时存在先后顺序问题,`tcpClient`比`tcpServer`先启动完成导致的,等`tcpServer`启动完成后会自动重连
详细请参考 - [系统安装](start-installation.md)
#### 3、cannot find "hack/config.yaml" in following paths:
> 报错信息:get cli configuration file:hack/config.yaml, err:cannot find "hack/config.yaml" in following paths:
系统运行目录下配置hack/config.yaml文件。如果是生产环境运行,并且不需要开发工具相关功能,可以将`manifest/config/config.yaml`配置文件中的`system.mode`值改为`product`,这样启动时不会加载开发工具相关功能
#### 4、非超管角色提示:你没有访问权限!
![1.4.0.png](./images/issue/1.4.0.png)
此问题因当前角色访问的页面包含未分配权限的接口所致,在多角色场景中较为常见,解决步骤如下:
1. 定位缺失权限的接口
- 用超管账号进入系统应用 -> 日志管理 -> 全局日志,搜索状态码为`62 无访问权限`的记录
- 示例:若请求接口为`/admin/hgexample/treeTable/list`,需处理的权限路径为`/hgexample/treeTable/list`(去除`/admin`前缀)
![1.4.1.png](./images/issue/1.4.1.png)
假如查到请求接口是:`/admin/hgexample/treeTable/list`,那需要分配的菜单权限就是:`/hgexample/treeTable/list`,要把`/admin`去掉
2. 配置菜单权限
- 进入权限管理 -> 菜单权限:
- 若目标菜单不存在,先新增菜单
- 若菜单已存在但未配置接口权限,就把步骤1中的权限路径添加到【分配权限】中(已配置则跳过)
![1.4.2.png](./images/issue/1.4.2.png)
3. 分配权限给角色
- 进入权限管理 -> 角色权限,找到对应角色并点击【菜单权限】
- 勾选步骤2中配置的菜单权限,保存即可
![1.4.3.png](./images/issue/1.4.3.png)
### 四、前端相关
#### 1、Error: connect ECONNREFUSED ::1:8000
```text
11:44:52 [vite] http proxy error at /member/info:
Error: connect ECONNREFUSED ::1:8000
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1246:16)
```
- 服务端没有启动
- `.\wen\.env.development`中的`VITE_PROXY`配置的服务器地址或端口与实际不一致
+21
View File
@@ -0,0 +1,21 @@
## 如何提问
#### 说明环境情况
- 平台: windows/linux
- 软件与版本:golang 1.19, Mysql 5.7 ...
- 系统版本:hotgo 2.6.2
#### 你做了什么?
- 我按文档(附链接)上写了一个控制器xxx,代码如下:xxxx
- 然后我使用xxx访问
- 这其中我还做了啥
- ...
#### 出现什么错误?
- 当我试图xxx的时候,xxx报了如下错误:(截图)
- 我确认过我的目录权限是正确的,xxx是正确的(必要时带上截图)
以上是举例提问时所应该描述的情况,当别人看到详细的情况时,也便于快速定位问题所在。
+333
View File
@@ -0,0 +1,333 @@
## 更新历史
升级说明:
> 创建一个Git版本库,并创建二个分支比如A、B,A分支默认为你的二开分支、B分支为你的版本库分支。
> 等需要升级的时候,把版本库分支(B)升级为最新的版本,然后切换到A分支,把B分支合并过来,解决掉合并冲突即可。
注意:
> 各个小版本升级例如 2.1.x 升级到 2.2.x 以上不能完美升级最好重新安装
> 如果升级(覆盖)代码后打开会出现 sql 报错, 请检查更新的数据库格式或自行调整
### v2.18.9
updated 2026.5.9
- 新增:新增 `AGENTS.md` 项目开发规范,统一前后端开发约定,作为 AI 辅助开发的唯一事实来源
- 优化:naive-ui版本升级到2.44.1
- 优化:gf版本升级到v2.10.0
### v2.18.6
updated 2025.10.25
- 新增:国际化(i18n)支持,内置简体中文、繁体中文、英文三种语言包
- 新增:附件管理支持外链图片转存至系统存储驱动
- 优化:全局微信公众号实例初始化逻辑,`AccessToken` 获取方式调整为稳定版接口
- 优化:菜单权限搜索功能,支持按菜单名称或路由地址进行检索
- 优化:角色权限管理中的菜单权限分配交互体验
- 优化:naive-ui版本升级到2.43.1
- 优化:gf版本升级到v2.9.4
- 修复:删除角色导致关联用户无法登录的问题,新增角色删除前置校验(检查是否存在关联用户)
### v2.17.8
updated 2025.7.13
- 优化:表格排序处理器兼容关联表别名
- 优化:优化动态统计数字在0值时显示问题
- 优化:优化首页快捷菜单点击事件范围
- 优化:优化短信、邮件验证码相关验证类数据排序
- 优化:优化Nginx配置,支持流式请求和兼容部分版本无法匹配到websocket规则问题
- 优化:naive-ui版本升级到2.42.0
- 优化:vue3-json-viewer版本升级到2.4.1
- 修复:修复可选用户选项`Fields`生成错误问题
### v2.16.10
updated 2025.3.22
- 增加:增加组合下拉用户筛选组件
- 优化:文件上传类型限制改为统一由后台配置控制
- 优化:websocket重连时间调整为10秒,初始化数据库中的网站地址调整为`127.0.0.1`
- 优化:优化访问日志字段展示方式
- 优化:系统公告发送编辑模态框调整为抽屉
- 优化:gf版本升级到v2.9.0
- 优化:naive-ui版本升级到2.41.0
- 修复:修复vue路由`Redirect`命名重复问题
- 修复:优化代码生成在插件中`使用生成字典选项`时model包名重复问题
### v2.15.11
updated 2024.11.27
- 增加:增加配置管理子页面选项参数记忆
- 修复:修复定时任务中日志配置的读取并发读问题
- 优化:gf版本升级到v2.8.2
- 优化:优化数据卡片展示
### v2.15.7
updated 2024.7.21
- 增加:访问日志、服务日志增加关键词搜索
- 增加:web端增加字典状态管理,重构字典选项使用方式,大幅减少冗余代码
- 修复:修复生成代码选项式树表已知的一些小bug
- 优化:gf版本升级到v2.7.2
- 优化:naive-ui版本升级到2.39.0
- 优化:访问日志不再记录过大的请求头参数,减少日志大小
### v2.15.1
updated 2024.4.22
- 增加:生成代码增加树表生成、字段排序、字段重置、操作人维护,调整关联表生成方式
- 增加:角色菜单权限增加一键导入支持
- 增加:访问日志增加忽略请求方式
- 增加:插件生成增加`consts`目录维护
- 增加:IP归属地定位增加默认重试,增加新的缓存机制
- 增加:登录日志增加UA信息、IP归属信息,解藕访问日志
- 增加:消息队列`rocketmq`驱动更新版本问题,增加延迟队列,支持自动创建主题
- 增加:部门增加类型支持,通过部门类型实现多租户业务支持
- 增加:增加多租户业务开发演示
- 修复:修复部门列表搜索算法在部分情况下无效问题
- 修复:修复短信、邮件IP发送数量限制
- 修复:修复分布式锁内存泄漏问题
- 优化:gf版本升级到v2.7.0
- 优化:naive-ui版本升级到2.38.1
- 优化:优化`cmd`服务退出流程
- 优化:优化生成代码格式化和移除未使用的包
- 优化:服务端配置文件参数`hotgo`改为`system`
- 优化:web端表格/表单增加自适应宽度计算
- 优化:日期选择组件不再使用默认值
- 优化:优化菜单添加/编辑表单,使其操作更加简单方便
### v2.13.1
updated 2024.3.7
- 增加:增加内置数据字典类型:`枚举字典`和`自定义方法字典`,支持代码生成时关联选项使用
- 增加:增加大文件上传,支持分片上传、断点续传,存储驱动已适配`本地存储`
- 增加:插件模块增加停止服务回调接口,调整静态资源默认存放位置,创建插件选项增加可选扩展功能
- 增加:功能案例插件增加`30+`常用组件示例,增加`websocket`消息收发测试
- 增加:文档增`加功能扩展库`、`websocket服务器`、`websocket客户端`使用说明,当前版本文档已完善
- 修复:修复省市区无法添加地区问题
- 优化:gf版本升级到v2.6.4
- 优化:优化缓存组件依赖关系
- 优化:调整部分前端表格自适应宽度
- 优化:HTTP错误码接管统一改为由响应中间件处理
### v2.12.1
updated 2023.12.29
- 修复:修复访问日志权限过滤
- 优化:gf版本升级到v2.6.1
- 优化:naive-ui版本升级到2.36.0,vue版本升级到3.4.0,vite版本升级到4.2.7
- 优化:优化curd代码生成,编辑表单增加自适应,详情改为`抽屉`,修复菜单权限关系树,简化`State`
- 优化:优化字典编辑和查询
- 优化:优化角色列表数据加载缓慢问题
### v2.11.5
updated 2023.11.25
- 增加`minio`上传驱动支持
- 增加定时任务调度日志,增加同方法多任务支持
- 增加gf运行模式控制,生产环境时隐藏开发相关菜单
- 增加树形表格新增/修改自动维护上下级关系通用处理
- 增加树形表格使用例子
- 优化:gf版本升级到v2.5.7
- 优化`websocket`默认路径配置
- 优化预处理中间件错误码跟随
- 优化插件静态目录为可选项
- 修复升级gf版本后服务日志记录内容为空问题
- 修复角色权限选择数据重复问题
- 修复服务日志无日志内容问题
- 修复用户批量删除可以删除超管问题
- 修复代码生成时偶尔找不到主键索引问题
- 修复树形选项组件在`BasicForm`无法使用问题
- 修复表格批量删除重复传参问题
- 修复文件缓存`GetOrSet`和`GetOrSetFunc`缓存过期不刷新问题
### v2.9.8
updated 2023.10.21
- 优化:gf版本升级到v2.5.5
- 优化:优化代码生成表单滑动
- 优化:优化字典列表编辑生效时机
- 修复:修复日期组件默认值无效问题
### v2.9.3
updated 2023.10.08
- 优化:gf版本升级到v2.5.4
- 优化:优化上传文件资源复用策略
- 修复:修复登录角色的部门状态检查
- 修复:修复配置参数`exceptLogin`命名
### v2.8.9
updated 2023.09.06
- 优化:gf版本升级到v2.5.2
- 优化:优化代码生成,自定义模板时自动过滤前缀,菜单权限生成预检查
- 优化:服务日志内容长度超出最大限制截取
- 修复:修复tcp服务器定时任务执行异常
- 修复:修复文件选择器错误传参导致的显示异常问题
### v2.8.4
updated 2023.07.20
- 增加:增加输入预处理中间件
- 增加:增加文件选择器
- 增加:增加在线服务监控,服务许可证管理
- 增加:TCP服务器增加RPC路由消息注册、路由分发、拦截器注册,更方便的进行应用开发
- 优化:gf版本升级到v2.5.0
- 优化:优化CURD代码生成,简化控制器代码逻辑,升级列表数据查询方式
- 修复:修复角色菜单子权限取消导致父级权限失效问题
- 修复:修复上传驱动路径错误
- 修复:修复CURD代码生成时间类型字段默认值获取异常
### v2.7.6
updated 2023.06.19
- 优化:gf版本升级到v2.4.3
- 修复:部门管理查询空指针问题
- 优化:附件md5生成方式调整(与更新前已上传的文件md5不兼容,如果业务有影响请注意调整)
### v2.7.3
updated 2023.05.14
- 增加:增加注册、手机号登录、二维码邀请注册功能
- 优化:优化管理员业务模块验证唯一属性接口,将登录相关功能拆分到管理员基础模块中
- 移除:移除web端mock插件
### v2.6.10
updated 2023.05.12
- 增加:增加jwt+缓存驱动实现的令牌认证,支持自动续约、可控过期失效策略
- 移除:移除旧jwt功能库
- 移除:移除`decimal`、`mahonia` package
### v2.6.7
updated 2023.05.10
- 增加:增加支付网关:集成支付宝、微信支付、QQ支付,可通过后台直接配置参数
- 增加:增加资金管理模块,包含在线充值、申请提现、资金变动明细功能
- 增加:增加redis延迟队列
- 增加:tcp服务增加rpc协议通讯支持
- 增加:通过微信访问后台自动获取openid,后续微信相关业务使用
- 优化:gf版本升级到v2.4.1
- 优化:优化CURD代码生成单表列表
- 优化:优化菜单权限路由,解决偶尔修改权限不能实时生效的情况
- 优化:优化后台导出功能搜索参数无效问题
- 优化:将定时任务拆分成可独立部署的程序,通过tcp长连接方式与后台交互
- 修复:修复相同文件多次上传页面无响应问题
- 修复:修复新用户不更新活跃时间问题
- 修复:修复本地缓存当缓存不存在时的异常错误问题
- 修复:修复访问日志偶尔出现无法记录问题
### v2.5.3
updated 2023.04.10
- 优化:优化IP地理位置定位,增加地理位置缓存,避免相同IP并发操作
- 优化:优化日志管理中相关列表字段过多在小屏设备列表不显示滚动条情况
- 增加:增加短信驱动:腾讯云短信,可通过后台一键切换
- 修复:修复角色权限选择自定义部门时,部门选项无法显示问题
### v2.4.9
updated 2023.04.05
- 优化:优化全局日志数据加载和分页加载,大分页增加分批次加载
- 优化:优化TCP服务认证,超出多端登录上限时直接踢掉所有在线让其重连
- 增加:增加上传驱动:腾讯云cos、阿里云oss、七牛云对象存储
- 优化:优化代码生成文档,数据库文档增加多数据库生成配置
- 修复:修复系统监控在苹果m1芯片空指针以及显示问题
### v2.4.4
updated 2023.03.16
- 优化:优化代码生成多库生成时的菜单sql默认指向默认数据库分组
- 优化:优化TCP服务认证机制
### v2.4.2
updated 2023.03.11
- 修复:修复字典管理列表无法添加/编辑问题
- 优化:优化代码生成文档
- 优化:优化部门树形选项排序问题
- 优化:优化基础设置中的LOGO图片上传组件
- 优化:优化黑名单IP过滤策略
- 增加:增加基于gtcp实现的C/S服务器,为后续多服务通讯建立基础库
- 优化:gf版本升级到v2.3.3
### v2.3.5
updated 2023.02.26
- 优化: 调整消息队列消费初始化方式,支持在插件模块下注册消费者
- 修复:后台用户添加模态框无法正常显示问题
- 优化:创建新插件生成增加web页面的生成
- 增加:增加生产部署、开发规范、生成代码、表单组件等相关文档
- 修复:修复后台已知的一些小bug
### v2.2.10
updated 2023.02.23
- 增加: 增加插件管理、设计新插件等插件模块
- 增加: 增加插件应用:`功能案例`
- 增加: 增加使用文档
- 优化: 生成代码适配插件模块
- 优化:消息队列增加磁盘队列,默认队列从`redis`改为`disk`
- 优化:缓存驱动增加文件适配器,默认驱动从`redis`改为`file`,项目安装不再依赖`redis`
- 优化:优化系统监控加载流程
- 优化:gf版本升级到v2.3.2
- 修复:生成代码刷新后偶尔出现加载失败问题
### v2.2.1
updated 2023.01.12
- 优化: 缓存清理各个应用的缓存文件夹读写判断
- 修复: Linux 环境下创建插件报找不到模板文件
- 修复: Excel 导入找不到最后一列数据
- 增加: 无权限菜单不显示
- 优化: 省市区数据为最新的 2023.01.11 的国家统计局省市区数据
- 优化: 前台关于图片上传的 js 和 css 引入,避免资源依赖找不到
- 优化: 插件模块查询机制, 增加数据缓存依赖,依赖时间为 360 秒
- 优化: 文件上传的处理
- 修复: 网站配置的多图上传引入路径错误
- 修复: 定时任务协程问题导致执行失败
-
### v2.1.1
updated 2022.9.26
- 优化: 多图上传的显示样式及功能
- 修复: 由于上传视频和语音开启了全域名返回导致的上传到服务器错误
- 增加: 由增加home页面入口,前台页面
### v2.0.8
updated 2022.9.21
- 优化: 优化菜单和角色数据权限,支持数据权限和按钮细分权限
- 优化: 优化附件上传,增加文件上传选项
### v2.0.6
updated 2022.9.18
- 增加: 增加阿里云短信配置和发送短信API
- 增加: 增加高德地图配置
- 优化: 优化数据字典,增加数据类型和标签支持
-
### v2.0.3
updated 2022.9.10
- 2.0 全新上线
### v2.0.2
updated 2022.6.11
- 增加: 增加生成代码功能,目前已支持一键生成CURD和关联表
- 增加: 增加云存储UCloud配置,后台一键切换驱动
### v2.0.0
updated 2022.5.20
- 初始化: 2.0 基础框架
+155
View File
@@ -0,0 +1,155 @@
## 权限控制
目录
- 一、菜单权限
- 二、数据权限
### 一、菜单权限
> 菜单权限可以控制web后台菜单功能,每个菜单可以绑定一个或多个API权限,API权限决定了用户是否可以调用服务端接口。
#### 为角色添加菜单权限
1、HotGo后台 -> 权限管理 -> 菜单权限 -> 找到添加菜单,按照表单提示添加你的菜单信息。
2、添加菜单完成后,到 权限管理 -> 角色权限 -> 找到一位已有角色或者添加新角色 -> 在列表右侧操作栏找到菜单权限打开 -> 将第1步中添加的菜单分配给该角色即可。
#### 细粒度权限
> 一般用于web后台页面上的功能按钮,对不同的角色展示不同的按钮时会用到。
1、参考【为角色添加菜单权限】添加一个按钮类型的菜单。并为菜单分配API权限,假设你分配权限是:/test/index
2、在web页面中创建一个按钮,如下:
```vue
<template>
<div>
<n-button v-if="hasPermission(['/test/index'])">拥有[/test/index]权限可见</n-button>
</div>
</template>
<script lang="ts" setup>
import { usePermission } from '@/hooks/web/usePermission';
const { hasPermission } = usePermission();
</script>
```
#### 菜单表主要字段解释
- 代码片段:[server/internal/model/input/adminin/menu.go](../../server/internal/model/input/adminin/menu.go)
```go
type MenuRouteMeta struct {
// 解释参考:https://naive-ui-admin-docs.vercel.app/guide/router.html#%E5%A4%9A%E7%BA%A7%E8%B7%AF%E7%94%B1
Title string `json:"title"` // 菜单名称 一般必填
// Disabled bool `json:"disabled,omitempty"` // 禁用菜单
Icon string `json:"icon,omitempty"` // 菜单图标
KeepAlive bool `json:"keepAlive,omitempty"` // 缓存该路由
Hidden bool `json:"hidden,omitempty"` // 隐藏菜单
Sort int `json:"sort,omitempty"` // 排序越小越排前
AlwaysShow bool `json:"alwaysShow,omitempty"` // 取消自动计算根路由模式
ActiveMenu string `json:"activeMenu,omitempty"` // 当路由设置了该属性,则会高亮相对应的侧边栏。
// 这在某些场景非常有用,比如:一个列表页路由为:/list/basic-list
// 点击进入详情页,这时候路由为/list/basic-info/1,但你想在侧边栏高亮列表的路由,就可以进行如下设置
// 注意是配置高亮路由 `name`,不是path
IsRoot bool `json:"isRoot,omitempty"` // 是否跟路由 顶部混合菜单,必须传 true,否则左侧会显示异常(场景就是,分割菜单之后,当一级菜单没有子菜单)
FrameSrc string `json:"frameSrc,omitempty" ` // 内联外部地址
Permissions string `json:"permissions,omitempty"` // 菜单包含权限集合,满足其中一个就会显示
Affix bool `json:"affix,omitempty"` // 是否固定 设置为 true 之后 多页签不可删除
Type int `json:"type"` // 菜单类型
}
```
#### API权限验证流程
```mermaid
graph TD
A(用户请求API) --> B(验证中间件<br> server/internal/logic/middleware/admin_auth.go)
B -->|没有登录,但API需要登录| H(提示登录)
B -->|没有登录,但API不需要登录| G(验证通过,进入业务)
B -->|已登录| C(检查用户角色是否拥有API权限<br> server/internal/logic/admin/role.go,方法:Verify) -->|有权限| G(验证通过,进入业务)
C -->|API无需验证权限| G(验证通过,进入业务流程)
C -->|无权限| D(提示无权限)
```
#### 菜单权限添加或修改后多久生效?
- API权限实时生效,web后台菜单刷新页面后生效,无需重启服务
### 二、数据权限
> 数据权限是某人只能看到某些数据,可以为某人设置一定的数据范围,让其只能看到允许他看到的数据。
> 例如:公司存在多个销售部门,不同的部门仅能查看到自己所属部门下的数据;再如:用户有多级代理商,只能查看自己和自己下级的数据。
#### 目前已经支持的数据权限如下
| 数据范围 | 描述 |
|---------|--------------------------------------------|
| 全部权限 | 不做过滤,可以查看所有数据 |
| 当前部门 | 仅可以看到所属部门下的数据 |
| 当前以及下部门 | 仅可以看到所属以及下级部门的数据 |
| 自定义部门 | 可以看到设置的指定部门,支持多个 |
| 仅自己 | 仅可以看到自己的数据 |
| 自己和直属下级 | 仅可以看到自己和直属下级的数据,直属下级是指当前用户往下1级的用户 |
| 自己和全部下级 | 仅可以看到自己和全部下级的数据,全部下级是指当前用户所有的下级用户,包含其下级的下级 |
#### 如何区分部门和下级用户?
- 在实际使用时,部门更多的是在公司或机构中使用,可以通过在 组织管理 -> 后台用户 ->为用户绑定部门
- 下级用户在代理商或分销系统中比较常见,后台用户由谁添加的,那么被添加的用户就是其下级用户
#### 如何判断数据是谁的?
- 在数据库建表时,只要表中包含字段:`created_by`或`member_id`即可过滤 [仅自己、自己和直属下级、自己和全部下级] 类型的数据权限
#### 如何在具体业务中使用数据权限过滤?
- 只需在查询、更新或删除等ORM链式操作中加入相应`Handler`方法即可
- 如果你想对Handler有更多详细了解,请查看:https://goframe.org/pages/viewpage.action?pageId=20087340
- 下面提供一个简单使用例子:
```go
package main
import (
"context"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/handler"
)
func test(ctx context.Context) {
dao.AdminPost.Ctx(ctx).Where("id", 1).Handler(handler.FilterAuth).Scan(&res)
}
```
- 如果表中没有字段:`created_by`或`member_id`,也可以使用自定义字段方法来过滤数据权限,如下:
```go
package main
import (
"context"
"hotgo/internal/dao"
"hotgo/internal/library/hgorm/handler"
)
func test(ctx context.Context) {
dao.AdminPost.Ctx(ctx).Where("id", 1).Handler(handler.FilterAuthWithField("test_field")).Scan(&res)
}
```
- 过滤方法实现请参考:[server/internal/library/hgorm/handler/filter_auth.go](../../server/internal/library/hgorm/handler/filter_auth.go)
+176
View File
@@ -0,0 +1,176 @@
## 目录结构
目录
- 服务端
- web前端
- uniapp端(待开放)
#### 服务端
```
/server
├── addons
│ ├── modules
│ ├── xxx插件
│ | ├── api
│ | ├── controller
│ | ├── crons
│ | ├── global
│ | ├── logic
│ | ├── model
│ | ├── queues
│ | ├── resource
│ | ├── router
│ | ├── service
│ | ├── main.go
│ | └── README.md
├── api
│ ├── admin
│ ├── api
│ ├── home
│ ├── websocket
├── hack
├── internal
│ ├── cmd
│ ├── consts
│ ├── controller
│ ├── crons
│ ├── dao
│ ├── global
│ ├── library
│ ├── logic
│ ├── model
│ | ├── do
│ │ ├── entity
│ │ └── input
│ ├── packed
│ ├── queues
│ ├── router
│ ├── service
│ └── websocket
├── manifest
├── resource
├── storage
├── utility
├── go.mod
├── main.go
├── Makefile
└── README.md
```
| 目录 | 描述 |
|--------------------------|-----------------------------------------------------------------|
| 基于gf的工程目录结构做了部分调整 | 参考地址: https://goframe.org/pages/viewpage.action?pageId=30740166 |
| **addons** | 所有的插件模块都放在这里 |
| --- modules | 为插件模块提供隐式初始化 |
| --- xxx插件 | 插件模块名称 |
| --- --- api | 对外接口。提供服务的输入/输出数据结构定义 |
| --- --- --- admin | 后台接口 |
| --- --- --- api | 前台通用接口,包含PC端、移动端接口等 |
| --- --- --- home | 前台PC端、H5端页面 |
| --- --- --- websocket | 可同时为多应用提供websocket接口 |
| --- --- consts | 插件内主要的常量定义 |
| --- --- controller | 接收/解析用户输入参数的入口/接口层,也可以理解为控制器 |
| --- --- crons | 项目中由系统统一接管的定时任务处理 |
| --- --- global | 项目内主要的全局变量和系统的一些初始化操作 |
| --- --- logic | 业务逻辑封装管理,特定的业务逻辑实现和封装往往是项目中最复杂的部分 |
| --- --- model | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 |
| --- --- --- input | 对内接口。用于controller调用service或service之间调用时的输入/输出结构定义,以及输入过滤和预处理 |
| --- queues | 为项目内所有的消息队列的消费者提供统一的初始化和处理 |
| --- resource | 静态资源文件。这些文件往往可以通过 资源打包/镜像编译 的形式注入到发布文件中 |
| --- --- router | 注册对外接口和分组中间件 |
| --- --- service | 用于业务模块解耦的接口定义层具体的接口实现在logic中进行注入 |
| --- main.go | 插件始化文件和模块插拔接口 |
| **api** | 对外接口。提供服务的输入/输出数据结构定义 |
| --- admin | 后台接口 |
| --- api | 前台通用接口,包含PC端、移动端接口等 |
| --- home | 前台PC端、H5端页面 |
| --- websocket | 可同时为多应用提供websocket接口 |
| **hack** | 存放项目开发工具、脚本等内容例如,CLI工具的配置,各种shell/bat脚本等文件 |
| **internal** | 业务逻辑存放目录通过Golang internal特性对外部隐藏可见性 |
| --- cmd | 命令行管理目录可以管理维护多个命令行 |
| --- consts | 项目内主要的常量定义 |
| --- controller | 接收/解析用户输入参数的入口/接口层,也可以理解为控制器 |
| --- crons | 项目中由系统统一接管的定时任务处理 |
| --- dao | 数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的 CURD 方法 |
| --- global | 项目内主要的全局变量和系统的一些初始化操作 |
| --- library | 项目内常用功能的扩展库 |
| --- logic | 业务逻辑封装管理,特定的业务逻辑实现和封装往往是项目中最复杂的部分 |
| --- model | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 |
| --- --- do | 用于dao数据操作中业务模型与实例模型转换,由工具维护,用户不能修改 |
| --- --- entity | 与数据集合绑定的程序数据结构定义,通常和数据表一一对应 |
| --- --- input | 对内接口。用于controller调用service或service之间调用时的输入/输出结构定义,以及输入过滤和预处理 |
| --- packed | 将静态资源打包进可执行文件,无需单独部署 |
| --- queues | 为项目内所有的消息队列的消费者提供统一的初始化和处理 |
| --- router | 注册对外接口和分组中间件 |
| --- service | 用于业务模块解耦的接口定义层具体的接口实现在logic中进行注入 |
| **manifest** | 包含程序编译、部署、运行、配置的文件常见内容如下: |
| --- config | 配置文件存放目录 |
| --- docker | Docker镜像相关依赖文件,脚本文件等等 |
| --- deploy | 部署相关的文件默认提供了Kubernetes集群化部署的Yaml模板,通过kustomize管理 |
| **resource** | 静态资源文件。这些文件往往可以通过 资源打包/镜像编译 的形式注入到发布文件中 |
| **storage** | 本地数据存储目录,例如文件缓存、磁盘队列数据、sql数据文件、SSL证书等 |
| **utility** | 一些常用的工具方法 |
| go.mod | 使用Go Module包管理的依赖描述文件 |
| main.go | 程序入口文件 |
| Makefile | 程序构建发布和开发快捷指令 |
| README.md | 项目介绍文件 |
#### web前端
```
/web
├── build # 打包脚本相关
│ ├── config # 配置文件
│ ├── generate # 生成器
│ ├── script # 脚本
│ └── vite # vite配置
├── mock # mock文件夹
├── public # 公共静态资源目录
├── src # 主目录
│ ├── api # 接口文件
│ ├── assets # 资源文件
│ │ ├── icons # icon sprite 图标文件夹
│ │ ├── images # 项目存放图片的文件夹
│ │ └── svg # 项目存放svg图片的文件夹
│ ├── components # 公共组件
│ ├── design # 样式文件
│ ├── directives # 指令
│ ├── enums # 枚举/常量
│ ├── hooks # hook
│ │ ├── component # 组件相关hook
│ │ ├── core # 基础hook
│ │ ├── event # 事件相关hook
│ │ ├── setting # 配置相关hook
│ │ └── web # web相关hook
│ ├── layouts # 布局文件
│ │ ├── default # 默认布局
│ │ ├── iframe # iframe布局
│ │ └── page # 页面布局
│ ├── locales # 多语言
│ ├── logics # 逻辑
│ ├── main.ts # 主入口
│ ├── router # 路由配置
│ ├── settings # 项目配置
│ │ ├── componentSetting.ts # 组件配置
│ │ ├── designSetting.ts # 样式配置
│ │ ├── encryptionSetting.ts # 加密配置
│ │ ├── localeSetting.ts # 多语言配置
│ │ ├── projectSetting.ts # 项目配置
│ │ └── siteSetting.ts # 站点配置
│ ├── store # 数据仓库
│ ├── utils # 工具类
│ └── views # 页面
├── types # 类型文件
├── vite.config.ts # vite配置文件
└── windi.config.ts # windcss配置文件
```
#### uniapp端
```
// 待开放
```
+379
View File
@@ -0,0 +1,379 @@
## 代码生成
目录
- 使用条件
- 模板配置
- CLI配置
- 生成CRUD表格
- 生成树形表格
- 多数据库使用
- 自定义生成模板
- 内置gf-cli
- 指定gf-cli版本
- 指定数据库驱动
> 在HotGo中可以通过后台开发工具快速的一键生成CRUD,自动生成Api、控制器、业务逻辑、Web页面、表单组件、菜单权限等。
### 使用条件
- hotgo 版本号 >= 2.13.1
- 使用前必须先看 [数据库](sys-db.md)
### 生成模板配置
- 配置路径:server/manifest/config/config.yaml
```yaml
# 生成代码
hggen:
allowedIPs: [ "127.0.0.1", "*" ] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能
selectDbs: [ "default" ] # 可选生成表的数据库配置名称,支持多库
disableTables: [ "hg_sys_gen_codes","hg_admin_role_casbin" ] # 禁用的表,禁用以后将不会在选择表中看到
delimiters: [ "@{", "}" ] # 模板引擎变量分隔符号
# 生成应用模型,所有生成模板允许自定义,可以参考default模板进行改造
application:
# CRUD和关系树列表模板
crud:
templates:
# 默认的主包模板
- group: "default" # 分组名称
isAddon: false # 是否为插件模板 false|true
masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联
templatePath: "./resource/generate/default/curd" # 模板路径
apiPath: "./api/admin" # goApi生成路径
controllerPath: "./internal/controller/admin/sys" # 控制器生成路径
logicPath: "./internal/logic/sys" # 主要业务生成路径
inputPath: "./internal/model/input/sysin" # 表单过滤器生成路径
routerPath: "./internal/router/genrouter" # 生成路由表路径
sqlPath: "./storage/data/generate" # 生成sql语句路径
webApiPath: "../web/src/api" # webApi生成路径
webViewsPath: "../web/src/views" # web页面生成路径
# 默认的插件包模板,{$name}会自动替换成实际的插件名称
- group: "addon" # 分组名称
isAddon: true # 是否为插件模板 false|true
masterPackage: "sys" # 主包名称,需和controllerPath、logicPath、inputPath保持关联
templatePath: "./resource/generate/default/curd" # 模板路径
apiPath: "./addons/{$name}/api/admin" # goApi生成路径
controllerPath: "./addons/{$name}/controller/admin/sys" # 控制器生成路径
logicPath: "./addons/{$name}/logic/sys" # 主要业务生成路径
inputPath: "./addons/{$name}/model/input/sysin" # 表单过滤器生成路径
routerPath: "./addons/{$name}/router/genrouter" # 生成路由表路径
sqlPath: "./storage/data/generate/addons" # 生成sql语句路径
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
# 消息队列模板
queue:
templates:
- group: "default"
templatePath: "./resource/generate/default/queue"
# 定时任务模板
cron:
templates:
- group: "default"
templatePath: "./resource/generate/default/cron"
# 生成插件模块,通过后台创建新插件时使用的模板,允许自定义,可以参考default模板进行改造
addon:
srcPath: "./resource/generate/default/addon" # 生成模板路径
webApiPath: "../web/src/api/addons/{$name}" # webApi生成路径
webViewsPath: "../web/src/views/addons/{$name}" # web页面生成路径
```
### 生成CLI配置
- hotgo在生成dao、service配置时,默认了和gf官方一致的配置方式和代码生成规则。所以无论你是通过hotgo亦或gf命令生成,最终代码格式完全一致,遵循一致的代码规范。
- 配置路径:[server/hack/config.yaml](../../server/hack/config.yaml)
```yaml
gfcli:
build:
name: "hotgo" # 编译后的可执行文件名称
# arch: "all" #不填默认当前系统架构,可选:386,amd64,arm,all
# system: "all" #不填默认当前系统平台,可选:linux,darwin,windows,all
mod: "none"
cgo: 0
packSrc: "resource" # 将resource目录打包进可执行文件,静态资源无需单独部署
packDst: "internal/packed/packed.go" # 打包后生成的Go文件路径,一般使用相对路径指定到本项目目录中
version: ""
output: "./temp/hotgo" # 可执行文件生成路径
extra: ""
gen:
dao:
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
group: "default" # 分组 使用hotgo代码生成功能时必须填
# tables: "" # 指定当前数据库中需要执行代码生成的数据表。如果为空,表示数据库的所有表都会生成。
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
removePrefix: "hg_"
descriptionTag: true
noModelComment: true
jsonCase: "CamelLower"
gJsonSupport: true
clear: true
service: # 生成业务配置
srcFolder: "internal/logic"
dstFolder: "internal/service"
dstFileNameCase: "CamelLower"
clear: true
```
### 生成CRUD表格
- 推荐使用热编译方式启动HotGo,这样生成完成页面自动刷新即可看到新生成内容,无需手动重启
- 服务端热编译启动:`gf run main.go`, web前端启动:`pnpm run dev` 或 `npm run dev`
1、创建数据表
1.1 创建测试表格表`hg_test_table`
```mysql
CREATE TABLE `hg_test_table` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`category_id` bigint(20) NOT NULL COMMENT '分类ID',
`title` varchar(64) NOT NULL COMMENT '标题',
`description` varchar(255) NOT NULL COMMENT '描述',
`content` text NOT NULL COMMENT '内容',
`image` varchar(255) DEFAULT NULL COMMENT '单图',
`attachfile` varchar(255) DEFAULT NULL COMMENT '附件',
`city_id` bigint(20) DEFAULT '0' COMMENT '所在城市',
`switch` int(11) DEFAULT '1' COMMENT '显示开关',
`sort` int(11) NOT NULL COMMENT '排序',
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
`created_by` bigint(20) DEFAULT '0' COMMENT '创建者',
`updated_by` bigint(20) DEFAULT '0' COMMENT '更新者',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='测试表格';
```
1.2 测试分类表`hg_test_category`:
- 注意:该表为了方便功能演示已经内置无需再次创建,此处提示表结构只是为了方便大家梳理关联表关系
```mysql
CREATE TABLE `hg_test_category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`name` varchar(255) NOT NULL COMMENT '分类名称',
`description` varchar(255) DEFAULT NULL COMMENT '描述',
`sort` int(11) NOT NULL COMMENT '排序',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`status` tinyint(1) DEFAULT '1' COMMENT '状态',
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '修改时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='测试分类';
```
1.3 插入测试数据
```mysql
INSERT INTO `hg_test_table` (`id`, `category_id`, `title`, `description`, `content`, `image`, `attachfile`, `city_id`, `switch`, `sort`, `status`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 1, '测试标题', '描述', '<h2><strong>不知道写点啥!</strong></h2><p><br></p><iframe class=\"ql-video\" frameborder=\"0\" allowfullscreen=\"true\" src=\"https://media.w3.org/2010/05/sintel/trailer.mp4\"></iframe><p><br></p><p><img src=\"https://gmycos.facms.cn/hotgo/attachment/2023-02-09/cqdq9iuv0phsg8patk.png\"></p>', 'https://gmycos.facms.cn/hotgo/logo.sig.png', 'https://gmycos.facms.cn/hotgo/attachment/2022-12-30/cpf1x44idoycrtajf2.xlsx', 110102, 1, 10, 1, 0, 1, '2022-12-15 19:30:14', '2023-02-23 13:55:32', NULL);
```
2、创建生成配置
- 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,选择和填写如下参数:
![生成添加演示图](images/sys-code-add.png)
3、自定义配置
- 确认配置无误后,点击生成配置会自动跳转到生成配置页面,如下:
![生成配置页面](images/sys-code-config-init.png)
- 你可以在该页面点击`预览代码`查看生成的内容,如果无需调整亦可以直接点击`提交生成`,以下是预览代码效果:
![生成预览](images/sys-code-preview.png)
- 如果你对默认的生成配置不满意,可以根据页面表单提示,自定义表格属性和字段属性
- 这里假设我们要一个关联表,让表`hg_test_table`字段`category_id`去关联 表`hg_test_category`字段`id`,我们可以这样做:
![生成关联配置](images/sys-code-config-join.png)
- 如果你想调整主表每列字段的显示方式,可以点击上方导航栏中的 [主表字段] 进行调整
- 这里我们不做任何调整直接进入下一步。目的是为了后续演示对最终生成结果不满意的再次调整方案
![生成关联配置](images/sys-code-master.png)
4、以上内容都已配置无误后,点击提交生成即可。
- 如果你使用的热编译,那么页面会在生成成功后立即刷新,刷新完成你即可在后台菜单栏中看到`测试表格`菜单。如果不是使用热编译启动,请手动重启服务后刷新。
- 注意:热编译环境下,web端往往会快于服务端重启并加载完成,此时访问新生成的菜单页面可能会存在某接口请求超时或404问题,这是服务端正在重启导致的,属于正常现象,一般稍等几秒再试即可。
- 接下来让我们看看生成的表格页面,效果如下:
![生成测试表格页面](images/sys-code-list.png)
5、假设我们对生成结果不满意,有新的优化需求如下:
1. 把`单图`、`附件`改换成上传组件
2. 把`创建者`、`更新者`隐藏
3. 把`分类ID`隐藏,然后把关联表中的`分类名称`显示出来
> 那么我们可以回到 开发工具 -> 代码生成 -> 找到刚刚生成的记录 -> 在右侧操作栏中点击生成配置,再次进入配置页面做如下操作即可:
1. 点击上方导航栏[主表字段],调整字段选项:
![修改主表配置](images/sys-code-master-save.png)
2. 点击上方导航栏[关联表],调整字段选项:
![修改关联表配置](images/sys-code-config-join-save.png)
3. 返回[基本信息],勾选强制覆盖,然后点击`预览代码`,确认生成代码无误后点击`提交生成`
![强制覆盖并提交生成](images/sys-code-config-post.png)
- 生成完成刷新页面后,再次来到`测试表格`,发现表格已经调整到了我们预期效果
1. 列表效果:
![最终列表效果](images/sys-code-list-ok.png)
2. 编辑表单效果
![最终编辑表单效果](images/sys-code-list-edit-ok.png)
- 至此生成增删改查列表示例结束!
### 生成树形表格
待写。
### 多数据库使用
- 假设我们要增加一个库名为`hotgo2`、分组为`default2`的数据库,并要为其生成代码
1. 配置[server/hack/config.yaml](../../server/hack/config.yaml) 如下:
```yaml
gen:
dao:
- link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
group: "default" # 分组 使用hotgo代码生成功能时必须填
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
removePrefix: "hg_"
descriptionTag: true
noModelComment: true
jsonCase: "CamelLower"
gJsonSupport: true
clear: false
- link: "mysql:hotgo2:hg123456.@tcp(127.0.0.1:3306)/hotgo2?loc=Local&parseTime=true"
group: "default2" # 分组 使用hotgo代码生成功能时必须填
tablesEx: "hg_sys_addons_install" # 指定当前数据库中需要排除代码生成的数据表。
removePrefix: ""
descriptionTag: true
noModelComment: true
jsonCase: "CamelLower"
gJsonSupport: true
clear: false
```
2. 配置`server/manifest/config/config.yaml`,
`database`配置如下:
```yaml
database:
logger:
level: "all"
stdout: true
default:
link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:3306)/hotgo?loc=Local&parseTime=true"
# - link: "pgsql:postgres:hg123456@tcp(127.0.0.1:5432)/hotgo"
debug: true
Prefix: "hg_"
default2:
link: "mysql:hotgo2:hg123456.@tcp(127.0.0.1:3306)/hotgo2?loc=Local&parseTime=true"
debug: true
Prefix: ""
```
`hggen`配置如下:
```yaml
hggen:
allowedIPs: ["127.0.0.1", "*"] # 白名单,*代表所有,只有允许的IP后台才能使用生成代码功能
selectDbs: [ "default", "default2" ] # 可选生成表的数据库配置名称,支持多库
disableTables : ["hg_sys_gen_codes","hg_admin_role_casbin"] # 禁用的表,禁用以后将不会在选择表中看到
delimiters: ["@{", "}"] # 模板引擎变量分隔符号
```
3. 登录HotGo后台 -> 开发工具 -> 代码生成 -> 找到立即生成按钮并打开,就会发现`数据库`选项增加了一个`default2`,后续生成步骤和生成例子完全一样
> 注意:上述的配置中所有的`default2`名称必须保持一致
### 自定义生成模板
> 系统内置了两组CURD生成模板,请参考[生成模板配置](sys-code.md#生成模板配置)。default:是默认的生成到主模块下;addon:是默认生成到指定的插件下
- HotGo允许你新建新的模板分组来满足你的需求,模板可根据现有模板基础拷贝一份出来做改造,默认模板目录:[server/resource/generate/default](../../server/resource/generate/default)
### 内置gf-cli
> 为了确保生成代码的依赖稳定性,在面对`gf`版本更新可能导致向下不兼容情况时,HotGo将`gf-cli`工具内置到系统中并进行在线执行调整,从而提供更可靠和一致的生成代码功能。
- 后续我们也将开放在线运行`gf gen ...`功能。在做插件开发时也会支持到在线生成插件下的service接口,这将会使得插件开发更加方便
### 指定gf-cli版本
> HotGo多数情况下会和最新版本的gf-cli保持同步,如果更新不及时或你不想使用最新版本的gf-cli来生成代码,可以找到自己想要的版本进行替换即可。
- 下面大致做一些替换步骤说明:
1. 打开https://github.com/gogf/gf,找到你想要使用的版本`clone`下来
2. 将`clone`代码中`gf/cmd/gf/internal/`目录覆盖到`server/internal/library/hggen/internal`
3. 将覆盖过来的目录文件中引入包名`github.com/gogf/gf/cmd/gf/v2/`批量改为`hotgo/internal/library/hggen/`
4. 运行`go mod tidy`
5. 运行`go run main.go`,如果没有报错,那么恭喜你已经完成了。如果有报错一般都是版本差异带来的影响,需要根据情况自行调整
### 指定数据库驱动
> HotGo默认使用mysql驱动,如果你想用其他数据库驱动打开下方文件中注释即可,目前已支持mysql、pgsql
修改文件路径:server/internal/library/hggen/internal/cmd/cmd_gen_dao.go
```go
package cmd
import (
//_ "github.com/gogf/gf/contrib/drivers/clickhouse/v2"
//_ "github.com/gogf/gf/contrib/drivers/mssql/v2"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
//_ "github.com/gogf/gf/contrib/drivers/oracle/v2"
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2"
//_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"hotgo/internal/library/hggen/internal/cmd/gendao"
)
type (
cGenDao = gendao.CGenDao
)
```
修改完成后运行`go mod tidy`
+75
View File
@@ -0,0 +1,75 @@
## 控制台
目录
- 启动所有服务
- HTTP服务
- 消息队列
- 定时任务
- 常用工具
- Makefile
### 启动所有服务
- 仅推荐在开发期间快速调试使用,线上实际部署时建议将各个服务分开部署,这样重新部署某个服务时无需全部重启。
```shell
# 默认
go run main.go
# 通过热编译启动
gf run main.go
```
### HTTP服务
- 启动HTTP服务,包含websocket。
```shell
# 默认
go run main.go http
# 通过热编译启动
gf run main.go --args "http"
```
### 消息队列
- 启动消息队列的消费者。
```shell
# 默认
go run main.go queue
# 通过热编译启动
gf run main.go --args "queue"
```
### 定时任务
- 启动系统中统一注册的定时任务。
```shell
# 默认
go run main.go cron
# 通过热编译启动
gf run main.go --args "cron"
```
### 常用工具
- 释放casbin权限,用于清理无效的权限设置。
```shell
go run main.go tools -m=casbin -a1=refresh
```
### Makefile
- 通过make提供一些快捷命令
```shell
# 一键编译,打包前后端代码到可执行文件
make build
# 更多请查看 /server/Makefile文件
```
+70
View File
@@ -0,0 +1,70 @@
## 定时任务
目录
- 实现接口
- 一个例子
- 更多
> 在实际的项目开发中,定时任务几乎成为不可或缺的一部分。HotGo为定时任务提供一个方便的后台操作界面,让您能够轻松地进行在线启停、修改和立即执行等操作。这样的设计可以极大地改善您在使用定时任务过程中的体验,让整个过程更加顺畅、高效。
### 实现接口
- 为了提供高度的扩展性,定时任务在设计上采用了接口化的思路。只需要实现以下接口,您就可以在任何地方注册和使用定时任务功能,从而实现更大的灵活性和可扩展性。
```go
// Cron 定时任务接口
type Cron interface {
// GetName 获取任务名称
GetName() string
// Execute 执行任务
Execute(ctx context.Context, parser *Parser) (err error)
}
```
### 一个例子
定时任务的文件结构可以根据具体需要进行调整,以下是一个常见的参考结构:
- 文件路径:server/internal/crons/test.go
```go
package crons
import (
"context"
"hotgo/internal/library/cron"
"time"
)
func init() {
cron.Register(Test)
}
// Test 测试任务(无参数)
var Test = &cTest{name: "test"}
type cTest struct {
name string
}
func (c *cTest) GetName() string {
return c.name
}
// Execute 执行任务
func (c *cTest) Execute(ctx context.Context, parser *cron.Parser) (err error) {
parser.Logger.Infof(ctx, "cron test Execute:%v", time.Now()) // 记录任务调度日志
return
}
```
继续在后台系统设置-定时任务-添加任务,填写的任务名称需要和上面的名称保持一致,再进行简单的策略配置以后,一个后台可控的定时任务就添加好了!
### 更多
定时任务源码路径:server/internal/library/cron/cron.go
更多文档请参考:https://goframe.org/pages/viewpage.action?pageId=1114187
+150
View File
@@ -0,0 +1,150 @@
## 数据库
目录
- 字段类型
- 特殊字段默认表单组件
- 特殊字段默认表单验证器
- SQL默认查询方式
- 其他
### 字段类型
- 创建数据库表当按如下的规则进行字段命名、类型、属性设置和备注后,再生成CRUD代码时会自动生成对应的Api、控制器、业务逻辑、Web页面、[表单组件](web-form.md)等的一些默认属性
- 当你了解这些默认技巧后,会有效提高你在实际开发中的生产效率
| 数据库类型 | 额外属性 | 转换Go类型 | 转换Ts类型 | 表单组件 |
|---------------------------------------------------------------|--------------|--------------|---------|-----------------------|
| int, tinyint,small_int,smallint,medium_int,mediumint,serial | / | int | number | InputNumber(数字输入框) |
| int, tinyint,small_int,smallint,medium_int,mediumint,serial | unsigned | uint | number | InputNumber(数字输入框) |
| big_int,bigint,bigserial | / | int64 | number | InputNumber(数字输入框) |
| big_int,bigint,bigserial | unsigned | uint64 | number | InputNumber(数字输入框) |
| real | / | float32 | number | InputNumber(数字输入框) |
| float,double,decimal,money,numeric,smallmoney | / | float64 | number | InputNumber(数字输入框) |
| bit(1) 、bit(true)、bit(false) | / | bool | boolean | Input(文本输入框,默认) |
| bit | / | int64-bytes | array | InputDynamic(动态KV表单) |
| bit | unsigned | uint64-bytes | array | InputDynamic (动态KV表单) |
| bool | / | bool | boolean | Input(文本输入框,默认) |
| date | / | *gtime.Time | string | Date(日期选择器) |
| datetime,timestamp,timestamptz | / | *gtime.Time | string | Time(时间选择器) |
| json | / | *gjson.Json | string | Input(文本输入框) |
| jsonb | / | *gjson.Json | string | Input(文本输入框) |
| 以下为物理类型中包含字段部分时的转换方式,默认情况 | / | / | / | / |
| text,char,character | / | string | string | Input(文本输入框) |
| float,double,numeric | / | string | string | Input(文本输入框) |
| bool | / | bool | boolean | Input(文本输入框,默认) |
| binary,blob | / | []byte | string | Input(文本输入框,默认) |
| int | / | int | number | InputNumber(数字输入框) |
| int | unsigned | int | number | InputNumber(数字输入框) |
| time | / | *gtime.Time | string | Time(时间选择器) |
| date | / | *gtime.Time | string | Date(日期选择器) |
| 没有满足以上任何条件的 | / | string | string | Input(文本输入框) |
### 特殊字段默认表单组件
- 以下字段在不设置表单组件时会默认使用的表单组件
| 数据库字段 | 字段名称 | 表单组件 |
|--------------|----------------------|----------------------|
| status | 状态字段(任意int类型) | Select (单选下拉框) |
| created_at | 创建时间字段 | TimeRange (时间范围选择) |
| province_id | 省份ID字段(任意int类型) | CitySelector (省市区选择) |
| city_id | 城市ID字段(任意int类型) | CitySelector (省市区选择) |
| 任意字串符字段 | 长度>= 200 and <= 500 | InputTextarea (文本域) |
| 任意字串符字段 | 长度> 500 | InputEditor (富文本) |
### 特殊字段默认表单验证器
- 以下字段在不设置表单组件时会默认使用的表单验证器
| 数据库字段/Go类型 | 字段名称 | 表单验证规则 |
|-------------------|--------|-----------------------|
| mobile | 手机号 | 不为空时必须是手机号码(国内) |
| qq | QQ | 不为空时必须是QQ号码 |
| email | 邮箱地址 | 不为空时必须是邮箱格式 |
| id_card | 身份证号码 | 不为空时必须是15或18位身份证号码 |
| bank_card | 银行卡号码 | 银行卡号码 |
| password | 密码 | 密码验证,必须包含6-18为字母和数字 |
| price | 价格 | 金额验证,最多允许输入10位整数及2位小数 |
| Go类型为uint、uint64 | 正整数 | 非零正整数验证 |
### SQL默认查询方式
- Go类型取决于数据库物理类型,请参考 [字段类型] 部分
| Go类型 | 查询方式 |
|-------------------------|--------------------------------------|
| string | LIKE |
| date,datetime | = |
| int,uint,int64,uint64 | = |
| []int,[]int64,[]uint64 | IN (...) |
| float32,float64 | = |
| []byte4 | =(默认) |
| time.Time,*gtime.Time | = |
| *gjson.Json | JSON_CONTAINS(json_doc, val[, path]) |
### 其他
#### 默认字典选项
- 数据库字段为 `status`且类型为任意数字类型的会使用系统默认的状态字典
#### 默认属性
- 默认必填,当数据库字段存在非空`IS_NULLABLE`属性时,默认勾选必填验证
- 默认唯一,当数据库字段索引存在`UNI`时,默认勾选唯一值验证
- 默认主键,当数据库字段索引存在`PRI`时,默认为主键,不允许编辑
- 默认排序,当数据库字段存在`sort`时,默认开启排序,添加表单自动获取最大排序增量值并填充表单
- 默认列名,默认使用字段注释作为表格的列名。当数据库字段未设置注释时,默认使用字段名称作为列名
#### 自动更新/插入
- 自动更新,当数据库字段为`updated_at`(更新时间),`updated_by`(更新者)
- 自动插入,当数据库字段为`created_at`(创建时间),`created_by`(创建者)
- 软删除,表存在字段`deleted_at`时,使用表的Orm模型查询条件将会自动加入[ `deleted_at` IS NULL ],删除时只更新删除时间而不会真的删除数据
- 树表:不论更新插入,都会根据表中字段`pid`(上级ID)自动维护`level`(树等级)和`tree`(关系树)
#### 操作人字段维护
- 生成列表中存在并且勾选展示字段`created_by`(创建者)、`updated_by`(修改者)、`deleted_by`(删除者)时,会自动到表`hg_admin_member`中获取操作人的基本信息摘要,并渲染到列表中,效果如下:
![](images/sys-db-by.png)
- 生成列表中存在并且勾选查询字段`created_by`(创建者)、`updated_by`(修改者)、`deleted_by`(删除者)时,会强制将查询表单改为关键词查询,从`hg_admin_member`查询操作人。效果如下:
![](images/sys-db-by2.png)
- 查询代码片段,参考路径:[server/internal/logic/admin/member.go](../../server/internal/logic/admin/member.go)
```go
// 查询创建者
if in.CreatedBy != "" {
ids, err := service.AdminMember().GetIdsByKeyword(ctx, in.CreatedBy)
if err != nil {
return nil, 0, err
}
mod = mod.WhereIn(dao.SysGenCurdDemo.Columns().CreatedBy, ids)
}
// GetIdsByKeyword 根据关键词查找符合条件的用户ID
func (s *sAdminMember) GetIdsByKeyword(ctx context.Context, ks string) (res []int64, err error) {
ks = gstr.Trim(ks)
if len(ks) == 0 {
return
}
array, err := dao.AdminMember.Ctx(ctx).Fields("id").
Where("`id` = ? or `real_name` = ? or `username` = ? or `mobile` = ?", ks, ks, ks, ks).
Array()
if err != nil {
err = gerror.Wrap(err, "根据关键词获取用户ID失败,请稍后重试!")
}
res = gvar.New(array).Int64s()
return
}
```
> 这里只列举了较为常用的默认规则,其他更多默认规则请参考:[server/internal/library/hggen/views/column_default.go](../../server/internal/library/hggen/views/column_default.go)
+201
View File
@@ -0,0 +1,201 @@
## 开发规范
目录
- 规范说明
- 规范建议
- 命名规范
- 编码规范
- 框架规范
- 插件规范
- 数据库规范
- 包功能规范
### 规范说明
- 开发规范意义是是让大家尽可能写出风格统一的代码,让团队代码有更好的可读性与可维护性。
- 在实际业务开发中,除了要提高业务开发效率,保证线上业务高性能外,好的编程习惯也是一个开发人员基本素养之一。
### 规范建议
- 在我们实际开发中,有很多开发人可能是由某一语言转到另外一个语言领域,在转到另外一门语言后, 我们都会保留着对旧语言的编程习惯,在这里,我建议的是,虽然不同语言之前的某些规范可能是相通的, 但是我们最好能够按照官方的一些demo来熟悉是渐渐适应当前语言的编程规范,而不是直接将原来语言的编程规范也随之迁移过来。
### 命名规范
#### 命名准则
* 当变量名称在定义和最后一次使用之间的距离很短时,简短的名称看起来会更好
* 变量命名应尽量描述其内容,而不是类型
* 常量命名应尽量描述其值,而不是如何使用这个值
* 在遇到for,if等循环或分支时,推荐单个字母命名来标识参数和返回值
* package名称也是命名的一部分,推荐全部使用小写,请尽量将其利用起来
* 使用一致的命名风格
#### 文件命名规范
* 全部小写
* 除unit test外避免下划线(_)
* 文件名称不宜过长,但不必为了简短而忽略含义
#### 变量命名规范参考
* 驼峰命名
* 见名知义,避免拼音替代英文
* 不建议包含下划线(_)
* 不建议包含数字
#### 函数、常量命名规范
* 驼峰式命名
* 可导出的必须首字母大写
* 不可导出的必须首字母小写
* 避免全部大写与下划线(_)组合
#### 参考文档
* [Practical Go: Real world advice for writing maintainable Go programs](https://dave.cheney.net/practical-go/presentations/gophercon-singapore-2019.html#_simplicity)
### 编码规范
#### import
* 单行import不建议用圆括号包裹
* 按照`官方包`,NEW LINE,`当前工程包`,NEW LINE,`第三方依赖包`顺序引入
```go
import (
"context"
"string"
"greet/user/internal/config"
"google.golang.org/grpc"
)
```
#### 函数返回
* 对象避免非指针返回
* 遵循有正常值返回则一定无error,有error则一定无正常值返回的原则
#### 错误处理
* 有error必须处理,如果不能处理就必须抛出。
* 避免下划线(_)接收error
#### 函数体编码
* 建议一个block结束空一行,如if、for等
```go
func main (){
if x==1{
// do something
}
fmt.println("xxx")
}
```
* return前尽可能空一行
```go
func getUser(id string)(string,error){
....
return "xx",nil
}
```
### 框架规范
- 项目启动初始化方法都放在`server/internal/global/init.go`,插件放在:`server/addons/xxx插件/global/init.go`
- Api 对外接口层 都放在`server/api`,根据实际应用和应用具体功能划分子目录,插件放在:`server/addons/xxx插件/api`
- Router 路由注册方式应尽可能按gf2.0版本风格定义,参考:https://goframe.org/pages/viewpage.action?pageId=30736904
- Controller 控制器层 都放在`server/internal/controller`,根据应用和具体功能拆分子目录,插件放在:`server/addons/xxx插件/controller`
- Input 对内接口层 都放在`server/internal/model/input`,根据应用和具体功能拆分子目录并以`in`结尾,插件放在:`server/addons/xxx插件/model/input`
- Logic 业务逻辑层 统一放在`server/internal/logic` 根据实际业务需求按logic/xxx/*.go拆分,插件放在:`server/addons/xxx插件/logic`
- Orm 模型实体层 统一放在`server/internal/model/entity`
- Dao 数据访问层 统一放在`server/internal/dao`
- 自定义工具函数都放在 `server/utility`,且应根据不同类型的方法做分包,相同类型的方法放在同一个包内
- 状态枚举统一调用`server/internal/consts`中的常量和属性
### 插件规范
- 插件命名统一全部小写
- 资源文件在 `addons/xxx插件/resource`或`server/resource`
- 每个插件之前应该是完全独立的,尽可能避免插件于插件之间的调用依赖
- 插件和主模块之前的调用应尽可能通过 Service 接口来调用
- 插件业务初始化顺序下应在主模块之后,应尽可能避免使用init方法隐式初始化
### 数据库规范
#### 命名规范
- 数据表名小写,多关键字使用下划线分割(关键字尽量全称)
- 字段名小写,多关键字使用下划线分割(关键字尽量全称)
- 禁止使用保留字并且尽量少用含有关键词来命名
- 临时表必须以tmp_开头、以日期结尾,备份表必须以bak_开头、以日期结尾
- 同数据库表名设置统一前缀,默认是`hg_`
- 插件模块表名建议以`hg_addon_`开头,如:`hg_addon_hgexample_table`,含义:`插件_案例_表格`。在生成代码时可自动识别实体命名为:`Table`
#### 基础规范
- 所有的字段必须添加注释
- 尽可能地使用InnoDB作为表的存储引擎
- 所有的表和字段必须添加注释
- 尽量控制表行数在500万以内
- 尽可能采用冷热数据分离策略
- 禁止以图片、文件等二进制数据
#### 表设计规范
- 尽可能每张表的索引数量控制在5个以内
- 每一张InnoDB表都必须含有一个主键,自增主键必须是`bigint`类型
- 尽可能避免使用外键约束
- 设置数据表架构应考虑后期扩展型
- 遵循范式与冗余平衡原则
#### 字段设计规范
- 尽可能将所有的数据列定义为 `NOT NULL` 类型
- 避免 ENUM 数据类型
- json 存储的数据用 `json`字段代替 `text`
- 表与表关联的键名保持一致或以关联表名的缩写为前缀
- 固定长度的字符串字段务必使用 `char`
- 使用 `UNSIGNEG` 存储非负整数
- 禁止敏感数据以明文形式存储
- 金额相关的数据必须使用 `DECIMAL` 数据类型
- 尽量所有表有 `status` 字段来标注数据状态(1:启用,2:禁用,3:已删除),业务状态请使用其他字段;`status`字段类型 为带符号的 `tinyint(1)`。如果还需要其他的数据状态 请先判断该状态的数据是有用的数据还是无意义的数据,有用的数据状态 > 0,无意义的数据状态 < -1
- 所有的删除(除开清空回收站操作) 请标记 `status` 为 3
- 创建时间字段为`created_at`,修改时间字段为`updated_at`,删除时间字段为`deleted_at`,类型`datetime`
- 用户关联 id 字段为 `member_id` 或 `created_by`
- 排序字段为 `sort`
- 区分应用字段为 `app_id`
- 区分插件来源需要增加字段为 `addon_name` 和 `is_addon`
- 关系树表中必须包含字段:`id`(主键)、`pid`(上级ID)、`level`(树等级)、`tree`(关系树)
- 多选、多图、多文件字段类型应尽量使用类型`json`
### 包功能规范
##### Input(对内接口 - 过滤验证)
- 接收Api 对外接口数据和参数过滤
- 对提交的复杂数据参数进行验证处理返回
##### View(视图)
- 数据处理显示
- 页面渲染
##### Controller(控制器)
- 数据接收
- 服务调用
- 简单CURD入库
- 简单业务逻辑
##### Logic(业务逻辑 - 服务接口功能)
- 数据逻辑处理
- 数据入库
- 数据查询
+73
View File
@@ -0,0 +1,73 @@
## 国际化
目录
- 介绍
- 配置文件
- 服务端使用
- Web端使用
- 更多文档
### 介绍
从`v2.18.6`版本开始,HotGo 提供了完善的国际化(i18n)支持,内置简体中文、繁体中文和英文三种语言包,默认语言为简体中文。目前已对后台管理首页进行了多语言适配,开发者可基于该机制扩展实现业务模块的多语言功能。
### 配置文件
国际化配置位于 `server/manifest/config/config.yaml` 文件中:
```yaml
# hotgo系统配置
system:
# ...
# 国际化
i18n:
switch: true # 国际化功能开关,可选值:false|true,默认:true
defaultLanguage: "zh-CN" # 默认语言,可选值:zh-CN|zh-TW|en,默认:zh-CN
```
### 服务端使用
多语言配置文件存放目录:`server/manifest/i18n`,您可以在该目录下添加自定义的语言映射文件。
**使用示例:**
```go
// 设置当前上下文的语言为英文
ctx := gctx.New()
gi18n.WithLanguage(ctx, "en")
// 基础翻译
gi18n.T(ctx, "你好,美丽世界")
// 输出:Hello, Beautiful World
// 格式化翻译(支持参数替换)
gi18n.Tf(ctx, "剩余%v余额", 100)
// 输出:Remaining 100 Balance
```
### Web端使用
多语言配置文件存放目录:`web/src/locale`,您可以在该目录下添加自定义的语言映射文件。
> 提示:启用国际化功能后,后台管理界面右上角将显示语言切换选项。
**使用示例(假设当前语言为英文):**
```vue
<!-- 基础翻译 -->
<div>{{ t('你好,美丽世界') }}</div>
<!-- 输出:Hello, Beautiful World -->
<!-- 格式化翻译(支持参数替换) -->
<div>{{ t('剩余{num}余额', {num:100}) }}</div>
<!-- 输出:Remaining 100 Balance -->
```
### 更多文档
- [gi18n](https://goframe.org/docs/core/gi18n)
- [vie-i18n](https://vue-i18n.intlify.dev)
+318
View File
@@ -0,0 +1,318 @@
## 功能扩展库
目录
- 缓存驱动
- 请求上下文
- JWT
- 数据字典
- 地理定位(待写)
- 通知(待写)
### 缓存驱动
> 系统默认的缓存驱动为file,目前已支持:memory|redis|file等多种驱动。请自行选择适合你的驱动使用。
- 配置文件:server/manifest/config/config.yaml
```yaml
#缓存
cache:
adapter: "file" # 缓存驱动方式,支持:memory|redis|file,不填默认memory
fileDir: "./storage/cache" # 文件缓存路径,adapter=file时必填
```
#### 使用方式
```go
package main
import (
"hotgo/internal/library/cache"
"github.com/gogf/gf/v2/os/gctx"
)
func test() {
ctx := gctx.New()
// 添加/修改
cache.Instance().Set(ctx, "qwe", 123, 0)
// 查询
cache.Instance().Get(ctx, "qwe")
// 删除
cache.Instance().Remove(ctx, "qwe")
// 更多方法请参考:https://goframe.org/pages/viewpage.action?pageId=27755640
}
```
### 请求上下文
- 主要用于在处理HTTP和websocket请求时通过中间件将用户、应用、插件等信息绑定到上下文中,方便在做业务处理时用到这些信息
```go
package admin
import (
"fmt"
"context"
"hotgo/internal/library/contexts"
"hotgo/internal/library/addons"
)
func test(ctx context.Context) {
// 获取当前请求的所有上下文变量
var ctxModel = contexts.Get(ctx)
fmt.Printf("当前请求的所有上下文变量:%+v\n", ctxModel)
// 获取当前请求的应用模块
var module = contexts.GetModule(ctx)
fmt.Printf("当前请求的应用:%+v\n", module)
// 获取当前请求的用户信息
var member = contexts.GetUser(ctx)
fmt.Printf("当前访问用户信息:%+v\n", member)
// 获取当前请求的插件模块
fmt.Printf("当前是否为插件请求:%v", contexts.IsAddonRequest(ctx))
if contexts.IsAddonRequest(ctx) {
fmt.Printf("当前插件名称:%v", contexts.GetAddonName(ctx))
fmt.Printf("当前插件信息:%v", addons.GetModule(contexts.GetAddonName(ctx)))
}
}
```
### JWT
- 基于jwt+缓存驱动实现的用户登录令牌功能,支持自动续约,解决了jwt服务端无法退出问题和jwt令牌无法主动失效问题
#### 配置示例
```yaml
# 登录令牌
token:
secretKey: "hotgo123" # 令牌加密秘钥,考虑安全问题生产环境中请修改默认值
expires: 604800 # 令牌有效期,单位:秒。默认7天
autoRefresh: true # 是否开启自动刷新过期时间, false|true 默认为true
refreshInterval: 86400 # 刷新间隔,单位:秒。必须小于expires,否则无法触发。默认1天内只允许刷新一次
maxRefreshTimes: 30 # 最大允许刷新次数,-1不限制。默认30次
multiLogin: true # 是否允许多端登录, false|true 默认为true
```
```go
package admin
import (
"fmt"
"context"
"hotgo/internal/library/token"
"hotgo/internal/model"
)
func test(ctx context.Context) {
// 登录
user := &model.Identity{
Id: mb.Id,
Pid: mb.Pid,
DeptId: mb.DeptId,
RoleId: ro.Id,
RoleKey: ro.Key,
Username: mb.Username,
RealName: mb.RealName,
Avatar: mb.Avatar,
Email: mb.Email,
Mobile: mb.Mobile,
App: consts.AppAdmin,
LoginAt: gtime.Now(),
}
loginToken, expires, err := token.Login(ctx, user)
if err != nil {
return nil, err
}
// gf请求对象
r := *ghttp.Request
// 获取登录用户信息
user, err := token.ParseLoginUser(r)
if err != nil {
return
}
// 注销登录
err = token.Logout(r)
}
```
### 数据字典
- hotgo增加了对枚举字典和自定义方法字典的内置支持,从而在系统中经常使用的一些特定数据上做出了增强。
#### 字典数据选项
- 文件路径:server/internal/model/dict.go
```go
package model
// Option 字典数据选项
type Option struct {
Key interface{} `json:"key"`
Label string `json:"label" description:"字典标签"`
Value interface{} `json:"value" description:"字典键值"`
ValueType string `json:"valueType" description:"键值数据类型"`
Type string `json:"type" description:"字典类型"`
ListClass string `json:"listClass" description:"表格回显样式"`
}
```
#### 枚举字典
- 适用于系统开发期间内置的枚举数据,这样即维护了枚举值,又关联了数据字典
##### 一个例子
- 定义枚举值和字典数据选项,并注册字典类型
- 文件路径:server/internal/consts/credit_log.go
```go
package consts
import (
"hotgo/internal/library/dict"
"hotgo/internal/model"
)
func init() {
dict.RegisterEnums("creditType", "资金变动类型", CreditTypeOptions)
dict.RegisterEnums("creditGroup", "资金变动分组", CreditGroupOptions)
}
const (
CreditTypeBalance = "balance" // 余额
CreditTypeIntegral = "integral" // 积分
)
const (
CreditGroupDecr = "decr" // 扣款
CreditGroupIncr = "incr" // 加款
CreditGroupOpDecr = "op_decr" // 操作扣款
CreditGroupOpIncr = "op_incr" // 操作加款
CreditGroupBalanceRecharge = "balance_recharge" // 余额充值
CreditGroupBalanceRefund = "balance_refund" // 余额退款
CreditGroupApplyCash = "apply_cash" // 申请提现
)
// CreditTypeOptions 变动类型
var CreditTypeOptions = []*model.Option{
dict.GenSuccessOption(CreditTypeBalance, "余额"),
dict.GenInfoOption(CreditTypeIntegral, "积分"),
}
// CreditGroupOptions 变动分组
var CreditGroupOptions = []*model.Option{
dict.GenWarningOption(CreditGroupDecr, "扣款"),
dict.GenSuccessOption(CreditGroupIncr, "加款"),
dict.GenWarningOption(CreditGroupOpDecr, "操作扣款"),
dict.GenSuccessOption(CreditGroupOpIncr, "操作加款"),
dict.GenWarningOption(CreditGroupBalanceRefund, "余额退款"),
dict.GenSuccessOption(CreditGroupBalanceRecharge, "余额充值"),
dict.GenInfoOption(CreditGroupApplyCash, "申请提现"),
}
```
#### 自定义方法字典
- 适用于非固定选项,如数据是从某个表/文件读取或从第三方读取,数据需要进行转换时使用
##### 方法字典接口
- 文件路径:server/internal/consts/credit_log.go
```go
package dict
// FuncDict 方法字典,实现本接口即可使用内置方法字典
type FuncDict func(ctx context.Context) (res []*model.Option, err error)
```
##### 一个例子
- 定义获取字典数据方法,并注册字典类型
- 文件路径:server/internal/logic/admin/post.go
```go
package admin
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"hotgo/internal/consts"
"hotgo/internal/dao"
"hotgo/internal/library/dict"
"hotgo/internal/model"
"hotgo/internal/model/entity"
"hotgo/internal/service"
)
type sAdminPost struct{}
func NewAdminPost() *sAdminPost {
return &sAdminPost{}
}
func init() {
service.RegisterAdminPost(NewAdminPost())
dict.RegisterFunc("adminPostOption", "岗位选项", service.AdminPost().Option)
}
// Option 岗位选项
func (s *sAdminPost) Option(ctx context.Context) (opts []*model.Option, err error) {
var list []*entity.AdminPost
if err = dao.AdminPost.Ctx(ctx).OrderAsc(dao.AdminPost.Columns().Sort).Scan(&list); err != nil {
return nil, err
}
if len(list) == 0 {
opts = make([]*model.Option, 0)
return
}
for _, v := range list {
opts = append(opts, dict.GenHashOption(v.Id, v.Name))
}
return
}
```
#### 代码生成支持
- 内置的枚举字典和自定义方法字典在生成代码时可以直接进行选择,生成代码格式和系统字典管理写法一致
![最终编辑表单效果](images/sys-library-dict.png)
#### 内置字典和系统字典的区分
##### 主要区别
- 系统字典由表:`hg_sys_dict_type`和`hg_sys_dict_data`共同进行维护,使用时需通过后台到字典管理中进行添加
- 内置字典是系统开发期间在代码层面事先定义和注册好的数据选项
##### 数据格式区别
- 系统字典所有ID都是大于0的int64类型
- 内置字典ID都是小于0的int64类型。枚举字典以20000开头,如:-200001381053496;方法字典以30000开头,如:-30000892528327;开头以外数字是根据数据选项的`key`值进行哈希算法得出
### 地理定位
```go
// 待写
```
### 通知
```go
// 待写
```
+250
View File
@@ -0,0 +1,250 @@
## 中间件/拦截器
目录
- 介绍
- 全局中间件
- 鉴权中间件
- 响应中间件
- 更多
### 介绍
- 在hotgo中,中间件/拦截器主要作用于web请求的上下文预设、跨域请求处理、鉴权处理、请求拦截和请求结束后统一响应处理等。
### 全局中间件
```go
package main
import (
"hotgo/internal/service"
)
func main() {
// 初始化请求上下文,一般需要第一个进行加载,后续中间件存在依赖关系
service.Middleware().Ctx()
// 跨域中间件,自动处理跨域问题
service.Middleware().CORS()
// IP黑名单中间件,如果请求IP被后台拉黑,所有请求将被拒绝
service.Middleware().Blacklist()
// 演示系統操作限制,当开启演示模式时,所有POST请求将被拒绝
service.Middleware().DemoLimit()
// 请求输入预处理,api使用gf规范路由并且XxxReq结构体实现了validate.Filter接口即可隐式预处理
service.Middleware().PreFilter()
// HTTP响应预处理,在业务处理完成后,对响应结果进行格式化和错误过滤,将处理后的数据发送给请求方
service.Middleware().ResponseHandler()
}
```
### 鉴权中间件
```go
package main
import (
"github.com/gogf/gf/v2/frame/g"
)
func main() {
// 在鉴权中间件下的路由如果没有通过权限验证,后续请求将被拒绝
// 在hotgo中,鉴权中间件一般是配合一个业务模块下的路由组进行使用
// 目前admin、api、home、websocket模块都已接入
// 如果你需要创建一个新的模块也需要用到鉴权中间件,可以参考:server/internal/logic/middleware/admin_auth.go
// 一个简单例子
s := g.Server()
s.Group("/api", func(group *ghttp.RouterGroup) {
group.Middleware(service.Middleware().ApiAuth)
group.Bind(
member.Member, // 管理员
)
})
}
```
### 响应中间件
- 文件路径:server/internal/logic/middleware/response.go
#### 常用响应类型
- hotgo为一些常用的响应类型做了统一格式封装,例如:`application/json`、`text/xml`、`text/html`、`text/event-stream`等,默认使用`application/json`。
- 下面我们以`text/xml`为例简单演示几种使用方法:
1. 当你使用规范化路由时,可直接在XxxRes结构体的`g.Meta`中声明响应类型:
```go
type HelloReq struct {
g.Meta `path:"/hello" tags:"Hello" method:"get" summary:"You first hello api"`
Name string `json:"name" d:"hotgo" dc:"名字"`
}
type HelloRes struct {
g.Meta `mime:"text/xml" type:"string"`
Tips string `json:"tips"`
}
```
2. 在响应前设置响应头:
```go
var (
Hello = cHello{}
)
type cHello struct{}
func (c *cHello) Hello(ctx context.Context, req *user.HelloReq) (res *user.HelloRes, err error) {
r := ghttp.RequestFromCtx(ctx)
r.Response.Header().Set("Content-Type", "text/xml")
res = &user.HelloRes{
Tips: fmt.Sprintf("hello %v, this is the api for %v applications.", req.Name, simple.AppName(ctx)),
}
return
}
```
- 浏览器中访问响应内容如下:
![./images/sys-middleware-com-response.png](./images/sys-middleware-com-response.png)
#### 自定义响应
- 在实际开发中,可能需要使用自定义的响应类型,由于响应中间件是全局的,因此您需要对其进行单独处理。
- 推荐以下几种处理方案,可做参考:
1. 使用`ghttp.ExitAll()`,需要注意的是此方法会终止后续所有的http处理
```go
package main
import (
"github.com/gogf/gf/v2/net/ghttp"
)
func main() {
r := new(ghttp.Request) // 当前请求对象
// 清空响应
r.Response.ClearBuffer()
// 写入响应
r.Response.Write("自定义响应内容")
// 终止后续http处理
r.ExitAll()
}
```
2. 在`server/internal/logic/middleware/response.go`中根据请求的独有特征进行单独的处理,兼容后续http处理。
#### 重写响应错误提示
- 在实际开发中,我们可能想要隐藏一些敏感错误,返回给客户端友好的错误提示,但开发者同时又想需要看到真实的敏感错误。对此hotgo已经进行了过滤处理,下面是一个简单的例子:
```go
package main
import (
"github.com/gogf/gf/v2/errors/gerror"
)
func test() error {
err = gerror.New("这是一个sql执行错误")
err = gerror.Wrap(err, "用户创建失败,请稍后重试!~")
return err
}
```
- 开启debug时的客户端响应:
```json
{
"code": -1,
"message": "用户创建失败,请稍后重试!~",
"error": [
"1. 用户创建失败,请稍后重试!~",
" 1). hotgo/internal/logic/admin.(*sAdminMember).List",
" E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/admin/member.go:526",
"2. 这是一个sql执行错误", " 1). hotgo/internal/logic/admin.(*sAdminMember).List",
" E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/admin/member.go:525",
" 2). hotgo/internal/controller/admin/admin.(*cMember).List",
" E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/controller/admin/admin/member.go:157", ""
],
"timestamp": 1684145107,
"traceID": "084022730d495f17f19e550140f3e1a8"
}
```
- 关闭debug时的客户端响应:
```json
{
"code": -1,
"message": "用户创建失败,请稍后重试!~",
"timestamp": 1684145107,
"traceID": "084022730d495f17f19e550140f3e1a8"
}
```
- 控制台的输出日志:
```shell
2023-05-15 18:05:07.776 {084022730d495f17f19e550140f3e1a8} 200 "GET http localhost:8000 /admin/member/list?page=1&pageSize=10&roleId=-1 HTTP/1.1" 0.002, 127.0.0.1, "http://192.168.0.207:8001/login", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36 Co
re/1.94.197.400 QQBrowser/11.7.5287.400", -1, "", ""
Stack:
1. 用户创建失败,请稍后重试!~
1). hotgo/internal/logic/admin.(*sAdminMember).List
E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/admin/member.go:526
2. 这是一个sql执行错误
1). hotgo/internal/logic/admin.(*sAdminMember).List
E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/middleware/response.go:24
13). hotgo/internal/logic/middleware.(*sMiddleware).DemoLimit
E:/Users/Administrator/Desktop/gosrc/hotgo_dev/server/internal/logic/middleware/init.go:90
```
- 如果你开启了访问日志,那么日志记录中会详细记录本次请求的相关信息,内容如下:
![./images/sys-middleware-error-log.png](./images/sys-middleware-error-log.png)
#### 重写错误码
- hotgo默认使用了gf内置的错误码进行业务处理,通常情况下成功状态码为`0`,失败状态码为`-1`
- 查看gf内置错误码:https://goframe.org/pages/viewpage.action?pageId=30739587
- 以下是自定义错误码的简单例子:
```go
package main
import (
"github.com/gogf/gf/v2/errors/gerror"
)
func test() error {
// 使用自定义状态码30001响应客户端
err = gerror.NewCode(gcode.New(30001, "用户创建失败,请稍后重试!~", nil))
return err
}
```
- 客户端响应如下:
```json
{
"code": 30001,
"message": "用户创建失败,请稍后重试!~",
"timestamp": 1684146313,
"traceID": "b4f90e16264a5f17cd3fc27141aba448"
}
```
### 更多
- 更多关于中间件/拦截器的介绍请参考:https://goframe.org/pages/viewpage.action?pageId=55289881
+102
View File
@@ -0,0 +1,102 @@
## 支付网关
目录
- 介绍
- 一个简单的支付流程
- 支付配置
- 创建支付订单
- 注册支付回调
- 订单退款
- 单笔转账(待更新)
- 其他
### 介绍
> 在web应用开发中,支付功能可以做为不可或缺的一部分,HotGo提供了相对通用的支付网关,目前已支持支付宝、微信支付、QQ支付,包含创建支付订单、支付回调、订单退款等功能,开发者几乎只需关注订单业务处理即可。
#### 一个简单的支付流程
```mermaid
graph TD
A(用户下单) --> B(创建订单<br> 创建成功后,将订单支付信息提交到支付网关处理)
B -->|网关返回失败| H(提示错误)
B -->|网关返回成功| G(拿到支付链接,用户侧跳转支付页面)
G -->|支付完成| C(支付平台通知支付网关<br> 支付网关进行验签和效验支付处理状态) -->|验证通过,回调订单业务| E(订单更新状态,发放业务产品)
G -->|支付取消| D(订单支付超时关闭)
```
### 支付配置
> 请到 系统设置 -> 配置管理 -> 支付配置 -> 找到你需要的支付方式,进行配置即可
### 创建支付订单
```go
package main
func main() {
// 创建支付网关订单
create, err := service.Pay().Create(ctx, payin.PayCreateInp{
Subject: "充值100元",
OrderSn: "唯一业务订单编号",
OrderGroup: "admin_order", // 订单分组,用于订单分类和绑定支付成功的回调方法
PayType: "wxpay", // 微信支付
TradeType: "scan", // 二维码扫码
PayAmount: "100", // 100元
})
}
```
### 注册支付回调
- 在文件`server/internal/logic/pay/notify.go` 加入你的业务订单分组回调方法,当订单支付成功验签通过后会自动进行回调,参考以下:
```go
package global
import (
"hotgo/internal/consts"
"hotgo/internal/library/payment"
"hotgo/internal/service"
)
// RegisterNotifyCall 注册支付成功回调方法
func (s *sPay) RegisterNotifyCall() {
payment.RegisterNotifyCallMap(map[string]payment.NotifyCallFunc{
consts.OrderGroupAdminOrder: service.AdminOrder().PayNotify, // 后台充值订单
// ...
})
}
```
### 订单退款
```go
package main
func main() {
refund, err := service.PayRefund().Refund(ctx, payin.PayRefundInp{
OrderSn: "唯一业务订单编号",
RefundMoney: "退款金额",
Reason: "买家申请退款原因",
Remark: "商家同意退款备注",
})
}
```
### 单笔转账(待更新)
### 其他
- 由于各个支付平台中交易方式较多,如果目前已有的交易方式不满足你的支付场景需求,请自行参考gopay文档对文件`server/internal/library/payment/payment.go`中的`PayClient`接口实现进行扩展
- gopay文档地址:https://github.com/go-pay/gopay
+203
View File
@@ -0,0 +1,203 @@
## 消息队列
目录
- 配置文件
- 实现接口
- 一个例子
- 控制台
- 自定义队列驱动
> 系统默认的队列驱动为disk(磁盘队列),目前已支持:disk、redis、rocketmq、kafka等多种驱动。请自行选择适合你的驱动使用。
### 配置文件
- 配置文件:server/manifest/config/config.yaml
```yaml
# 消息队列
queue:
switch: true # 队列开关,可选:true|false,默认为true
driver: "disk" # 队列驱动,可选:disk|redis|rocketmq|kafka,默认为disk
groupName: "hotgo" # mq群组名称
# 磁盘队列
disk:
path: "./storage/diskqueue" # 数据存放路径
batchSize: 100 # 每100条消息同步一次,batchSize和batchTime满足其一就会同步一次
batchTime: 1 # 每1秒消息同步一次
segmentSize: 10485760 # 每个topic分片数据文件最大字节,默认10M
segmentLimit: 3000 # 每个topic最大分片数据文件数量,超出部分将会丢弃
# redis,默认使用全局redis运行队列
redis:
timeout: 0 # 队列超时时间以秒为单位,0表示永不超时。如果队列在设定的超时时间内没有被消费,则会被销毁
rocketmq:
nameSrvAdders: ["127.0.0.1:9876"] # nameSrvAdder+端口,支持多个
accessKey: "" # 选填。如果开启了acl控制就必填
secretKey: "" # 选填。如果开启了acl控制就必填
brokerAddr: "127.0.0.1:10911" # brokerAddr+端口,选填。用于消费者订阅主题前会检查主题是否存在,不存在会自动创建。你也可以在rocketmq控制台手动创建主题
retry: 0 # 重试次数
logLevel: "info" # 系统日志级别,可选:all|close|debug|info|warn|error|fatal
kafka:
address: "127.0.0.1:9092" # kafka地址+端口
version: "2.0.0.0" # kafka专属配置,默认2.0.0.0
randClient: true # 开启随机生成clientID,可以实现启动多实例同时一起消费相同topic,加速消费能力的特性,默认为true
multiConsumer: true # 是否支持创建多个消费者
```
### 实现接口
- 为了提供高度的扩展性,消费队列在设计上采用了接口化的思路。只需要实现以下接口,您就可以在任何地方注册和使用消费队列消费功能,从而实现更大的灵活性和可扩展性。
```go
// Consumer 消费者接口,实现该接口即可加入到消费队列中
type Consumer interface {
GetTopic() string // 获取消费主题
Handle(ctx context.Context, mqMsg MqMsg) (err error) // 处理消息的方法
}
```
### 一个例子
每个被发送到队列的消息应该被定义为一个单独的文件结构。
例如,如果您需要异步记录系统日志,内容大致如下:
- 文件路径:server/internal/queues/sys_log.go
```go
package queues
import (
"context"
"encoding/json"
"hotgo/internal/consts"
"hotgo/internal/library/queue"
"hotgo/internal/model/entity"
"hotgo/internal/service"
)
func init() {
queue.RegisterConsumer(SysLog)
}
// SysLog 系统日志
var SysLog = &qSysLog{}
type qSysLog struct{}
// GetTopic 主题
func (q *qSysLog) GetTopic() string {
return consts.QueueLogTopic
}
// Handle 处理消息
func (q *qSysLog) Handle(ctx context.Context, mqMsg queue.MqMsg) (err error) {
var data entity.SysLog
if err = json.Unmarshal(mqMsg.Body, &data); err != nil {
return err
}
return service.SysLog().RealWrite(ctx, data)
}
```
下面是将消息添加到队列的方式,大概内容如下:
```go
package main
import (
"fmt"
"hotgo/internal/consts"
"hotgo/internal/library/queue"
"hotgo/internal/model/entity"
)
func test() {
data := &entity.SysLog{
//...
}
if err := queue.Push(consts.QueueLogTopic, data); err != nil {
fmt.Printf("queue.Push err:%+v", err)
}
}
```
延迟队列,目前只有redis、rocketmq驱动支持:
```go
package main
import (
"fmt"
"hotgo/internal/consts"
"hotgo/internal/library/queue"
"hotgo/internal/model/entity"
)
func test() {
data := &entity.SysLog{
//...
}
// redis 延迟10秒
if err := queue.SendDelayMsg(consts.QueueLogTopic, data, 10); err != nil {
fmt.Printf("queue.Push err:%+v", err)
}
// rocketmq 延迟5秒
// 注意rocketmq这里传入的是延迟级别,而不是秒
// 消息的延时级别level一共有18级,分别为:
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
// 参考:https://github.com/apache/rocketmq-client-go/blob/0e19ee654819bda396a08d950c883f9008b8222b/primitive/message.go#L174
if err := queue.SendDelayMsg(consts.QueueLogTopic, data, 2); err != nil {
fmt.Printf("queue.Push err:%+v", err)
}
}
```
### 控制台
控制台用于处理队列消息,即消费者。
相关命令请参考: [控制台](sys-console.md)
### 自定义队列驱动
只需实现消息队列的生成者和消费者接口,并加入到初始化中进行相应调用即可。
- 接口片段:server/internal/library/queue/init.go
```go
package queue
import (
"time"
)
type MqMsg struct {
RunType int `json:"run_type"`
Topic string `json:"topic"`
MsgId string `json:"msg_id"`
Offset int64 `json:"offset"`
Partition int32 `json:"partition"`
Timestamp time.Time `json:"timestamp"`
Body []byte `json:"body"`
}
type MqProducer interface {
SendMsg(topic string, body string) (mqMsg MqMsg, err error)
SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error)
SendDelayMsg(topic string, body string, delay int64) (mqMsg MqMsg, err error)
}
type MqConsumer interface {
ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error)
}
```
将实现过接口(MqProducer、MqConsumer)的实例方法分别加入到NewProducer、NewConsumer中进行相应调用即可。
+271
View File
@@ -0,0 +1,271 @@
## TCP服务器
目录
- 配置文件
- 一个基本的消息收发例子
- 注册路由
- 拦截器
- 服务认证
- 更多
> HotGo基于GoFrame的TCP服务器组件,提供了一个简单而灵活的方式快速搭建基于TCP的服务应用。集成了许多常用功能,如长连接、服务认证、路由分发、RPC消息、拦截器和数据绑定等,大大简化和规范了服务器开发流程。
### 配置文件
- 配置文件:server/manifest/config/config.yaml
```yaml
tcp:
# 服务器
server:
address: ":8099"
# 客户端
client:
# 定时任务
cron:
group: "cron" # 分组名称
name: "cron1" # 客户端名称
address: "127.0.0.1:8099" # 服务器地址
appId: "1002" # 应用名称
secretKey: "hotgo" # 密钥
# 系统授权
auth:
group: "auth" # 分组名称
name: "auth1" # 客户端名称
address: "127.0.0.1:8099" # 服务器地址
appId: "mengshuai" # 应用名称
secretKey: "123456" # 密钥
```
- 可以看到,除了服务器配置外,还有两个客户端配置`cron` 和`auth`
- `cron`是HotGo内置的定时任务服务,和http服务通过RPC通讯以实现和后台交互,使其可以独立、集群部署。
- `auth`可以为第三方平台提供授权服务。如果你需要他,可以将它部署在第三方程序中,在重要的位置进行授权验证。
### 一个基本的消息收发测试用例
- 文件路径:server/internal/library/network/tcp/tcp_example_test.go
```go
package tcp_test
import (
"context"
"fmt"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/test/gtest"
"hotgo/internal/library/network/tcp"
"testing"
"time"
)
var T *testing.T // 声明一个全局的 *testing.T 变量
type TestMsgReq struct {
Name string `json:"name"`
}
type TestMsgRes struct {
tcp.ServerRes
}
type TestRPCMsgReq struct {
Name string `json:"name"`
}
type TestRPCMsgRes struct {
tcp.ServerRes
}
func onTestMsg(ctx context.Context, req *TestMsgReq) {
fmt.Printf("服务器收到消息 ==> onTestMsg:%+v\n", req)
conn := tcp.ConnFromCtx(ctx)
gtest.C(T, func(t *gtest.T) {
t.AssertNE(conn, nil)
})
res := new(TestMsgRes)
res.Message = fmt.Sprintf("你的名字:%v", req.Name)
conn.Send(ctx, res)
}
func onResponseTestMsg(ctx context.Context, req *TestMsgRes) {
fmt.Printf("客户端收到响应消息 ==> TestMsgRes:%+v\n", req)
err := req.GetError()
gtest.C(T, func(t *gtest.T) {
t.AssertNil(err)
})
}
func onTestRPCMsg(ctx context.Context, req *TestRPCMsgReq) (res *TestRPCMsgRes, err error) {
fmt.Printf("服务器收到消息 ==> onTestRPCMsg:%+v\n", req)
res = new(TestRPCMsgRes)
res.Message = fmt.Sprintf("你的名字:%v", req.Name)
return
}
func startTCPServer() {
serv := tcp.NewServer(&tcp.ServerConfig{
Name: "hotgo",
Addr: ":8002",
})
// 注册路由
serv.RegisterRouter(
onTestMsg,
)
// 注册RPC路由
serv.RegisterRPCRouter(
onTestRPCMsg,
)
// 服务监听
err := serv.Listen()
gtest.C(T, func(t *gtest.T) {
t.AssertNil(err)
})
}
// 一个基本的消息收发
func TestSendMsg(t *testing.T) {
T = t
go startTCPServer()
ctx := gctx.New()
client := tcp.NewClient(&tcp.ClientConfig{
Addr: "127.0.0.1:8002",
})
// 注册路由
client.RegisterRouter(
onResponseTestMsg,
)
go func() {
err := client.Start()
gtest.C(T, func(t *gtest.T) {
t.AssertNil(err)
})
}()
// 确保服务都启动完成
time.Sleep(time.Second * 1)
// 拿到客户端的连接
conn := client.Conn()
gtest.C(T, func(t *gtest.T) {
t.AssertNE(conn, nil)
})
// 向服务器发送tcp消息,不会阻塞程序执行
err := conn.Send(ctx, &TestMsgReq{Name: "Tom"})
gtest.C(T, func(t *gtest.T) {
t.AssertNil(err)
})
// 向服务器发送rpc消息,会等待服务器响应结果,直到拿到结果或响应超时才会继续
var res TestRPCMsgRes
if err = conn.RequestScan(ctx, &TestRPCMsgReq{Name: "Tony"}, &res); err != nil {
gtest.C(T, func(t *gtest.T) {
t.AssertNil(err)
})
}
fmt.Printf("客户端收到RPC消息响应 ==> TestRPCMsgRes:%+v\n", res)
time.Sleep(time.Second * 1)
}
```
### 注册路由
- 从上面的例子可以看到,不管是普通TCP消息和RPC消息的请求/响应结构体都采用类似GF框架的规范路由的结构,请求`XxxRes`/响应`XxxRes`的格式,是不是很亲切?
### 拦截器
- 不管是服务端还是客户端,在初始化时都可以注册多个拦截器来满足更多场景的服务开发,下面是一个使用例子:
```go
package main
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"hotgo/internal/library/network/tcp"
)
func main() {
serv = tcp.NewServer(&tcp.ServerConfig{
Name: "hotgo",
Addr: ":8002",
})
// 注册拦截器
// 执行顺序是从前到后,即Interceptor -> Interceptor2 -> Interceptor3。如果中间有任意一个抛出错误,则会中断后续处理
serv.RegisterInterceptor(Interceptor, Interceptor2, Interceptor3)
// 服务监听
if err := serv.Listen(); err != nil {
if !serv.IsClose() {
g.Log().Warningf(ctx, "TCPServer Listen err:%v", err)
}
}
}
func Interceptor(ctx context.Context, msg *tcp.Message) (err error) {
// 可以在拦截器中通过上下文拿到连接
conn := tcp.ConnFromCtx(ctx)
// 拿到原始请求消息
g.Dump(msg)
// 如果想要中断后续处理只需返回一个错误即可,但注意两种情况
// tcp消息:如果你还想对该消息进行回复应在拦截器中进行处理,例如:conn.Send(ctx, 回复消息内容)
// rpc消息:返回一个错误后系统会将错误自动回复到rpc响应中,无需单独处理
return
}
func Interceptor2(ctx context.Context, msg *tcp.Message) (err error) {
// ...
return
}
func Interceptor3(ctx context.Context, msg *tcp.Message) (err error) {
// ...
return
}
```
### 服务认证
- 一般情况下,建议客户端连接到服务器时都通过`授权许可证`的方式进行登录认证,当初始化客户端配置认证数据时,连接成功后会自动进行登录认证。
```go
// 创建客户端配置
clientConfig := &tcp.ClientConfig{
Addr: "127.0.0.1:8002",
AutoReconnect: true,
// 认证数据
// 认证数据可以在后台-系统监控-在线服务-许可证列表中添加,同一个授权支持多个服务使用,但多个服务不能使用相同的名称进行连接
Auth: &tcp.AuthMeta{
Name: "服务名称",
Group: "服务分组",
AppId: "APPID",
SecretKey: "SecretKey",
},
}
// 初始化客户端
client = tcp.NewClient(clientConfig)
```
### 更多
TCP服务器源码路径:server/internal/library/network/tcp
更多文档请参考:https://goframe.org/pages/viewpage.action?pageId=1114625
+175
View File
@@ -0,0 +1,175 @@
## SaaS多租户
目录
- 介绍
- 职责划分
- 如何在HotGo开发多租户业务
- 多租户数据库设计
- 多租户功能演示
### 介绍
SaaS系统多租户多应用设计,已成为互联网企业的重要发展建设方向。核心在于多租户SAAS系统独立前台、共享后台、共享数据库的平台应用架构。
> 目前HotGo的部分基础功能并不完全支持多租户设计,但这并不妨碍开发者基于HotGo构建自己的多租户产品
### 职责划分
在SaaS系统多租户中,不同身份用户的职责功能划分假设可以是这样:
> 这只是一个粗略的概念,实际开发时应根据业务来进行调整,每个身份也都不是必须的。
| 身份 | 标识 | 职责和功能划分 |
|-----------------|----------|-------------------------------------------------------|
| 公司| company | 管理整个平台,包括商户和用户账户、系统设置以及其他全局性业务流程。 |
| 租户 | tenant | 多租户系统中顶层实体客户、组织或实体。有自己的多个商户、用户、产品、订单等。拥有独立的数据隔离和安全边界。 |
| 商户 | merchant | 受租户的监管和管理,可独立经营的实体。提供产品或服务,管理自己的业务,包括库存管理、订单处理、结算等。 |
| 用户 | user | 真正购买产品或享受服务的人,与商户互动,管理个人信息等个性化功能。 |
### 如何在HotGo开发多租户业务
#### 一、应用功能
根据角色来划分用户的后台功能,在创建用户时为其绑定角色,然后为不同角色分配不同的功能菜单
请参考: [权限控制](sys-auth.md)
#### 二、 数据隔离
根据部门来划定用户的数据权限范围,在创建用户时为其绑定部门
- 在用户登录成功后,server端可通过上下文来获取用户部门类型来确定用户身份
- 文件路径:server/internal/library/contexts/context.go
```go
package contexts
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"hotgo/internal/consts"
"hotgo/internal/model"
)
// GetDeptType 获取用户部门类型
func GetDeptType(ctx context.Context) string {
user := GetUser(ctx)
if user == nil {
return ""
}
return user.DeptType
}
// IsCompanyDept 是否为公司部门
func IsCompanyDept(ctx context.Context) bool {
return GetDeptType(ctx) == consts.DeptTypeCompany
}
// IsTenantDept 是否为租户部门
func IsTenantDept(ctx context.Context) bool {
return GetDeptType(ctx) == consts.DeptTypeTenant
}
// IsMerchantDept 是否为商户部门
func IsMerchantDept(ctx context.Context) bool {
return GetDeptType(ctx) == consts.DeptTypeMerchant
}
// IsUserDept 是否为普通用户部门
func IsUserDept(ctx context.Context) bool {
return GetDeptType(ctx) == consts.DeptTypeUser
}
```
- 在用户登录成功后,web端可通`useUserStore`来获取用户部门类型来确定用户身份
- 文件路径:web/src/store/modules/user.ts
```vue
<script lang="ts" setup>
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
console.log('用户部门类型:' + userStore.info?.deptType);
console.log('是否为公司:' + userStore.isCompanyDept);
console.log('是否为租户:' + userStore.isTenantDept);
console.log('是否为商户:' + userStore.isMerchantDept);
console.log('是否为用户:' + userStore.isUserDept);
</script>
```
### 多租户数据库设计
HotGo定位是中小型应用开发,推荐采用一套数据库不同Schema。就是在多租户业务表中加入用户标识字段,来区分不同用户的数据,如:`tenant_id`
- 参考文章:https://blog.csdn.net/haponchang/article/details/104246317
### 多租户功能演示
请登录后台【插件应用】-【功能案例】-【多租户功能演示】查看
#### 自动维护租户关系
- 只需在表设计时包含以下字段,即可使用handler和hook实现租户权限过滤和租户关系维护
| 字段名称 | 数据类型 | 字段注释 | 必选 |
|------|----------|------|-----------------------------------------------------|
| tenant_id | bigint(20) | 租户ID | 否 |
| merchant_id | bigint(20) | 商户ID | 否 |
| user_id | bigint(20) | 用户ID | 否 |
下面是多租户功能演示例子代码中的使用片段
- 封装查询Model
```go
// Model 多租户功能演示ORM模型
func (s *sSysTenantOrder) Model(ctx context.Context, option ...*handler.Option) *gdb.Model {
if len(option) == 0 {
// 过滤多租户数据权限
option = append(option, &handler.Option{
FilterTenant: true,
//FilterAuth: true, // 如果还需要维护created_by、member_id等部门数据权限范围可放开注释
})
}
return handler.Model(dao.AddonHgexampleTenantOrder.Ctx(ctx), option...)
}
```
- 增改数据自动维护租户关系
```go
// Edit 修改/新增多租户功能演示
func (s *sSysTenantOrder) Edit(ctx context.Context, in *sysin.TenantOrderEditInp) (err error) {
return g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
// 修改
if in.Id > 0 {
if _, err = s.Model(ctx).
Fields(sysin.TenantOrderUpdateFields{}).
WherePri(in.Id).Data(in).
Hook(hook.SaveTenant). // 自动维护租户关系更新
Update(); err != nil {
}
return
}
// 新增
if _, err = dao.AddonHgexampleTenantOrder.Ctx(ctx).
Fields(sysin.TenantOrderInsertFields{}).
Hook(hook.SaveTenant). // 自动维护租户关系更新
Data(in).
Insert(); err != nil {
}
return
})
}
```
相关代码文件:/server/addons/hgexample/logic/sys/tenant_order.go
+3
View File
@@ -0,0 +1,3 @@
## 单元测试
请参考:https://goframe.org/pages/viewpage.action?pageId=1114153
+20
View File
@@ -0,0 +1,20 @@
## 工具方法
HotGo还提供一些系统中常用的工具库方法,在这里简单说明:
```
/server
├── utility
│ ├── charset # 字符串处理
│ ├── convert # 数据类型转换
│ ├── encrypt # 数据加密/解密
│ ├── excel # 电子表格导出/导入
│ ├── file # 文件/目录处理
│ ├── format # 数据格式化
│ ├── simple # 一些简捷函数
│ ├── tree # 树形结构
│ ├── url # URL处理
│ ├── useragent # 请求头代理处理
└── └── validate # 数据验证
```
+3
View File
@@ -0,0 +1,3 @@
## WebHook
请参考:https://goframe.org/pages/viewpage.action?pageId=1114387
+213
View File
@@ -0,0 +1,213 @@
## WebSocket客户端
目录
- 全局消息监听
- 单页面消息监听
- 发送消息
> 基于WebSocket服务器,hotgo还对客户端的上做了一些封装,使其使用起来更加方便
- [WebSocket服务器](sys-websocket-server.md)
### 全局消息监听
- 所有全局的消息监听都在这里
- 文件路径:web/src/utils/websocket/registerMessage.ts
```ts
import { TABS_ROUTES } from '@/store/mutation-types';
import { SocketEnum } from '@/enums/socketEnum';
import { useUserStoreWidthOut } from '@/store/modules/user';
import { notificationStoreWidthOut } from '@/store/modules/notification';
import { addOnMessage, WebSocketMessage } from '@/utils/websocket/index';
// 注册全局消息监听
export function registerGlobalMessage() {
// 心跳
addOnMessage(SocketEnum.EventPing, function (_message: WebSocketMessage) {
// console.log('ping..');
});
// 强制退出
addOnMessage(SocketEnum.EventKick, function (_message: WebSocketMessage) {
const useUserStore = useUserStoreWidthOut();
useUserStore.logout().then(() => {
// 移除标签页
localStorage.removeItem(TABS_ROUTES);
location.reload();
});
});
// 消息通知
addOnMessage(SocketEnum.EventNotice, function (message: WebSocketMessage) {
const notificationStore = notificationStoreWidthOut();
notificationStore.triggerNewMessages(message.data);
});
// 更多全局消息处理都可以在这里注册
// ...
}
```
#### 单页面消息监听
- 当你只需要某个页面使用WebSocket,这将是一个不错的选择,下面是一个简单的演示例子
- 文件路径:web/src/views/addons/hgexample/portal/websocketTest.vue
```vue
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="测试websocket">
尝试在下方输入框中输入任意文字消息内容,发送后websocket服务器收到会原样返回
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<n-space vertical>
<n-input-group style="width: 520px">
<n-input
@keyup.enter="sendMessage"
:style="{ width: '78%' }"
placeholder="请输入消息内容"
:on-focus="onFocus"
:on-blur="onBlur"
v-model:value="inputMessage"
/>
<n-button type="primary" @click="sendMessage"> 发送消息</n-button>
</n-input-group>
<div class="mt-5"></div>
<n-timeline :icon-size="20">
<n-timeline-item color="grey" content="输入中.." v-if="isInput">
<template #icon>
<n-icon>
<MessageOutlined />
</n-icon>
</template>
</n-timeline-item>
<n-timeline-item
v-for="item in messages"
:key="item"
:type="item.type == Enum.SendType ? 'success' : 'info'"
:title="item.type == Enum.SendType ? '发送消息' : '收到消息'"
:content="item.content"
:time="item.time"
>
<template #icon>
<n-icon>
<SendOutlined v-if="item.type == Enum.SendType" />
<SoundOutlined v-if="item.type == Enum.ReceiveType" />
</n-icon>
</template>
</n-timeline-item>
</n-timeline>
</n-space>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { MessageOutlined, SendOutlined, SoundOutlined } from '@vicons/antd';
import { format } from 'date-fns';
import { addOnMessage, removeOnMessage, sendMsg, WebSocketMessage } from '@/utils/websocket';
import { useMessage } from 'naive-ui';
interface Message {
type: Enum;
content: string;
time: string;
}
const message = useMessage();
const messages = ref<Message[]>([]);
const inputMessage = ref('你好,HotGo');
const isInput = ref(false);
const testMessageEvent = 'admin/addons/hgexample/testMessage';
enum Enum {
SendType = 1, // 发送类型
ReceiveType = 2, // 接受类型
}
function onFocus() {
isInput.value = true;
}
function onBlur() {
isInput.value = false;
}
function sendMessage() {
if (inputMessage.value == '') {
message.error('消息内容不能为空');
return;
}
// 发送消息
sendMsg(testMessageEvent, {
message: inputMessage.value,
});
const msg: Message = {
type: Enum.SendType,
content: inputMessage.value,
time: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
};
insertMessage(msg);
inputMessage.value = '';
}
// 存入本地记录
function insertMessage(msg: Message): void {
messages.value.unshift(msg); // 在头部插入消息
if (messages.value.length > 10) {
messages.value = messages.value.slice(0, 10); // 如果超过10个,则只保留最前面10个
}
}
const onMessage = (res: WebSocketMessage) => {
const msg: Message = {
type: Enum.ReceiveType,
content: res.data.message,
time: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
};
insertMessage(msg);
};
onMounted(() => {
// 在当前页面注册消息监听
addOnMessage(testMessageEvent, onMessage);
});
onBeforeUnmount(() => {
// 移除消息监听
removeOnMessage(testMessageEvent);
});
</script>
<style scoped></style>
```
#### 发送消息
- 向服务器发送一条消息
```ts
import { sendMsg } from '@/utils/websocket';
const event = 'admin/addons/hgexample/testMessage'; // 消息路由
const data: object | null = { // 消息内容
message: 'message content...',
};
const isRetry = false; // 发送失败是否重试,不传默认为true
// 基本使用
sendMsg(event, data);
// 无消息内容
sendMsg(event);
// 发送失败不重试
sendMsg(event, data, isRetry);
```
+148
View File
@@ -0,0 +1,148 @@
## WebSocket服务器
目录
- 一个基本的消息收发例子
- 常用方法
- HTTP接口
- 其他
> hotgo提供了一个WebSocket服务器,随`HTTP服务`启停。集成了许多常用功能,如JWT身份认证、路由消息处理器、一对一消息/群组消息/广播消息、在线用户管理、心跳保持等,大大简化和规范了WebSocket服务器的开发流程。
- [Websocket客户端](sys-websocket-client.md)
### 一个基本的消息收发例子
- 这是一个基本的消息接收并进行处理的简单例子
#### 1.消息处理接口
- 消息处理在设计上采用了接口化的思路。只需要实现以下接口,即可进行WebSocket消息注册
- 文件路径:server/internal/websocket/model.go
```go
package websocket
// EventHandler 消息处理器
type EventHandler func(client *Client, req *WRequest)
```
#### 2.定义消息处理方法
- 以下是功能案例中的一个简单演示,实现了消息处理接口,并将收到的消息原样发送给客户端
- 文件路径:server/addons/hgexample/controller/websocket/handler/index.go
```go
package handler
import (
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"hotgo/internal/websocket"
)
var (
Index = cIndex{}
)
type cIndex struct{}
// TestMessage 测试消息
func (c *cIndex) TestMessage(client *websocket.Client, req *websocket.WRequest) {
g.Log().Infof(client.Context(), "收到客户端测试消息:%v", gjson.New(req).String())
// 将收到的消息原样发送给客户端
websocket.SendSuccess(client, req.Event, req.Data)
}
```
#### 3.注册消息
- 定义消息处理方法后,需要将其注册到WebSocket消息处理器,一般放在对应应用模块的`router/websocket.go`下即可
- 文件路径:server/addons/hgexample/router/websocket.go
```go
package router
import (
"context"
"github.com/gogf/gf/v2/net/ghttp"
"hotgo/addons/hgexample/controller/websocket"
"hotgo/addons/hgexample/controller/websocket/handler"
ws "hotgo/internal/websocket"
)
// WebSocket ws路由配置
func WebSocket(ctx context.Context, group *ghttp.RouterGroup) {
// 注册消息路由
ws.RegisterMsg(ws.EventHandlers{
"admin/addons/hgexample/testMessage": handler.Index.TestMessage, // 测试消息
})
// 这里"admin/addons/hgexample/testMessage"代表的是一个消息处理ID,可以自定义。建议的格式是和HTTP接口格式保持一致,这样还可以便于对用户请求的消息进行权限验证
// 客户端连接后,向WebSocket服务器发送event为"admin/addons/hgexample/testMessage"的消息时,会调用TestMessage方法
}
```
- 到此,你已了解了WebSocket消息接收并进行处理的基本流程
### 常用方法
- websocket服务器还提供了一些常用的方法,下面只对部分进行说明
```go
func test() {
websocket.SendToAll() // 发送全部客户端
websocket.SendToClientID() // 发送单个客户端
websocket.SendToUser() // 发送单个用户
websocket.SendToTag() // 发送某个标签、群组
client := websocket.Manager().GetClient(id) // 通过连接ID获取客户端连接
client := websocket.Manager().GetUserClient(userId) // 通过用户ID获取客户端连接,因为用户是可多端登录的,这里返回的是一个切片
websocket.SendSuccess(client, "admin/addons/hgexample/testMessage", "消息内容") // 向指定客户端发送一条成功的消息
websocket.SendError(client, "admin/addons/hgexample/testMessage", gerror.New("错误内容")) // 向指定客户端发送一条失败的消息
}
```
### HTTP接口
- 你还可以通过http接口方式调用WebSocket发送消息
- 参考文件:server/internal/controller/websocket/send.go
### 其他
- WebSocket被连接时需验证用户认证中间件,所以用户必须登录成功后才能连接成功
- 参考文件:server/internal/logic/middleware/weboscket_auth.go
```go
package middleware
import (
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/text/gstr"
"hotgo/internal/consts"
"hotgo/internal/library/response"
"hotgo/utility/simple"
)
// WebSocketAuth websocket鉴权中间件
func (s *sMiddleware) WebSocketAuth(r *ghttp.Request) {
var (
ctx = r.Context()
path = gstr.Replace(r.URL.Path, simple.RouterPrefix(ctx, consts.AppWebSocket), "", 1)
)
// 不需要验证登录的路由地址
if s.IsExceptLogin(ctx, consts.AppWebSocket, path) {
r.Middleware.Next()
return
}
// 将用户信息传递到上下文中
if err := s.DeliverUserContext(r); err != nil {
response.JsonExit(r, gcode.CodeNotAuthorized.Code(), err.Error())
return
}
r.Middleware.Next()
}
```
- 如果您不要求用户进行登录即可使用 WebSocket,那么需要对身份验证中间件进行修改。然而,这样做会降低连接的安全性,并且无法应用于需要确定用户身份的情景,因此并不建议采取这种策略
+261
View File
@@ -0,0 +1,261 @@
## 独立部署
目录
- 构建
- 旧版浏览器兼容
- 预览
- 分析构建文件体积
- 压缩
- 部署
> 在实际开发中,web前端也可能需要独立部署,所以在此提供一下部署方案。
### 构建
项目开发完成之后,执行以下命令进行构建
```shell
pnpm run build 或 npm run build
```
构建打包成功之后,会在根目录生成 dist 文件夹,里面就是构建打包好的文件
### 旧版浏览器兼容
在 .env.production 内
设置 `VITE_LEGACY=true` 即可打包出兼容旧版浏览器的代码
```shell
VITE_LEGACY = true
```
### 预览
发布之前可以在本地进行预览,有多种方式,这里介绍两种
##### 不能直接打开构建后的 html 文件
使用项目自定的命令进行预览(推荐)
```shell
# 先打包在进行预览
pnpm run preview 或 npm run preview
# 直接预览本地 dist 文件目录
pnpm run preview:dist 或 npm run preview:dist
```
- 本地服务器预览(通过 live-server)
```shell
# 1.全局安装live-server
npm -g install live-server
# 2. 进入打包的后目录
cd ./dist
# 本地预览,默认端口8080
live-server
# 指定端口
live-server --port 9000
```
### 分析构建文件体积
如果你的构建文件很大,可以通过项目内置 [rollup-plugin-analyzer](https://github.com/doesdev/rollup-plugin-analyzer) 插件进行代码体积分析,从而优化你的代码。
```shell
pnpm run report 或 npm run report
```
运行之后,在自动打开的页面可以看到具体的体积分布,以分析哪些依赖有问题。
- 左上角可以切换 显示 gzip 或者 brotli
![start-deploy-report.png](./images/start-deploy-report.png)
### 压缩
#### 开启 gzip 压缩
开启 gzip,并配合 nginx 的 `gzip_static` 功能可以大大加快页面访问速度
- 只需开启 `VITE_BUILD_COMPRESS='gzip'` 即可在打包的同时生成 .gz 文件
```shell
# 根据自己路径来配置更改
# 例如部署在nginx /next/路径下 则VITE_PUBLIC_PATH=/next/
VITE_PUBLIC_PATH=/
```
#### 开启 brotli 压缩
brotli 是比 gzip 压缩率更高的算法,可以与 gzip 共存不会冲突,需要 nginx 安装指定模块并开启即可。
- 只需开启 VITE_BUILD_COMPRESS='brotli' 即可在打包的同时生成 .br 文件
```shell
# 根据自己路径来配置更改
# 例如部署在nginx /next/路径下 则VITE_PUBLIC_PATH=/next/
VITE_PUBLIC_PATH=/
```
#### 同时开启 gzip 与 brotli
只需开启 VITE_BUILD_COMPRESS='brotli,gzip' 即可在打包的同时生成 .gz 和 .br 文件。
#### gzip 与 brotli 在 nginx 内的配置
```
http {
# 开启gzip
gzip on;
# 开启gzip_static
# gzip_static 开启后可能会报错,需要安装相应的模块, 具体安装方式可以自行查询
# 只有这个开启,vue文件打包的.gz文件才会有效果,否则不需要开启gzip进行打包
gzip_static on;
gzip_proxied any;
gzip_min_length 1k;
gzip_buffers 4 16k;
#如果nginx中使用了多层代理 必须设置这个才可以开启gzip。
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";
# 开启 brotli压缩
# 需要安装对应的nginx模块,具体安装方式可以自行查询
# 可以与gzip共存不会冲突
brotli on;
brotli_comp_level 6;
brotli_buffers 16 8k;
brotli_min_length 20;
brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;
}
```
### 部署
- 注意:项目默认是在生产环境开启 Mock,这样做非常不好,只是为了演示环境有数据,不建议在生产环境使用 Mock,而应该使用真实的后台接口,并将 Mock 关闭。
#### 发布
简单的部署只需要将最终生成的静态文件,dist 文件夹的静态文件发布到你的 cdn 或者静态服务器即可,需要注意的是其中的 index.html 通常会是你后台服务的入口页面,在确定了 js 和 css 的静态之后可能需要改变页面的引入路径。
例如上传到 nginx `/srv/www/project/index.html`
```
# nginx配置
location / {
# 不缓存html,防止程序更新后缓存继续生效
if ($request_filename ~* .*\.(?:htm|html)$) {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
access_log on;
}
# 这里是vue打包文件dist内的文件的存放路径
root /srv/www/project/;
index index.html index.htm;
}
```
部署时可能会发现资源路径不对,只需要修改`.env.production`文件即可。
```shell
# 根据自己路径来配置更改
# 注意需要以 / 开头和结尾
VITE_PUBLIC_PATH=/
VITE_PUBLIC_PATH=/xxx/
```
#### 前端路由与服务端的结合
项目前端路由使用的是 vue-router,所以你可以选择两种方式:history 和 hash。
- hash 默认会在 url 后面拼接#
- history 则不会,不过 history 需要服务器配合
可在 src/router/index.ts 内进行 mode 修改
```vue
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
createRouter({
history: createWebHashHistory(),
// or
history: createWebHistory(),
});
```
#### history 路由模式下服务端配置
开启 history 模式需要服务器配置,更多的服务器配置详情可以看 [history-mode](https://router.vuejs.org/guide/essentials/history-mode.html#html5-mode)
这里以 nginx 配置为例
##### 部署到根目录
```
server {
listen 80;
location / {
# 用于配合 History 使用
try_files $uri $uri/ /index.html;
}
}
```
#### 部署到非根目录
1. 首先需要在打包的时候更改配置
```shell
# 在.env.production内,配置子目录路径
VITE_PUBLIC_PATH = /sub/
```
```
server {
listen 80;
server_name localhost;
location /sub/ {
# 这里是vue打包文件dist内的文件的存放路径
alias /srv/www/project/;
index index.html index.htm;
try_files $uri $uri/ /sub/index.html;
}
}
```
#### 使用 nginx 处理跨域
使用 nginx 处理项目部署后的跨域问题
1. 配置前端项目接口地址
```
# 在.env.production内,配置接口地址
VITE_GLOB_API_URL=/api
```
2. 在 nginx 配置请求转发到后台
```
server {
listen 8080;
server_name localhost;
# 接口代理,用于解决跨域问题
location /api {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 后台接口地址
proxy_pass http://110.110.1.1:8080/api;
proxy_redirect default;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
}
}
```
+879
View File
@@ -0,0 +1,879 @@
## 表单组件
目录
- 文本输入 Input
- 数字输入 Input Number
- 文本域 InputTextarea
- 富文本 InputEditor
- 动态键值对 InputDynamic
- 日期选择 Date(Y-M-D)
- 日期范围选择 DateRange
- 时间选择 Time(Y-M-D H:i:s)
- 时间范围选择 TimeRange
- 单选按钮 Radio
- 复选框 Checkbox
- 单选下拉框 Select
- 多选下拉框 SelectMultiple
- 树型选择 Tree Select
- 单图上传 UploadImage
- 多图上传 UploadImage
- 单文件上传 UploadFile
- 多文件上传 UploadFile
- 文件选择器 FileChooser
- 大文件上传 MultipartUpload
- 开关 Switch
- 评分 Rate
- 省市区选择器 CitySelector
- 图标选择器 IconSelector
### 文本输入 Input
```vue
<template>
<n-input v-model:value="value" type="text" placeholder="基本的 Input" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 数字输入 Input Number
```vue
<template>
<n-input-number v-model:value="value" clearable />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 文本域 InputTextarea
```vue
<template>
<n-input
v-model:value="value"
type="textarea"
placeholder="基本的 Textarea"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 富文本 InputEditor
```vue
<template>
<Editor style="height: 450px" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Editor from '@/components/Editor/editor.vue';
const value = ref(null);
</script>
```
### 动态键值对 InputDynamic
```vue
<template>
<n-dynamic-input
v-model:value="value"
preset="pair"
key-placeholder="键名"
value-placeholder="键值"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 日期选择 Date(Y-M-D)
```vue
<template>
<DatePicker v-model:formValue="value" type="date" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import DatePicker from '@/components/DatePicker/datePicker.vue';
const value = ref(null);
</script>
```
### 日期范围选择 DateRange
```vue
<template>
<DatePicker
v-model:startValue="startValue"
v-model:endValue="endValue"
type="datetimerange"
/>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import DatePicker from '@/components/DatePicker/datePicker.vue';
const startValue = ref(null);
const endValue = ref(null);
</script>
```
### 时间选择 Time(Y-M-D H:i:s)
```vue
<template>
<n-time-picker :default-formatted-value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 时间范围选择 TimeRange
```vue
<template>
<template>
<n-space>
<n-time-picker :default-value="startValue" />
<n-time-picker :default-value="endValue" />
</n-space>
</template>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const startValue = ref(null);
const endValue = ref(null);
</script>
```
### 单选按钮 Radio
```vue
<template>
<n-space vertical>
<n-radio-group v-model:value="value" name="radiobuttongroup1">
<n-radio-button
v-for="song in songs"
:key="song.value"
:value="song.value"
:disabled="
(song.label === 'Live Forever' && disabled1) ||
(song.label === 'Shakermaker' && disabled2)
"
:label="song.label"
/>
</n-radio-group>
<n-space>
<n-checkbox v-model:checked="disabled2" style="margin-right: 12px">
禁用 Shakemaker
</n-checkbox>
<n-checkbox v-model:checked="disabled1">
禁用 Live Forever
</n-checkbox>
</n-space>
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
value: ref(null),
disabled2: ref(false),
disabled1: ref(false),
songs: [
{
value: "Rock'n'Roll Star",
label: "Rock'n'Roll Star"
},
{
value: 'Shakermaker',
label: 'Shakermaker'
},
{
value: 'Live Forever',
label: 'Live Forever'
},
{
value: 'Up in the Sky',
label: 'Up in the Sky'
},
{
value: '...',
label: '...'
}
].map((s) => {
s.value = s.value.toLowerCase()
return s
})
}
}
})
</script>
```
### 复选框 Checkbox
```vue
<template>
<n-space item-style="display: flex;" align="center">
<n-checkbox v-model:checked="value">
复选框
</n-checkbox>
<n-checkbox v-model:checked="value" />
<n-checkbox v-model:checked="value" :disabled="disabled">
复选框
</n-checkbox>
<n-button size="small" @click="disabled = !disabled">
禁用
</n-button>
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
value: ref(false),
disabled: ref(true)
}
}
})
</script>
```
### 单选下拉框 Select
```vue
<template>
<n-space vertical>
<n-select v-model:value="value" :options="options" />
<n-select v-model:value="value" disabled :options="options" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
value: ref(null),
options: [
{
label: "Everybody's Got Something to Hide Except Me and My Monkey",
value: 'song0',
disabled: true
},
{
label: 'Drive My Car',
value: 'song1'
},
{
label: 'Norwegian Wood',
value: 'song2'
},
{
label: "You Won't See",
value: 'song3',
disabled: true
},
{
label: 'Nowhere Man',
value: 'song4'
},
{
label: 'Think For Yourself',
value: 'song5'
},
{
label: 'The Word',
value: 'song6'
},
{
label: 'Michelle',
value: 'song7',
disabled: true
},
{
label: 'What goes on',
value: 'song8'
},
{
label: 'Girl',
value: 'song9'
},
{
label: "I'm looking through you",
value: 'song10'
},
{
label: 'In My Life',
value: 'song11'
},
{
label: 'Wait',
value: 'song12'
}
]
}
}
})
</script>
```
### 多选下拉框 SelectMultiple
```vue
<template>
<n-space vertical>
<n-select v-model:value="value" multiple :options="options" />
<n-select v-model:value="value" multiple disabled :options="options" />
</n-space>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
return {
value: ref(['song3']),
options: [
{
label: "Everybody's Got Something to Hide Except Me and My Monkey",
value: 'song0',
disabled: true
},
{
label: 'Drive My Car',
value: 'song1'
},
{
label: 'Norwegian Wood',
value: 'song2'
},
{
label: "You Won't See",
value: 'song3',
disabled: true
},
{
label: 'Nowhere Man',
value: 'song4'
},
{
label: 'Think For Yourself',
value: 'song5'
},
{
label: 'The Word',
value: 'song6'
},
{
label: 'Michelle',
value: 'song7',
disabled: true
},
{
label: 'What goes on',
value: 'song8'
},
{
label: 'Girl',
value: 'song9'
},
{
label: "I'm looking through you",
value: 'song10'
},
{
label: 'In My Life',
value: 'song11'
},
{
label: 'Wait',
value: 'song12'
}
]
}
}
})
</script>
```
### 树型选择 Tree Select
```vue
<template>
<n-tree-select
:options="options"
default-value="Drive My Car"
@update:value="handleUpdateValue"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { TreeSelectOption } from 'naive-ui'
export default defineComponent({
setup () {
return {
handleUpdateValue (
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option)
},
options: [
{
label: 'Rubber Soul',
key: 'Rubber Soul',
children: [
{
label:
"Everybody's Got Something to Hide Except Me and My Monkey",
key: "Everybody's Got Something to Hide Except Me and My Monkey"
},
{
label: 'Drive My Car',
key: 'Drive My Car',
disabled: true
},
{
label: 'Norwegian Wood',
key: 'Norwegian Wood'
},
{
label: "You Won't See",
key: "You Won't See",
disabled: true
},
{
label: 'Nowhere Man',
key: 'Nowhere Man'
},
{
label: 'Think For Yourself',
key: 'Think For Yourself'
},
{
label: 'The Word',
key: 'The Word'
},
{
label: 'Michelle',
key: 'Michelle',
disabled: true
},
{
label: 'What goes on',
key: 'What goes on'
},
{
label: 'Girl',
key: 'Girl'
},
{
label: "I'm looking through you",
key: "I'm looking through you"
},
{
label: 'In My Life',
key: 'In My Life'
},
{
label: 'Wait',
key: 'Wait'
}
]
},
{
label: 'Let It Be',
key: 'Let It Be Album',
children: [
{
label: 'Two Of Us',
key: 'Two Of Us'
},
{
label: 'Dig A Pony',
key: 'Dig A Pony'
},
{
label: 'Across The Universe',
key: 'Across The Universe'
},
{
label: 'I Me Mine',
key: 'I Me Mine'
},
{
label: 'Dig It',
key: 'Dig It'
},
{
label: 'Let It Be',
key: 'Let It Be'
},
{
label: 'Maggie Mae',
key: 'Maggie Mae'
},
{
label: "I've Got A Feeling",
key: "I've Got A Feeling"
},
{
label: 'One After 909',
key: 'One After 909'
},
{
label: 'The Long And Winding Road',
key: 'The Long And Winding Road'
},
{
label: 'For You Blue',
key: 'For You Blue'
},
{
label: 'Get Back',
key: 'Get Back'
}
]
}
]
}
}
})
</script><template>
<n-tree-select
:options="options"
default-value="Drive My Car"
@update:value="handleUpdateValue"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { TreeSelectOption } from 'naive-ui'
export default defineComponent({
setup () {
return {
handleUpdateValue (
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option)
},
options: [
{
label: 'Rubber Soul',
key: 'Rubber Soul',
children: [
{
label:
"Everybody's Got Something to Hide Except Me and My Monkey",
key: "Everybody's Got Something to Hide Except Me and My Monkey"
},
{
label: 'Drive My Car',
key: 'Drive My Car',
disabled: true
},
{
label: 'Norwegian Wood',
key: 'Norwegian Wood'
},
{
label: "You Won't See",
key: "You Won't See",
disabled: true
},
{
label: 'Nowhere Man',
key: 'Nowhere Man'
},
{
label: 'Think For Yourself',
key: 'Think For Yourself'
},
{
label: 'The Word',
key: 'The Word'
},
{
label: 'Michelle',
key: 'Michelle',
disabled: true
},
{
label: 'What goes on',
key: 'What goes on'
},
{
label: 'Girl',
key: 'Girl'
},
{
label: "I'm looking through you",
key: "I'm looking through you"
},
{
label: 'In My Life',
key: 'In My Life'
},
{
label: 'Wait',
key: 'Wait'
}
]
},
{
label: 'Let It Be',
key: 'Let It Be Album',
children: [
{
label: 'Two Of Us',
key: 'Two Of Us'
},
{
label: 'Dig A Pony',
key: 'Dig A Pony'
},
{
label: 'Across The Universe',
key: 'Across The Universe'
},
{
label: 'I Me Mine',
key: 'I Me Mine'
},
{
label: 'Dig It',
key: 'Dig It'
},
{
label: 'Let It Be',
key: 'Let It Be'
},
{
label: 'Maggie Mae',
key: 'Maggie Mae'
},
{
label: "I've Got A Feeling",
key: "I've Got A Feeling"
},
{
label: 'One After 909',
key: 'One After 909'
},
{
label: 'The Long And Winding Road',
key: 'The Long And Winding Road'
},
{
label: 'For You Blue',
key: 'For You Blue'
},
{
label: 'Get Back',
key: 'Get Back'
}
]
}
]
}
}
})
</script>
```
### 单图上传 UploadImage
```vue
<template>
<UploadImage :maxNumber="1" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import UploadImage from '@/components/Upload/uploadImage.vue';
const value = ref(null);
</script>
```
### 多图上传 UploadImage
```vue
<template>
<UploadImage :maxNumber="10" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import UploadImage from '@/components/Upload/uploadImage.vue';
const value = ref(null);
</script>
```
### 单文件上传 UploadFile
```vue
<template>
<UploadFile :maxNumber="1" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import UploadFile from '@/components/Upload/uploadFile.vue';
const value = ref(null);
</script>
```
### 多文件上传 UploadFile
```vue
<template>
<UploadFile :maxNumber="10" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import UploadFile from '@/components/Upload/uploadFile.vue';
const value = ref(null);
</script>
```
### 文件选择器 FileChooser
- 基础用法
```vue
<template>
<FileChooser v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import FileChooser from '@/components/FileChooser/index.vue';
const value = ref(null);
</script>
```
- 指定fileType,支持多种选择器类型,默认情况是全部都可以选择
```ts
type FileType = 'image' | 'doc' | 'audio' | 'video' | 'zip' | 'other' | 'default';
```
- 图片选择器
```vue
<FileChooser v-model:value="value" fileType="image" />
```
- 多选支持,指定`maxNumber`多选数量
```vue
<FileChooser v-model:value="value" :maxNumber="10" fileType="image" />
```
### 大文件上传 MultipartUpload
- 基础用法
```vue
<template>
<MultipartUpload ref="multipartUploadRef" @onFinish="handleFinishCall" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import MultipartUpload from '@/components/Upload/multipartUpload.vue';
import { Attachment } from '@/components/FileChooser/src/model';
const multipartUploadRef = ref();
// 打开上传Modal
function handleMultipartUpload() {
multipartUploadRef.value.openModal();
}
// 上传成功回调附件内容
function handleFinishCall(result: Attachment, success: boolean) {
if (success) {
reloadTable();
}
}
</script>
```
### 开关 Switch
```vue
<template>
<n-switch v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 评分 Rate
```vue
<template>
<n-rate allow-half :default-value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const value = ref(null);
</script>
```
### 省市区选择器 CitySelector
```vue
<template>
<CitySelector v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import CitySelector from '@/components/CitySelector/citySelector.vue';
const value = ref(null);
</script>
```
### 图标选择器 IconSelector
```vue
<template>
<IconSelector style="width: 100%" v-model:value="value" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import IconSelector from '@/components/IconSelector/index.vue';
const value = ref(null);
</script>
```
更多组件请参考:https://www.naiveui.com/zh-CN/os-theme/components/button
+187
View File
@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1.0, shrink-to-fit=no, viewport-fit=cover">
<!-- Replace with your own title and description. -->
<title>HotGo-V2</title>
<meta name="description" content="基于全新GoFrame2+Vue3+NaiveUI+uniapp开发的全栖框架,为二次开发而生,适合中小型完整应用开发。">
<!-- Default Theme (see //docsify.js.org/#/themes) -->
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify@4/lib/themes/vue.css">
</head>
<body>
<div id="app"></div>
<script>
// Docsify Configuration (see //docsify.js.org/#/configuration)
window.$docsify = {
name: "HotGo-V2",
nameLink: {
'/': '/hotgo/',
},
relativePath: true,
// alias: {
// "/.*/_navbar.md": "/_navbar.md",
// },
// themeColor: "#42b983",
// logo: "//bufanyun.cn-bj.ufileos.com/hotgo/logo.sig.png",
// coverpage: true,
// homepage: "README.md",
// Sidebar Configuration
auto2top: true,
loadSidebar: "sidebar.md",
// maxLevel: 2,
// Set subMaxLevel to 0 to remove automatic display of page table of contents (TOC) in Sidebar
// subMaxLevel: 3,
// Navbar Configuration
// loadNavbar: true,
// Search Plugin Configuration
search: {
placeholder: {
"/": "搜索",
// "/": "Type to search"
},
noData: {
"/": "找不到结果",
// "/": "No Results"
},
// Headline depth, 1 - 6
// depth: 2,
},
// Flexible-alerts Plugin Configuration
"flexible-alerts": {
important: {
label: "Important",
// localization
label: {
// "/zh-cn": "重要",
"/": "Important"
},
// Assuming that we use Font Awesome
icon: "far fa-message",
className: "important"
},
warning: {
label: "Warning",
// localization
label: {
// "/zh-cn": "警告",
"/": "Warning"
},
// Assuming that we use Font Awesome
icon: "fas fa-triangle-exclamation",
className: "warning"
},
caution: {
label: "Caution",
// localization
label: {
// "/zh-cn": "注意",
"/": "Caution"
},
// Assuming that we use Font Awesome
icon: "fas fa-circle-exclamation",
className: "attention"
},
},
// Hide-code Plugin Configuration
hideCode: {
// scroll: false, // Enable scrolling
height: 300 // Max height
},
// Versioned Plugin Configuration
// versions: [
// { folder: "/", label: "v2", default: true },
// ],
// versionSelectorLabel: "Version",
// Progress Plugin Configuration
progress: {
position: "top",
// color: "var(--theme-color,#42b983)",
height: "3px",
},
};
</script>
<!-- Required -->
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/docsify.min.js"></script>
<!-- Recommended -->
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/zoom-image.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-copy-code/dist/docsify-copy-code.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-pagination/dist/docsify-pagination.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-hide-code/dist/docsify-hide-code.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-progress@latest/dist/progress.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/docsify-example-panels"></script>
<!-- Prism code highlight -->
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-bash.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1/components/prism-matlab.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-go.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-yaml.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-vue.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-json.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-sql.min.js"></script>
<!--mermaid插件-->
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
mermaid.initialize({ startOnLoad: true });
window.mermaid = mermaid;
</script>
<script src="//unpkg.com/docsify-mermaid@2.0.1/dist/docsify-mermaid.js"></script>
<!-- docsify-dark-switcher -->
<script src="//cdn.jsdelivr.net/gh/LIGMATV/docsify-dark-switcher@latest/docsify-dark-switcher.js"></script>
<style>
:root {
--dark-base-background: #222;
--dark-base-color: #bbc0c4;
--dark-theme-color: var(--theme-color, #42b983);
--dark-code-color: var(--dark-color);
--dark-heading-color: var(--dark-theme-color);
--dark-cover-background: #000000a8;
--dark-code-background: #303030;
--dark-tip-background: #2c0000;
--dark-warn-background: #005842;
--dark-icon-size: 25px;
--dark-icon-transition: .1s ease-in-out .1s;
--dark-moon-color: #000000;
--dark-sun-color: #ffffff;
}
</style>
<!-- docsify-plugin-flexible-alerts -->
<script src="//cdn.jsdelivr.net/npm/docsify-plugin-flexible-alerts/dist/docsify-plugin-flexible-alerts.min.js"></script>
<script src="//cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/js/all.min.js"></script>
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free/css/fontawesome.min.css">
<!-- docsify-versioned-plugin -->
<!-- <script src="//cdn.jsdelivr.net/npm/docsify-versioned-plugin@0.0.1/index.js"></script>
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify-versioned-plugin@0.0.1/styles.css"> -->
</body>
</html>
+1
View File
@@ -0,0 +1 @@
* linguist-language=GO
+39
View File
@@ -0,0 +1,39 @@
.buildpath
.hgignore.swp
.project
.orig
.swp
.idea/
.settings/
.vscode/
vendor/
composer.lock
pkg/
cbuild
**/.DS_Store
/test/
main
output/
manifest/config/config.yaml
manifest/output/
resource/log/
resource/public/admin/
resource/public/attachment/
storage/cache/
storage/diskqueue/
storage/cert/ssl/server.crt
storage/cert/ssl/server.key
storage/cert/pay/alipay/
storage/cert/pay/wxpay/
temp/
logs/
main.exe
main.exe~
*.exe
*.log
*.zip
.idea
!hotgo/
*.lock
/bin/package.sh
internal/packed/packed.go
+287
View File
@@ -0,0 +1,287 @@
## This file contains all available configuration options
## with their default values.
# See https://github.com/golangci/golangci-lint#config-file
# See https://golangci-lint.run/usage/configuration/
# Options for analysis running.
run:
# Exit code when at least one issue was found.
# Default: 1
issues-exit-code: 2
# Include test files or not.
# Default: true
tests: false
go: "1.20"
# Which dirs to skip: issues from them won't be reported.
# Can use regexp here: `generated.*`, regexp is applied on full path.
# Default value is empty list,
# but default dirs are skipped independently of this option's value (see skip-dirs-use-default).
# "/" will be replaced by current OS file path separator to properly work on Windows.
# skip-dirs:
# - internal/library/hggen/internal
# Which files to skip: they will be analyzed, but issues from them won't be reported.
# Default value is empty list,
# but there is no need to include all autogenerated files,
# we confidently recognize autogenerated files.
# If it's not please let us know.
# "/" will be replaced by current OS file path separator to properly work on Windows.
# skip-files: []
# Main linters configurations.
# See https://golangci-lint.run/usage/linters
linters:
# Disable all default enabled linters.
disable-all: true
# Custom enable linters we want to use.
enable:
- errcheck # Errcheck is a program for checking for unchecked errors in go programs.
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted.
- funlen # Tool for detection of long functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # Provides diagnostics that check for bugs, performance and style issues.
# - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- gosimple # Linter for Go source code that specializes in simplifying code
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- misspell # Finds commonly misspelled English words in comments
- nolintlint # Reports ill-formed or insufficient nolint directives
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
- staticcheck # It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary.
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library.
- whitespace # Tool for detection of leading and trailing whitespace
issues:
exclude-rules:
# helpers in tests often (rightfully) pass a *testing.T as their first argument
- path: _test\.go
text: "context.Context should be the first parameter of a function"
linters:
- revive
# Yes, they are, but it's okay in a test
- path: _test\.go
text: "exported func.*returns unexported type.*which can be annoying to use"
linters:
- revive
# https://github.com/go-critic/go-critic/issues/926
- linters:
- gocritic
text: "unnecessaryDefer:"
exclude-dirs:
- internal/library/hggen/internal
exclude-files: []
# https://golangci-lint.run/usage/linters
linters-settings:
# https://golangci-lint.run/usage/linters/#misspell
misspell:
locale: US
ignore-words:
- cancelled
# https://golangci-lint.run/usage/linters/#revive
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
revive:
ignore-generated-header: true
severity: error
rules:
- name: atomic
- name: line-length-limit
severity: error
arguments: [ 1024 ]
- name: unhandled-error
severity: warning
disabled: true
arguments: []
- name: var-naming
severity: warning
disabled: true
arguments:
# AllowList
- [ "ID","URL","IP","HTTP","JSON","API","UID","Id","Api","Uid","Http","Json","Ip","Url" ]
# DenyList
- [ "VM" ]
- name: string-format
severity: warning
disabled: false
arguments:
- - 'core.WriteError[1].Message'
- '/^([^A-Z]|$)/'
- must not start with a capital letter
- - 'fmt.Errorf[0]'
- '/(^|[^\.!?])$/'
- must not end in punctuation
- - panic
- '/^[^\n]*$/'
- must not contain line breaks
- name: function-result-limit
severity: warning
disabled: false
arguments: [ 4 ]
# https://golangci-lint.run/usage/linters/#funlen
funlen:
# Checks the number of lines in a function.
# If lower than 0, disable the check.
# Default: 60
lines: 330
# Checks the number of statements in a function.
# If lower than 0, disable the check.
# Default: 40
statements: -1
# https://golangci-lint.run/usage/linters/#goconst
goconst:
# Minimal length of string constant.
# Default: 3
min-len: 4
# Minimum occurrences of constant string count to trigger issue.
# Default: 3
# For subsequent optimization, the value is reduced.
min-occurrences: 30
# Ignore test files.
# Default: false
ignore-tests: true
# Look for existing constants matching the values.
# Default: true
match-constant: false
# Search also for duplicated numbers.
# Default: false
numbers: true
# Minimum value, only works with goconst.numbers
# Default: 3
min: 5
# Maximum value, only works with goconst.numbers
# Default: 3
max: 20
# Ignore when constant is not used as function argument.
# Default: true
ignore-calls: false
# https://golangci-lint.run/usage/linters/#gocritic
gocritic:
disabled-checks:
- ifElseChain
- assignOp
- appendAssign
- singleCaseSwitch
- regexpMust
- typeSwitchVar
- elseif
# https://golangci-lint.run/usage/linters/#gosimple
gosimple:
# Select the Go version to target.
# Default: 1.13
# Deprecated: use the global `run.go` instead.
# go: "1.15"
# Sxxxx checks in https://staticcheck.io/docs/configuration/options/#checks
# Default: ["*"]
checks: [
"all", "-S1000", "-S1001", "-S1002", "-S1008", "-S1009", "-S1016", "-S1023", "-S1025", "-S1029", "-S1034", "-S1040"
]
# https://golangci-lint.run/usage/linters/#govet
govet:
# Report about shadowed variables.
# Default: false
# check-shadowing: true
# Settings per analyzer.
settings:
# Analyzer name, run `go tool vet help` to see all analyzers.
printf:
# Comma-separated list of print function names to check (in addition to default, see `go tool vet help printf`).
# Default: []
funcs:
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
# shadow:
# Whether to be strict about shadowing; can be noisy.
# Default: false
# strict: false
unusedresult:
# Comma-separated list of functions whose results must be used
# (in addition to defaults context.WithCancel,context.WithDeadline,context.WithTimeout,context.WithValue,
# errors.New,fmt.Errorf,fmt.Sprint,fmt.Sprintf,sort.Reverse)
# Default []
funcs:
- pkg.MyFunc
- context.WithCancel
# Comma-separated list of names of methods of type func() string whose results must be used
# (in addition to default Error,String)
# Default []
stringmethods:
- MyMethod
# Enable all analyzers.
# Default: false
enable-all: true
# Disable analyzers by name.
# Run `go tool vet help` to see all analyzers.
# Default: []
disable:
- asmdecl
- assign
- atomic
- atomicalign
- bools
- buildtag
- cgocall
- composites
- copylocks
- deepequalerrors
- errorsas
- fieldalignment
- findcall
- framepointer
- httpresponse
- ifaceassert
- loopclosure
- lostcancel
- nilfunc
- nilness
- reflectvaluecompare
- shift
- shadow
- sigchanyzer
- sortslice
- stdmethods
- stringintconv
- structtag
- testinggoroutine
- tests
- unmarshal
- unreachable
- unsafeptr
- unusedwrite
# https://golangci-lint.run/usage/linters/#staticcheck
staticcheck:
# Select the Go version to target.
# Default: "1.13"
# Deprecated: use the global `run.go` instead.
# go: "1.15"
# SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks
# Default: ["*"]
checks: [ "all","-SA1019","-SA4015","-SA1029","-SA1016","-SA9003","-SA4006","-SA6003" ]
# https://golangci-lint.run/usage/linters/#gofmt
gofmt:
# Simplify code: gofmt with `-s` option.
# Default: true
simplify: true
# Apply the rewrite rules to the source before reformatting.
# https://pkg.go.dev/cmd/gofmt
# Default: []
rewrite-rules: [ ]
# - pattern: 'interface{}'
# replacement: 'any'
# - pattern: 'a[b:len(a)]'
# replacement: 'a[b:]'
+113
View File
@@ -0,0 +1,113 @@
ROOT_DIR = $(shell pwd)
NAMESPACE = "default"
DEPLOY_NAME = "template-single"
DOCKER_NAME = "template-single"
ADMIN_RESOURCE_PATH = "/resource/public/admin/"
# 一键编译。编译web前端,并将编译后的包移动到服务端对应静态资源路径下,最后编译服务端
.PHONY: build
build:
@rm -rf ./$(ADMIN_RESOURCE_PATH)
@mkdir ./$(ADMIN_RESOURCE_PATH)
@cd ../web && pnpm run build && \cp -rf ./dist/* ../server$(ADMIN_RESOURCE_PATH)
@cd ../server
@echo "y" | gf build
# 通过热编译启动所有服务
.PHONY: all
all:
gf run main.go --args "all"
.PHONY: http
http:
gf run main.go --args "http"
.PHONY: queue
queue:
gf run main.go --args "queue"
.PHONY: cron
cron:
gf run main.go --args "cron"
.PHONY: auth
auth:
gf run main.go --args "auth"
# 启动web服务
.PHONY: web
web:
@cd ../web && pnpm run dev
# 刷新casbin权限
.PHONY: refresh
refresh:
@go run main.go tools -m=casbin -a1=refresh
# 清理casbin权限
.PHONY: clear
clear:
@go run main.go tools -m=casbin -a1=clear
# 运行代码质量分析工具
# https://github.com/ywanbing/golangci
.PHONY: lint
lint:
golangci-lint run
.PHONY: killmain
killmain:
@kill -9 $(ps -ef|grep main|grep -v grep|awk '{print $2}')
# Install/Update to the latest CLI tool.
.PHONY: cli
cli:
@set -e; \
wget -O gf https://github.com/gogf/gf/releases/latest/download/gf_$(shell go env GOOS)_$(shell go env GOARCH) && \
chmod +x gf && \
./gf install -y && \
rm ./gf
# Check and install CLI tool.
.PHONY: cli.install
cli.install:
@set -e; \
gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \
echo "GoFame CLI is not installed, start proceeding auto installation..."; \
make cli; \
fi;
# Generate Go files for DAO/DO/Entity.
.PHONY: dao
dao: cli.install
@gf gen dao
# Generate Go files for Service.
.PHONY: service
service: cli.install
@gf gen service
# Build image, deploy image and yaml to current kubectl environment and make port forward to local machine.
.PHONY: start
start:
@set -e; \
make image; \
make deploy; \
make port;
# Build docker image and commit to the repository.
# example: make image tag=v0.0.1
.PHONY: image
image:
@echo "y" | gf docker main.go -p -tn hotgo:$(tag)
# Deploy image and yaml to current kubectl environment.
.PHONY: deploy
deploy:
$(eval _TAG = $(if ${TAG}, ${TAG}, develop))
@set -e; \
mkdir -p $(ROOT_DIR)/temp/kustomize;\
cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_TAG};\
kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\
kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \
kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}";
+1
View File
@@ -0,0 +1 @@
## HotGo 管理后台服务端
+6
View File
@@ -0,0 +1,6 @@
// Package addons
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package addons
+39
View File
@@ -0,0 +1,39 @@
## 功能案例
### 简介
系统的一些功能案例
### 使用说明
系统自带的功能使用示例及其说明,包含一些简单的交互
### 安装
1、安装 HotGo (2.11.4及以上)
项目介绍:https://github.com/bufanyun/hotgo
2、将当前插件项目拷贝进 HotGo 根目录的 server/addons 目录下
3、在 HotGo 根目录的 server/addons/modules 目录下创建go文件:hgexample.go,内容如下:
```go
package modules
import _ "hotgo/addons/hgexample"
```
4、HotGo 后台进入 开发工具->插件管理->找到 功能案例 (hgexample) 进行安装
5、重启服务即可生效
### 常用命令行
```shell
# 接口维护-gen service
gf gen service -s=addons/hgexample/logic -d=addons/hgexample/service
```
@@ -0,0 +1,26 @@
// Package comp
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package comp
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
// ImportExcelReq 导入Excel
type ImportExcelReq struct {
g.Meta `path:"/comp/importExcel" method:"post" tags:"功能案例" summary:"导入Excel"`
File *ghttp.UploadFile `json:"file" type:"file" dc:"分片文件"`
}
type ImportExcelSheet struct {
Sheet string `json:"sheet"`
Rows [][]string `json:"rows"`
}
type ImportExcelRes struct {
Sheets []*ImportExcelSheet `json:"sheets"`
}
@@ -0,0 +1,30 @@
// Package config
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package config
import (
"github.com/gogf/gf/v2/frame/g"
"hotgo/addons/hgexample/model/input/sysin"
)
// GetReq 获取指定分组的配置
type GetReq struct {
g.Meta `path:"/config/get" method:"get" tags:"配置" summary:"获取指定分组的配置"`
sysin.GetConfigInp
}
type GetRes struct {
*sysin.GetConfigModel
}
// UpdateReq 获取指定分组的配置
type UpdateReq struct {
g.Meta `path:"/config/update" method:"post" tags:"配置" summary:"获取指定分组的配置"`
sysin.UpdateConfigInp
}
type UpdateRes struct {
}
@@ -0,0 +1,21 @@
// Package index
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package index
import (
"github.com/gogf/gf/v2/frame/g"
"hotgo/addons/hgexample/model/input/sysin"
)
// TestReq 测试
type TestReq struct {
g.Meta `path:"/index/test" method:"get" tags:"功能案例" summary:"测试"`
sysin.IndexTestInp
}
type TestRes struct {
*sysin.IndexTestModel
}
@@ -0,0 +1,83 @@
// Package table
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package table
import (
"github.com/gogf/gf/v2/frame/g"
"hotgo/addons/hgexample/model/input/sysin"
"hotgo/internal/model/input/form"
)
// ListReq 查询列表
type ListReq struct {
g.Meta `path:"/table/list" method:"get" tags:"表格例子" summary:"获取表格列表"`
sysin.TableListInp
}
type ListRes struct {
form.PageRes
List []*sysin.TableListModel `json:"list" dc:"数据列表"`
}
// ExportReq 导出列表
type ExportReq struct {
g.Meta `path:"/table/export" method:"get" tags:"表格例子" summary:"导出表格列表"`
sysin.TableListInp
}
type ExportRes struct{}
// ViewReq 获取信息
type ViewReq struct {
g.Meta `path:"/table/view" method:"get" tags:"表格例子" summary:"获取指定信息"`
sysin.TableViewInp
}
type ViewRes struct {
*sysin.TableViewModel
}
// EditReq 修改/新增
type EditReq struct {
g.Meta `path:"/table/edit" method:"post" tags:"表格例子" summary:"修改/新增表格"`
sysin.TableEditInp
}
type EditRes struct{}
// DeleteReq 删除
type DeleteReq struct {
g.Meta `path:"/table/delete" method:"post" tags:"表格例子" summary:"删除表格"`
sysin.TableDeleteInp
}
type DeleteRes struct{}
// MaxSortReq 最大排序
type MaxSortReq struct {
g.Meta `path:"/table/maxSort" method:"get" tags:"表格例子" summary:"表格最大排序"`
sysin.TableMaxSortInp
}
type MaxSortRes struct {
*sysin.TableMaxSortModel
}
// StatusReq 更新状态
type StatusReq struct {
g.Meta `path:"/table/status" method:"post" tags:"表格例子" summary:"更新表格状态"`
sysin.TableStatusInp
}
type StatusRes struct{}
// SwitchReq 更新开关状态
type SwitchReq struct {
g.Meta `path:"/table/switch" method:"post" tags:"表格例子" summary:"更新表格状态"`
sysin.TableSwitchInp
}
type SwitchRes struct{}
@@ -0,0 +1,59 @@
// Package tenantorder
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2024 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
// @AutoGenerate Version 2.13.1
package tenantorder
import (
"hotgo/addons/hgexample/model/input/sysin"
"hotgo/internal/model/input/form"
"github.com/gogf/gf/v2/frame/g"
)
// ListReq 查询多租户功能演示列表
type ListReq struct {
g.Meta `path:"/tenantOrder/list" method:"get" tags:"多租户功能演示" summary:"获取多租户功能演示列表"`
sysin.TenantOrderListInp
}
type ListRes struct {
form.PageRes
List []*sysin.TenantOrderListModel `json:"list" dc:"数据列表"`
}
// ExportReq 导出多租户功能演示列表
type ExportReq struct {
g.Meta `path:"/tenantOrder/export" method:"get" tags:"多租户功能演示" summary:"导出多租户功能演示列表"`
sysin.TenantOrderListInp
}
type ExportRes struct{}
// ViewReq 获取多租户功能演示指定信息
type ViewReq struct {
g.Meta `path:"/tenantOrder/view" method:"get" tags:"多租户功能演示" summary:"获取多租户功能演示指定信息"`
sysin.TenantOrderViewInp
}
type ViewRes struct {
*sysin.TenantOrderViewModel
}
// EditReq 修改/新增多租户功能演示
type EditReq struct {
g.Meta `path:"/tenantOrder/edit" method:"post" tags:"多租户功能演示" summary:"修改/新增多租户功能演示"`
sysin.TenantOrderEditInp
}
type EditRes struct{}
// DeleteReq 删除多租户功能演示
type DeleteReq struct {
g.Meta `path:"/tenantOrder/delete" method:"post" tags:"多租户功能演示" summary:"删除多租户功能演示"`
sysin.TenantOrderDeleteInp
}
type DeleteRes struct{}
@@ -0,0 +1,92 @@
// Package treetable
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
package treetable
import (
"github.com/gogf/gf/v2/frame/g"
"hotgo/addons/hgexample/model/input/sysin"
"hotgo/internal/model/input/form"
)
// ListReq 查询列表
type ListReq struct {
g.Meta `path:"/treeTable/list" method:"get" tags:"树形表格例子" summary:"获取表格列表"`
sysin.TreeTableListInp
}
type ListRes struct {
form.PageRes
List []*sysin.TreeTableListModel `json:"list" dc:"数据列表"`
}
// ExportReq 导出列表
type ExportReq struct {
g.Meta `path:"/treeTable/export" method:"get" tags:"树形表格例子" summary:"导出表格列表"`
sysin.TableListInp
}
type ExportRes struct{}
// ViewReq 获取信息
type ViewReq struct {
g.Meta `path:"/treeTable/view" method:"get" tags:"树形表格例子" summary:"获取指定信息"`
sysin.TableViewInp
}
type ViewRes struct {
*sysin.TableViewModel
}
// EditReq 修改/新增
type EditReq struct {
g.Meta `path:"/treeTable/edit" method:"post" tags:"树形表格例子" summary:"修改/新增表格"`
sysin.TableEditInp
}
type EditRes struct{}
// DeleteReq 删除
type DeleteReq struct {
g.Meta `path:"/treeTable/delete" method:"post" tags:"树形表格例子" summary:"删除表格"`
sysin.TableDeleteInp
}
type DeleteRes struct{}
// MaxSortReq 最大排序
type MaxSortReq struct {
g.Meta `path:"/treeTable/maxSort" method:"get" tags:"树形表格例子" summary:"表格最大排序"`
sysin.TableMaxSortInp
}
type MaxSortRes struct {
*sysin.TableMaxSortModel
}
// StatusReq 更新状态
type StatusReq struct {
g.Meta `path:"/treeTable/status" method:"post" tags:"树形表格例子" summary:"更新表格状态"`
sysin.TableStatusInp
}
type StatusRes struct{}
// SwitchReq 更新开关状态
type SwitchReq struct {
g.Meta `path:"/treeTable/switch" method:"post" tags:"树形表格例子" summary:"更新表格状态"`
sysin.TableSwitchInp
}
type SwitchRes struct{}
// SelectReq 树形选项
type SelectReq struct {
g.Meta `path:"/treeTable/select" method:"get" tags:"树形表格例子" summary:"树形选项"`
}
type SelectRes struct {
List []*sysin.TableTree `json:"list" dc:"数据列表"`
}

Some files were not shown because too many files have changed in this diff Show More