This commit is contained in:
孟帅
2024-03-07 20:08:56 +08:00
parent 6dd8cbadad
commit 0fbc1ad47c
246 changed files with 9441 additions and 2293 deletions

View File

@@ -26,7 +26,7 @@
- [消息队列](sys-queue.md)
- [功能扩展库](sys-library.md)
- [工具方法](sys-utility.md)
- Websocket服务器
- [WebSocket服务器](sys-websocket-server.md)
- [TCP服务器](sys-tcp-server.md)
- [单元测试](sys-test.md)
@@ -40,8 +40,7 @@
### 前端开发
- [表单组件](web-form.md)
- Websocket客户端
- 工具库
- [WebSocket客户端](sys-websocket-client.md)
- [独立部署](web-deploy.md)

View File

@@ -19,8 +19,9 @@
1. /server/addons/hgexample/ # 插件模块目录
2. /server/addons/modules/hgexample.go # 隐式注册插件文件
3. /web/src/api/addons/hgexample # webApi目录
4. /web/src/views/addons/hgexample # web页面目录
3. /server/resource/addons/hgexample # 静态资源和页面模板目录,属于扩展功能选项,勾选对应选项后才会生成
4. /web/src/api/addons/hgexample # webApi目录
5. /web/src/views/addons/hgexample # web页面目录
# 默认情况下没有为web页面生成菜单权限因为在实际场景中插件不一定需要用到web页面所以如有需要请手动到后台 权限管理 -> 菜单权限->自行添加菜单和配置权限
```

View File

@@ -30,13 +30,13 @@ func (s *Skeleton) GetModule() Module {
// Module 插件模块
type Module interface {
Init(ctx context.Context) // 初始化
InitRouter(ctx context.Context, group *ghttp.RouterGroup) // 初始化并注册路由
Ctx() context.Context // 上下文
GetSkeleton() *Skeleton // 架子
Install(ctx context.Context) error // 安装模块
Upgrade(ctx context.Context) error // 更新模块
UnInstall(ctx context.Context) error // 卸载模块
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) // 卸载模块
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

View File

@@ -26,6 +26,6 @@
> 需要本地具有 git node golang 环境
- node版本 >= 16.0.0
- golang版本 >= v1.19
- mysql 引擎需要是 innoDB
- golang版本 >= 1.19
- mysql版本 >= 5.7引擎需要是 innoDB
- IDE推荐Goland

View File

@@ -9,7 +9,7 @@
- node版本 >= v16.0.0
- golang版本 >= v1.19
- goframe版本 >=v2.6.1
- goframe版本 >=v2.6.4
- mysql版本 >=5.7
> 必须先看[环境搭建文档](start-environment.md),如果安装遇到问题务必先查看[常见问题文档](start-issue.md)

View File

@@ -11,6 +11,20 @@
> 如果升级(覆盖)代码后打开会出现 sql 报错, 请检查更新的数据库格式或自行调整
### v2.13.1
updated 2024.3.7
- 增加:增加内置数据字典类型:`枚举字典``自定义方法字典`,支持代码生成时关联选项使用
- 增加:增加大文件上传,支持分片上传、断点续传,存储驱动已适配`本地存储`
- 增加:插件模块增加停止服务回调接口,调整静态资源默认存放位置,创建插件选项增加可选扩展功能
- 增加:功能案例插件增加`30+`常用组件示例,增加`websocket`消息收发测试
- 增加:文档增`加功能扩展库``websocket服务器``websocket客户端`使用说明,当前版本文档已完善
- 修复:修复省市区无法添加地区问题
- 优化gf版本升级到v2.6.4
- 优化:优化缓存组件依赖关系
- 优化:调整部分前端表格自适应宽度
- 优化HTTP错误码接管统一改为由响应中间件处理
### v2.12.1
updated 2023.12.29

View File

@@ -105,7 +105,7 @@ graph TD
#### 如何区分部门和下级用户?
- 在实际使用时,部门更多的是在公司或机构中使用,可以通过在 组织管理 -> 后台用户 ->为用户绑定部门
- 下级用户在代理商或分销系统中比较常见,后台用户由谁添加的,那么被添加的用户就是其下级用户。后续也将开放邀请码绑定下级功能。
- 下级用户在代理商或分销系统中比较常见,后台用户由谁添加的,那么被添加的用户就是其下级用户
#### 如何判断数据是谁的?

View File

@@ -5,6 +5,7 @@
- 缓存驱动
- 请求上下文
- JWT
- 数据字典
- 地理定位(待写)
- 通知(待写)
@@ -151,6 +152,159 @@ func test(ctx context.Context) {
```
### 数据字典
- 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
// 待写

View File

@@ -55,7 +55,7 @@ func main() {
### 注册支付回调
- 在文件`server/internal/global/pay.go` 加入你的业务订单分组回调方法,当订单支付成功验签通过后会自动进行回调,参考以下:
- 在文件`server/internal/logic/pay/notify.go` 加入你的业务订单分组回调方法,当订单支付成功验签通过后会自动进行回调,参考以下:
```go
package global
@@ -66,12 +66,13 @@ import (
"hotgo/internal/service"
)
// 注册支付成功回调方法
func payNotifyCall() {
payment.RegisterNotifyCall(consts.OrderGroupAdminOrder, service.AdminOrder().PayNotify) // 后台充值订单
// ...
// RegisterNotifyCall 注册支付成功回调方法
func (s *sPay) RegisterNotifyCall() {
payment.RegisterNotifyCallMap(map[string]payment.NotifyCallFunc{
consts.OrderGroupAdminOrder: service.AdminOrder().PayNotify, // 后台充值订单
// ...
})
}
```
### 订单退款

View File

@@ -9,7 +9,7 @@
- 服务认证
- 更多
> HotGo基于GF框架的TCP服务器组件提供了一个简单而灵活的方式快速搭建基于TCP的服务应用。集成了许多常用功能如长连接、服务认证、路由分发、RPC消息、拦截器和数据绑定等大大简化和规范了服务器开发流程。
> HotGo基于GoFrame的TCP服务器组件提供了一个简单而灵活的方式快速搭建基于TCP的服务应用。集成了许多常用功能如长连接、服务认证、路由分发、RPC消息、拦截器和数据绑定等大大简化和规范了服务器开发流程。
### 配置文件
- 配置文件server/manifest/config/config.yaml

View File

@@ -1,3 +1,3 @@
## WebHook
待写
请参考https://goframe.org/pages/viewpage.action?pageId=1114387

View File

@@ -0,0 +1,210 @@
## 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);
```

View File

@@ -0,0 +1,143 @@
## 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那么需要对身份验证中间件进行修改。然而这样做会降低连接的安全性并且无法应用于需要确定用户身份的情景因此并不建议采取这种策略

View File

@@ -21,6 +21,7 @@
- 单文件上传 UploadFile
- 多文件上传 UploadFile
- 文件选择器 FileChooser
- 大文件上传 MultipartUpload
- 开关 Switch
- 评分 Rate
- 省市区选择器 CitySelector
@@ -795,6 +796,33 @@ type FileType = 'image' | 'doc' | 'audio' | 'video' | 'zip' | 'other' | 'default
<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>