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
+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