From 92f5e576143df8b4798afbce23646aac0c057d56 Mon Sep 17 00:00:00 2001 From: 0016 <0016@drgyen.cn> Date: Tue, 23 Jul 2024 13:50:17 +0800 Subject: [PATCH] =?UTF-8?q?docs:=E5=88=9D=E5=A7=8B=E5=8C=96go-view-server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 1 + .gitignore | 22 ++ LICENSE | 21 ++ Makefile | 16 + README.MD | 215 ++++++++++++ api/project/project.go | 22 ++ api/project/v1/project.go | 84 +++++ api/site/site.go | 19 ++ api/site/v1/site.go | 56 +++ go.mod | 43 +++ go.sum | 98 ++++++ hack/config.yaml | 30 ++ hack/hack-cli.mk | 19 ++ hack/hack.mk | 75 ++++ internal/cmd/cmd.go | 54 +++ internal/consts/app.go | 12 + internal/consts/cache.go | 12 + internal/consts/context.go | 14 + internal/consts/debris.go | 14 + internal/consts/error.go | 35 ++ internal/consts/http.go | 19 ++ internal/consts/status.go | 14 + internal/consts/upload.go | 12 + internal/controller/project/project.go | 5 + internal/controller/project/project_new.go | 15 + .../controller/project/project_v1_create.go | 14 + .../controller/project/project_v1_delete.go | 14 + .../controller/project/project_v1_edit.go | 13 + .../controller/project/project_v1_get_data.go | 14 + .../controller/project/project_v1_list.go | 26 ++ .../controller/project/project_v1_publish.go | 13 + .../project/project_v1_save_data.go | 13 + .../controller/project/project_v1_upload.go | 18 + internal/controller/site/site.go | 5 + internal/controller/site/site_new.go | 15 + .../controller/site/site_v1_account_login.go | 19 ++ .../controller/site/site_v1_get_oss_info.go | 14 + .../controller/site/site_v1_login_logout.go | 14 + internal/controller/site/site_v1_register.go | 13 + internal/controller/site/site_v1_test_data.go | 48 +++ internal/dao/admin_attachment.go | 27 ++ internal/dao/admin_member.go | 27 ++ internal/dao/admin_project.go | 27 ++ internal/dao/internal/admin_attachment.go | 105 ++++++ internal/dao/internal/admin_member.go | 93 +++++ internal/dao/internal/admin_project.go | 91 +++++ internal/library/cache/cache.go | 58 ++++ internal/library/cache/file/file.go | 322 ++++++++++++++++++ internal/library/contexts/context.go | 120 +++++++ internal/library/contexts/detached.go | 35 ++ internal/library/response/response.go | 116 +++++++ internal/library/storager/config.go | 30 ++ internal/library/storager/mime.go | 232 +++++++++++++ internal/library/storager/model.go | 17 + internal/library/storager/upload.go | 236 +++++++++++++ internal/library/storager/upload_local.go | 47 +++ internal/library/token/token.go | 313 +++++++++++++++++ internal/logic/.gitkeep | 0 internal/logic/admin/project.go | 167 +++++++++ internal/logic/admin/site.go | 125 +++++++ internal/logic/logic.go | 10 + internal/logic/middleware/admin_auth.go | 35 ++ internal/logic/middleware/init.go | 123 +++++++ internal/logic/middleware/pre_filter.go | 102 ++++++ internal/logic/middleware/response.go | 123 +++++++ internal/model/.gitkeep | 0 internal/model/config_load.go | 28 ++ internal/model/context.go | 30 ++ internal/model/do/.gitkeep | 0 internal/model/do/admin_attachment.go | 32 ++ internal/model/do/admin_member.go | 26 ++ internal/model/do/admin_project.go | 25 ++ internal/model/entity/.gitkeep | 0 internal/model/entity/admin_attachment.go | 30 ++ internal/model/entity/admin_member.go | 24 ++ internal/model/entity/admin_project.go | 23 ++ internal/model/input/adminin/project.go | 127 +++++++ internal/model/input/adminin/site.go | 61 ++++ internal/model/input/form/page.go | 67 ++++ internal/model/response.go | 16 + internal/packed/packed.go | 1 + internal/service/.gitkeep | 0 internal/service/admin.go | 66 ++++ internal/service/middleware.go | 57 ++++ main.go | 18 + manifest/config/config.yaml | 103 ++++++ .../deploy/kustomize/base/deployment.yaml | 21 ++ .../deploy/kustomize/base/kustomization.yaml | 8 + manifest/deploy/kustomize/base/service.yaml | 12 + .../kustomize/overlays/develop/configmap.yaml | 14 + .../overlays/develop/deployment.yaml | 10 + .../overlays/develop/kustomization.yaml | 14 + manifest/docker/Dockerfile | 16 + manifest/docker/docker.sh | 8 + manifest/i18n/.gitkeep | 0 manifest/protobuf/.keep-if-necessary | 0 resource/public/html/.gitkeep | 0 resource/public/plugin/.gitkeep | 0 resource/public/resource/css/.gitkeep | 0 resource/public/resource/image/.gitkeep | 0 resource/public/resource/js/.gitkeep | 0 resource/template/.gitkeep | 0 storage/data/goview.sql | 152 +++++++++ utility/.gitkeep | 0 utility/charset/charset.go | 74 ++++ utility/format/format.go | 73 ++++ utility/simple/simple.go | 78 +++++ utility/url/url.go | 74 ++++ utility/validate/filter.go | 32 ++ utility/validate/filter_test.go | 67 ++++ utility/validate/include.go | 38 +++ utility/validate/validate.go | 192 +++++++++++ utility/validate/validate_test.go | 11 + 113 files changed, 5389 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.MD create mode 100644 api/project/project.go create mode 100644 api/project/v1/project.go create mode 100644 api/site/site.go create mode 100644 api/site/v1/site.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hack/config.yaml create mode 100644 hack/hack-cli.mk create mode 100644 hack/hack.mk create mode 100644 internal/cmd/cmd.go create mode 100644 internal/consts/app.go create mode 100644 internal/consts/cache.go create mode 100644 internal/consts/context.go create mode 100644 internal/consts/debris.go create mode 100644 internal/consts/error.go create mode 100644 internal/consts/http.go create mode 100644 internal/consts/status.go create mode 100644 internal/consts/upload.go create mode 100644 internal/controller/project/project.go create mode 100644 internal/controller/project/project_new.go create mode 100644 internal/controller/project/project_v1_create.go create mode 100644 internal/controller/project/project_v1_delete.go create mode 100644 internal/controller/project/project_v1_edit.go create mode 100644 internal/controller/project/project_v1_get_data.go create mode 100644 internal/controller/project/project_v1_list.go create mode 100644 internal/controller/project/project_v1_publish.go create mode 100644 internal/controller/project/project_v1_save_data.go create mode 100644 internal/controller/project/project_v1_upload.go create mode 100644 internal/controller/site/site.go create mode 100644 internal/controller/site/site_new.go create mode 100644 internal/controller/site/site_v1_account_login.go create mode 100644 internal/controller/site/site_v1_get_oss_info.go create mode 100644 internal/controller/site/site_v1_login_logout.go create mode 100644 internal/controller/site/site_v1_register.go create mode 100644 internal/controller/site/site_v1_test_data.go create mode 100644 internal/dao/admin_attachment.go create mode 100644 internal/dao/admin_member.go create mode 100644 internal/dao/admin_project.go create mode 100644 internal/dao/internal/admin_attachment.go create mode 100644 internal/dao/internal/admin_member.go create mode 100644 internal/dao/internal/admin_project.go create mode 100644 internal/library/cache/cache.go create mode 100644 internal/library/cache/file/file.go create mode 100644 internal/library/contexts/context.go create mode 100644 internal/library/contexts/detached.go create mode 100644 internal/library/response/response.go create mode 100644 internal/library/storager/config.go create mode 100644 internal/library/storager/mime.go create mode 100644 internal/library/storager/model.go create mode 100644 internal/library/storager/upload.go create mode 100644 internal/library/storager/upload_local.go create mode 100644 internal/library/token/token.go create mode 100644 internal/logic/.gitkeep create mode 100644 internal/logic/admin/project.go create mode 100644 internal/logic/admin/site.go create mode 100644 internal/logic/logic.go create mode 100644 internal/logic/middleware/admin_auth.go create mode 100644 internal/logic/middleware/init.go create mode 100644 internal/logic/middleware/pre_filter.go create mode 100644 internal/logic/middleware/response.go create mode 100644 internal/model/.gitkeep create mode 100644 internal/model/config_load.go create mode 100644 internal/model/context.go create mode 100644 internal/model/do/.gitkeep create mode 100644 internal/model/do/admin_attachment.go create mode 100644 internal/model/do/admin_member.go create mode 100644 internal/model/do/admin_project.go create mode 100644 internal/model/entity/.gitkeep create mode 100644 internal/model/entity/admin_attachment.go create mode 100644 internal/model/entity/admin_member.go create mode 100644 internal/model/entity/admin_project.go create mode 100644 internal/model/input/adminin/project.go create mode 100644 internal/model/input/adminin/site.go create mode 100644 internal/model/input/form/page.go create mode 100644 internal/model/response.go create mode 100644 internal/packed/packed.go create mode 100644 internal/service/.gitkeep create mode 100644 internal/service/admin.go create mode 100644 internal/service/middleware.go create mode 100644 main.go create mode 100644 manifest/config/config.yaml create mode 100644 manifest/deploy/kustomize/base/deployment.yaml create mode 100644 manifest/deploy/kustomize/base/kustomization.yaml create mode 100644 manifest/deploy/kustomize/base/service.yaml create mode 100644 manifest/deploy/kustomize/overlays/develop/configmap.yaml create mode 100644 manifest/deploy/kustomize/overlays/develop/deployment.yaml create mode 100644 manifest/deploy/kustomize/overlays/develop/kustomization.yaml create mode 100644 manifest/docker/Dockerfile create mode 100644 manifest/docker/docker.sh create mode 100644 manifest/i18n/.gitkeep create mode 100644 manifest/protobuf/.keep-if-necessary create mode 100644 resource/public/html/.gitkeep create mode 100644 resource/public/plugin/.gitkeep create mode 100644 resource/public/resource/css/.gitkeep create mode 100644 resource/public/resource/image/.gitkeep create mode 100644 resource/public/resource/js/.gitkeep create mode 100644 resource/template/.gitkeep create mode 100644 storage/data/goview.sql create mode 100644 utility/.gitkeep create mode 100644 utility/charset/charset.go create mode 100644 utility/format/format.go create mode 100644 utility/simple/simple.go create mode 100644 utility/url/url.go create mode 100644 utility/validate/filter.go create mode 100644 utility/validate/filter_test.go create mode 100644 utility/validate/include.go create mode 100644 utility/validate/validate.go create mode 100644 utility/validate/validate_test.go diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1fbf887 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* linguist-language=GO \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4588bbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +.buildpath +.hgignore.swp +.project +.orig +.swp +.idea/ +.settings/ +.vscode/ +bin/ +**/.DS_Store +gf +main +main.exe +main.exe~ +output/ +manifest/output/ +temp/ +temp.yaml +bin +resource/public/attachment +storage/cache +logs/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f87ec73 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-present HotGo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0866e43 --- /dev/null +++ b/Makefile @@ -0,0 +1,16 @@ +ROOT_DIR = $(shell pwd) +NAMESPACE = "default" +DEPLOY_NAME = "template-single" +DOCKER_NAME = "template-single" + +include ./hack/hack.mk + + +# 通过热编译启动所有服务 +.PHONY: all +all: + gf run main.go --args "all" + +.PHONY: http +http: + gf run main.go \ No newline at end of file diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..6cf8784 --- /dev/null +++ b/README.MD @@ -0,0 +1,215 @@ +## 项目介绍 + +- 本项目是由 `GoLang` 实现的 `GoView` 后端接口,基于 `GoFrame` 开发 +- `GoView` 是一个高效的拖拽式低代码数据可视化开发平台,将图表或页面元素封装为基础组件,无需编写代码即可制作数据大屏,减少心智负担。当然低代码也不是 “银弹”,希望所有人员都能理智看待此技术。 +- 如果在使用中遇到问题,请提交 [issues](https://gitee.com/bufanyun/go-view-server/issues/new) 或联系作者 +- 作者QQ:133814250 + + +### 主要技术栈 + +| 名称 | 版本 | +| ------------------- | ------ | +| goframe | 2.6.3 | +| golang-jwt | 5.2.0 | +| 详见 `go.mod` | 😁 | + + +### 运行环境 +1. 下载golang安装 版本号需>=1.19 +2. 国际: https://golang.org/dl/ +3. 国内: https://golang.google.cn/dl/ +4. 命令行运行 go 若控制台输出各类提示命令 则安装成功 输入 `go version` 确认版本大于1.19 +5. 开发工具推荐 [Goland](https://www.jetbrains.com/go/) + + +### 使用说明 + +> 需要本地具有 git node golang mysql 环境 + +- node版本 16.14.x +- golang版本 >= v1.19 +- mysql版本 >=5.7 +- IDE推荐:Goland + + +### 安装go-view-server + +一、克隆项目 + +``` +git clone https://gitee.com/bufanyun/go-view-server.git +``` + +二、初始化数据库 + +- 项目数据库文件 [storage/data/goview.sql](storage/data/goview.sql) 创建数据库并导入 +- 将`manifest/config/config.yaml`中的数据库配置改为你自己的: +```yaml +database: + logger: + path: "logs/database" # 日志文件路径。默认为空,表示关闭,仅输出到终端 + <<: *defaultLogger + stdout: true + default: + link: "mysql:goview:nWkG43Xxnbi2Y44C@tcp(127.0.0.1:3306)/goview?loc=Local&parseTime=true&charset=utf8mb4" + debug: true + Prefix: "hg_" +``` + +三、 启动服务 + +```shell script + # 设置国内代理,如果已经设置好了代理可以跳过 + go env -w GOPROXY=https://goproxy.io,direct + + # 更新包 + go mod tidy + + # 启动所有服务 + go run main.go # 热编译启动: gf run main.go +``` + +## 安装go-view + +一、克隆项目 + +- 注意:`master-fetch` 分支是带有后端接口请求的分支!! +``` +git clone --branch master-fetch https://gitee.com/dromara/go-view.git +``` + +二、修改和`go-view-server`一致的端口号 + +- 接口地址修改:.env +```env +# port +VITE_DEV_PORT = '8080' + +# development path +VITE_DEV_PATH = 'http://127.0.0.1:8090' + +# production path +VITE_PRO_PATH = 'http://127.0.0.1:8090' +``` + +三、 启动服务 + +- 安装 +- 推荐使用 pnpm 管理项目,并使用 nrm 切换到阿里镜像,整体安装步骤如下: +```shell script + # 1. 安装 pnpm + npm install -g pnpm + + # 2. 安装 nrm + npm install -g nrm + + # 3. 使用 nrm 添加阿里镜像 + nrm add taobao https://registry.npmmirror.com/ + + # 4. nrm 查看镜像列表 + nrm ls + + # 5. nrm 应用对应镜像 + nrm use taobao +``` + +- 安装项目依赖 +```shell script + # 推荐使用 pnpm + pnpm install + + # 或 yarn + yarn install +``` + +- 启动 +```shell script + # 推荐使用 pnpm + pnpm dev + + # 或 yarn + yarn dev + + # 或 Makefile(需要自行配置系统环境,谷歌 make 命令环境搭建) + make dev +``` + +- 编译 +```shell script + # 推荐使用 pnpm + pnpm run build + + # 或 yarn + yarn run build + + # 或 Makefile + make dist +``` + +四、 登录go-view +- 如果一切正常,启动 `go-view` 后可以通过浏览器打开下方地址进入 `go-view` 登录页面 +``` +登录地址:http://localhost:3000/ +账号:admin +密码:admin +``` + + +## 测试项目 +- 在满足 `go-view` 后台接口使用的基础上,我们内置了一个测试项目和测试设备数据接口,方便日常配置大屏和报表的数据调试需求。 +- 测试接口,获取测试数据: +```http request +GET http://127.0.0.1:8090/api/goview/sys/testData +``` + +- 响应数据内容 +- `data.deviceInfo` 为模拟的设备基本信息,数据固定 +- `data.deviceData` 为模拟的设备动态上报数据,数据和数值类型每次请求都会随机生成 +```json +{ + "code": 200, + "data": { + "deviceInfo": { + "GPSLatitude": "-42.12345", + "GPSLongitude": "73.56789", + "deviceId": "IoTDevice001", + "deviceStatus": true, + "deviceTime": "2024-03-04 10:38:20", + "location": "河南郑州高新区科学大道001号" + }, + "deviceData": { + "airQuality": 66, + "ambientLight": 22.13, + "current": 66.3, + "deviceCurrent": 87.85, + "deviceTemperature": 21, + "deviceVibration": 62.04, + "deviceVoltage": 70, + "factoryData": "WQLZbKQf", + "humidity": "QMEHBqmT", + "noiseLevel": 14, + "powerConsumption": 5.18, + "pressure": "mOiEPEiZ", + "temperature": 8, + "vibration": "DPemPsNi", + "voltage": "YLkSKFXm", + "waterLevel": 30, + "windSpeed": 62.51 + } + }, + "msg": "获取设备数据成功" +} +``` + +## 相关开源地址 + +- [go-view](https://gitee.com/dromara/go-view) +- [goframe](https://gitee.com/johng/gf) +- [go-view-server](https://gitee.com/bufanyun/go-view-server) + + +## 相关文档 + +- [go-view](https://www.mtruning.club/) +- [goframe](https://goframe.org/) \ No newline at end of file diff --git a/api/project/project.go b/api/project/project.go new file mode 100644 index 0000000..06afc7c --- /dev/null +++ b/api/project/project.go @@ -0,0 +1,22 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package project + +import ( + "context" + + "hotgo/api/project/v1" +) + +type IProjectV1 interface { + List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) + GetData(ctx context.Context, req *v1.GetDataReq) (res *v1.GetDataRes, err error) + Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) + Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) + SaveData(ctx context.Context, req *v1.SaveDataReq) (res *v1.SaveDataRes, err error) + Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) + Publish(ctx context.Context, req *v1.PublishReq) (res *v1.PublishRes, err error) + Upload(ctx context.Context, req *v1.UploadReq) (res *v1.UploadRes, err error) +} diff --git a/api/project/v1/project.go b/api/project/v1/project.go new file mode 100644 index 0000000..6efc5e2 --- /dev/null +++ b/api/project/v1/project.go @@ -0,0 +1,84 @@ +// Package v1 +// @Description +// @Author Ms <133814250@qq.com> +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" + "hotgo/internal/model/input/adminin" + "hotgo/internal/model/input/form" +) + +// ListReq 查询列表 +type ListReq struct { + g.Meta `path:"/project/list" method:"get" tags:"项目" summary:"获取项目列表"` + adminin.ProjectListInp +} + +type ListRes struct { + g.Meta `mime:"custom"` + List []*adminin.ProjectListModel `json:"list" dc:"数据列表"` + form.PageRes +} + +// GetDataReq 获取信息 +type GetDataReq struct { + g.Meta `path:"/project/getData" method:"get" tags:"项目" summary:"获取指定信息"` + adminin.ProjectGetDataInp +} + +type GetDataRes struct { + *adminin.ProjectGetDataModel +} + +// CreateReq 新增项目 +type CreateReq struct { + g.Meta `path:"/project/create" method:"post" tags:"项目" summary:"新增项目"` + adminin.ProjectCreateInp +} + +type CreateRes struct { + *adminin.ProjectCreateModel +} + +// EditReq 修改/新增 +type EditReq struct { + g.Meta `path:"/project/edit" method:"post" tags:"项目" summary:"修改/新增项目"` + adminin.ProjectEditInp +} + +type EditRes struct{} + +// SaveDataReq 保存项目数据 +type SaveDataReq struct { + g.Meta `path:"/project/save/data" method:"post" tags:"项目" summary:"保存项目数据"` + adminin.ProjectSaveDataInp +} + +type SaveDataRes struct{} + +// DeleteReq 删除 +type DeleteReq struct { + g.Meta `path:"/project/delete" method:"post" tags:"项目" summary:"删除项目"` + adminin.ProjectDeleteInp +} + +type DeleteRes struct{} + +// PublishReq 修改发布状态 +type PublishReq struct { + g.Meta `path:"/project/publish" method:"put" tags:"项目" summary:"修改发布状态"` + adminin.ProjectPublishInp +} + +type PublishRes struct{} + +// UploadReq 文件上传 +type UploadReq struct { + g.Meta `path:"/project/upload" method:"post" tags:"项目" summary:"文件上传"` + adminin.ProjectUploadInp +} + +type UploadRes struct { + FileUrl string `json:"fileurl"` +} diff --git a/api/site/site.go b/api/site/site.go new file mode 100644 index 0000000..b126e92 --- /dev/null +++ b/api/site/site.go @@ -0,0 +1,19 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package site + +import ( + "context" + + "hotgo/api/site/v1" +) + +type ISiteV1 interface { + Register(ctx context.Context, req *v1.RegisterReq) (res *v1.RegisterRes, err error) + AccountLogin(ctx context.Context, req *v1.AccountLoginReq) (res *v1.AccountLoginRes, err error) + LoginLogout(ctx context.Context, req *v1.LoginLogoutReq) (res *v1.LoginLogoutRes, err error) + GetOssInfo(ctx context.Context, req *v1.GetOssInfoReq) (res *v1.GetOssInfoRes, err error) + TestData(ctx context.Context, req *v1.TestDataReq) (res *v1.TestDataRes, err error) +} diff --git a/api/site/v1/site.go b/api/site/v1/site.go new file mode 100644 index 0000000..17a62f7 --- /dev/null +++ b/api/site/v1/site.go @@ -0,0 +1,56 @@ +// Package v1 +// @Description +// @Author Ms <133814250@qq.com> +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" + "hotgo/internal/model/input/adminin" +) + +// RegisterReq 提交账号注册 +type RegisterReq struct { + g.Meta `path:"/sys/register" method:"post" tags:"后台基础" summary:"账号注册"` + adminin.RegisterInp +} + +type RegisterRes struct { + *adminin.LoginModel +} + +// AccountLoginReq 提交账号登录 +type AccountLoginReq struct { + g.Meta `path:"/sys/login" method:"post" tags:"后台基础" summary:"账号登录"` + adminin.AccountLoginInp +} + +type AccountLoginRes struct { + *adminin.LoginModel +} + +// LoginLogoutReq 注销登录 +type LoginLogoutReq struct { + g.Meta `path:"/sys/logout" method:"get" tags:"后台基础" summary:"注销登录"` +} + +type LoginLogoutRes struct{} + +// GetOssInfoReq 获取文件上传oss信息 +type GetOssInfoReq struct { + g.Meta `path:"/sys/getOssInfo" method:"get" tags:"后台基础" summary:"获取文件上传oss信息"` +} + +type GetOssInfoRes struct { + BucketURL string `json:"bucketUrl"` +} + +// TestDataReq 模拟测试数据 +type TestDataReq struct { + g.Meta `path:"/sys/testData" method:"all" tags:"后台基础" summary:"模拟测试数据"` +} + +type TestDataRes struct { + g.Meta `mime:"custom"` + DeviceInfo map[string]interface{} `json:"deviceInfo" dc:"设备信息"` + DeviceData map[string]interface{} `json:"deviceData" dc:"随机设备数值"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2045595 --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module hotgo + +go 1.21 + +require ( + github.com/gogf/gf/contrib/drivers/mysql/v2 v2.6.3 + github.com/gogf/gf/contrib/nosql/redis/v2 v2.7.2 + github.com/gogf/gf/v2 v2.7.2 + github.com/golang-jwt/jwt/v5 v5.2.0 +) + +require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/redis/go-redis/v9 v9.5.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/sdk v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..446918b --- /dev/null +++ b/go.sum @@ -0,0 +1,98 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/gogf/gf/contrib/drivers/mysql/v2 v2.6.3 h1:dRGGKKiT9FEnxhfHFerojy34uCKHgReKgpMbAOtqhsY= +github.com/gogf/gf/contrib/drivers/mysql/v2 v2.6.3/go.mod h1:sGdaCPgN1AY0tho+WYAgYdUHJkXwuDf76M3ASgHXWRQ= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.7.2 h1:V1hdGnyjU9kT0I3DDDFDl6Ll8yC6aAIFJa/lMQwB8V4= +github.com/gogf/gf/contrib/nosql/redis/v2 v2.7.2/go.mod h1:XzkPv3G8TdKczqoB/ydR3bxvBRdQLQNCOCEgxso/c3o= +github.com/gogf/gf/v2 v2.7.2 h1:uZDfyblasI12lZUtFd1mfxsIr8b14cd/F88DJUTCSDM= +github.com/gogf/gf/v2 v2.7.2/go.mod h1:EBXneAg/wes86rfeh68XC0a2JBNQylmT7Sp6/8Axk88= +github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= +github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/config.yaml b/hack/config.yaml new file mode 100644 index 0000000..6089ec1 --- /dev/null +++ b/hack/config.yaml @@ -0,0 +1,30 @@ + +# CLI tool, only in development environment. +# https://goframe.org/pages/viewpage.action?pageId=3673173 +gfcli: + build: + name: "goview" # 编译后的可执行文件名称 + # 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/goview" # 可执行文件生成路径 + extra: "" + + docker: + build: "-a amd64 -s linux -p temp -ew" + tagPrefixes: + - my.image.pub/my-app + + gen: + dao: + - link: "mysql:goview:nWkG43Xxnbi2Y44C@tcp(127.0.0.1:3306)/goview?loc=Local&parseTime=true" + removePrefix: "hg_" + descriptionTag: true + noModelComment: true + jsonCase: "CamelLower" + gJsonSupport: true + clear: false \ No newline at end of file diff --git a/hack/hack-cli.mk b/hack/hack-cli.mk new file mode 100644 index 0000000..7ba0d72 --- /dev/null +++ b/hack/hack-cli.mk @@ -0,0 +1,19 @@ + +# Install/Update to the latest CLI tool. +.PHONY: cli +cli: + @set -e; \ + wget -O gf https://github.com/gogf/gf/releases/latest/download/gf_$(shell go env GOOS)_$(shell go env GOARCH) && \ + chmod +x gf && \ + ./gf install -y && \ + rm ./gf + + +# Check and install CLI tool. +.PHONY: cli.install +cli.install: + @set -e; \ + gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \ + echo "GoFame CLI is not installed, start proceeding auto installation..."; \ + make cli; \ + fi; \ No newline at end of file diff --git a/hack/hack.mk b/hack/hack.mk new file mode 100644 index 0000000..1a42d77 --- /dev/null +++ b/hack/hack.mk @@ -0,0 +1,75 @@ +include ./hack/hack-cli.mk + +# Update GoFrame and its CLI to latest stable version. +.PHONY: up +up: cli.install + @gf up -a + +# Build binary using configuration from hack/config.yaml. +.PHONY: build +build: cli.install + @gf build -ew + +# Parse api and generate controller/sdk. +.PHONY: ctrl +ctrl: cli.install + @gf gen ctrl + +# Generate Go files for DAO/DO/Entity. +.PHONY: dao +dao: cli.install + @gf gen dao + +# Parse current project go files and generate enums go file. +.PHONY: enums +enums: cli.install + @gf gen enums + +# Generate Go files for Service. +.PHONY: service +service: cli.install + @gf gen service + + +# Build docker image. +.PHONY: image +image: cli.install + $(eval _TAG = $(shell git describe --dirty --always --tags --abbrev=8 --match 'v*' | sed 's/-/./2' | sed 's/-/./2')) +ifneq (, $(shell git status --porcelain 2>/dev/null)) + $(eval _TAG = $(_TAG).dirty) +endif + $(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG))) + $(eval _PUSH = $(if ${PUSH}, ${PUSH}, )) + @gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG}; + + +# Build docker image and automatically push to docker repo. +.PHONY: image.push +image.push: + @make image PUSH=-p; + + +# Deploy image and yaml to current kubectl environment. +.PHONY: deploy +deploy: + $(eval _TAG = $(if ${TAG}, ${TAG}, develop)) + + @set -e; \ + mkdir -p $(ROOT_DIR)/temp/kustomize;\ + cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_ENV};\ + kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\ + kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \ + if [ $(DEPLOY_NAME) != "" ]; then \ + kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; \ + fi; + + +# Parsing protobuf files and generating go files. +.PHONY: pb +pb: cli.install + @gf gen pb + +# Generate protobuf files for database tables. +.PHONY: pbentity +pbentity: cli.install + @gf gen pbentity \ No newline at end of file diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go new file mode 100644 index 0000000..fe4daf7 --- /dev/null +++ b/internal/cmd/cmd.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "context" + "hotgo/internal/controller/project" + "hotgo/internal/controller/site" + "hotgo/internal/library/cache" + "hotgo/internal/library/storager" + "hotgo/internal/library/token" + "hotgo/internal/service" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gcmd" +) + +var ( + Main = gcmd.Command{ + Name: "main", + Usage: "main", + Brief: "start http server", + Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + + // 设置缓存适配器 + cache.SetAdapter(ctx) + + token.InitConfig(ctx) + + storager.InitConfig(ctx) + + s := g.Server() + + // 注册全局中间件 + s.BindMiddleware("/*any", []ghttp.HandlerFunc{ + service.Middleware().Ctx, // 初始化请求上下文,一般需要第一个进行加载,后续中间件存在依赖关系 + service.Middleware().CORS, // 跨域中间件,自动处理跨域问题 + service.Middleware().PreFilter, // 请求输入预处理,api使用gf规范路由并且XxxReq结构体实现了validate.Filter接口即可隐式预处理 + service.Middleware().ResponseHandler, // HTTP响应预处理,在业务处理完成后,对响应结果进行格式化和错误过滤,将处理后的数据发送给请求方 + }...) + + s.Group("/api/goview", func(group *ghttp.RouterGroup) { + group.Bind( + site.NewV1(), + ) + group.Middleware(service.Middleware().AdminAuth) + group.Bind( + project.NewV1(), + ) + }) + s.Run() + return nil + }, + } +) diff --git a/internal/consts/app.go b/internal/consts/app.go new file mode 100644 index 0000000..78eb5f5 --- /dev/null +++ b/internal/consts/app.go @@ -0,0 +1,12 @@ +// Package consts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package consts + +// 应用类型 +const ( + AppAdmin = "admin" + AppDefault = "default" +) diff --git a/internal/consts/cache.go b/internal/consts/cache.go new file mode 100644 index 0000000..499b7f8 --- /dev/null +++ b/internal/consts/cache.go @@ -0,0 +1,12 @@ +// Package consts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package consts + +// cache +const ( + CacheToken = "token" // 登录token + CacheTokenBind = "token_bind" // 登录用户身份绑定 +) diff --git a/internal/consts/context.go b/internal/consts/context.go new file mode 100644 index 0000000..9bb5a3a --- /dev/null +++ b/internal/consts/context.go @@ -0,0 +1,14 @@ +// Package consts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package consts + +type CtxKey string + +// ContextKey 上下文 + +const ( + ContextHTTPKey CtxKey = "httpContext" // http上下文变量名称 +) diff --git a/internal/consts/debris.go b/internal/consts/debris.go new file mode 100644 index 0000000..e49a8dc --- /dev/null +++ b/internal/consts/debris.go @@ -0,0 +1,14 @@ +// Package consts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package consts + +// 碎片 + +// curd. +const ( + DefaultPage = 10 // 默认列表分页加载数量 + DefaultPageSize = 1 // 默认列表分页加载页码 +) diff --git a/internal/consts/error.go b/internal/consts/error.go new file mode 100644 index 0000000..fa392ed --- /dev/null +++ b/internal/consts/error.go @@ -0,0 +1,35 @@ +// Package consts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package consts + +import ( + "github.com/gogf/gf/v2/text/gstr" +) + +// 错误解释 +const ( + ErrorORM = "sql执行异常" + ErrorNotData = "数据不存在" + ErrorRotaPointer = "指针转换异常" +) + +// 需要隐藏真实错误的Wrap,开启访问日志后仍然会将真实错误记录 +var concealErrorSlice = []string{ErrorORM, ErrorRotaPointer} + +// ErrorMessage 错误显示信息,非debug模式有效 +func ErrorMessage(err error) (message string) { + if err == nil { + return "操作失败!~" + } + + message = err.Error() + for _, e := range concealErrorSlice { + if gstr.Contains(message, e) { + return "操作失败,请稍后重试!~" + } + } + return +} diff --git a/internal/consts/http.go b/internal/consts/http.go new file mode 100644 index 0000000..ba31eff --- /dev/null +++ b/internal/consts/http.go @@ -0,0 +1,19 @@ +// Package consts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package consts + +const ( + HTTPContentTypeXml = "text/xml" + HTTPContentTypeHtml = "text/html" + HTTPContentTypeStream = "text/event-stream" + HTTPContentTypeJson = "application/json" + HTTPContentTypeCustom = "custom" +) + +const ( + ApiStatusOk = 200 + ApiStatusTokenOverdue = 886 +) diff --git a/internal/consts/status.go b/internal/consts/status.go new file mode 100644 index 0000000..ef244bc --- /dev/null +++ b/internal/consts/status.go @@ -0,0 +1,14 @@ +// Package consts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package consts + +// 状态码 +const ( + StatusALL int = -1 // 全部状态 + StatusEnabled int = 1 // 启用 + StatusDisable int = 2 // 禁用 + StatusDelete int = 3 // 已删除 +) diff --git a/internal/consts/upload.go b/internal/consts/upload.go new file mode 100644 index 0000000..a7fd4e3 --- /dev/null +++ b/internal/consts/upload.go @@ -0,0 +1,12 @@ +// Package consts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package consts + +// 上传存储驱动 + +const ( + UploadDriveLocal = "local" // 本地驱动 +) diff --git a/internal/controller/project/project.go b/internal/controller/project/project.go new file mode 100644 index 0000000..c92eafb --- /dev/null +++ b/internal/controller/project/project.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package project diff --git a/internal/controller/project/project_new.go b/internal/controller/project/project_new.go new file mode 100644 index 0000000..791eac8 --- /dev/null +++ b/internal/controller/project/project_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package project + +import ( + "hotgo/api/project" +) + +type ControllerV1 struct{} + +func NewV1() project.IProjectV1 { + return &ControllerV1{} +} diff --git a/internal/controller/project/project_v1_create.go b/internal/controller/project/project_v1_create.go new file mode 100644 index 0000000..29eb8cc --- /dev/null +++ b/internal/controller/project/project_v1_create.go @@ -0,0 +1,14 @@ +package project + +import ( + "context" + "hotgo/internal/service" + + "hotgo/api/project/v1" +) + +func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) { + res = new(v1.CreateRes) + res.ProjectCreateModel, err = service.AdminProject().Create(ctx, &req.ProjectCreateInp) + return +} diff --git a/internal/controller/project/project_v1_delete.go b/internal/controller/project/project_v1_delete.go new file mode 100644 index 0000000..8367660 --- /dev/null +++ b/internal/controller/project/project_v1_delete.go @@ -0,0 +1,14 @@ +package project + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + "hotgo/api/project/v1" +) + +func (c *ControllerV1) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/controller/project/project_v1_edit.go b/internal/controller/project/project_v1_edit.go new file mode 100644 index 0000000..28bba52 --- /dev/null +++ b/internal/controller/project/project_v1_edit.go @@ -0,0 +1,13 @@ +package project + +import ( + "context" + "hotgo/internal/service" + + "hotgo/api/project/v1" +) + +func (c *ControllerV1) Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) { + err = service.AdminProject().Edit(ctx, &req.ProjectEditInp) + return +} diff --git a/internal/controller/project/project_v1_get_data.go b/internal/controller/project/project_v1_get_data.go new file mode 100644 index 0000000..a4b149b --- /dev/null +++ b/internal/controller/project/project_v1_get_data.go @@ -0,0 +1,14 @@ +package project + +import ( + "context" + "hotgo/internal/service" + + "hotgo/api/project/v1" +) + +func (c *ControllerV1) GetData(ctx context.Context, req *v1.GetDataReq) (res *v1.GetDataRes, err error) { + res = new(v1.GetDataRes) + res.ProjectGetDataModel, err = service.AdminProject().GetData(ctx, &req.ProjectGetDataInp) + return +} diff --git a/internal/controller/project/project_v1_list.go b/internal/controller/project/project_v1_list.go new file mode 100644 index 0000000..2a050fe --- /dev/null +++ b/internal/controller/project/project_v1_list.go @@ -0,0 +1,26 @@ +package project + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "hotgo/internal/library/response" + "hotgo/internal/service" + + "hotgo/api/project/v1" +) + +func (c *ControllerV1) List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) { + list, count, err := service.AdminProject().List(ctx, &req.ProjectListInp) + if err != nil { + return nil, err + } + + response.CustomJson(ghttp.RequestFromCtx(ctx), g.Map{ + "code": 200, + "count": count, + "data": list, + "msg": "获取成功", + }) + return +} diff --git a/internal/controller/project/project_v1_publish.go b/internal/controller/project/project_v1_publish.go new file mode 100644 index 0000000..23d0925 --- /dev/null +++ b/internal/controller/project/project_v1_publish.go @@ -0,0 +1,13 @@ +package project + +import ( + "context" + "hotgo/internal/service" + + "hotgo/api/project/v1" +) + +func (c *ControllerV1) Publish(ctx context.Context, req *v1.PublishReq) (res *v1.PublishRes, err error) { + err = service.AdminProject().Publish(ctx, &req.ProjectPublishInp) + return +} diff --git a/internal/controller/project/project_v1_save_data.go b/internal/controller/project/project_v1_save_data.go new file mode 100644 index 0000000..48be19e --- /dev/null +++ b/internal/controller/project/project_v1_save_data.go @@ -0,0 +1,13 @@ +package project + +import ( + "context" + "hotgo/internal/service" + + "hotgo/api/project/v1" +) + +func (c *ControllerV1) SaveData(ctx context.Context, req *v1.SaveDataReq) (res *v1.SaveDataRes, err error) { + err = service.AdminProject().SaveData(ctx, &req.ProjectSaveDataInp) + return +} diff --git a/internal/controller/project/project_v1_upload.go b/internal/controller/project/project_v1_upload.go new file mode 100644 index 0000000..220f23b --- /dev/null +++ b/internal/controller/project/project_v1_upload.go @@ -0,0 +1,18 @@ +package project + +import ( + "context" + "hotgo/api/project/v1" + "hotgo/internal/library/storager" +) + +func (c *ControllerV1) Upload(ctx context.Context, req *v1.UploadReq) (res *v1.UploadRes, err error) { + attachment, err := storager.DoUpload(ctx, "default", req.File) + if err != nil { + return + } + + res = new(v1.UploadRes) + res.FileUrl = storager.LastUrl(ctx, attachment.FileUrl, attachment.Drive) + return +} diff --git a/internal/controller/site/site.go b/internal/controller/site/site.go new file mode 100644 index 0000000..e0760c4 --- /dev/null +++ b/internal/controller/site/site.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package site diff --git a/internal/controller/site/site_new.go b/internal/controller/site/site_new.go new file mode 100644 index 0000000..9d83c27 --- /dev/null +++ b/internal/controller/site/site_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package site + +import ( + "hotgo/api/site" +) + +type ControllerV1 struct{} + +func NewV1() site.ISiteV1 { + return &ControllerV1{} +} diff --git a/internal/controller/site/site_v1_account_login.go b/internal/controller/site/site_v1_account_login.go new file mode 100644 index 0000000..9a241ca --- /dev/null +++ b/internal/controller/site/site_v1_account_login.go @@ -0,0 +1,19 @@ +package site + +import ( + "context" + "github.com/gogf/gf/v2/util/gconv" + "hotgo/internal/service" + + "hotgo/api/site/v1" +) + +func (c *ControllerV1) AccountLogin(ctx context.Context, req *v1.AccountLoginReq) (res *v1.AccountLoginRes, err error) { + model, err := service.AdminSite().AccountLogin(ctx, &req.AccountLoginInp) + if err != nil { + return + } + + err = gconv.Scan(model, &res) + return +} diff --git a/internal/controller/site/site_v1_get_oss_info.go b/internal/controller/site/site_v1_get_oss_info.go new file mode 100644 index 0000000..5645214 --- /dev/null +++ b/internal/controller/site/site_v1_get_oss_info.go @@ -0,0 +1,14 @@ +package site + +import ( + "context" + "hotgo/utility/url" + + "hotgo/api/site/v1" +) + +func (c *ControllerV1) GetOssInfo(ctx context.Context, req *v1.GetOssInfoReq) (res *v1.GetOssInfoRes, err error) { + res = new(v1.GetOssInfoRes) + res.BucketURL = url.GetAddr(ctx) + return +} diff --git a/internal/controller/site/site_v1_login_logout.go b/internal/controller/site/site_v1_login_logout.go new file mode 100644 index 0000000..ba72270 --- /dev/null +++ b/internal/controller/site/site_v1_login_logout.go @@ -0,0 +1,14 @@ +package site + +import ( + "context" + "github.com/gogf/gf/v2/net/ghttp" + "hotgo/internal/library/token" + + "hotgo/api/site/v1" +) + +func (c *ControllerV1) LoginLogout(ctx context.Context, req *v1.LoginLogoutReq) (res *v1.LoginLogoutRes, err error) { + err = token.Logout(ghttp.RequestFromCtx(ctx)) + return +} diff --git a/internal/controller/site/site_v1_register.go b/internal/controller/site/site_v1_register.go new file mode 100644 index 0000000..feb1458 --- /dev/null +++ b/internal/controller/site/site_v1_register.go @@ -0,0 +1,13 @@ +package site + +import ( + "context" + "hotgo/internal/service" + + "hotgo/api/site/v1" +) + +func (c *ControllerV1) Register(ctx context.Context, req *v1.RegisterReq) (res *v1.RegisterRes, err error) { + err = service.AdminSite().Register(ctx, &req.RegisterInp) + return +} diff --git a/internal/controller/site/site_v1_test_data.go b/internal/controller/site/site_v1_test_data.go new file mode 100644 index 0000000..00f9e3e --- /dev/null +++ b/internal/controller/site/site_v1_test_data.go @@ -0,0 +1,48 @@ +package site + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "hotgo/internal/library/response" + "hotgo/utility/format" + "math/rand" + + "hotgo/api/site/v1" +) + +func (c *ControllerV1) TestData(ctx context.Context, req *v1.TestDataReq) (res *v1.TestDataRes, err error) { + res = new(v1.TestDataRes) + + // 模拟设备信息 + res.DeviceInfo = map[string]interface{}{ + "deviceId": "IoTDevice001", + "GPSLongitude": "73.56789", + "GPSLatitude": "-42.12345", + "location": "河南郑州高新区科学大道001号", + "deviceStatus": true, + "deviceTime": gtime.Now(), + } + + fields := []string{ + "temperature", "humidity", "pressure", "flow", + "factoryData", "voltage", "current", "waterLevel", + "airQuality", "vibration", "noiseLevel", "ambientLight", + "windSpeed", "deviceTemperature", "deviceVibration", + "deviceVoltage", "deviceCurrent", "powerConsumption", + } + res.DeviceData = make(map[string]interface{}, len(fields)) + for _, field := range fields { + // 模拟设备上报随机数值 + res.DeviceData[field] = format.Round2Float64(rand.Float64() * 100) + } + + // 自定义返回字段格式 + response.CustomJson(ghttp.RequestFromCtx(ctx), g.Map{ + "code": 200, + "data": res, + "msg": "获取设备数据成功", + }) + return +} diff --git a/internal/dao/admin_attachment.go b/internal/dao/admin_attachment.go new file mode 100644 index 0000000..88caa71 --- /dev/null +++ b/internal/dao/admin_attachment.go @@ -0,0 +1,27 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// internalAdminAttachmentDao is internal type for wrapping internal DAO implements. +type internalAdminAttachmentDao = *internal.AdminAttachmentDao + +// adminAttachmentDao is the data access object for table hg_admin_attachment. +// You can define custom methods on it to extend its functionality as you wish. +type adminAttachmentDao struct { + internalAdminAttachmentDao +} + +var ( + // AdminAttachment is globally public accessible object for table hg_admin_attachment operations. + AdminAttachment = adminAttachmentDao{ + internal.NewAdminAttachmentDao(), + } +) + +// Fill with you ideas below. diff --git a/internal/dao/admin_member.go b/internal/dao/admin_member.go new file mode 100644 index 0000000..8c8d581 --- /dev/null +++ b/internal/dao/admin_member.go @@ -0,0 +1,27 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// internalAdminMemberDao is internal type for wrapping internal DAO implements. +type internalAdminMemberDao = *internal.AdminMemberDao + +// adminMemberDao is the data access object for table hg_admin_member. +// You can define custom methods on it to extend its functionality as you wish. +type adminMemberDao struct { + internalAdminMemberDao +} + +var ( + // AdminMember is globally public accessible object for table hg_admin_member operations. + AdminMember = adminMemberDao{ + internal.NewAdminMemberDao(), + } +) + +// Fill with you ideas below. diff --git a/internal/dao/admin_project.go b/internal/dao/admin_project.go new file mode 100644 index 0000000..10a53d2 --- /dev/null +++ b/internal/dao/admin_project.go @@ -0,0 +1,27 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package dao + +import ( + "hotgo/internal/dao/internal" +) + +// internalAdminProjectDao is internal type for wrapping internal DAO implements. +type internalAdminProjectDao = *internal.AdminProjectDao + +// adminProjectDao is the data access object for table hg_admin_project. +// You can define custom methods on it to extend its functionality as you wish. +type adminProjectDao struct { + internalAdminProjectDao +} + +var ( + // AdminProject is globally public accessible object for table hg_admin_project operations. + AdminProject = adminProjectDao{ + internal.NewAdminProjectDao(), + } +) + +// Fill with you ideas below. diff --git a/internal/dao/internal/admin_attachment.go b/internal/dao/internal/admin_attachment.go new file mode 100644 index 0000000..e200a8e --- /dev/null +++ b/internal/dao/internal/admin_attachment.go @@ -0,0 +1,105 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// AdminAttachmentDao is the data access object for table hg_admin_attachment. +type AdminAttachmentDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of current DAO. + columns AdminAttachmentColumns // columns contains all the column names of Table for convenient usage. +} + +// AdminAttachmentColumns defines and stores column names for table hg_admin_attachment. +type AdminAttachmentColumns struct { + Id string // 文件ID + AppId string // 应用ID + MemberId string // 管理员ID + CateId string // 上传分类 + Drive string // 上传驱动 + Name string // 文件原始名 + Kind string // 上传类型 + MimeType string // 扩展类型 + NaiveType string // NaiveUI类型 + Path string // 本地路径 + FileUrl string // url + Size string // 文件大小 + Ext string // 扩展名 + Md5 string // md5校验码 + Status string // 状态 + CreatedAt string // 创建时间 + UpdatedAt string // 修改时间 +} + +// adminAttachmentColumns holds the columns for table hg_admin_attachment. +var adminAttachmentColumns = AdminAttachmentColumns{ + Id: "id", + AppId: "app_id", + MemberId: "member_id", + CateId: "cate_id", + Drive: "drive", + Name: "name", + Kind: "kind", + MimeType: "mime_type", + NaiveType: "naive_type", + Path: "path", + FileUrl: "file_url", + Size: "size", + Ext: "ext", + Md5: "md5", + Status: "status", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// NewAdminAttachmentDao creates and returns a new DAO object for table data access. +func NewAdminAttachmentDao() *AdminAttachmentDao { + return &AdminAttachmentDao{ + group: "default", + table: "hg_admin_attachment", + columns: adminAttachmentColumns, + } +} + +// DB retrieves and returns the underlying raw database management object of current DAO. +func (dao *AdminAttachmentDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of current dao. +func (dao *AdminAttachmentDao) Table() string { + return dao.table +} + +// Columns returns all column names of current dao. +func (dao *AdminAttachmentDao) Columns() AdminAttachmentColumns { + return dao.columns +} + +// Group returns the configuration group name of database of current dao. +func (dao *AdminAttachmentDao) Group() string { + return dao.group +} + +// Ctx creates and returns the Model for current DAO, It automatically sets the context for current operation. +func (dao *AdminAttachmentDao) Ctx(ctx context.Context) *gdb.Model { + return dao.DB().Model(dao.table).Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rollbacks the transaction and returns the error from function f if it returns non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note that, you should not Commit or Rollback the transaction in function f +// as it is automatically handled by this function. +func (dao *AdminAttachmentDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/admin_member.go b/internal/dao/internal/admin_member.go new file mode 100644 index 0000000..abd2683 --- /dev/null +++ b/internal/dao/internal/admin_member.go @@ -0,0 +1,93 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// AdminMemberDao is the data access object for table hg_admin_member. +type AdminMemberDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of current DAO. + columns AdminMemberColumns // columns contains all the column names of Table for convenient usage. +} + +// AdminMemberColumns defines and stores column names for table hg_admin_member. +type AdminMemberColumns struct { + Id string // 管理员ID + Nickname string // 昵称 + Username string // 帐号 + PasswordHash string // 密码 + Salt string // 密码盐 + Avatar string // 头像 + LastActiveAt string // 最后活跃时间 + Remark string // 备注 + Status string // 状态 + CreatedAt string // 创建时间 + UpdatedAt string // 修改时间 +} + +// adminMemberColumns holds the columns for table hg_admin_member. +var adminMemberColumns = AdminMemberColumns{ + Id: "id", + Nickname: "nickname", + Username: "username", + PasswordHash: "password_hash", + Salt: "salt", + Avatar: "avatar", + LastActiveAt: "last_active_at", + Remark: "remark", + Status: "status", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// NewAdminMemberDao creates and returns a new DAO object for table data access. +func NewAdminMemberDao() *AdminMemberDao { + return &AdminMemberDao{ + group: "default", + table: "hg_admin_member", + columns: adminMemberColumns, + } +} + +// DB retrieves and returns the underlying raw database management object of current DAO. +func (dao *AdminMemberDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of current dao. +func (dao *AdminMemberDao) Table() string { + return dao.table +} + +// Columns returns all column names of current dao. +func (dao *AdminMemberDao) Columns() AdminMemberColumns { + return dao.columns +} + +// Group returns the configuration group name of database of current dao. +func (dao *AdminMemberDao) Group() string { + return dao.group +} + +// Ctx creates and returns the Model for current DAO, It automatically sets the context for current operation. +func (dao *AdminMemberDao) Ctx(ctx context.Context) *gdb.Model { + return dao.DB().Model(dao.table).Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rollbacks the transaction and returns the error from function f if it returns non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note that, you should not Commit or Rollback the transaction in function f +// as it is automatically handled by this function. +func (dao *AdminMemberDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/admin_project.go b/internal/dao/internal/admin_project.go new file mode 100644 index 0000000..2f49cea --- /dev/null +++ b/internal/dao/internal/admin_project.go @@ -0,0 +1,91 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// AdminProjectDao is the data access object for table hg_admin_project. +type AdminProjectDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of current DAO. + columns AdminProjectColumns // columns contains all the column names of Table for convenient usage. +} + +// AdminProjectColumns defines and stores column names for table hg_admin_project. +type AdminProjectColumns struct { + Id string // 项目id + ProjectName string // 项目名称 + IndexImage string // 预览图片url + Content string // 项目数据 + Status string // 项目状态,-1: 未发布'1: 已发布 + Remarks string // 项目备注 + CreatedBy string // 创建人 + UpdatedAt string // 更新时间 + CreatedAt string // 创建时间 + DeletedAt string // 删除时间 +} + +// adminProjectColumns holds the columns for table hg_admin_project. +var adminProjectColumns = AdminProjectColumns{ + Id: "id", + ProjectName: "project_name", + IndexImage: "index_image", + Content: "content", + Status: "status", + Remarks: "remarks", + CreatedBy: "created_by", + UpdatedAt: "updated_at", + CreatedAt: "created_at", + DeletedAt: "deleted_at", +} + +// NewAdminProjectDao creates and returns a new DAO object for table data access. +func NewAdminProjectDao() *AdminProjectDao { + return &AdminProjectDao{ + group: "default", + table: "hg_admin_project", + columns: adminProjectColumns, + } +} + +// DB retrieves and returns the underlying raw database management object of current DAO. +func (dao *AdminProjectDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of current dao. +func (dao *AdminProjectDao) Table() string { + return dao.table +} + +// Columns returns all column names of current dao. +func (dao *AdminProjectDao) Columns() AdminProjectColumns { + return dao.columns +} + +// Group returns the configuration group name of database of current dao. +func (dao *AdminProjectDao) Group() string { + return dao.group +} + +// Ctx creates and returns the Model for current DAO, It automatically sets the context for current operation. +func (dao *AdminProjectDao) Ctx(ctx context.Context) *gdb.Model { + return dao.DB().Model(dao.table).Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rollbacks the transaction and returns the error from function f if it returns non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note that, you should not Commit or Rollback the transaction in function f +// as it is automatically handled by this function. +func (dao *AdminProjectDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/library/cache/cache.go b/internal/library/cache/cache.go new file mode 100644 index 0000000..29a3570 --- /dev/null +++ b/internal/library/cache/cache.go @@ -0,0 +1,58 @@ +// Package cache +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package cache + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcache" + "github.com/gogf/gf/v2/os/gfile" + "hotgo/internal/library/cache/file" +) + +// cache 缓存驱动 +var cache *gcache.Cache + +// Instance 缓存实例 +func Instance() *gcache.Cache { + if cache == nil { + panic("cache uninitialized.") + } + return cache +} + +// SetAdapter 设置缓存适配器 +func SetAdapter(ctx context.Context) { + var adapter gcache.Adapter + + switch g.Cfg().MustGet(ctx, "cache.adapter").String() { + case "redis": + adapter = gcache.NewAdapterRedis(g.Redis()) + case "file": + fileDir := g.Cfg().MustGet(ctx, "cache.fileDir").String() + if fileDir == "" { + g.Log().Fatal(ctx, "file path must be configured for file caching.") + return + } + + if !gfile.Exists(fileDir) { + if err := gfile.Mkdir(fileDir); err != nil { + g.Log().Fatalf(ctx, "failed to create the cache directory. procedure, err:%+v", err) + return + } + } + adapter = file.NewAdapterFile(fileDir) + default: + adapter = gcache.NewAdapterMemory() + } + + // 数据库缓存,默认和通用缓冲驱动一致,如果你不想使用默认的,可以自行调整 + //g.DB().GetCache().SetAdapter(adapter) + + // 通用缓存 + cache = gcache.New() + cache.SetAdapter(adapter) +} diff --git a/internal/library/cache/file/file.go b/internal/library/cache/file/file.go new file mode 100644 index 0000000..26138ff --- /dev/null +++ b/internal/library/cache/file/file.go @@ -0,0 +1,322 @@ +// Package file +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package file + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gcache" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/util/gconv" + "os" + "path/filepath" + "time" +) + +type ( + // AdapterFile is the gcache adapter implements using file server. + AdapterFile struct { + dir string + } + + fileContent struct { + Duration int64 `json:"duration"` + Data interface{} `json:"data,omitempty"` + } +) + +const perm = 0o666 + +var ( + CacheExpiredErr = errors.New("cache expired") +) + +// NewAdapterFile creates and returns a new memory cache object. +func NewAdapterFile(dir string) gcache.Adapter { + return &AdapterFile{ + dir: dir, + } +} + +func (c *AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, lifeTime time.Duration) (err error) { + fileKey := gconv.String(key) + if value == nil || lifeTime < 0 { + return c.Delete(fileKey) + } + return c.Save(fileKey, gconv.String(value), lifeTime) +} + +func (c *AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) (err error) { + return gerror.New("implement me") +} + +func (c *AdapterFile) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) { + return false, gerror.New("implement me") +} + +func (c *AdapterFile) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) { + return false, gerror.New("implement me") +} + +func (c *AdapterFile) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) { + return false, gerror.New("implement me") +} + +func (c *AdapterFile) Get(ctx context.Context, key interface{}) (*gvar.Var, error) { + fetch, err := c.Fetch(gconv.String(key)) + if err != nil { + return nil, err + } + return gvar.New(fetch), nil +} + +func (c *AdapterFile) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) { + result, err = c.Get(ctx, key) + if err != nil && !errors.Is(err, CacheExpiredErr) { + return nil, err + } + if result.IsNil() { + return gvar.New(value), c.Set(ctx, key, value, duration) + } + return +} + +func (c *AdapterFile) GetOrSetFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) { + v, err := c.Get(ctx, key) + if err != nil && !errors.Is(err, CacheExpiredErr) { + return nil, err + } + if v.IsNil() { + value, err := f(ctx) + if err != nil { + return nil, err + } + if value == nil { + return nil, nil + } + return gvar.New(value), c.Set(ctx, key, value, duration) + } else { + return v, nil + } +} + +func (c *AdapterFile) GetOrSetFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) { + return c.GetOrSetFunc(ctx, key, f, duration) +} + +func (c *AdapterFile) Contains(ctx context.Context, key interface{}) (bool, error) { + return c.Has(gconv.String(key)), nil +} + +func (c *AdapterFile) Size(ctx context.Context) (size int, err error) { + return 0, nil +} + +func (c *AdapterFile) Data(ctx context.Context) (data map[interface{}]interface{}, err error) { + return nil, gerror.New("implement me") +} + +func (c *AdapterFile) Keys(ctx context.Context) (keys []interface{}, err error) { + return nil, gerror.New("implement me") +} + +func (c *AdapterFile) Values(ctx context.Context) (values []interface{}, err error) { + return nil, gerror.New("implement me") +} + +func (c *AdapterFile) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) { + return nil, false, gerror.New("implement me") +} + +func (c *AdapterFile) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) { + var ( + v *gvar.Var + oldTTL int64 + fileKey = gconv.String(key) + ) + // TTL. + expire, err := c.GetExpire(ctx, fileKey) + if err != nil { + return + } + oldTTL = int64(expire) + if oldTTL == -2 { + // It does not exist. + return + } + oldDuration = time.Duration(oldTTL) * time.Second + // DEL. + if duration < 0 { + err = c.Delete(fileKey) + return + } + v, err = c.Get(ctx, fileKey) + if err != nil { + return + } + err = c.Set(ctx, fileKey, v.Val(), duration) + return +} + +func (c *AdapterFile) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) { + content, err := c.read(gconv.String(key)) + if err != nil { + return -1, nil + } + + if content.Duration <= time.Now().Unix() { + return -1, nil + } + return time.Duration(time.Now().Unix()-content.Duration) * time.Second, nil +} + +func (c *AdapterFile) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) { + if len(keys) == 0 { + return nil, nil + } + // Retrieves the last key value. + if lastValue, err = c.Get(ctx, gconv.String(keys[len(keys)-1])); err != nil { + return nil, err + } + // Deletes all given keys. + err = c.DeleteMulti(gconv.Strings(keys)...) + return +} + +func (c *AdapterFile) Clear(ctx context.Context) error { + return c.Flush() +} + +func (c *AdapterFile) Close(ctx context.Context) error { + return nil +} + +func (c *AdapterFile) createName(key string) string { + h := sha256.New() + _, _ = h.Write([]byte(key)) + hash := hex.EncodeToString(h.Sum(nil)) + return filepath.Join(c.dir, fmt.Sprintf("%s.cache", hash)) +} + +func (c *AdapterFile) read(key string) (*fileContent, error) { + rp := gfile.RealPath(c.createName(key)) + if rp == "" { + return nil, nil + } + + value, err := os.ReadFile(rp) + if err != nil { + return nil, err + } + + content := &fileContent{} + if err := json.Unmarshal(value, content); err != nil { + return nil, err + } + + if content.Duration == 0 { + return content, nil + } + + if content.Duration <= time.Now().Unix() { + _ = c.Delete(key) + return nil, CacheExpiredErr + } + return content, nil +} + +// Has checks if the cached key exists into the File storage +func (c *AdapterFile) Has(key string) bool { + fc, err := c.read(key) + return err == nil && fc != nil +} + +// Delete the cached key from File storage +func (c *AdapterFile) Delete(key string) error { + _, err := os.Stat(c.createName(key)) + if err != nil && os.IsNotExist(err) { + return nil + } + return os.Remove(c.createName(key)) +} + +// DeleteMulti the cached key from File storage +func (c *AdapterFile) DeleteMulti(keys ...string) (err error) { + for _, key := range keys { + if err = c.Delete(key); err != nil { + return + } + } + return +} + +// Fetch retrieves the cached value from key of the File storage +func (c *AdapterFile) Fetch(key string) (interface{}, error) { + content, err := c.read(key) + if err != nil { + return nil, err + } + + if content == nil { + return nil, nil + } + return content.Data, nil +} + +// FetchMulti retrieve multiple cached values from keys of the File storage +func (c *AdapterFile) FetchMulti(keys []string) map[string]interface{} { + result := make(map[string]interface{}) + for _, key := range keys { + if value, err := c.Fetch(key); err == nil { + result[key] = value + } + } + return result +} + +// Flush removes all cached keys of the File storage +func (c *AdapterFile) Flush() error { + dir, err := os.Open(c.dir) + if err != nil { + return err + } + + defer func() { + _ = dir.Close() + }() + + names, _ := dir.Readdirnames(-1) + + for _, name := range names { + _ = os.Remove(filepath.Join(c.dir, name)) + } + return nil +} + +// Save a value in File storage by key +func (c *AdapterFile) Save(key string, value string, lifeTime time.Duration) error { + duration := int64(0) + + if lifeTime > 0 { + duration = time.Now().Unix() + int64(lifeTime.Seconds()) + } + + content := &fileContent{duration, value} + + data, err := json.Marshal(content) + if err != nil { + return err + } + + err = os.WriteFile(c.createName(key), data, perm) + return err +} diff --git a/internal/library/contexts/context.go b/internal/library/contexts/context.go new file mode 100644 index 0000000..f5f6e65 --- /dev/null +++ b/internal/library/contexts/context.go @@ -0,0 +1,120 @@ +// Package contexts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package contexts + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "hotgo/internal/consts" + "hotgo/internal/model" +) + +// Init 初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改 +func Init(r *ghttp.Request, customCtx *model.Context) { + r.SetCtxVar(consts.ContextHTTPKey, customCtx) +} + +// Get 获得上下文变量,如果没有设置,那么返回nil +func Get(ctx context.Context) *model.Context { + value := ctx.Value(consts.ContextHTTPKey) + if value == nil { + return nil + } + if localCtx, ok := value.(*model.Context); ok { + return localCtx + } + return nil +} + +// SetUser 将上下文信息设置到上下文请求中,注意是完整覆盖 +func SetUser(ctx context.Context, user *model.Identity) { + c := Get(ctx) + if c == nil { + g.Log().Warning(ctx, "contexts.SetUser, c == nil ") + return + } + c.User = user +} + +// SetResponse 设置组件响应 用于访问日志使用 +func SetResponse(ctx context.Context, response *model.Response) { + c := Get(ctx) + if c == nil { + g.Log().Warning(ctx, "contexts.SetResponse, c == nil ") + return + } + c.Response = response +} + +// SetModule 设置应用模块 +func SetModule(ctx context.Context, module string) { + c := Get(ctx) + if c == nil { + g.Log().Warning(ctx, "contexts.SetModule, c == nil ") + return + } + c.Module = module +} + +// GetUser 获取用户信息 +func GetUser(ctx context.Context) *model.Identity { + c := Get(ctx) + if c == nil { + return nil + } + return c.User +} + +// GetUserId 获取用户ID +func GetUserId(ctx context.Context) int64 { + user := GetUser(ctx) + if user == nil { + return 0 + } + return user.Id +} + +// GetModule 获取应用模块 +func GetModule(ctx context.Context) string { + c := Get(ctx) + if c == nil { + return "" + } + return c.Module +} + +// SetData 设置额外数据 +func SetData(ctx context.Context, k string, v interface{}) { + c := Get(ctx) + if c == nil { + g.Log().Warning(ctx, "contexts.SetData, c == nil ") + return + } + Get(ctx).Data[k] = v +} + +// SetDataMap 设置额外数据 +func SetDataMap(ctx context.Context, vs g.Map) { + c := Get(ctx) + if c == nil { + g.Log().Warning(ctx, "contexts.SetData, c == nil ") + return + } + + for k, v := range vs { + Get(ctx).Data[k] = v + } +} + +// GetData 获取额外数据 +func GetData(ctx context.Context) g.Map { + c := Get(ctx) + if c == nil { + return nil + } + return c.Data +} diff --git a/internal/library/contexts/detached.go b/internal/library/contexts/detached.go new file mode 100644 index 0000000..e7c1310 --- /dev/null +++ b/internal/library/contexts/detached.go @@ -0,0 +1,35 @@ +// Package contexts +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package contexts + +import ( + "context" + "time" +) + +type detached struct { + ctx context.Context +} + +func (detached) Deadline() (time.Time, bool) { + return time.Time{}, false +} + +func (detached) Done() <-chan struct{} { + return nil +} + +func (detached) Err() error { + return nil +} + +func (d detached) Value(key interface{}) interface{} { + return d.ctx.Value(key) +} + +func Detach(ctx context.Context) context.Context { + return detached{ctx: ctx} +} diff --git a/internal/library/response/response.go b/internal/library/response/response.go new file mode 100644 index 0000000..6ff7fe9 --- /dev/null +++ b/internal/library/response/response.go @@ -0,0 +1,116 @@ +// Package response +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package response + +import ( + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" + "hotgo/internal/consts" + "hotgo/internal/library/contexts" + "hotgo/internal/model" +) + +// JsonExit 返回JSON数据并退出当前HTTP执行函数 +func JsonExit(r *ghttp.Request, code int, message string, data ...interface{}) { + RJson(r, code, message, data...) + r.Exit() +} + +// RXml xml +func RXml(r *ghttp.Request, code int, message string, data ...interface{}) { + responseData := interface{}(nil) + if len(data) > 0 { + responseData = data[0] + } + res := &model.Response{ + Code: code, + Message: message, + Timestamp: gtime.Timestamp(), + TraceID: gctx.CtxId(r.Context()), + } + + // 如果不是正常的返回,则将data转为error + if gcode.CodeOK.Code() == code { + res.Data = responseData + } else { + res.Error = responseData + } + + // 清空响应 + r.Response.ClearBuffer() + + // 写入响应 + r.Response.WriteXml(gconv.Map(res)) + + // 加入到上下文 + contexts.SetResponse(r.Context(), res) +} + +// RJson 标准返回结果数据结构封装 +func RJson(r *ghttp.Request, code int, message string, data ...interface{}) { + responseData := interface{}(nil) + if len(data) > 0 { + responseData = data[0] + } + res := &model.Response{ + Code: code, + Message: message, + Timestamp: gtime.Timestamp(), + TraceID: gctx.CtxId(r.Context()), + } + + // 如果不是正常的返回,则将data转为error + if consts.ApiStatusOk == code { + res.Data = responseData + } else { + res.Error = responseData + } + + // 清空响应 + r.Response.ClearBuffer() + + // 写入响应 + r.Response.WriteJson(res) + + // 加入到上下文 + contexts.SetResponse(r.Context(), res) +} + +// CustomJson 自定义JSON +func CustomJson(r *ghttp.Request, content interface{}) { + // 清空响应 + r.Response.ClearBuffer() + + // 写入响应 + r.Response.WriteJson(content) + + // 加入到上下文 + contexts.SetResponse(r.Context(), &model.Response{ + Code: consts.ApiStatusOk, + Message: "", + Data: content, + Error: nil, + Timestamp: gtime.Timestamp(), + TraceID: gctx.CtxId(r.Context()), + }) +} + +// Redirect 重定向 +func Redirect(r *ghttp.Request, location string, code ...int) { + r.Response.RedirectTo(location, code...) +} + +// RText 返回成功文本 +func RText(r *ghttp.Request, message string) { + // 清空响应 + r.Response.ClearBuffer() + + // 写入响应 + r.Response.Write(message) +} diff --git a/internal/library/storager/config.go b/internal/library/storager/config.go new file mode 100644 index 0000000..0224557 --- /dev/null +++ b/internal/library/storager/config.go @@ -0,0 +1,30 @@ +// Package storager +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package storager + +import ( + "context" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "hotgo/internal/model" +) + +var config *model.UploadConfig + +func InitConfig(ctx context.Context) { + err := g.Cfg().MustGet(ctx, "storager").Scan(&config) + if err != nil { + panic(err) + } + + if config == nil { + panic("没有找到storager配置") + } +} + +func GetModel(ctx context.Context) *gdb.Model { + return g.Model("admin_attachment").Ctx(ctx) +} diff --git a/internal/library/storager/mime.go b/internal/library/storager/mime.go new file mode 100644 index 0000000..ac77ebe --- /dev/null +++ b/internal/library/storager/mime.go @@ -0,0 +1,232 @@ +// Package storager +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package storager + +import ( + "crypto/md5" + "fmt" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/text/gstr" + "io" + "path" +) + +// 文件归属分类 +const ( + KindImg = "image" // 图片 + KindDoc = "doc" // 文档 + KindAudio = "audio" // 音频 + KindVideo = "video" // 视频 + KindZip = "zip" // 压缩包 + KindOther = "other" // 其他 +) + +var KindSlice = []string{KindImg, KindDoc, KindAudio, KindVideo, KindZip, KindOther} + +var ( + // 图片类型 + imgType = g.MapStrStr{ + "jpeg": "image/jpeg", + "jpg": "image/jpeg", + "png": "image/png", + "gif": "image/gif", + "webp": "image/webp", + "cr2": "image/x-canon-cr2", + "tif": "image/tiff", + "bmp": "image/bmp", + "heif": "image/heif", + "jxr": "image/vnd.ms-photo", + "psd": "image/vnd.adobe.photoshop", + "ico": "image/vnd.microsoft.icon", + "dwg": "image/vnd.dwg", + } + + // 文档类型 + docType = g.MapStrStr{ + "doc": "application/msword", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "dot": "application/msword", + "xls": "application/vnd.ms-excel", + "xlt": "application/vnd.ms-excel", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "ppt": "application/vnd.ms-powerpoint", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "pdf": "application/pdf", + "txt": "text/plain", + "csv": "text/csv", + "html": "text/html", + "xml": "text/xml", + "pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12", + "pot": "application/vnd.ms-powerpoint", + "wpd": "application/wordperfect", + "md": "text/markdown", + "json": "application/json", + "yaml": "application/x-yaml", + "markdown": "text/markdown", + "asciidoc": "text/asciidoc", + "xsl": "application/xml", + "wps": "application/vnd.ms-works", + "sxi": "application/vnd.sun.xml.impress", + "sti": "application/vnd.sun.xml.impress.template", + "odp": "application/vnd.oasis.opendocument.presentation-template", + } + + // 音频类型 + audioType = g.MapStrStr{ + "mid": "audio/midi", + "mp3": "audio/mpeg", + "m4a": "audio/mp4", + "ogg": "audio/ogg", + "flac": "audio/x-flac", + "wav": "audio/x-wav", + "amr": "audio/amr", + "aac": "audio/aac", + "aiff": "audio/x-aiff", + } + + // 视频类型 + videoType = g.MapStrStr{ + "mp4": "video/mp4", + "m4v": "video/x-m4v", + "mkv": "video/x-matroska", + "webm": "video/webm", + "mov": "video/quicktime", + "avi": "video/x-msvideo", + "wmv": "video/x-ms-wmv", + "mpg": "video/mpeg", + "flv": "video/x-flv", + "3gp": "video/3gpp", + } + + // 压缩包 + zipType = g.MapStrStr{ + "zip": "application/zip", + "rar": "application/x-rar-compressed", + "tar": "application/x-tar", + "gz": "application/gzip", + "7z": "application/octet-stream", + "tar.gz": "application/octet-stream", + } + + // 其他 + otherType = g.MapStrStr{ + "dwf": "model/vnd.dwf", + "ics": "text/calendar", + "vcard": "text/vcard", + "apk": "application/vnd.android.package-archive", + "ipa": "application/octet-stream", + } +) + +// IsImgType 判断是否为图片 +func IsImgType(ext string) bool { + _, ok := imgType[ext] + return ok +} + +// IsDocType 判断是否为文档 +func IsDocType(ext string) bool { + _, ok := docType[ext] + return ok +} + +// IsAudioType 判断是否为音频 +func IsAudioType(ext string) bool { + _, ok := audioType[ext] + return ok +} + +// IsVideoType 判断是否为视频 +func IsVideoType(ext string) bool { + _, ok := videoType[ext] + return ok +} + +// IsZipType 判断是否为压缩包 +func IsZipType(ext string) bool { + _, ok := zipType[ext] + return ok +} + +// GetFileMimeType 获取文件扩展类型 +// 如果文件类型没有加入系统映射类型,默认认为不是合法的文件类型。建议将常用的上传文件类型加入映射关系。 +// 当然你也可以不做限制,可以上传任意文件。但需要谨慎处理和设置相应的安全措施。 +// 获取任意扩展名的扩展类型:mime.TypeByExtension(".xls") +func GetFileMimeType(ext string) (string, error) { + if mime, ok := imgType[ext]; ok { + return mime, nil + } + if mime, ok := docType[ext]; ok { + return mime, nil + } + if mime, ok := audioType[ext]; ok { + return mime, nil + } + if mime, ok := videoType[ext]; ok { + return mime, nil + } + if mime, ok := zipType[ext]; ok { + return mime, nil + } + if mime, ok := otherType[ext]; ok { + return mime, nil + } + return "", gerror.Newf("Invalid file type:%v", ext) +} + +// GetFileKind 获取文件上传类型 +func GetFileKind(ext string) string { + if _, ok := imgType[ext]; ok { + return KindImg + } + if _, ok := docType[ext]; ok { + return KindDoc + } + if _, ok := audioType[ext]; ok { + return KindAudio + } + if _, ok := videoType[ext]; ok { + return KindVideo + } + if _, ok := zipType[ext]; ok { + return KindZip + } + return KindOther +} + +// Ext 获取文件后缀 +func Ext(baseName string) string { + return gstr.ToLower(gstr.StrEx(path.Ext(baseName), ".")) +} + +// UploadFileByte 获取上传文件的byte +func UploadFileByte(file *ghttp.UploadFile) ([]byte, error) { + open, err := file.Open() + if err != nil { + return nil, err + } + return io.ReadAll(open) +} + +// CalcFileMd5 计算文件md5值 +func CalcFileMd5(file *ghttp.UploadFile) (string, error) { + f, err := file.Open() + if err != nil { + err = gerror.Wrapf(err, `os.Open failed for name "%s"`, file.Filename) + return "", err + } + defer f.Close() + h := md5.New() + _, err = io.Copy(h, f) + if err != nil { + err = gerror.Wrap(err, `io.Copy failed`) + return "", err + } + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/internal/library/storager/model.go b/internal/library/storager/model.go new file mode 100644 index 0000000..d7b9aaf --- /dev/null +++ b/internal/library/storager/model.go @@ -0,0 +1,17 @@ +// Package storager +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package storager + +// FileMeta 文件元数据 +type FileMeta struct { + Filename string `json:"filename"` // 文件名称 + Size int64 `json:"size"` // 文件大小 + Kind string `json:"kind"` // 文件上传类型 + MimeType string `json:"mimeType"` // 文件扩展类型 + NaiveType string `json:"naiveType"` // NaiveUI类型 + Ext string `json:"ext"` // 文件扩展名 + Md5 string `json:"md5"` // 文件hash +} diff --git a/internal/library/storager/upload.go b/internal/library/storager/upload.go new file mode 100644 index 0000000..f079839 --- /dev/null +++ b/internal/library/storager/upload.go @@ -0,0 +1,236 @@ +// Package storager +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package storager + +import ( + "context" + "fmt" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/grand" + "hotgo/internal/consts" + "hotgo/internal/library/contexts" + "hotgo/internal/model/entity" + "hotgo/utility/url" + "hotgo/utility/validate" + "strconv" + "strings" +) + +// UploadDrive 存储驱动 +type UploadDrive interface { + // Upload 上传 + Upload(ctx context.Context, file *ghttp.UploadFile) (fullPath string, err error) +} + +// New 初始化存储驱动 +func New(name ...string) UploadDrive { + var ( + driveType = consts.UploadDriveLocal + drive UploadDrive + ) + + if len(name) > 0 && name[0] != "" { + driveType = name[0] + } + + switch driveType { + case consts.UploadDriveLocal: + drive = &LocalDrive{} + default: + panic(fmt.Sprintf("暂不支持的存储驱动:%v", driveType)) + } + return drive +} + +// DoUpload 上传入口 +func DoUpload(ctx context.Context, typ string, file *ghttp.UploadFile) (result *entity.AdminAttachment, err error) { + if file == nil { + err = gerror.New("文件必须!") + return + } + + meta, err := GetFileMeta(file) + if err != nil { + return + } + + if err = ValidateFileMeta(typ, meta); err != nil { + return + } + + result, err = HasFile(ctx, meta.Md5) + if err != nil { + return + } + + // 相同存储相同身份才复用 + if result != nil && result.Drive == config.Drive && result.MemberId == contexts.GetUserId(ctx) && result.AppId == contexts.GetModule(ctx) { + return + } + + // 上传到驱动 + fullPath, err := New(config.Drive).Upload(ctx, file) + if err != nil { + return + } + // 写入附件记录 + return write(ctx, meta, fullPath) +} + +// ValidateFileMeta 验证文件元数据 +func ValidateFileMeta(typ string, meta *FileMeta) (err error) { + if _, err = GetFileMimeType(meta.Ext); err != nil { + return + } + + switch typ { + case KindImg: + if !IsImgType(meta.Ext) { + err = gerror.New("上传的文件不是图片") + return + } + if config.ImageSize > 0 && meta.Size > config.ImageSize*1024*1024 { + err = gerror.Newf("图片大小不能超过%vMB", config.ImageSize) + return + } + + if len(config.ImageType) > 0 && !validate.InSlice(strings.Split(config.ImageType, `,`), meta.Ext) { + err = gerror.New("上传图片类型未经允许") + return + } + case KindDoc: + if !IsDocType(meta.Ext) { + err = gerror.New("上传的文件不是文档") + return + } + case KindAudio: + if !IsAudioType(meta.Ext) { + err = gerror.New("上传的文件不是音频") + return + } + case KindVideo: + if !IsVideoType(meta.Ext) { + err = gerror.New("上传的文件不是视频") + return + } + case KindZip: + if !IsZipType(meta.Ext) { + err = gerror.New("上传的文件不是压缩文件") + return + } + case KindOther: + fallthrough + default: + // 默认为通用的文件上传 + if config.FileSize > 0 && meta.Size > config.FileSize*1024*1024 { + err = gerror.Newf("文件大小不能超过%vMB", config.FileSize) + return + } + + if len(config.FileType) > 0 && !validate.InSlice(strings.Split(config.FileType, `,`), meta.Ext) { + err = gerror.New("上传文件类型未经允许") + return + } + } + return +} + +// LastUrl 根据驱动获取最终文件访问地址 +func LastUrl(ctx context.Context, fullPath, drive string) string { + if validate.IsURL(fullPath) { + return fullPath + } + + switch drive { + case consts.UploadDriveLocal: + return url.GetAddr(ctx) + "/" + fullPath + default: + return fullPath + } +} + +// GetFileMeta 获取上传文件元数据 +func GetFileMeta(file *ghttp.UploadFile) (meta *FileMeta, err error) { + meta = new(FileMeta) + meta.Filename = file.Filename + meta.Size = file.Size + meta.Ext = Ext(file.Filename) + meta.Kind = GetFileKind(meta.Ext) + meta.MimeType, err = GetFileMimeType(meta.Ext) + if err != nil { + return + } + + // 兼容naiveUI + naiveType := "text/plain" + if IsImgType(Ext(file.Filename)) { + naiveType = "" + } + meta.NaiveType = naiveType + + // 计算md5值 + meta.Md5, err = CalcFileMd5(file) + return +} + +// GenFullPath 根据目录和文件类型生成一个绝对地址 +func GenFullPath(basePath, ext string) string { + fileName := strconv.FormatInt(gtime.TimestampNano(), 36) + grand.S(6) + fileName = fileName + ext + return basePath + gtime.Date() + "/" + strings.ToLower(fileName) +} + +// write 写入附件记录 +func write(ctx context.Context, meta *FileMeta, fullPath string) (models *entity.AdminAttachment, err error) { + models = &entity.AdminAttachment{ + Id: 0, + AppId: contexts.GetModule(ctx), + MemberId: contexts.GetUserId(ctx), + Drive: config.Drive, + Size: meta.Size, + Path: fullPath, + FileUrl: fullPath, + Name: meta.Filename, + Kind: meta.Kind, + MimeType: meta.MimeType, + NaiveType: meta.NaiveType, + Ext: meta.Ext, + Md5: meta.Md5, + Status: consts.StatusEnabled, + } + + id, err := GetModel(ctx).Data(models).InsertAndGetId() + if err != nil { + return + } + models.Id = id + return +} + +// HasFile 检查附件是否存在 +func HasFile(ctx context.Context, md5 string) (res *entity.AdminAttachment, err error) { + if err = GetModel(ctx).Where("md5", md5).Scan(&res); err != nil { + err = gerror.Wrap(err, "检查文件hash时出现错误") + return + } + + if res == nil { + return + } + + // 只有在上传时才会检查md5值,如果附件存在则更新最后上传时间,保证上传列表更新显示在最前面 + if res.Id > 0 { + update := g.Map{ + "status": consts.StatusEnabled, + "updated_at": gtime.Now(), + } + _, _ = GetModel(ctx).WherePri(res.Id).Data(update).Update() + } + return +} diff --git a/internal/library/storager/upload_local.go b/internal/library/storager/upload_local.go new file mode 100644 index 0000000..0153124 --- /dev/null +++ b/internal/library/storager/upload_local.go @@ -0,0 +1,47 @@ +// Package storager +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package storager + +import ( + "context" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "strings" +) + +// LocalDrive 本地驱动 +type LocalDrive struct { +} + +// Upload 上传到本地 +func (d *LocalDrive) Upload(ctx context.Context, file *ghttp.UploadFile) (fullPath string, err error) { + var ( + sp = g.Cfg().MustGet(ctx, "server.serverRoot") + nowDate = gtime.Date() + ) + + if sp.IsEmpty() { + err = gerror.New("本地上传驱动必须配置静态路径!") + return + } + + if config.LocalPath == "" { + err = gerror.New("本地上传驱动必须配置本地存储路径!") + return + } + + // 包含静态文件夹的路径 + fullDirPath := strings.Trim(sp.String(), "/") + "/" + config.LocalPath + nowDate + fileName, err := file.Save(fullDirPath, true) + if err != nil { + return + } + // 不含静态文件夹的路径 + fullPath = config.LocalPath + nowDate + "/" + fileName + return +} diff --git a/internal/library/token/token.go b/internal/library/token/token.go new file mode 100644 index 0000000..56a0b3d --- /dev/null +++ b/internal/library/token/token.go @@ -0,0 +1,313 @@ +// Package token +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package token + +import ( + "context" + "fmt" + "github.com/gogf/gf/v2/crypto/gmd5" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" + "github.com/golang-jwt/jwt/v5" + "hotgo/internal/consts" + "hotgo/internal/library/cache" + "hotgo/internal/library/contexts" + "hotgo/internal/model" + "hotgo/utility/simple" + "time" +) + +type Claims struct { + *model.Identity + jwt.RegisteredClaims +} + +type Token struct { + ExpireAt int64 `json:"exp"` // token过期时间 + RefreshAt int64 `json:"ra"` // 刷新时间 + RefreshCount int64 `json:"rc"` // 刷新次数 +} + +var ( + config *model.TokenConfig + errorLogin = gerror.New("登录身份已失效,请重新登录!") + errorMultiLogin = gerror.New("账号已在其他地方登录,如非本人操作请及时修改登录密码!") +) + +func InitConfig(ctx context.Context) { + err := g.Cfg().MustGet(ctx, "token").Scan(&config) + if err != nil { + panic(err) + } + + if config == nil { + panic("没有找到token配置") + } +} + +// Login 登录 +func Login(ctx context.Context, user *model.Identity) (string, int64, error) { + claims := Claims{ + user, + jwt.RegisteredClaims{}, + } + + header, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(config.SecretKey)) + if err != nil { + return "", 0, err + } + + var ( + now = gtime.Now() + // 认证key + authKey = GetAuthKey(header) + // 登录token + tokenKey = GetTokenKey(user.App, authKey) + // 身份绑定 + bindKey = GetBindKey(user.App, user.Id) + // 有效时长 + duration = time.Second * gconv.Duration(config.Expires) + ) + + token := &Token{ + ExpireAt: now.Unix() + config.Expires, + RefreshAt: now.Unix(), + RefreshCount: 0, + } + + if err = cache.Instance().Set(ctx, tokenKey, token, duration); err != nil { + return "", 0, err + } + + if err = cache.Instance().Set(ctx, bindKey, tokenKey, duration); err != nil { + return "", 0, err + } + return header, config.Expires, nil +} + +// Logout 注销登录 +func Logout(r *ghttp.Request) (err error) { + var ( + ctx = r.Context() + header = GetAuthorization(r) + ) + + if header == "" { + err = errorLogin + return + } + + claims, err := parseToken(ctx, header) + if err != nil { + g.Log().Debugf(ctx, "logout parseToken err:%+v", err) + err = errorLogin + return + } + + var ( + // 认证key + authKey = GetAuthKey(header) + // 登录token + tokenKey = GetTokenKey(contexts.GetModule(ctx), authKey) + // 身份绑定 + bindKey = GetBindKey(contexts.GetModule(ctx), claims.Id) + ) + + // 删除token + if _, err = cache.Instance().Remove(ctx, tokenKey); err != nil { + return + } + + if !config.MultiLogin { + if _, err = cache.Instance().Remove(ctx, bindKey); err != nil { + return + } + } + return +} + +// ParseLoginUser 解析登录用户信息 +func ParseLoginUser(r *ghttp.Request) (user *model.Identity, err error) { + var ( + ctx = r.Context() + header = GetAuthorization(r) + ) + + if header == "" { + g.Log().Debugf(ctx, "ParseLoginUser header:%v", header) + err = errorLogin + return + } + + claims, err := parseToken(ctx, header) + if err != nil { + g.Log().Debugf(ctx, "parseToken err:%+v", err) + err = errorLogin + return + } + + var ( + // 认证key + authKey = GetAuthKey(header) + // 登录token + tokenKey = GetTokenKey(claims.App, authKey) + // 身份绑定 + bindKey = GetBindKey(claims.App, claims.Id) + ) + + // 检查token是否存在 + tk, err := cache.Instance().Get(ctx, tokenKey) + if err != nil { + g.Log().Debugf(ctx, "get tokenKey err:%+v", err) + err = errorLogin + return + } + + if tk.IsEmpty() { + g.Log().Debug(ctx, "token isEmpty") + err = errorLogin + return + } + + var token *Token + if err = tk.Scan(&token); err != nil { + g.Log().Debugf(ctx, "token scan err:%+v", err) + err = errorLogin + return + } + + if token == nil { + g.Log().Debugf(ctx, "token = nil") + err = errorLogin + return + } + + now := gtime.Now() + if token.ExpireAt < now.Unix() { + g.Log().Debugf(ctx, "token expired.") + err = errorLogin + return + } + + // 是否允许多端登录 + if !config.MultiLogin { + origin, err := cache.Instance().Get(ctx, bindKey) + if err != nil { + g.Log().Debugf(ctx, "bindKey get err:%+v", err) + err = errorLogin + return nil, err + } + + if origin == nil || origin.IsEmpty() { + g.Log().Debug(ctx, "bindKey isEmpty") + err = errorLogin + return nil, err + } + + if tokenKey != origin.String() { + g.Log().Debugf(ctx, "bindKey offsite login tokenKey:%v, origin:%v", tokenKey, origin.String()) + err = errorMultiLogin + return nil, err + } + } + + // 自动刷新token有效期 + refreshToken := func() { + // 未开启自动刷新 + if !config.AutoRefresh { + return + } + + // 刷新次数已达上限 + if config.MaxRefreshTimes != -1 && token.RefreshCount >= config.MaxRefreshTimes { + return + } + + // 未达到刷新间隔 + if gtime.New(token.RefreshAt).Unix()+config.RefreshInterval > now.Unix() { + return + } + + // 刷新有效期 + token.ExpireAt = now.Unix() + config.Expires + token.RefreshAt = now.Unix() + token.RefreshCount += 1 + + duration := time.Second * gconv.Duration(config.Expires) + + if err = cache.Instance().Set(ctx, tokenKey, token, duration); err != nil { + return + } + + if err = cache.Instance().Set(ctx, bindKey, tokenKey, duration); err != nil { + return + } + } + + simple.SafeGo(ctx, func(ctx context.Context) { + refreshToken() + }) + + user = claims.Identity + return +} + +// parseToken 解析jwt令牌 +func parseToken(ctx context.Context, header string) (*Claims, error) { + token, err := jwt.ParseWithClaims(header, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(config.SecretKey), nil + }) + + if err != nil { + g.Log().Debugf(ctx, "parseToken err:%+v", err) + return nil, err + } + + if !token.Valid { + return nil, errorLogin + } + + claims, ok := token.Claims.(*Claims) + if !ok { + return nil, errorLogin + } + return claims, nil +} + +// GetAuthorization 获取authorization +func GetAuthorization(r *ghttp.Request) string { + // 默认从请求头获取 + var authorization = r.Header.Get("Authorization") + + if authorization == "" { + authorization = r.Cookie.Get("Authorization").String() + } + + // 如果请求头不存在则从get参数获取 + if authorization == "" { + return r.Get("authorization").String() + } + return gstr.Replace(authorization, "Bearer ", "") +} + +// GetAuthKey 认证key +func GetAuthKey(token string) string { + return gmd5.MustEncryptString("hotgo" + token) +} + +// GetTokenKey 令牌缓存key +func GetTokenKey(appName, authKey string) string { + return fmt.Sprintf("%v:%v:%v", consts.CacheToken, appName, authKey) +} + +// GetBindKey 令牌身份绑定key +func GetBindKey(appName string, userId int64) string { + return fmt.Sprintf("%v:%v:%v", consts.CacheTokenBind, appName, userId) +} diff --git a/internal/logic/.gitkeep b/internal/logic/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/logic/admin/project.go b/internal/logic/admin/project.go new file mode 100644 index 0000000..fcf159a --- /dev/null +++ b/internal/logic/admin/project.go @@ -0,0 +1,167 @@ +// Package admin +// @Description +// @Author Ms <133814250@qq.com> +package admin + +import ( + "context" + "fmt" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/util/grand" + "hotgo/internal/dao" + "hotgo/internal/library/contexts" + "hotgo/internal/model/entity" + "hotgo/internal/model/input/adminin" + "hotgo/internal/service" +) + +type sAdminProject struct{} + +func NewAdminProject() *sAdminProject { + return &sAdminProject{} +} + +func init() { + service.RegisterAdminProject(NewAdminProject()) +} + +// Model ORM模型 +func (s *sAdminProject) Model(ctx context.Context) *gdb.Model { + return dao.AdminProject.Ctx(ctx).Handler(func(m *gdb.Model) *gdb.Model { + return m.Where(dao.AdminProject.Columns().CreatedBy, contexts.GetUserId(ctx)) + }) +} + +// List 查询列表 +func (s *sAdminProject) List(ctx context.Context, in *adminin.ProjectListInp) (list []*adminin.ProjectListModel, totalCount int, err error) { + mod := s.Model(ctx) + totalCount, err = mod.Count() + if err != nil { + return + } + + if totalCount == 0 { + return + } + + if err = mod.Page(in.Page, in.PerPage).OrderDesc(dao.AdminProject.Columns().Id).Scan(&list); err != nil { + return + } + + for _, v := range list { + v.State = v.Status + v.CreateUserId = v.CreatedBy + v.CreateTime = v.CreatedAt + } + return +} + +// Delete 删除 +func (s *sAdminProject) Delete(ctx context.Context, in *adminin.ProjectDeleteInp) (err error) { + if exists, err := s.ExistById(ctx, in.Id); err != nil || !exists { + err = gerror.New("没有找到项目!") + return err + } + _, err = s.Model(ctx).WherePri(in.Id).Delete() + return +} + +// Create 新增 +func (s *sAdminProject) Create(ctx context.Context, in *adminin.ProjectCreateInp) (res *adminin.ProjectCreateModel, err error) { + data := &entity.AdminProject{ + Id: gconv.Int64(fmt.Sprintf("%v%v", gtime.Now().Format("YmdH"), grand.N(10000, 99999))), + ProjectName: in.ProjectName, + IndexImage: in.IndexImage, + Status: -1, + Remarks: in.Remarks, + CreatedBy: contexts.GetUserId(ctx), + } + + if _, err = dao.AdminProject.Ctx(ctx).Data(data).Insert(); err != nil { + return nil, err + } + + res = &adminin.ProjectCreateModel{ + Id: data.Id, + ProjectName: data.ProjectName, + State: data.Status, + CreateTime: gtime.Now(), + CreateUserId: contexts.GetUserId(ctx), + IndexImage: data.IndexImage, + Remarks: data.Remarks, + } + return +} + +// Edit 修改 +func (s *sAdminProject) Edit(ctx context.Context, in *adminin.ProjectEditInp) (err error) { + if exists, err := s.ExistById(ctx, in.Id); err != nil || !exists { + err = gerror.New("没有找到项目!") + return err + } + + data := make(g.Map) + if len(in.IndexImage) > 0 { + data["index_image"] = in.IndexImage + } + if len(in.ProjectName) > 0 { + data["project_name"] = in.ProjectName + } + if len(in.Remarks) > 0 { + data["remarks"] = in.Remarks + } + + if len(data) > 0 { + _, err = s.Model(ctx).WherePri(in.Id).Data(data).Update() + } + return +} + +// SaveData 保存数据 +func (s *sAdminProject) SaveData(ctx context.Context, in *adminin.ProjectSaveDataInp) (err error) { + if exists, err := s.ExistById(ctx, in.Id); err != nil || !exists { + err = gerror.New("没有找到项目!") + return err + } + _, err = s.Model(ctx).WherePri(in.Id).Data("content", in.Content).Update() + return +} + +// Publish 修改发布状态 +func (s *sAdminProject) Publish(ctx context.Context, in *adminin.ProjectPublishInp) (err error) { + if exists, err := s.ExistById(ctx, in.Id); err != nil || !exists { + err = gerror.New("没有找到项目!") + return err + } + _, err = s.Model(ctx).WherePri(in.Id).Data("status", in.State).Update() + return +} + +// GetData 获取指定信息 +func (s *sAdminProject) GetData(ctx context.Context, in *adminin.ProjectGetDataInp) (res *adminin.ProjectGetDataModel, err error) { + if err = dao.AdminProject.Ctx(ctx).WherePri(in.Id).Scan(&res); err != nil { + return nil, err + } + + if res == nil { + err = gerror.New("项目不存在") + return + } + + res.State = res.Status + return +} + +// ExistById 判断指定项目是否存在 +func (s *sAdminProject) ExistById(ctx context.Context, id int64) (exists bool, err error) { + count, err := s.Model(ctx).WherePri(id).Count() + if err != nil { + return + } + exists = count > 0 + return +} diff --git a/internal/logic/admin/site.go b/internal/logic/admin/site.go new file mode 100644 index 0000000..bbdef38 --- /dev/null +++ b/internal/logic/admin/site.go @@ -0,0 +1,125 @@ +package admin + +import ( + "context" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gtime" + "hotgo/internal/consts" + "hotgo/internal/dao" + "hotgo/internal/library/contexts" + "hotgo/internal/library/token" + "hotgo/internal/model" + "hotgo/internal/model/entity" + "hotgo/internal/model/input/adminin" + "hotgo/internal/service" + "hotgo/utility/simple" +) + +type sAdminSite struct{} + +func NewAdminSite() *sAdminSite { + return &sAdminSite{} +} + +func init() { + service.RegisterAdminSite(NewAdminSite()) +} + +// Register 账号注册 +func (s *sAdminSite) Register(ctx context.Context, in *adminin.RegisterInp) (err error) { + err = gerror.New("暂未开放注册") + return +} + +// AccountLogin 账号登录 +func (s *sAdminSite) AccountLogin(ctx context.Context, in *adminin.AccountLoginInp) (res *adminin.LoginModel, err error) { + var mb *entity.AdminMember + if err = dao.AdminMember.Ctx(ctx).Where("username", in.Username).Scan(&mb); err != nil { + err = gerror.Wrap(err, consts.ErrorORM) + return + } + + if mb == nil { + err = gerror.New("账号不存在") + return + } + + if mb.Salt == "" { + err = gerror.New("用户信息错误") + return + } + + if err = simple.CheckPassword(in.Password, mb.Salt, mb.PasswordHash); err != nil { + return + } + + if mb.Status != consts.StatusEnabled { + err = gerror.New("账号已被禁用") + return + } + + res, err = s.handleLogin(ctx, mb) + return +} + +// handleLogin . +func (s *sAdminSite) handleLogin(ctx context.Context, mb *entity.AdminMember) (res *adminin.LoginModel, err error) { + user := &model.Identity{ + Id: mb.Id, + Username: mb.Username, + Nickname: mb.Nickname, + Avatar: mb.Avatar, + App: consts.AppAdmin, + LoginAt: gtime.Now(), + } + + tk, expires, err := token.Login(ctx, user) + if err != nil { + return nil, err + } + + res = &adminin.LoginModel{ + UserInfo: &adminin.LoginUserInfo{ + Id: user.Id, + Username: user.Username, + Nickname: user.Nickname, + }, + Token: &adminin.LoginToken{ + TokenName: "Authorization", + TokenValue: tk, + Expires: expires, + }, + } + return +} + +// BindUserContext 绑定用户上下文 +func (s *sAdminSite) BindUserContext(ctx context.Context, claims *model.Identity) (err error) { + var mb *entity.AdminMember + if err = dao.AdminMember.Ctx(ctx).Where("id", claims.Id).Scan(&mb); err != nil { + err = gerror.Wrap(err, "获取用户信息失败,请稍后重试!") + return + } + + if mb == nil { + err = gerror.Wrap(err, "账号不存在或已被删除!") + return + } + + if mb.Status != consts.StatusEnabled { + err = gerror.New("账号已被禁用,如有疑问请联系管理员") + return + } + + user := &model.Identity{ + Id: mb.Id, + Username: mb.Username, + Nickname: mb.Nickname, + Avatar: mb.Avatar, + App: claims.App, + LoginAt: claims.LoginAt, + } + + contexts.SetUser(ctx, user) + return +} diff --git a/internal/logic/logic.go b/internal/logic/logic.go new file mode 100644 index 0000000..ed0c237 --- /dev/null +++ b/internal/logic/logic.go @@ -0,0 +1,10 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package logic + +import ( + _ "hotgo/internal/logic/admin" + _ "hotgo/internal/logic/middleware" +) diff --git a/internal/logic/middleware/admin_auth.go b/internal/logic/middleware/admin_auth.go new file mode 100644 index 0000000..fb44b66 --- /dev/null +++ b/internal/logic/middleware/admin_auth.go @@ -0,0 +1,35 @@ +// Package middleware +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package middleware + +import ( + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/text/gstr" + "hotgo/internal/consts" + "hotgo/internal/library/response" + "hotgo/utility/simple" +) + +// AdminAuth 后台鉴权中间件 +func (s *sMiddleware) AdminAuth(r *ghttp.Request) { + var ( + ctx = r.Context() + path = gstr.Replace(r.URL.Path, simple.RouterPrefix(ctx, consts.AppAdmin), "", 1) + ) + + // 不需要验证登录的路由地址 + if s.IsExceptLogin(ctx, consts.AppAdmin, path) { + r.Middleware.Next() + return + } + + // 将用户信息传递到上下文中 + if err := s.DeliverUserContext(r); err != nil { + response.JsonExit(r, consts.ApiStatusTokenOverdue, err.Error()) + return + } + r.Middleware.Next() +} diff --git a/internal/logic/middleware/init.go b/internal/logic/middleware/init.go new file mode 100644 index 0000000..6df62a2 --- /dev/null +++ b/internal/logic/middleware/init.go @@ -0,0 +1,123 @@ +// Package middleware +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package middleware + +import ( + "context" + "fmt" + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gctx" + "hotgo/internal/consts" + "hotgo/internal/library/contexts" + "hotgo/internal/library/token" + "hotgo/internal/model" + "hotgo/internal/service" + "hotgo/utility/validate" + "strings" + "sync" +) + +type sMiddleware struct { + LoginUrl string // 登录路由地址 + NotRecordRequest g.Map // 不记录请求数据的路由(当前请求数据过大时会影响响应效率,可以将路径放到该选项中改善) + FilterRoutes map[string]ghttp.RouterItem // 支持预处理的web路由 + routeMutex sync.Mutex +} + +func init() { + service.RegisterMiddleware(NewMiddleware()) +} + +func NewMiddleware() *sMiddleware { + return &sMiddleware{ + NotRecordRequest: g.Map{ + "/api/goview/project/upload": struct{}{}, // 上传文件 + }, + } +} + +// Ctx 初始化请求上下文 +func (s *sMiddleware) Ctx(r *ghttp.Request) { + data := make(g.Map) + if _, ok := s.NotRecordRequest[r.URL.Path]; ok { + data["request.body"] = gjson.New(nil) + } else { + data["request.body"] = gjson.New(r.GetBodyString()) + } + + contexts.Init(r, &model.Context{ + Data: data, + Module: getModule(r.URL.Path), + }) + + if len(r.Cookie.GetSessionId()) == 0 { + r.Cookie.SetSessionId(gctx.CtxId(r.Context())) + } + r.Middleware.Next() +} + +func getModule(path string) (module string) { + slice := strings.Split(path, "/") + if len(slice) < 2 { + module = consts.AppDefault + return + } + + if slice[1] == "" { + module = consts.AppDefault + return + } + return slice[1] +} + +// CORS allows Cross-origin resource sharing. +func (s *sMiddleware) CORS(r *ghttp.Request) { + r.Response.CORSDefault() + r.Middleware.Next() +} + +// DeliverUserContext 将用户信息传递到上下文中 +func (s *sMiddleware) DeliverUserContext(r *ghttp.Request) (err error) { + user, err := token.ParseLoginUser(r) + if err != nil { + return + } + + switch user.App { + case consts.AppAdmin: + if err = service.AdminSite().BindUserContext(r.Context(), user); err != nil { + return + } + default: + contexts.SetUser(r.Context(), user) + } + return +} + +// IsExceptAuth 是否是不需要验证权限的路由地址 +func (s *sMiddleware) IsExceptAuth(ctx context.Context, appName, path string) bool { + pathList := g.Cfg().MustGet(ctx, fmt.Sprintf("router.%v.exceptAuth", appName)).Strings() + + for i := 0; i < len(pathList); i++ { + if validate.InSliceExistStr(pathList[i], path) { + return true + } + } + return false +} + +// IsExceptLogin 是否是不需要登录的路由地址 +func (s *sMiddleware) IsExceptLogin(ctx context.Context, appName, path string) bool { + pathList := g.Cfg().MustGet(ctx, fmt.Sprintf("router.%v.exceptLogin", appName)).Strings() + for i := 0; i < len(pathList); i++ { + if validate.InSliceExistStr(pathList[i], path) { + return true + } + } + return false +} diff --git a/internal/logic/middleware/pre_filter.go b/internal/logic/middleware/pre_filter.go new file mode 100644 index 0000000..72f5dc0 --- /dev/null +++ b/internal/logic/middleware/pre_filter.go @@ -0,0 +1,102 @@ +package middleware + +import ( + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/util/gconv" + "hotgo/internal/library/response" + "hotgo/utility/validate" + "reflect" +) + +// GetFilterRoutes 获取支持预处理的web路由 +func (s *sMiddleware) GetFilterRoutes(r *ghttp.Request) map[string]ghttp.RouterItem { + // 首次访问时加载 + if s.FilterRoutes == nil { + s.routeMutex.Lock() + defer s.routeMutex.Unlock() + + if s.FilterRoutes != nil { + return s.FilterRoutes + } + + s.FilterRoutes = make(map[string]ghttp.RouterItem) + for _, v := range r.Server.GetRoutes() { + // 非规范路由不加载 + if v.Handler.Info.Type.NumIn() != 2 { + continue + } + + key := s.GenFilterRouteKey(v.Handler.Router) + if _, ok := s.FilterRoutes[key]; !ok { + s.FilterRoutes[key] = v + } + } + } + return s.FilterRoutes +} + +// GenFilterRequestKey 根据请求生成唯一key +func (s *sMiddleware) GenFilterRequestKey(r *ghttp.Request) string { + return s.GenRouteKey(r.Method, r.Request.URL.Path) +} + +// GenFilterRouteKey 根据路由生成唯一key +func (s *sMiddleware) GenFilterRouteKey(r *ghttp.Router) string { + return s.GenRouteKey(r.Method, r.Uri) +} + +// GenRouteKey 生成唯一key +func (s *sMiddleware) GenRouteKey(method, path string) string { + return method + " " + path +} + +// PreFilter 请求输入预处理 +// api使用gf规范路由并且XxxReq结构体实现了validate.Filter接口即可 +func (s *sMiddleware) PreFilter(r *ghttp.Request) { + router, ok := s.GetFilterRoutes(r)[s.GenFilterRequestKey(r)] + if !ok { + r.Middleware.Next() + return + } + + funcInfo := router.Handler.Info + + // 非规范路由不处理 + if funcInfo.Type.NumIn() != 2 { + r.Middleware.Next() + return + } + + inputType := funcInfo.Type.In(1) + var inputObject reflect.Value + if inputType.Kind() == reflect.Ptr { + inputObject = reflect.New(inputType.Elem()) + } else { + inputObject = reflect.New(inputType.Elem()).Elem() + } + + // 先验证基本校验规则 + if err := r.Parse(inputObject.Interface()); err != nil { + resp := gerror.Code(err) + response.JsonExit(r, resp.Code(), gerror.Current(err).Error(), resp.Detail()) + return + } + + // 没有实现预处理 + if _, ok = inputObject.Interface().(validate.Filter); !ok { + r.Middleware.Next() + return + } + + // 执行预处理 + if err := validate.PreFilter(r.Context(), inputObject.Interface()); err != nil { + resp := gerror.Code(err) + response.JsonExit(r, resp.Code(), gerror.Current(err).Error(), resp.Detail()) + return + } + + // 过滤后的参数 + r.SetParamMap(gconv.Map(inputObject.Interface())) + r.Middleware.Next() +} diff --git a/internal/logic/middleware/response.go b/internal/logic/middleware/response.go new file mode 100644 index 0000000..93c36d9 --- /dev/null +++ b/internal/logic/middleware/response.go @@ -0,0 +1,123 @@ +// Package middleware +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package middleware + +import ( + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/util/gmeta" + "hotgo/internal/consts" + "hotgo/internal/library/contexts" + "hotgo/internal/library/response" + "hotgo/utility/charset" + "hotgo/utility/simple" +) + +// ResponseHandler HTTP响应预处理 +func (s *sMiddleware) ResponseHandler(r *ghttp.Request) { + r.Middleware.Next() + + // 错误状态码接管 + switch r.Response.Status { + case 403: + r.Response.Writeln("403 - 网站拒绝显示此网页") + return + case 404: + r.Response.Writeln("404 - 你似乎来到了没有知识存在的荒原…") + return + } + + contentType := getContentType(r) + // 已存在响应 + if contentType != consts.HTTPContentTypeStream && r.Response.BufferLength() > 0 && contexts.Get(r.Context()).Response != nil { + return + } + + switch contentType { + case consts.HTTPContentTypeHtml: + s.responseHtml(r) + return + case consts.HTTPContentTypeXml: + s.responseXml(r) + return + case consts.HTTPContentTypeStream: + default: + responseJson(r) + } +} + +// responseHtml html模板响应 +func (s *sMiddleware) responseHtml(r *ghttp.Request) { + code, message, resp := parseResponse(r) + if code == gcode.CodeOK.Code() { + return + } + + r.Response.ClearBuffer() + _ = r.Response.WriteTplContent(simple.DefaultErrorTplContent(r.Context()), g.Map{"code": code, "message": message, "stack": resp}) +} + +// responseXml xml响应 +func (s *sMiddleware) responseXml(r *ghttp.Request) { + code, message, data := parseResponse(r) + response.RXml(r, code, message, data) +} + +// responseJson json响应 +func responseJson(r *ghttp.Request) { + code, message, data := parseResponse(r) + response.RJson(r, code, message, data) +} + +// parseResponse 解析响应数据 +func parseResponse(r *ghttp.Request) (code int, message string, resp interface{}) { + ctx := r.Context() + err := r.GetError() + if err == nil { + return consts.ApiStatusOk, "操作成功", r.GetHandlerResponse() + } + + // 是否输出错误堆栈到页面 + if g.Cfg().MustGet(ctx, "hotgo.debug", true).Bool() { + message = gerror.Current(err).Error() + if getContentType(r) == consts.HTTPContentTypeHtml { + resp = charset.SerializeStack(err) + } else { + resp = charset.ParseErrStack(err) + } + } else { + message = consts.ErrorMessage(gerror.Current(err)) + } + + code = gerror.Code(err).Code() + + // 记录异常日志 + // 如果你想对错误做不同的处理,可以通过定义不同的错误码来区分 + // 默认-1为安全可控错误码只记录文件日志,非-1为不可控错误,记录文件日志+服务日志并打印堆栈 + if code == gcode.CodeNil.Code() { + g.Log().Stdout(false).Infof(ctx, "exception:%v", err) + } else { + g.Log().Errorf(ctx, "exception:%v", err) + } + return +} + +func getContentType(r *ghttp.Request) (contentType string) { + contentType = r.Response.Header().Get("Content-Type") + if contentType != "" { + return + } + + mime := gmeta.Get(r.GetHandlerResponse(), "mime").String() + if mime == "" { + contentType = consts.HTTPContentTypeJson + } else { + contentType = mime + } + return +} diff --git a/internal/model/.gitkeep b/internal/model/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/model/config_load.go b/internal/model/config_load.go new file mode 100644 index 0000000..65a9d9a --- /dev/null +++ b/internal/model/config_load.go @@ -0,0 +1,28 @@ +// Package model +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package model + +// 本地配置. + +// TokenConfig 登录令牌配置 +type TokenConfig struct { + SecretKey string `json:"secretKey"` + Expires int64 `json:"expires"` + AutoRefresh bool `json:"autoRefresh"` + RefreshInterval int64 `json:"refreshInterval"` + MaxRefreshTimes int64 `json:"maxRefreshTimes"` + MultiLogin bool `json:"multiLogin"` +} + +// UploadConfig 上传配置 +type UploadConfig struct { + Drive string `json:"drive"` + FileSize int64 `json:"fileSize"` + FileType string `json:"fileType"` + ImageSize int64 `json:"imageSize"` + ImageType string `json:"imageType"` + LocalPath string `json:"localPath"` +} diff --git a/internal/model/context.go b/internal/model/context.go new file mode 100644 index 0000000..15bd6f0 --- /dev/null +++ b/internal/model/context.go @@ -0,0 +1,30 @@ +// Package model +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package model + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// Context 请求上下文结构 +type Context struct { + Module string // 应用模块 admin|api|home|websocket + AddonName string // 插件名称 如果不是插件模块请求,可能为空 + User *Identity // 上下文用户信息 + Response *Response // 请求响应 + Data g.Map // 自定kv变量 业务模块根据需要设置,不固定 +} + +// Identity 通用身份模型 +type Identity struct { + Id int64 `json:"id" description:"用户ID"` + Username string `json:"username" description:"用户名"` + Nickname string `json:"nickname" description:"昵称"` + Avatar string `json:"avatar" description:"头像"` + App string `json:"app" description:"登录应用"` + LoginAt *gtime.Time `json:"loginAt" description:"登录时间"` +} diff --git a/internal/model/do/.gitkeep b/internal/model/do/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/model/do/admin_attachment.go b/internal/model/do/admin_attachment.go new file mode 100644 index 0000000..6ab2267 --- /dev/null +++ b/internal/model/do/admin_attachment.go @@ -0,0 +1,32 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// AdminAttachment is the golang structure of table hg_admin_attachment for DAO operations like Where/Data. +type AdminAttachment struct { + g.Meta `orm:"table:hg_admin_attachment, do:true"` + Id interface{} // 文件ID + AppId interface{} // 应用ID + MemberId interface{} // 管理员ID + CateId interface{} // 上传分类 + Drive interface{} // 上传驱动 + Name interface{} // 文件原始名 + Kind interface{} // 上传类型 + MimeType interface{} // 扩展类型 + NaiveType interface{} // NaiveUI类型 + Path interface{} // 本地路径 + FileUrl interface{} // url + Size interface{} // 文件大小 + Ext interface{} // 扩展名 + Md5 interface{} // md5校验码 + Status interface{} // 状态 + CreatedAt *gtime.Time // 创建时间 + UpdatedAt *gtime.Time // 修改时间 +} diff --git a/internal/model/do/admin_member.go b/internal/model/do/admin_member.go new file mode 100644 index 0000000..d2cb544 --- /dev/null +++ b/internal/model/do/admin_member.go @@ -0,0 +1,26 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// AdminMember is the golang structure of table hg_admin_member for DAO operations like Where/Data. +type AdminMember struct { + g.Meta `orm:"table:hg_admin_member, do:true"` + Id interface{} // 管理员ID + Nickname interface{} // 昵称 + Username interface{} // 帐号 + PasswordHash interface{} // 密码 + Salt interface{} // 密码盐 + Avatar interface{} // 头像 + LastActiveAt *gtime.Time // 最后活跃时间 + Remark interface{} // 备注 + Status interface{} // 状态 + CreatedAt *gtime.Time // 创建时间 + UpdatedAt *gtime.Time // 修改时间 +} diff --git a/internal/model/do/admin_project.go b/internal/model/do/admin_project.go new file mode 100644 index 0000000..5bb38f4 --- /dev/null +++ b/internal/model/do/admin_project.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// AdminProject is the golang structure of table hg_admin_project for DAO operations like Where/Data. +type AdminProject struct { + g.Meta `orm:"table:hg_admin_project, do:true"` + Id interface{} // 项目id + ProjectName interface{} // 项目名称 + IndexImage interface{} // 预览图片url + Content interface{} // 项目数据 + Status interface{} // 项目状态,-1: 未发布'1: 已发布 + Remarks interface{} // 项目备注 + CreatedBy interface{} // 创建人 + UpdatedAt *gtime.Time // 更新时间 + CreatedAt *gtime.Time // 创建时间 + DeletedAt *gtime.Time // 删除时间 +} diff --git a/internal/model/entity/.gitkeep b/internal/model/entity/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/model/entity/admin_attachment.go b/internal/model/entity/admin_attachment.go new file mode 100644 index 0000000..5f7a752 --- /dev/null +++ b/internal/model/entity/admin_attachment.go @@ -0,0 +1,30 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// AdminAttachment is the golang structure for table admin_attachment. +type AdminAttachment struct { + Id int64 `json:"id" description:"文件ID"` + AppId string `json:"appId" description:"应用ID"` + MemberId int64 `json:"memberId" description:"管理员ID"` + CateId uint64 `json:"cateId" description:"上传分类"` + Drive string `json:"drive" description:"上传驱动"` + Name string `json:"name" description:"文件原始名"` + Kind string `json:"kind" description:"上传类型"` + MimeType string `json:"mimeType" description:"扩展类型"` + NaiveType string `json:"naiveType" description:"NaiveUI类型"` + Path string `json:"path" description:"本地路径"` + FileUrl string `json:"fileUrl" description:"url"` + Size int64 `json:"size" description:"文件大小"` + Ext string `json:"ext" description:"扩展名"` + Md5 string `json:"md5" description:"md5校验码"` + Status int `json:"status" description:"状态"` + CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" description:"修改时间"` +} diff --git a/internal/model/entity/admin_member.go b/internal/model/entity/admin_member.go new file mode 100644 index 0000000..ed9a7e5 --- /dev/null +++ b/internal/model/entity/admin_member.go @@ -0,0 +1,24 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// AdminMember is the golang structure for table admin_member. +type AdminMember struct { + Id int64 `json:"id" description:"管理员ID"` + Nickname string `json:"nickname" description:"昵称"` + Username string `json:"username" description:"帐号"` + PasswordHash string `json:"passwordHash" description:"密码"` + Salt string `json:"salt" description:"密码盐"` + Avatar string `json:"avatar" description:"头像"` + LastActiveAt *gtime.Time `json:"lastActiveAt" description:"最后活跃时间"` + Remark string `json:"remark" description:"备注"` + Status int `json:"status" description:"状态"` + CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" description:"修改时间"` +} diff --git a/internal/model/entity/admin_project.go b/internal/model/entity/admin_project.go new file mode 100644 index 0000000..cd56788 --- /dev/null +++ b/internal/model/entity/admin_project.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// AdminProject is the golang structure for table admin_project. +type AdminProject struct { + Id int64 `json:"id" description:"项目id"` + ProjectName string `json:"projectName" description:"项目名称"` + IndexImage string `json:"indexImage" description:"预览图片url"` + Content string `json:"content" description:"项目数据"` + Status int `json:"status" description:"项目状态,-1: 未发布'1: 已发布"` + Remarks string `json:"remarks" description:"项目备注"` + CreatedBy int64 `json:"createdBy" description:"创建人"` + UpdatedAt *gtime.Time `json:"updatedAt" description:"更新时间"` + CreatedAt *gtime.Time `json:"createdAt" description:"创建时间"` + DeletedAt *gtime.Time `json:"deletedAt" description:"删除时间"` +} diff --git a/internal/model/input/adminin/project.go b/internal/model/input/adminin/project.go new file mode 100644 index 0000000..d154110 --- /dev/null +++ b/internal/model/input/adminin/project.go @@ -0,0 +1,127 @@ +// Package adminin +// @Description +// @Author Ms <133814250@qq.com> +package adminin + +import ( + "context" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "hotgo/internal/model/entity" + "hotgo/internal/model/input/form" +) + +// ProjectListInp 查询列表 +type ProjectListInp struct { + form.PageReq +} + +type ProjectListModel struct { + entity.AdminProject + State int `json:"state"` + CreateTime *gtime.Time `json:"createTime"` + CreateUserId int64 `json:"createUserId"` +} + +// ProjectCreateInp 新增项目 +type ProjectCreateInp struct { + ProjectName string `json:"projectName"` + Remarks string `json:"remarks"` + IndexImage string `json:"indexImage"` +} + +type ProjectCreateModel struct { + Id int64 `json:"id"` + ProjectName string `json:"projectName"` + State int `json:"state"` + CreateTime *gtime.Time `json:"createTime"` + CreateUserId int64 `json:"createUserId"` + IndexImage string `json:"indexImage"` + Remarks string `json:"remarks"` +} + +// ProjectEditInp 修改项目数据 +type ProjectEditInp struct { + entity.AdminProject +} + +type ProjectEditModel struct{} + +func (in *ProjectEditInp) Filter(ctx context.Context) (err error) { + if in.Id <= 0 { + err = gerror.New("ID不能为空") + return + } + return +} + +// ProjectSaveDataInp 保存项目数据 +type ProjectSaveDataInp struct { + Id int64 `json:"projectId"` + Content string `json:"content" description:"项目数据"` +} + +type ProjectSaveDataModel struct{} + +func (in *ProjectSaveDataInp) Filter(ctx context.Context) (err error) { + if in.Id <= 0 { + err = gerror.New("ID不能为空") + return + } + return +} + +// ProjectDeleteInp 删除项目类型 +type ProjectDeleteInp struct { + Id int64 `json:"id" v:"required#项目ID不能为空" dc:"项目ID"` +} + +type ProjectDeleteModel struct{} + +// ProjectGetDataInp 获取信息 +type ProjectGetDataInp struct { + Id int64 `json:"projectId" dc:"项目ID"` +} + +func (in *ProjectGetDataInp) Filter(ctx context.Context) (err error) { + if in.Id <= 0 { + err = gerror.New("ID不能为空") + return + } + return +} + +type ProjectGetDataModel struct { + Id int64 `json:"id"` + ProjectName string `json:"projectName"` + State int `json:"state"` + CreateTime *gtime.Time `json:"createTime"` + CreateUserId int64 `json:"createUserId"` + IndexImage string `json:"indexImage"` + Remarks string `json:"remarks"` + Content string `json:"content" description:"项目数据"` + Status int `json:"status"` +} + +// ProjectPublishInp 修改发布状态 +type ProjectPublishInp struct { + Id int64 `json:"id"` + ProjectName string `json:"projectName"` + State int `json:"state"` +} + +func (in *ProjectPublishInp) Filter(ctx context.Context) (err error) { + if in.Id <= 0 { + err = gerror.New("ID不能为空") + return + } + return +} + +type ProjectPublishModel struct{} + +// ProjectUploadInp 文件上传 +type ProjectUploadInp struct { + File *ghttp.UploadFile `json:"object" type:"file" dc:"文件"` +} diff --git a/internal/model/input/adminin/site.go b/internal/model/input/adminin/site.go new file mode 100644 index 0000000..39a94e0 --- /dev/null +++ b/internal/model/input/adminin/site.go @@ -0,0 +1,61 @@ +package adminin + +import ( + "context" + "github.com/gogf/gf/v2/os/gtime" +) + +// RegisterInp 账号注册 +type RegisterInp struct { + Username string `json:"username" v:"required#用户名不能为空" dc:"用户名"` + Password string `json:"password" v:"required#密码不能为空" dc:"密码"` + Nickname string `json:"nickname" dc:"昵称"` +} + +func (in *RegisterInp) Filter(ctx context.Context) (err error) { + return +} + +type LoginToken struct { + TokenValue string `json:"tokenValue"` + TokenName string `json:"tokenName"` + Expires int64 `json:"expires"` +} + +type LoginUserInfo struct { + Id int64 `json:"id" dc:"用户ID"` + Username string `json:"username" dc:"用户名"` + Nickname string `json:"nickname" dc:"昵称"` +} + +// LoginModel 统一登录响应 +type LoginModel struct { + UserInfo *LoginUserInfo `json:"userinfo" dc:"用户信息"` + Token *LoginToken `json:"token" dc:"登录token"` +} + +// AccountLoginInp 账号登录 +type AccountLoginInp struct { + Username string `json:"username" v:"required#用户名不能为空" dc:"用户名"` + Password string `json:"password" v:"required#密码不能为空" dc:"密码"` +} + +// MobileLoginInp 手机号登录 +type MobileLoginInp struct { + Mobile string `json:"mobile" v:"required|phone-loose#手机号不能为空|手机号格式不正确" dc:"手机号"` + Code string `json:"code" v:"required#验证码不能为空" dc:"验证码"` +} + +// MemberLoginPermissions 登录用户角色信息 +type MemberLoginPermissions []string + +// MemberLoginStatInp 用户登录统计 +type MemberLoginStatInp struct { + MemberId int64 +} + +type MemberLoginStatModel struct { + LoginCount int `json:"loginCount" dc:"登录次数"` + LastLoginAt *gtime.Time `json:"lastLoginAt" dc:"最后登录时间"` + LastLoginIp string `json:"lastLoginIp" dc:"最后登录IP"` +} diff --git a/internal/model/input/form/page.go b/internal/model/input/form/page.go new file mode 100644 index 0000000..dc21499 --- /dev/null +++ b/internal/model/input/form/page.go @@ -0,0 +1,67 @@ +// Package form +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package form + +import ( + "hotgo/internal/consts" +) + +type ReqPageFunc interface { + GetPage() int + GetPerPage() int +} + +// PageReq 分页请求 +type PageReq struct { + Page int `json:"page" example:"10" d:"1" v:"min:1#页码最小值不能低于1" dc:"当前页码"` + PerPage int `json:"limit" example:"1" d:"10" v:"min:1|max:200#每页数量最小值不能低于1|最大值不能大于200" dc:"每页数量"` +} + +// GetPage 获取当前页码 +func (req *PageReq) GetPage() int { + return req.Page +} + +// GetPerPage 获取每页数量 +func (req *PageReq) GetPerPage() int { + return req.PerPage +} + +// PageRes 分页响应 +type PageRes struct { + PageReq + PageCount int `json:"pageCount" example:"0" dc:"分页个数"` + TotalCount int `json:"count" example:"0" dc:"数据总行数"` +} + +// Pack 打包分页数据 +func (res *PageRes) Pack(req ReqPageFunc, totalCount int) { + res.TotalCount = totalCount + res.PageCount = CalPageCount(totalCount, req.GetPerPage()) + res.Page = req.GetPage() + res.PerPage = req.GetPerPage() +} + +func CalPageCount(totalCount int, perPage int) int { + return (totalCount + perPage - 1) / perPage +} + +// CalPage 计算分页偏移量 +func CalPage(page, perPage int) (newPage, newPerPage int, offset int) { + if page <= 0 { + newPage = consts.DefaultPage + } else { + newPage = page + } + if perPage <= 0 { + newPerPage = consts.DefaultPageSize + } else { + newPerPage = perPage + } + + offset = (newPage - 1) * newPerPage + return +} diff --git a/internal/model/response.go b/internal/model/response.go new file mode 100644 index 0000000..8dc5a21 --- /dev/null +++ b/internal/model/response.go @@ -0,0 +1,16 @@ +// Package model +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package model + +// Response HTTP响应 +type Response struct { + Code int `json:"code" example:"0" description:"状态码"` + Message string `json:"msg,omitempty" example:"操作成功" description:"提示消息"` + Data interface{} `json:"data,omitempty" description:"数据集"` + Error interface{} `json:"error,omitempty" description:"错误信息"` + Timestamp int64 `json:"timestamp" example:"1640966400" description:"服务器时间戳"` + TraceID string `json:"traceID" v:"0" example:"d0bb93048bc5c9164cdee845dcb7f820" description:"链路ID"` +} diff --git a/internal/packed/packed.go b/internal/packed/packed.go new file mode 100644 index 0000000..e20ab1e --- /dev/null +++ b/internal/packed/packed.go @@ -0,0 +1 @@ +package packed diff --git a/internal/service/.gitkeep b/internal/service/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/service/admin.go b/internal/service/admin.go new file mode 100644 index 0000000..8bbd501 --- /dev/null +++ b/internal/service/admin.go @@ -0,0 +1,66 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "hotgo/internal/model" + "hotgo/internal/model/input/adminin" +) + +type ( + IAdminProject interface { + // List 查询列表 + List(ctx context.Context, in *adminin.ProjectListInp) (list []*adminin.ProjectListModel, totalCount int, err error) + // Delete 删除 + Delete(ctx context.Context, in *adminin.ProjectDeleteInp) (err error) + // Create 新增 + Create(ctx context.Context, in *adminin.ProjectCreateInp) (res *adminin.ProjectCreateModel, err error) + // Edit 修改 + Edit(ctx context.Context, in *adminin.ProjectEditInp) (err error) + // SaveData 保存数据 + SaveData(ctx context.Context, in *adminin.ProjectSaveDataInp) (err error) + // Publish 修改发布状态 + Publish(ctx context.Context, in *adminin.ProjectPublishInp) (err error) + // GetData 获取指定信息 + GetData(ctx context.Context, in *adminin.ProjectGetDataInp) (res *adminin.ProjectGetDataModel, err error) + } + IAdminSite interface { + // Register 账号注册 + Register(ctx context.Context, in *adminin.RegisterInp) (err error) + // AccountLogin 账号登录 + AccountLogin(ctx context.Context, in *adminin.AccountLoginInp) (res *adminin.LoginModel, err error) + // BindUserContext 绑定用户上下文 + BindUserContext(ctx context.Context, claims *model.Identity) (err error) + } +) + +var ( + localAdminProject IAdminProject + localAdminSite IAdminSite +) + +func AdminProject() IAdminProject { + if localAdminProject == nil { + panic("implement not found for interface IAdminProject, forgot register?") + } + return localAdminProject +} + +func RegisterAdminProject(i IAdminProject) { + localAdminProject = i +} + +func AdminSite() IAdminSite { + if localAdminSite == nil { + panic("implement not found for interface IAdminSite, forgot register?") + } + return localAdminSite +} + +func RegisterAdminSite(i IAdminSite) { + localAdminSite = i +} diff --git a/internal/service/middleware.go b/internal/service/middleware.go new file mode 100644 index 0000000..9143739 --- /dev/null +++ b/internal/service/middleware.go @@ -0,0 +1,57 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + + "github.com/gogf/gf/v2/net/ghttp" +) + +type ( + IMiddleware interface { + // AdminAuth 后台鉴权中间件 + AdminAuth(r *ghttp.Request) + // Ctx 初始化请求上下文 + Ctx(r *ghttp.Request) + // CORS allows Cross-origin resource sharing. + CORS(r *ghttp.Request) + // DeliverUserContext 将用户信息传递到上下文中 + DeliverUserContext(r *ghttp.Request) (err error) + // IsExceptAuth 是否是不需要验证权限的路由地址 + IsExceptAuth(ctx context.Context, appName, path string) bool + // IsExceptLogin 是否是不需要登录的路由地址 + IsExceptLogin(ctx context.Context, appName, path string) bool + // GetFilterRoutes 获取支持预处理的web路由 + GetFilterRoutes(r *ghttp.Request) map[string]ghttp.RouterItem + // GenFilterRequestKey 根据请求生成唯一key + GenFilterRequestKey(r *ghttp.Request) string + // GenFilterRouteKey 根据路由生成唯一key + GenFilterRouteKey(r *ghttp.Router) string + // GenRouteKey 生成唯一key + GenRouteKey(method, path string) string + // PreFilter 请求输入预处理 + // api使用gf规范路由并且XxxReq结构体实现了validate.Filter接口即可 + PreFilter(r *ghttp.Request) + // ResponseHandler HTTP响应预处理 + ResponseHandler(r *ghttp.Request) + } +) + +var ( + localMiddleware IMiddleware +) + +func Middleware() IMiddleware { + if localMiddleware == nil { + panic("implement not found for interface IMiddleware, forgot register?") + } + return localMiddleware +} + +func RegisterMiddleware(i IMiddleware) { + localMiddleware = i +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..1a146bd --- /dev/null +++ b/main.go @@ -0,0 +1,18 @@ +package main + +import ( + _ "github.com/gogf/gf/contrib/drivers/mysql/v2" + _ "github.com/gogf/gf/contrib/nosql/redis/v2" + + _ "hotgo/internal/packed" + + _ "hotgo/internal/logic" + + "github.com/gogf/gf/v2/os/gctx" + + "hotgo/internal/cmd" +) + +func main() { + cmd.Main.Run(gctx.GetInitCtx()) +} diff --git a/manifest/config/config.yaml b/manifest/config/config.yaml new file mode 100644 index 0000000..ab79c5a --- /dev/null +++ b/manifest/config/config.yaml @@ -0,0 +1,103 @@ + +# hotgo配置 +hotgo: + # debug开关,开启后接口出现错误时会向前端输出堆栈信息,可选:false|true,默认为true + debug: true + + +# 路由配置 +router: + # 后台 + admin: + # 前缀 + prefix: "/admin" + # 不需要验证登录的路由地址 + exceptLogin: [ + "/api/goview/project/getData", # 获取项目数据 + ] + + +# 统一默认日志配置 +defaultLogger: &defaultLogger + level: "all" + flags: 42 + file: "{Y-m-d}.log" # 日志文件格式。默认为"{Y-m-d}.log" + stdoutColorDisabled: false # 关闭终端的颜色打印。可选:false|true,默认false + writerColorEnable: false # 日志文件是否带上颜色。可选:false|true,默认false,表示不带颜色 + rotateExpire: "7d" # 日志保留天数 + rotateBackupLimit: 2 # 最大备份数量 + rotateBackupCompress: 2 # 日志文件压缩级别,0-9,9最高 + + +# gf配置,配置参考:https://goframe.org/pages/viewpage.action?pageId=44449486 +server: + address: ":8090" # 本地监听地址 + openapiPath: "/api.json" # OpenAPI接口文档地址 + swaggerPath: "/swagger" # 内置SwaggerUI展示地址 + serverRoot: "resource/public" # 静态文件服务的目录根路径,配置时自动开启静态文件服务。 + DumpRouterMap: false # 是否在Server启动时打印所有的路由列表。 + logPath: "logs/server" # 服务日志保存路径 + ErrorStack: true # 当Server捕获到异常时是否记录堆栈信息到日志中。默认为true + ErrorLogEnabled: true # 是否记录异常日志信息到日志中。默认为true + errorLogPattern: "error/{Y-m-d}.log" # 异常错误日志文件格式。默认为"error-{Ymd}.log" + accessLogEnabled: true # 是否记录访问日志。默认为false + accessLogPattern: "access/{Y-m-d}.log" # 访问日志文件格式。默认为"access-{Ymd}.log" + maxHeaderBytes: "100KB" # 请求头大小限制,请求头包括客户端提交的Cookie数据,默认设置为100KB + clientMaxBodySize: "300MB" # 客户端提交的Body大小限制,同时也影响文件上传大小,默认设置为300MB + serverAgent: "HG HTTP Server" # 服务端Agent信息。默认为"HG HTTP Server" + # PProf配置 + pprofEnabled: true # 是否开启PProf性能调试特性。默认为false + pprofPattern: "/pprof" # 开启PProf时有效,表示PProf特性的页面访问路径,对当前Server绑定的所有域名有效。 + # 服务日志配置 + logger: + path: "logs/server" # 日志文件路径。默认为空,表示关闭,仅输出到终端 + <<: *defaultLogger + + +#缓存 +cache: + adapter: "file" # 缓存驱动方式,支持:memory|redis|file,不填默认memory + fileDir: "./storage/cache" # 文件缓存路径,adapter=file时必填 + + +# 登录令牌 +token: + secretKey: "hotgo123" # 令牌加密秘钥,考虑安全问题生产环境中请修改默认值 + expires: 604800 # 令牌有效期,单位:秒。默认7天 + autoRefresh: true # 是否开启自动刷新过期时间, false|true 默认为true + refreshInterval: 86400 # 刷新间隔,单位:秒。必须小于expires,否则无法触发。默认1天内只允许刷新一次 + maxRefreshTimes: 30 # 最大允许刷新次数,-1不限制。默认30次 + multiLogin: true # 是否允许多端登录, false|true 默认为true + +# 上传驱动 +storager: + # 通用配置 + drive: "local" # 上传驱动。local:本地存储 + fileSize: "10" # 上传图片大小限制,单位:MB + fileType: "doc,docx,pdf,zip,tar,xls,xlsx,rar,jpg,jpeg,gif,npm,png,svg" # 上传文件类型限制,文件上传后缀类型限制 + imageSize: "your_image_size_value" # 上传图片大小限制,单位:MB + imageType: "jpg,jpeg,gif,npm,png,svg" # 上传图片类型限制,图片上传后缀类型限制 + localPath: "attachment/" # 本地存储路径,对外访问的相对路径 + + +# Redis. 配置参考:https://goframe.org/pages/viewpage.action?pageId=1114217 +redis: + default: + address: "192.168.1.100:6379" + db: "3" + pass: "jhkdjhkjdhsIUTYURTU_37fMei" + idleTimeout: "20" + + +# Database. 配置参考:https://goframe.org/pages/viewpage.action?pageId=1114245 +database: + logger: + path: "logs/database" # 日志文件路径。默认为空,表示关闭,仅输出到终端 + <<: *defaultLogger + stdout: true + default: + link: "mysql:goview:ZZ6amaa8xnHJQffb@tcp(192.168.1.100:3306)/goview?loc=Local&parseTime=true&charset=utf8mb4" + debug: true + Prefix: "hg_" + + diff --git a/manifest/deploy/kustomize/base/deployment.yaml b/manifest/deploy/kustomize/base/deployment.yaml new file mode 100644 index 0000000..28f1d69 --- /dev/null +++ b/manifest/deploy/kustomize/base/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: template-single + labels: + app: template-single +spec: + replicas: 1 + selector: + matchLabels: + app: template-single + template: + metadata: + labels: + app: template-single + spec: + containers: + - name : main + image: template-single + imagePullPolicy: Always + diff --git a/manifest/deploy/kustomize/base/kustomization.yaml b/manifest/deploy/kustomize/base/kustomization.yaml new file mode 100644 index 0000000..302d92d --- /dev/null +++ b/manifest/deploy/kustomize/base/kustomization.yaml @@ -0,0 +1,8 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- deployment.yaml +- service.yaml + + + diff --git a/manifest/deploy/kustomize/base/service.yaml b/manifest/deploy/kustomize/base/service.yaml new file mode 100644 index 0000000..608771c --- /dev/null +++ b/manifest/deploy/kustomize/base/service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: template-single +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 8000 + selector: + app: template-single + diff --git a/manifest/deploy/kustomize/overlays/develop/configmap.yaml b/manifest/deploy/kustomize/overlays/develop/configmap.yaml new file mode 100644 index 0000000..3b1d0af --- /dev/null +++ b/manifest/deploy/kustomize/overlays/develop/configmap.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: template-single-configmap +data: + config.yaml: | + server: + address: ":8000" + openapiPath: "/api.json" + swaggerPath: "/swagger" + + logger: + level : "all" + stdout: true diff --git a/manifest/deploy/kustomize/overlays/develop/deployment.yaml b/manifest/deploy/kustomize/overlays/develop/deployment.yaml new file mode 100644 index 0000000..04e4851 --- /dev/null +++ b/manifest/deploy/kustomize/overlays/develop/deployment.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: template-single +spec: + template: + spec: + containers: + - name : main + image: template-single:develop \ No newline at end of file diff --git a/manifest/deploy/kustomize/overlays/develop/kustomization.yaml b/manifest/deploy/kustomize/overlays/develop/kustomization.yaml new file mode 100644 index 0000000..4731c47 --- /dev/null +++ b/manifest/deploy/kustomize/overlays/develop/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- ../../base +- configmap.yaml + +patchesStrategicMerge: +- deployment.yaml + +namespace: default + + + diff --git a/manifest/docker/Dockerfile b/manifest/docker/Dockerfile new file mode 100644 index 0000000..d3abe8f --- /dev/null +++ b/manifest/docker/Dockerfile @@ -0,0 +1,16 @@ +FROM loads/alpine:3.8 + +############################################################################### +# INSTALLATION +############################################################################### + +ENV WORKDIR /app +ADD resource $WORKDIR/ +ADD ./temp/linux_amd64/main $WORKDIR/main +RUN chmod +x $WORKDIR/main + +############################################################################### +# START +############################################################################### +WORKDIR $WORKDIR +CMD ./main diff --git a/manifest/docker/docker.sh b/manifest/docker/docker.sh new file mode 100644 index 0000000..ff393f9 --- /dev/null +++ b/manifest/docker/docker.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# This shell is executed before docker build. + + + + + diff --git a/manifest/i18n/.gitkeep b/manifest/i18n/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/manifest/protobuf/.keep-if-necessary b/manifest/protobuf/.keep-if-necessary new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/html/.gitkeep b/resource/public/html/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/plugin/.gitkeep b/resource/public/plugin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/resource/css/.gitkeep b/resource/public/resource/css/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/resource/image/.gitkeep b/resource/public/resource/image/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/public/resource/js/.gitkeep b/resource/public/resource/js/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resource/template/.gitkeep b/resource/template/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/storage/data/goview.sql b/storage/data/goview.sql new file mode 100644 index 0000000..469d1c5 --- /dev/null +++ b/storage/data/goview.sql @@ -0,0 +1,152 @@ +-- phpMyAdmin SQL Dump +-- version 5.2.1 +-- https://www.phpmyadmin.net/ +-- +-- 主机: localhost +-- 生成日期: 2024-03-04 11:50:47 +-- 服务器版本: 5.7.40-log +-- PHP 版本: 7.4.33 + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; +SET time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- 数据库: `goview` +-- + +-- -------------------------------------------------------- + +-- +-- 表的结构 `hg_admin_attachment` +-- + +CREATE TABLE `hg_admin_attachment` ( + `id` bigint(20) NOT NULL COMMENT '文件ID', + `app_id` varchar(64) NOT NULL COMMENT '应用ID', + `member_id` bigint(20) DEFAULT '0' COMMENT '管理员ID', + `cate_id` bigint(20) UNSIGNED DEFAULT '0' COMMENT '上传分类', + `drive` varchar(64) DEFAULT NULL COMMENT '上传驱动', + `name` varchar(1000) DEFAULT NULL COMMENT '文件原始名', + `kind` varchar(16) DEFAULT NULL COMMENT '上传类型', + `mime_type` varchar(128) NOT NULL DEFAULT '' COMMENT '扩展类型', + `naive_type` varchar(32) NOT NULL COMMENT 'NaiveUI类型', + `path` varchar(1000) DEFAULT NULL COMMENT '本地路径', + `file_url` varchar(1000) DEFAULT NULL COMMENT 'url', + `size` bigint(20) DEFAULT '0' COMMENT '文件大小', + `ext` varchar(50) DEFAULT NULL COMMENT '扩展名', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5校验码', + `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态', + `created_at` datetime DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime DEFAULT NULL COMMENT '修改时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='附件管理' ROW_FORMAT=COMPACT; + +-- -------------------------------------------------------- + +-- +-- 表的结构 `hg_admin_member` +-- + +CREATE TABLE `hg_admin_member` ( + `id` bigint(20) NOT NULL COMMENT '管理员ID', + `nickname` varchar(32) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '昵称', + `username` varchar(20) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '帐号', + `password_hash` char(32) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '密码', + `salt` char(16) CHARACTER SET utf8mb4 NOT NULL COMMENT '密码盐', + `avatar` char(150) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '头像', + `last_active_at` datetime DEFAULT NULL COMMENT '最后活跃时间', + `remark` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '备注', + `status` tinyint(1) DEFAULT '1' COMMENT '状态', + `created_at` datetime DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime DEFAULT NULL COMMENT '修改时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表' ROW_FORMAT=COMPACT; + +-- +-- 转存表中的数据 `hg_admin_member` +-- + +INSERT INTO `hg_admin_member` (`id`, `nickname`, `username`, `password_hash`, `salt`, `avatar`, `last_active_at`, `remark`, `status`, `created_at`, `updated_at`) VALUES +(1, '孟帅', 'admin', 'ce72f929f1acdd4f21a5f8337e6a9aed', '6541561', 'http://bufanyun.cn-bj.ufileos.com/hotgo/attachment/2023-02-09/cqdq8er9nfkchdopav.png', '2024-03-01 17:19:34', NULL, 1, '2024-03-01 17:09:45', '2024-03-01 17:19:34'); + +-- -------------------------------------------------------- + +-- +-- 表的结构 `hg_admin_project` +-- + +CREATE TABLE `hg_admin_project` ( + `id` bigint(20) NOT NULL COMMENT '项目id', + `project_name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '项目名称', + `index_image` longtext CHARACTER SET utf8mb4 COMMENT '预览图片url', + `content` longtext CHARACTER SET utf8mb4 COMMENT '项目数据', + `status` tinyint(1) NOT NULL COMMENT '项目状态,-1: 未发布''1: 已发布', + `remarks` longtext CHARACTER SET utf8mb4 COMMENT '项目备注', + `created_by` bigint(20) NOT NULL COMMENT '创建人', + `updated_at` datetime NOT NULL COMMENT '更新时间', + `created_at` datetime NOT NULL COMMENT '创建时间', + `deleted_at` datetime DEFAULT NULL COMMENT '删除时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='项目' ROW_FORMAT=COMPACT; + +-- +-- 转存表中的数据 `hg_admin_project` +-- + +INSERT INTO `hg_admin_project` (`id`, `project_name`, `index_image`, `content`, `status`, `remarks`, `created_by`, `updated_at`, `created_at`, `deleted_at`) VALUES +(202403040972038, '测试项目23', 'http://bufanyun.cn-bj.ufileos.com/haokav2/attachment/images/2024-03-04/czknnv7p1xi6gsruzi.png', '{\n \"editCanvasConfig\": {\n \"projectName\": \"测试项目23\",\n \"width\": 1920,\n \"height\": 1080,\n \"filterShow\": false,\n \"hueRotate\": 0,\n \"saturate\": 1,\n \"contrast\": 1,\n \"brightness\": 1,\n \"opacity\": 1,\n \"rotateZ\": 0,\n \"rotateX\": 0,\n \"rotateY\": 0,\n \"skewX\": 0,\n \"skewY\": 0,\n \"blendMode\": \"normal\",\n \"background\": null,\n \"backgroundImage\": \"http://bufanyun.cn-bj.ufileos.com/haokav2/attachment/images/2024-03-04/czknnv7p1xi6gsruzi.png\",\n \"selectColor\": false,\n \"chartThemeColor\": \"macarons\",\n \"chartCustomThemeColorInfo\": null,\n \"chartThemeSetting\": {\n \"title\": {\n \"show\": true,\n \"textStyle\": {\n \"color\": \"#BFBFBF\",\n \"fontSize\": 18\n },\n \"subtextStyle\": {\n \"color\": \"#A2A2A2\",\n \"fontSize\": 14\n }\n },\n \"xAxis\": {\n \"show\": true,\n \"name\": \"\",\n \"nameGap\": 15,\n \"nameTextStyle\": {\n \"color\": \"#B9B8CE\",\n \"fontSize\": 12\n },\n \"inverse\": false,\n \"axisLabel\": {\n \"show\": true,\n \"fontSize\": 12,\n \"color\": \"#B9B8CE\",\n \"rotate\": 0\n },\n \"position\": \"bottom\",\n \"axisLine\": {\n \"show\": true,\n \"lineStyle\": {\n \"color\": \"#B9B8CE\",\n \"width\": 1\n },\n \"onZero\": true\n },\n \"axisTick\": {\n \"show\": true,\n \"length\": 5\n },\n \"splitLine\": {\n \"show\": false,\n \"lineStyle\": {\n \"color\": \"#484753\",\n \"width\": 1,\n \"type\": \"solid\"\n }\n }\n },\n \"yAxis\": {\n \"show\": true,\n \"name\": \"\",\n \"nameGap\": 15,\n \"nameTextStyle\": {\n \"color\": \"#B9B8CE\",\n \"fontSize\": 12\n },\n \"inverse\": false,\n \"axisLabel\": {\n \"show\": true,\n \"fontSize\": 12,\n \"color\": \"#B9B8CE\",\n \"rotate\": 0\n },\n \"position\": \"left\",\n \"axisLine\": {\n \"show\": true,\n \"lineStyle\": {\n \"color\": \"#B9B8CE\",\n \"width\": 1\n },\n \"onZero\": true\n },\n \"axisTick\": {\n \"show\": true,\n \"length\": 5\n },\n \"splitLine\": {\n \"show\": true,\n \"lineStyle\": {\n \"color\": \"#484753\",\n \"width\": 1,\n \"type\": \"solid\"\n }\n }\n },\n \"legend\": {\n \"show\": true,\n \"type\": \"scroll\",\n \"x\": \"center\",\n \"y\": \"top\",\n \"icon\": \"circle\",\n \"orient\": \"horizontal\",\n \"textStyle\": {\n \"color\": \"#B9B8CE\",\n \"fontSize\": 18\n },\n \"itemHeight\": 15,\n \"itemWidth\": 15,\n \"pageTextStyle\": {\n \"color\": \"#B9B8CE\"\n }\n },\n \"grid\": {\n \"show\": false,\n \"left\": \"10%\",\n \"top\": \"60\",\n \"right\": \"10%\",\n \"bottom\": \"60\"\n },\n \"dataset\": null,\n \"renderer\": \"svg\"\n },\n \"previewScaleType\": \"fit\"\n },\n \"componentList\": [\n {\n \"id\": \"17a3z1foapcw00\",\n \"isGroup\": false,\n \"attr\": {\n \"x\": 11,\n \"y\": 594,\n \"w\": 617,\n \"h\": 464,\n \"offsetX\": 0,\n \"offsetY\": 0,\n \"zIndex\": -1\n },\n \"styles\": {\n \"filterShow\": false,\n \"hueRotate\": 0,\n \"saturate\": 1,\n \"contrast\": 1,\n \"brightness\": 1,\n \"opacity\": 1,\n \"rotateZ\": 0,\n \"rotateX\": 0,\n \"rotateY\": 0,\n \"skewX\": 0,\n \"skewY\": 0,\n \"blendMode\": \"normal\",\n \"animations\": []\n },\n \"preview\": {\n \"overFlowHidden\": false\n },\n \"status\": {\n \"lock\": false,\n \"hide\": false\n },\n \"request\": {\n \"requestDataType\": 0,\n \"requestHttpType\": \"get\",\n \"requestUrl\": \"\",\n \"requestInterval\": null,\n \"requestIntervalUnit\": \"second\",\n \"requestContentType\": 0,\n \"requestParamsBodyType\": \"none\",\n \"requestSQLContent\": {\n \"sql\": \"select * from where\"\n },\n \"requestParams\": {\n \"Body\": {\n \"form-data\": {},\n \"x-www-form-urlencoded\": {},\n \"json\": \"\",\n \"xml\": \"\"\n },\n \"Header\": {},\n \"Params\": {}\n }\n },\n \"filter\": null,\n \"events\": {\n \"baseEvent\": {\n \"click\": null,\n \"dblclick\": null,\n \"mouseenter\": null,\n \"mouseleave\": null\n },\n \"advancedEvents\": {\n \"vnodeMounted\": null,\n \"vnodeBeforeMount\": null\n },\n \"interactEvents\": []\n },\n \"key\": \"BarCrossrange\",\n \"chartConfig\": {\n \"key\": \"BarCrossrange\",\n \"chartKey\": \"VBarCrossrange\",\n \"conKey\": \"VCBarCrossrange\",\n \"title\": \"横向柱状图\",\n \"category\": \"Bars\",\n \"categoryName\": \"柱状图\",\n \"package\": \"Charts\",\n \"chartFrame\": \"echarts\",\n \"image\": \"bar_y.png\"\n },\n \"option\": {\n \"legend\": {\n \"show\": true,\n \"type\": \"scroll\",\n \"x\": \"center\",\n \"y\": \"top\",\n \"icon\": \"circle\",\n \"orient\": \"horizontal\",\n \"textStyle\": {\n \"color\": \"#B9B8CE\",\n \"fontSize\": 18\n },\n \"itemHeight\": 15,\n \"itemWidth\": 15,\n \"pageTextStyle\": {\n \"color\": \"#B9B8CE\"\n }\n },\n \"xAxis\": {\n \"show\": true,\n \"name\": \"\",\n \"nameGap\": 15,\n \"nameTextStyle\": {\n \"color\": \"#B9B8CE\",\n \"fontSize\": 12\n },\n \"inverse\": false,\n \"axisLabel\": {\n \"show\": true,\n \"fontSize\": 12,\n \"color\": \"#B9B8CE\",\n \"rotate\": 0\n },\n \"position\": \"bottom\",\n \"axisLine\": {\n \"show\": true,\n \"lineStyle\": {\n \"color\": \"#B9B8CE\",\n \"width\": 1\n },\n \"onZero\": true\n },\n \"axisTick\": {\n \"show\": true,\n \"length\": 5\n },\n \"splitLine\": {\n \"show\": false,\n \"lineStyle\": {\n \"color\": \"#484753\",\n \"width\": 1,\n \"type\": \"solid\"\n }\n },\n \"type\": \"value\"\n },\n \"yAxis\": {\n \"show\": true,\n \"name\": \"\",\n \"nameGap\": 15,\n \"nameTextStyle\": {\n \"color\": \"#B9B8CE\",\n \"fontSize\": 12\n },\n \"inverse\": false,\n \"axisLabel\": {\n \"show\": true,\n \"fontSize\": 12,\n \"color\": \"#B9B8CE\",\n \"rotate\": 0\n },\n \"position\": \"left\",\n \"axisLine\": {\n \"show\": true,\n \"lineStyle\": {\n \"color\": \"#B9B8CE\",\n \"width\": 1\n },\n \"onZero\": true\n },\n \"axisTick\": {\n \"show\": true,\n \"length\": 5\n },\n \"splitLine\": {\n \"show\": true,\n \"lineStyle\": {\n \"color\": \"#484753\",\n \"width\": 1,\n \"type\": \"solid\"\n }\n },\n \"type\": \"category\"\n },\n \"grid\": {\n \"show\": false,\n \"left\": \"10%\",\n \"top\": \"60\",\n \"right\": \"10%\",\n \"bottom\": \"60\"\n },\n \"tooltip\": {\n \"show\": true,\n \"trigger\": \"axis\",\n \"axisPointer\": {\n \"show\": true,\n \"type\": \"shadow\"\n }\n },\n \"dataset\": {\n \"dimensions\": [\n \"product\",\n \"data1\",\n \"data2\"\n ],\n \"source\": [\n {\n \"product\": \"Mon\",\n \"data1\": 120,\n \"data2\": 130\n },\n {\n \"product\": \"Tue\",\n \"data1\": 200,\n \"data2\": 130\n },\n {\n \"product\": \"Wed\",\n \"data1\": 150,\n \"data2\": 312\n },\n {\n \"product\": \"Thu\",\n \"data1\": 80,\n \"data2\": 268\n },\n {\n \"product\": \"Fri\",\n \"data1\": 70,\n \"data2\": 155\n },\n {\n \"product\": \"Sat\",\n \"data1\": 110,\n \"data2\": 117\n },\n {\n \"product\": \"Sun\",\n \"data1\": 130,\n \"data2\": 160\n }\n ]\n },\n \"series\": [\n {\n \"type\": \"bar\",\n \"barWidth\": null,\n \"label\": {\n \"show\": true,\n \"position\": \"right\",\n \"color\": \"#fff\",\n \"fontSize\": 12\n },\n \"itemStyle\": {\n \"color\": null,\n \"borderRadius\": 0\n }\n },\n {\n \"type\": \"bar\",\n \"barWidth\": null,\n \"label\": {\n \"show\": true,\n \"position\": \"right\",\n \"color\": \"#fff\",\n \"fontSize\": 12\n },\n \"itemStyle\": {\n \"color\": null,\n \"borderRadius\": 0\n }\n }\n ],\n \"backgroundColor\": \"rgba(0,0,0,0)\"\n }\n },\n {\n \"id\": \"2kypivesa7o000\",\n \"isGroup\": false,\n \"attr\": {\n \"x\": 1177,\n \"y\": 425,\n \"w\": 514,\n \"h\": 141,\n \"offsetX\": 0,\n \"offsetY\": 0,\n \"zIndex\": -1\n },\n \"styles\": {\n \"filterShow\": false,\n \"hueRotate\": 0,\n \"saturate\": 1,\n \"contrast\": 1,\n \"brightness\": 1,\n \"opacity\": 1,\n \"rotateZ\": 0,\n \"rotateX\": 0,\n \"rotateY\": 0,\n \"skewX\": 0,\n \"skewY\": 0,\n \"blendMode\": \"normal\",\n \"animations\": []\n },\n \"preview\": {\n \"overFlowHidden\": false\n },\n \"status\": {\n \"lock\": false,\n \"hide\": false\n },\n \"request\": {\n \"requestDataType\": 2,\n \"requestHttpType\": \"patch\",\n \"requestUrl\": \"/sys/testData\",\n \"requestInterval\": 5,\n \"requestIntervalUnit\": \"second\",\n \"requestContentType\": 0,\n \"requestParamsBodyType\": \"none\",\n \"requestSQLContent\": {\n \"sql\": \"select * from where\"\n },\n \"requestParams\": {\n \"Body\": {\n \"form-data\": {},\n \"x-www-form-urlencoded\": {},\n \"json\": \"\",\n \"xml\": \"\"\n },\n \"Header\": {},\n \"Params\": {}\n },\n \"requestDataPondId\": \"5mb1sm98wag000\"\n },\n \"filter\": \"return data.deviceInfo.deviceTime\",\n \"events\": {\n \"baseEvent\": {\n \"click\": null,\n \"dblclick\": null,\n \"mouseenter\": null,\n \"mouseleave\": null\n },\n \"advancedEvents\": {\n \"vnodeMounted\": null,\n \"vnodeBeforeMount\": null\n },\n \"interactEvents\": []\n },\n \"key\": \"TextCommon\",\n \"chartConfig\": {\n \"key\": \"TextCommon\",\n \"chartKey\": \"VTextCommon\",\n \"conKey\": \"VCTextCommon\",\n \"title\": \"时间\",\n \"category\": \"Texts\",\n \"categoryName\": \"文本\",\n \"package\": \"Informations\",\n \"chartFrame\": \"common\",\n \"image\": \"text_static.png\"\n },\n \"option\": {\n \"link\": \"\",\n \"linkHead\": \"http://\",\n \"dataset\": \"2024-03-04 11:44:33\",\n \"fontSize\": 36,\n \"fontColor\": \"#0E0101FF\",\n \"paddingX\": 10,\n \"paddingY\": 10,\n \"textAlign\": \"start\",\n \"fontWeight\": \"bold\",\n \"borderWidth\": 0,\n \"borderColor\": \"#ffffff\",\n \"borderRadius\": 5,\n \"letterSpacing\": 5,\n \"writingMode\": \"horizontal-tb\",\n \"backgroundColor\": \"#00000000\"\n }\n },\n {\n \"id\": \"6ysezyw5ub400\",\n \"isGroup\": false,\n \"attr\": {\n \"x\": 1177,\n \"y\": 378,\n \"w\": 442,\n \"h\": 76,\n \"offsetX\": 0,\n \"offsetY\": 0,\n \"zIndex\": -1\n },\n \"styles\": {\n \"filterShow\": false,\n \"hueRotate\": 0,\n \"saturate\": 1,\n \"contrast\": 1,\n \"brightness\": 1,\n \"opacity\": 1,\n \"rotateZ\": 0,\n \"rotateX\": 0,\n \"rotateY\": 0,\n \"skewX\": 0,\n \"skewY\": 0,\n \"blendMode\": \"normal\",\n \"animations\": []\n },\n \"preview\": {\n \"overFlowHidden\": false\n },\n \"status\": {\n \"lock\": false,\n \"hide\": false\n },\n \"request\": {\n \"requestDataType\": 2,\n \"requestHttpType\": \"patch\",\n \"requestUrl\": \"/sys/testData\",\n \"requestInterval\": 5,\n \"requestIntervalUnit\": \"second\",\n \"requestContentType\": 0,\n \"requestParamsBodyType\": \"none\",\n \"requestSQLContent\": {\n \"sql\": \"select * from where\"\n },\n \"requestParams\": {\n \"Body\": {\n \"form-data\": {},\n \"x-www-form-urlencoded\": {},\n \"json\": \"\",\n \"xml\": \"\"\n },\n \"Header\": {},\n \"Params\": {}\n },\n \"requestDataPondId\": \"5mb1sm98wag000\"\n },\n \"filter\": \"return \'当前温度:\'+data.deviceData.temperature\",\n \"events\": {\n \"baseEvent\": {\n \"click\": null,\n \"dblclick\": null,\n \"mouseenter\": null,\n \"mouseleave\": null\n },\n \"advancedEvents\": {\n \"vnodeMounted\": null,\n \"vnodeBeforeMount\": null\n },\n \"interactEvents\": []\n },\n \"key\": \"TextCommon\",\n \"chartConfig\": {\n \"key\": \"TextCommon\",\n \"chartKey\": \"VTextCommon\",\n \"conKey\": \"VCTextCommon\",\n \"title\": \"设备温度\",\n \"category\": \"Texts\",\n \"categoryName\": \"文本\",\n \"package\": \"Informations\",\n \"chartFrame\": \"common\",\n \"image\": \"text_static.png\"\n },\n \"option\": {\n \"link\": \"\",\n \"linkHead\": \"http://\",\n \"dataset\": \"当前温度:98.28°C\",\n \"fontSize\": 36,\n \"fontColor\": \"#0D0202FF\",\n \"paddingX\": 10,\n \"paddingY\": 10,\n \"textAlign\": \"start\",\n \"fontWeight\": \"bold\",\n \"borderWidth\": 0,\n \"borderColor\": \"#ffffff\",\n \"borderRadius\": 5,\n \"letterSpacing\": 5,\n \"writingMode\": \"horizontal-tb\",\n \"backgroundColor\": \"#00000000\"\n }\n },\n {\n \"id\": \"4tzqumjxyia000\",\n \"isGroup\": false,\n \"attr\": {\n \"x\": 1177,\n \"y\": 278,\n \"w\": 481,\n \"h\": 131,\n \"offsetX\": 0,\n \"offsetY\": 0,\n \"zIndex\": -1\n },\n \"styles\": {\n \"filterShow\": false,\n \"hueRotate\": 0,\n \"saturate\": 1,\n \"contrast\": 1,\n \"brightness\": 1,\n \"opacity\": 1,\n \"rotateZ\": 0,\n \"rotateX\": 0,\n \"rotateY\": 0,\n \"skewX\": 0,\n \"skewY\": 0,\n \"blendMode\": \"normal\",\n \"animations\": []\n },\n \"preview\": {\n \"overFlowHidden\": false\n },\n \"status\": {\n \"lock\": false,\n \"hide\": false\n },\n \"request\": {\n \"requestDataType\": 2,\n \"requestHttpType\": \"post\",\n \"requestUrl\": \"/sys/testData\",\n \"requestInterval\": null,\n \"requestIntervalUnit\": \"second\",\n \"requestContentType\": 0,\n \"requestParamsBodyType\": \"none\",\n \"requestSQLContent\": {\n \"sql\": \"select * from test where 1\"\n },\n \"requestParams\": {\n \"Body\": {\n \"form-data\": {},\n \"x-www-form-urlencoded\": {},\n \"json\": \"\",\n \"xml\": \"\"\n },\n \"Header\": {},\n \"Params\": {}\n }\n },\n \"filter\": \"console.log(\'data:\'+JSON.stringify(data))\\r\\nreturn data.deviceInfo.deviceId\",\n \"events\": {\n \"baseEvent\": {\n \"click\": null,\n \"dblclick\": null,\n \"mouseenter\": null,\n \"mouseleave\": null\n },\n \"advancedEvents\": {\n \"vnodeMounted\": null,\n \"vnodeBeforeMount\": null\n },\n \"interactEvents\": []\n },\n \"key\": \"TextCommon\",\n \"chartConfig\": {\n \"key\": \"TextCommon\",\n \"chartKey\": \"VTextCommon\",\n \"conKey\": \"VCTextCommon\",\n \"title\": \"设备名称\",\n \"category\": \"Texts\",\n \"categoryName\": \"文本\",\n \"package\": \"Informations\",\n \"chartFrame\": \"common\",\n \"image\": \"text_static.png\"\n },\n \"option\": {\n \"link\": \"\",\n \"linkHead\": \"http://\",\n \"dataset\": \"IoTDevice001\",\n \"fontSize\": 36,\n \"fontColor\": \"#0D0000FF\",\n \"paddingX\": 10,\n \"paddingY\": 10,\n \"textAlign\": \"start\",\n \"fontWeight\": \"bold\",\n \"borderWidth\": 0,\n \"borderColor\": \"#ffffff\",\n \"borderRadius\": 5,\n \"letterSpacing\": 5,\n \"writingMode\": \"horizontal-tb\",\n \"backgroundColor\": \"#00000000\"\n }\n }\n ],\n \"requestGlobalConfig\": {\n \"requestDataPond\": [\n {\n \"dataPondId\": \"5mb1sm98wag000\",\n \"dataPondName\": \"5mb1sm98wag000\",\n \"dataPondRequestConfig\": {\n \"requestDataType\": 2,\n \"requestHttpType\": \"patch\",\n \"requestUrl\": \"/sys/testData\",\n \"requestInterval\": 5,\n \"requestIntervalUnit\": \"second\",\n \"requestContentType\": 0,\n \"requestParamsBodyType\": \"none\",\n \"requestSQLContent\": {\n \"sql\": \"select * from where\"\n },\n \"requestParams\": {\n \"Body\": {\n \"form-data\": {},\n \"x-www-form-urlencoded\": {},\n \"json\": \"\",\n \"xml\": \"\"\n },\n \"Header\": {},\n \"Params\": {}\n }\n }\n }\n ],\n \"requestOriginUrl\": \"http://127.0.0.1:8090/api/goview\",\n \"requestInterval\": 30,\n \"requestIntervalUnit\": \"second\",\n \"requestParams\": {\n \"Body\": {\n \"form-data\": {},\n \"x-www-form-urlencoded\": {},\n \"json\": \"\",\n \"xml\": \"\"\n },\n \"Header\": {},\n \"Params\": {}\n }\n }\n}', 1, '32211', 1, '2024-03-04 11:44:40', '2024-03-04 09:30:24', NULL); + +-- +-- 转储表的索引 +-- + +-- +-- 表的索引 `hg_admin_attachment` +-- +ALTER TABLE `hg_admin_attachment` + ADD PRIMARY KEY (`id`), + ADD KEY `md5` (`md5`); + +-- +-- 表的索引 `hg_admin_member` +-- +ALTER TABLE `hg_admin_member` + ADD PRIMARY KEY (`id`); + +-- +-- 表的索引 `hg_admin_project` +-- +ALTER TABLE `hg_admin_project` + ADD PRIMARY KEY (`id`) USING BTREE, + ADD KEY `created_by` (`created_by`); + +-- +-- 在导出的表使用AUTO_INCREMENT +-- + +-- +-- 使用表AUTO_INCREMENT `hg_admin_attachment` +-- +ALTER TABLE `hg_admin_attachment` + MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '文件ID', AUTO_INCREMENT=24; + +-- +-- 使用表AUTO_INCREMENT `hg_admin_member` +-- +ALTER TABLE `hg_admin_member` + MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '管理员ID', AUTO_INCREMENT=13; + +-- +-- 使用表AUTO_INCREMENT `hg_admin_project` +-- +ALTER TABLE `hg_admin_project` + MODIFY `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '项目id', AUTO_INCREMENT=20240304092998015; +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/utility/.gitkeep b/utility/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/utility/charset/charset.go b/utility/charset/charset.go new file mode 100644 index 0000000..a9c5613 --- /dev/null +++ b/utility/charset/charset.go @@ -0,0 +1,74 @@ +// Package charset +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package charset + +import ( + "bytes" + "crypto/rand" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gutil" + r "math/rand" + "strings" + "time" +) + +// RandomCreateBytes 生成随机字串符 +func RandomCreateBytes(n int, alphabets ...byte) []byte { + if len(alphabets) == 0 { + alphabets = []byte(`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`) + } + var bytes = make([]byte, n) + var randBy bool + if num, err := rand.Read(bytes); num != n || err != nil { + randBy = true + } + for i, b := range bytes { + if randBy { + bytes[i] = alphabets[r.New(r.NewSource(time.Now().UnixNano())).Intn(len(alphabets))] + } else { + bytes[i] = alphabets[b%byte(len(alphabets))] + } + } + return bytes +} + +// ParseErrStack 解析错误的堆栈信息 +func ParseErrStack(err error) []string { + return ParseStack(gerror.Stack(err)) +} + +// ParseStack 解析堆栈信息 +func ParseStack(st string) []string { + stack := gstr.Split(st, "\n") + for i := 0; i < len(stack); i++ { + stack[i] = gstr.Replace(stack[i], "\t", "--> ") + } + return stack +} + +// SerializeStack 解析错误并序列化堆栈信息 +func SerializeStack(err error) string { + buffer := bytes.NewBuffer(nil) + gutil.DumpTo(buffer, ParseErrStack(err), gutil.DumpOption{ + WithType: false, + ExportedOnly: false, + }) + return buffer.String() +} + +// SubstrAfter 截取指定字符后的内容 +func SubstrAfter(str string, symbol string) string { + comma := strings.Index(str, symbol) + if comma < 0 { // -1 不存在 + return "" + } + pos := strings.Index(str[comma:], symbol) + if comma+pos+1 > len(str) { + return "" + } + return str[comma+pos+1:] +} diff --git a/utility/format/format.go b/utility/format/format.go new file mode 100644 index 0000000..85e8139 --- /dev/null +++ b/utility/format/format.go @@ -0,0 +1,73 @@ +// Package format +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package format + +import ( + "fmt" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv" + "strconv" +) + +// Round2String 四舍五入保留小数,默认2位 +func Round2String(value float64, args ...interface{}) string { + var places = 2 + if len(args) > 0 { + places = gconv.Int(args[0]) + } + return fmt.Sprintf("%0."+strconv.Itoa(places)+"f", value) +} + +// Round2Float64 四舍五入保留小数,默认2位 +func Round2Float64(value float64, args ...interface{}) float64 { + return gconv.Float64(Round2String(value, args...)) +} + +// FileSize 字节的单位转换 保留两位小数 +func FileSize(data int64) string { + var factor float64 = 1024 + res := float64(data) + for _, unit := range []string{"", "K", "M", "G", "T", "P"} { + if res < factor { + return fmt.Sprintf("%.2f%sB", res, unit) + } + res /= factor + } + return fmt.Sprintf("%.2f%sB", res, "P") +} + +// AgoTime 多久以前 +func AgoTime(gt *gtime.Time) string { + if gt == nil { + return "" + } + n := gtime.Now().Timestamp() + t := gt.Timestamp() + + var ys int64 = 31536000 + var ds int64 = 86400 + var hs int64 = 3600 + var ms int64 = 60 + var ss int64 = 1 + var rs string + + d := n - t + switch { + case d > ys: + rs = fmt.Sprintf("%d年前", int(d/ys)) + case d > ds: + rs = fmt.Sprintf("%d天前", int(d/ds)) + case d > hs: + rs = fmt.Sprintf("%d小时前", int(d/hs)) + case d > ms: + rs = fmt.Sprintf("%d分钟前", int(d/ms)) + case d > ss: + rs = fmt.Sprintf("%d秒前", int(d/ss)) + default: + rs = "刚刚" + } + return rs +} diff --git a/utility/simple/simple.go b/utility/simple/simple.go new file mode 100644 index 0000000..02b147d --- /dev/null +++ b/utility/simple/simple.go @@ -0,0 +1,78 @@ +// Package simple +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package simple + +import ( + "context" + "github.com/gogf/gf/v2/crypto/gmd5" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/glog" + "github.com/gogf/gf/v2/os/grpool" + "github.com/gogf/gf/v2/util/gconv" +) + +// RouterPrefix 获取应用路由前缀 +func RouterPrefix(ctx context.Context, app string) string { + return g.Cfg().MustGet(ctx, "router."+app+".prefix", "/"+app+"").String() +} + +// DefaultErrorTplContent 获取默认的错误模板内容 +func DefaultErrorTplContent(ctx context.Context) string { + return gfile.GetContents(g.Cfg().MustGet(ctx, "viewer.paths").String() + "/error/default.html") +} + +// CheckPassword 检查密码 +func CheckPassword(input, salt, hash string) (err error) { + if hash != gmd5.MustEncryptString(input+salt) { + err = gerror.New("用户密码不正确") + return + } + return +} + +// SafeGo 安全的调用协程,遇到错误时输出错误日志而不是抛出panic +func SafeGo(ctx context.Context, f func(ctx context.Context), lv ...interface{}) { + var level = glog.LEVEL_ERRO + if len(lv) > 0 { + level = gconv.Int(lv[0]) + } + + err := grpool.AddWithRecover(ctx, func(ctx context.Context) { + f(ctx) + }, func(ctx context.Context, err error) { + Logf(level, ctx, "SafeGo exec failed:%+v", err) + }) + + if err != nil { + Logf(level, ctx, "SafeGo AddWithRecover err:%+v", err) + return + } +} + +func Logf(level int, ctx context.Context, format string, v ...interface{}) { + switch level { + case glog.LEVEL_DEBU: + g.Log().Debugf(ctx, format, v...) + case glog.LEVEL_INFO: + g.Log().Infof(ctx, format, v...) + case glog.LEVEL_NOTI: + g.Log().Noticef(ctx, format, v...) + case glog.LEVEL_WARN: + g.Log().Warningf(ctx, format, v...) + case glog.LEVEL_ERRO: + g.Log().Errorf(ctx, format, v...) + case glog.LEVEL_CRIT: + g.Log().Criticalf(ctx, format, v...) + case glog.LEVEL_PANI: + g.Log().Panicf(ctx, format, v...) + case glog.LEVEL_FATA: + g.Log().Fatalf(ctx, format, v...) + default: + g.Log().Errorf(ctx, format, v...) + } +} diff --git a/utility/url/url.go b/utility/url/url.go new file mode 100644 index 0000000..4d979e1 --- /dev/null +++ b/utility/url/url.go @@ -0,0 +1,74 @@ +// Package url +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package url + +import ( + "context" + "fmt" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/text/gstr" + "hotgo/utility/validate" + "strings" +) + +// UriToMap 将URL参数转为map +func UriToMap(uri string) g.MapStrStr { + m := make(map[string]string) + if len(uri) < 1 { + return nil + } + if uri[0:1] == "?" { + uri = uri[1:] + } + pars := strings.Split(uri, "&") + for _, par := range pars { + kv := strings.Split(par, "=") + m[kv[0]] = kv[1] + } + return m +} + +// MapToUri 将map转为URL参数 +func MapToUri(params g.MapStrStr) string { + escape := "" + for k, v := range params { + if escape != "" { + escape = escape + "&" + } + escape = escape + k + "=" + v + } + return escape +} + +// GetAddr 获取请求中的请求地址,协议+域名/IP:端口 +func GetAddr(ctx context.Context) string { + r := ghttp.RequestFromCtx(ctx) + if r == nil { + return "" + } + var ( + scheme = "http" + proto = r.Header.Get("X-Forwarded-Proto") + ) + if r.TLS != nil || gstr.Equal(proto, "https") { + scheme = "https" + } + return fmt.Sprintf(`%s://%s`, scheme, r.Host) +} + +// GetDomain 获取请求中的域名,如果请求不是域名则返回空 +func GetDomain(ctx context.Context) string { + r := ghttp.RequestFromCtx(ctx) + if r == nil { + g.Log().Info(ctx, "GetDomain ctx not request") + return "" + } + if validate.IsDNSName(r.Host) { + return r.Host + } + return "" +} diff --git a/utility/validate/filter.go b/utility/validate/filter.go new file mode 100644 index 0000000..265e568 --- /dev/null +++ b/utility/validate/filter.go @@ -0,0 +1,32 @@ +// Package validate +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package validate + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" +) + +// Filter 通用过滤器 + +// Filter 预处理和数据过滤接口,目前主要用于input层的输入过滤和内部效验。 +// 你可以在任意地方使用它,只要实现了Filter接口即可 +type Filter interface { + // Filter gf效验规则 https://goframe.org/pages/viewpage.action?pageId=1114367 + Filter(ctx context.Context) error +} + +// PreFilter 预过滤 +func PreFilter(ctx context.Context, in interface{}) error { + return g.Try(ctx, func(ctx context.Context) { + if c, ok := in.(Filter); ok { + if err := c.Filter(ctx); err != nil { + g.Throw(err) + } + return + } + }) +} diff --git a/utility/validate/filter_test.go b/utility/validate/filter_test.go new file mode 100644 index 0000000..bc45665 --- /dev/null +++ b/utility/validate/filter_test.go @@ -0,0 +1,67 @@ +package validate_test + +import ( + "context" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/test/gtest" + "hotgo/utility/validate" + "testing" +) + +// MockFilter 是 Filter 接口的模拟实现。 +type MockFilter struct { + Foo string + Bar int +} + +func (f *MockFilter) Filter(ctx context.Context) error { + // 模拟过滤逻辑 + + // 过滤出错的例子 + if f.Foo == "" { + return gerror.New("Foo 字段是必需的") + } + + // 过滤操作的例子 + f.Bar += 10 + return nil +} + +func TestPreFilter(t *testing.T) { + ctx := context.Background() + input := &MockFilter{ + Foo: "test", + Bar: 5, + } + + err := validate.PreFilter(ctx, input) + gtest.C(t, func(t *gtest.T) { + t.AssertNil(err) + }) + + t.Logf("input:%+v", input) + + // 验证过滤结果 + expectedBar := 15 + gtest.C(t, func(t *gtest.T) { + t.Assert(input.Bar, expectedBar) + }) +} + +func TestPreFilter_Error(t *testing.T) { + ctx := context.Background() + input := &MockFilter{ + Foo: "", + Bar: 5, + } + + err := validate.PreFilter(ctx, input) + gtest.C(t, func(t *gtest.T) { + t.AssertNE(err, nil) + }) + + expectedError := "Foo 字段是必需的" + gtest.C(t, func(t *gtest.T) { + t.Assert(err.Error(), expectedError) + }) +} diff --git a/utility/validate/include.go b/utility/validate/include.go new file mode 100644 index 0000000..7b445a4 --- /dev/null +++ b/utility/validate/include.go @@ -0,0 +1,38 @@ +// Package validate +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package validate + +import ( + "github.com/gogf/gf/v2/util/gconv" +) + +// 包含判断 + +// InSliceExistStr 判断字符或切片字符是否存在指定字符 +func InSliceExistStr(elems any, search string) bool { + switch elems.(type) { + case []string: + elem := gconv.Strings(elems) + for i := 0; i < len(elem); i++ { + if gconv.String(elem[i]) == search { + return true + } + } + default: + return gconv.String(elems) == search + } + return false +} + +// InSlice 元素是否存在于切片中 +func InSlice[K comparable](slice []K, key K) bool { + for _, v := range slice { + if v == key { + return true + } + } + return false +} diff --git a/utility/validate/validate.go b/utility/validate/validate.go new file mode 100644 index 0000000..532c354 --- /dev/null +++ b/utility/validate/validate.go @@ -0,0 +1,192 @@ +// Package validate +// @Link https://github.com/bufanyun/hotgo +// @Copyright Copyright (c) 2023 HotGo CLI +// @Author Ms <133814250@qq.com> +// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE +package validate + +import ( + "context" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/text/gstr" + "net" + "net/url" + "regexp" + "strings" + "time" +) + +// 是否判断 + +// IsDNSName 是否是域名地址 +func IsDNSName(s string) bool { + DNSName := `^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$` + rxDNSName := regexp.MustCompile(DNSName) + return s != "" && rxDNSName.MatchString(s) +} + +// IsHTTPS 是否是https请求 +func IsHTTPS(ctx context.Context) bool { + r := ghttp.RequestFromCtx(ctx) + if r == nil { + g.Log().Info(ctx, "IsHTTPS ctx not request") + return false + } + return r.TLS != nil || gstr.Equal(r.Header.Get("X-Forwarded-Proto"), "https") +} + +// IsIp 是否为ipv4 +func IsIp(ip string) bool { + return net.ParseIP(ip) != nil +} + +// IsPublicIp 是否是公网IP +func IsPublicIp(ip string) bool { + i := net.ParseIP(ip) + + if i.IsLoopback() || i.IsPrivate() || i.IsMulticast() || i.IsUnspecified() || i.IsLinkLocalUnicast() || i.IsLinkLocalMulticast() { + return false + } + + if ip4 := i.To4(); ip4 != nil { + return !i.Equal(net.IPv4bcast) + } + return true +} + +// IsLocalIPAddr 检测 IP 地址字符串是否是内网地址 +func IsLocalIPAddr(ip string) bool { + if "localhost" == ip { + return true + } + return HasLocalIP(net.ParseIP(ip)) +} + +// HasLocalIP 检测 IP 地址是否是内网地址 +func HasLocalIP(ip net.IP) bool { + if ip.IsLoopback() { + return true + } + + ip4 := ip.To4() + if ip4 == nil { + return false + } + + return ip4[0] == 10 || // 10.0.0.0/8 + (ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) || // 172.16.0.0/12 + (ip4[0] == 169 && ip4[1] == 254) || // 169.254.0.0/16 + (ip4[0] == 192 && ip4[1] == 168) // 192.168.0.0/16 +} + +// IsMobile 是否为手机号码 +func IsMobile(mobile string) bool { + pattern := `^(1[2|3|4|5|6|7|8|9][0-9]\d{4,8})$` + reg := regexp.MustCompile(pattern) + return reg.MatchString(mobile) +} + +// IsEmail 是否为邮箱地址 +func IsEmail(email string) bool { + // pattern := `^[0-9a-z][_.0-9a-z-]{0,31}@([0-9a-z][0-9a-z-]{0,30}[0-9a-z].){1,4}[a-z]{2,4}$` //匹配电子邮箱 + pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` + reg := regexp.MustCompile(pattern) + return reg.MatchString(email) +} + +// IsURL 是否是url地址 +func IsURL(u string) bool { + _, err := url.ParseRequestURI(u) + if err != nil { + return false + } + URL, err := url.Parse(u) + if err != nil || URL.Scheme == "" || URL.Host == "" { + return false + } + return true +} + +// IsIDCard 是否为身份证 +func IsIDCard(idCard string) bool { + sz := len(idCard) + if sz != 18 { + return false + } + weight := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2} + validate := []byte{'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'} + sum := 0 + for i := 0; i < len(weight); i++ { + sum += weight[i] * int(byte(idCard[i])-'0') + } + m := sum % 11 + return validate[m] == idCard[sz-1] +} + +// IsSameDay 是否为同一天 +func IsSameDay(t1, t2 int64) bool { + y1, m1, d1 := time.Unix(t1, 0).Date() + y2, m2, d2 := time.Unix(t2, 0).Date() + return y1 == y2 && m1 == m2 && d1 == d2 +} + +// IsSameMinute 是否为同一分钟 +func IsSameMinute(t1, t2 int64) bool { + d1 := time.Unix(t1, 0).Format("2006-01-02 15:04") + d2 := time.Unix(t2, 0).Format("2006-01-02 15:04") + return d1 == d2 +} + +// IsMobileVisit 是否为移动端访问 +func IsMobileVisit(userAgent string) bool { + if len(userAgent) == 0 { + return false + } + + is := false + mobileKeywords := []string{"Mobile", "Android", "Silk/", "Kindle", "BlackBerry", "Opera Mini", "Opera Mobi"} + for i := 0; i < len(mobileKeywords); i++ { + if strings.Contains(userAgent, mobileKeywords[i]) { + is = true + break + } + } + return is +} + +// IsWxBrowserVisit 是否为微信访问 +func IsWxBrowserVisit(userAgent string) bool { + if len(userAgent) == 0 { + return false + } + + is := false + userAgent = strings.ToLower(userAgent) + mobileKeywords := []string{"MicroMessenger"} + for i := 0; i < len(mobileKeywords); i++ { + if strings.Contains(userAgent, strings.ToLower(mobileKeywords[i])) { + is = true + break + } + } + return is +} + +// IsWxMiniProgramVisit 是否为微信小程序访问 +func IsWxMiniProgramVisit(userAgent string) bool { + if len(userAgent) == 0 { + return false + } + + is := false + userAgent = strings.ToLower(userAgent) + mobileKeywords := []string{"miniProgram"} + for i := 0; i < len(mobileKeywords); i++ { + if strings.Contains(userAgent, strings.ToLower(mobileKeywords[i])) { + is = true + break + } + } + return is +} diff --git a/utility/validate/validate_test.go b/utility/validate/validate_test.go new file mode 100644 index 0000000..6a07e8a --- /dev/null +++ b/utility/validate/validate_test.go @@ -0,0 +1,11 @@ +package validate + +import ( + "github.com/gogf/gf/v2/test/gtest" + "testing" +) + +func TestIsEmail(t *testing.T) { + b := IsEmail("QTT123456@163.com") + gtest.Assert(true, b) +}