第一次提交

main v1.0.0.00001
black1552 2026-01-30 15:51:42 +08:00
commit 7218ba6508
26 changed files with 3461 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

6
README.md Normal file
View File

@ -0,0 +1,6 @@
Copyright 2021 black1552
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

View File

@ -0,0 +1,89 @@
package autoMigrate
import (
"context"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var (
ctx context.Context
db *gorm.DB
)
type AutoMigrate struct {
ctx context.Context
db *gorm.DB
dns string
}
var am *AutoMigrate
func New() {
am = &AutoMigrate{}
am.ctx = gctx.New()
err := g.DB().PingMaster()
if err != nil {
g.Log().Error(ctx, "数据库连接失败", err)
return
}
dns, err := gcfg.Instance().Get(ctx, "dns", "")
if err != nil {
g.Log().Error(ctx, "获取配置失败", err)
return
}
if g.IsEmpty(dns) {
g.Log().Error(ctx, "gormDNS未配置", "请检查配置文件")
return
}
am.dns = dns.String()
am.db, err = gorm.Open(mysql.New(mysql.Config{
DSN: dns.String(),
DefaultStringSize: 255,
}), &gorm.Config{})
if err != nil {
g.Log().Error(ctx, "gorm连接数据库失败", err)
return
}
}
func SetAutoMigrate(models ...interface{}) {
New()
if g.IsNil(am.db) {
g.Log().Error(ctx, "数据库连接失败")
return
}
db = am.db.Set("gorm:table_options", "ENGINE=InnoDB")
err := db.AutoMigrate(models...)
if err != nil {
g.Log().Error(ctx, "数据库迁移失败", err)
}
}
func RenameColumn(dst interface{}, name, newName string) {
if am.db.Migrator().HasColumn(dst, name) {
err := am.db.Migrator().RenameColumn(dst, name, newName)
if err != nil {
g.Log().Error(am.ctx, "数据库修改字段失败", err)
}
} else {
g.Log().Info(am.ctx, "数据库字段不存在", name)
}
}
// DropColumn
// 删除字段
// 例DropColumn(&User{}, "Sex")
func DropColumn(dst interface{}, name string) {
if am.db.Migrator().HasColumn(dst, name) {
err := am.db.Migrator().DropColumn(dst, name)
if err != nil {
g.Log().Error(am.ctx, "数据库删除字段失败", err)
}
} else {
g.Log().Info(am.ctx, "数据库字段不存在", name)
}
}

102
baseGrpc/grpc.go Normal file
View File

@ -0,0 +1,102 @@
package baseGrpc
import (
"context"
"time"
v2 "git.magicany.cc/black1552/gf-common/utils"
"github.com/duke-git/lancet/v2/slice"
"github.com/gogf/gf/contrib/registry/etcd/v2"
"github.com/gogf/gf/contrib/rpc/grpcx/v2"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gsvc"
"github.com/gogf/gf/v2/os/gctx"
"google.golang.org/grpc"
)
var (
name string
open bool
)
type SGrpc struct {
}
func New() *SGrpc {
name = v2.GenerateString(20)
config, _ := g.Config().Get(gctx.New(), "grpc.open", false)
open = config.Bool()
return &SGrpc{}
}
func (s *SGrpc) clientTimeout(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,
) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
err := invoker(ctx, method, req, reply, cc, opts...)
return err
}
// InitServer 初始化服务 初始化后使用需要服务的grpc可以利用其进行注册
func (s *SGrpc) InitServer() *grpcx.GrpcServer {
s.RegisterResolver(gctx.New())
config := grpcx.Server.NewConfig()
config.Options = append(config.Options, []grpc.ServerOption{
grpcx.Server.ChainUnary(
grpcx.Server.UnaryError,
)}...,
)
config.Name = s.GetServerName()
return grpcx.Server.New(config)
}
// Client 获取对应服务CONN 需要客户端使用它进行初始化
func (s *SGrpc) Client(ctx context.Context, name string) *grpc.ClientConn {
servers, err := s.GetServers(ctx)
if err != nil {
g.Log().Errorf(ctx, "%+v", err)
return nil
}
_, ok := slice.FindBy(servers, func(index int, item gsvc.Service) bool {
return item.GetName() == name
})
if !ok {
return nil
}
var conn = grpcx.Client.MustNewGrpcClientConn(name, grpcx.Client.ChainUnary(
s.clientTimeout,
))
return conn
}
// GetServers 获取所有服务
func (s *SGrpc) GetServers(ctx context.Context, inputConfig ...gsvc.SearchInput) ([]gsvc.Service, error) {
input := gsvc.SearchInput{}
if len(inputConfig) > 0 {
input = inputConfig[0]
}
servers, err := gsvc.GetRegistry().Search(ctx, input)
if err != nil {
return nil, err
}
return servers, nil
}
// RegisterResolver 注册服务发现
func (s *SGrpc) RegisterResolver(ctx context.Context) {
etcdConfig, err := g.Config().Get(ctx, "etcd.host")
if err != nil {
panic(err)
}
grpcx.Resolver.Register(etcd.New(etcdConfig.String()))
}
// GetServerName 获取服务名
func (s *SGrpc) GetServerName() string {
return name
}
func (s *SGrpc) IsOpen() bool {
return open
}

141
excel/index.go Normal file
View File

@ -0,0 +1,141 @@
package excel
import (
"fmt"
"log"
"path/filepath"
"github.com/gogf/gf/v2/container/gmap"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/xuri/excelize/v2"
)
func CreateExcel(sheetName string, headers, cols []string, drops []DropdownCol, fileName string, centerCol []string, creatCol func(f *excelize.File, cols []string) *excelize.File) string {
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
panic(fmt.Sprintf("关闭Excel文件错误:%s", err))
}
}()
index, err := f.NewSheet(sheetName)
if err != nil {
panic(fmt.Sprintf("创建工作表错误:%s", err))
}
f.SetActiveSheet(index)
_ = f.SetRowHeight(sheetName, 0, 150)
for i, v := range headers {
_ = f.SetCellValue("sheet1", cols[i]+"1", v)
}
f = creatCol(f, cols)
if !g.IsEmpty(drops) {
ExcelDownCol(f, drops)
}
if !g.IsEmpty(centerCol) {
for _, col := range centerCol {
_ = f.SetColStyle("sheet1", col, CenterCol(f))
}
}
basePath := filepath.Join("excel", fileName)
fPath := filepath.Join(gfile.Pwd(), "resource", basePath)
if err := f.SaveAs(fPath); err != nil {
panic(fmt.Sprintf("导出房源Excel模板文件错误:%s", err))
}
return "/uploads/" + basePath
}
// ExcelDownCol 导出模板列下拉数据
// 添加下拉框和注释
// @param f *excelize.File
// @param dropdownCols []model.DropdownCol
// @return 返回文件
func ExcelDownCol(f *excelize.File, dropdownCols []DropdownCol) *excelize.File {
// 为导入模板的列添加下拉数据
for _, dropdown := range dropdownCols {
if dropdown.EndRow < dropdown.StartRow {
// 默认设置1000行下拉框
dropdown.EndRow = dropdown.StartRow + 999
}
// 构建范围字符串 (如 "B2:B1000")
dvRange := fmt.Sprintf(
"%s%d:%s%d",
dropdown.Column,
dropdown.StartRow,
dropdown.Column,
dropdown.EndRow,
)
// 创建数据验证对象
dv := excelize.NewDataValidation(true) // 允许空值
dv.SetSqref(dvRange) // 设置应用范围
// 设置下拉列表选项
if err := dv.SetDropList(dropdown.Options); err != nil {
fmt.Errorf("设置下拉选项失败: %w", err)
continue
}
// 添加数据验证到工作表
if err := f.AddDataValidation("sheet1", dv); err != nil {
fmt.Errorf("添加下拉框失败: %w", err)
continue
}
// 添加注释提示使用新版API
commentCell := fmt.Sprintf("%s1", dropdown.Column)
if err := f.AddComment("sheet1", excelize.Comment{
Cell: commentCell,
Author: "系统提示",
Paragraph: []excelize.RichTextRun{
{Text: "请从下拉列表中选择:\n"},
{Text: JoinOptions(dropdown.Options)},
},
}); err != nil {
log.Printf("添加注释失败: %v", err)
continue
}
}
return f
}
// JoinOptions 将选项列表转换为逗号分隔的字符串(用于注释)
func JoinOptions(options []string) string {
result := ""
for i, opt := range options {
if i > 0 {
result += ", "
}
result += opt
}
return result
}
// TransMap 闯入MAP 并根据isCn查询key 返回value
func TransMap(maps map[string]string, key string, isCn bool) string {
tagMap := gmap.NewStrStrMap()
tagMap.Sets(maps)
if !isCn {
tagMap.Flip()
}
val, ok := tagMap.Search(key)
if ok {
return val
} else {
return ""
}
}
func CenterCol(f *excelize.File) int {
style, err := f.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
},
})
if err != nil {
fmt.Println("创建样式失败:", err)
return 0
}
return style
}

8
excel/model.go Normal file
View File

@ -0,0 +1,8 @@
package excel
type DropdownCol struct {
Column string // 需要下拉框的列标识如B, C
Options []string // 下拉选项列表
StartRow int // 下拉框起始行从1开始
EndRow int // 下拉框结束行
}

73
go.mod Normal file
View File

@ -0,0 +1,73 @@
module git.magicany.cc/black1552/gf-common
go 1.24.3
require (
github.com/duke-git/lancet/v2 v2.3.7
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.3
github.com/gogf/gf/contrib/registry/etcd/v2 v2.9.3
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.9.3
github.com/gogf/gf/v2 v2.9.3
github.com/gorilla/websocket v1.5.3
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/xuri/excelize/v2 v2.9.1
google.golang.org/grpc v1.76.0
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.2 // indirect
github.com/gogf/gf/contrib/registry/file/v2 v2.9.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.0.9 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/tiendc/go-deepcopy v1.6.0 // indirect
github.com/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.1 // indirect
go.etcd.io/etcd/api/v3 v3.5.17 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect
go.etcd.io/etcd/client/v3 v3.5.17 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

217
go.sum Normal file
View File

@ -0,0 +1,217 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
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/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/duke-git/lancet/v2 v2.3.7 h1:nnNBA9KyoqwbPm4nFmEFVIbXeAmpqf6IDCH45+HHHNs=
github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
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.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/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.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.3 h1:P4jrnp+Vmh3kDeaH/kyHPI6rfoMmQD+sPJa716aMbS0=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.3/go.mod h1:yEhfx78wgpxUJhH9C9bWJ7I3JLcVCzUg11A4ORYTKeg=
github.com/gogf/gf/contrib/registry/etcd/v2 v2.9.3 h1:4ztKAHfwtddPRwxlVRRfLdJMzp42Z+9K5tFHrPPZl1Q=
github.com/gogf/gf/contrib/registry/etcd/v2 v2.9.3/go.mod h1:ey99pcs/hSwShOLWjd4mUUmq4S9fLy7elf7SslUAs20=
github.com/gogf/gf/contrib/registry/file/v2 v2.9.3 h1:7f+55KmwJzW0Kmam+N3VrLMlMkCTc5LTPDaZeElCLd0=
github.com/gogf/gf/contrib/registry/file/v2 v2.9.3/go.mod h1:kOYsqBlbf6pElgfLyIEFN6FpXnSSdgkV4w+7+w/78do=
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.9.3 h1:HoUu3a7t0iLI0/GbVP46rPHbrfBlKa6rfHsnCvry4+4=
github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.9.3/go.mod h1:F5v+4GumRPIFZsAiz6H5QNoOiPM7cluMCvGF0zbzsGE=
github.com/gogf/gf/v2 v2.9.3 h1:qjN4s55FfUzxZ1AE8vUHNDX3V0eIOUGXhF2DjRTVZQ4=
github.com/gogf/gf/v2 v2.9.3/go.mod h1:w6rcfD13SmO7FKI80k9LSLiSMGqpMYp50Nfkrrc2sEE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.17 h1:cQB8eb8bxwuxOilBpMJAEo8fAONyrdXTHUNcMd8yT1w=
go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4=
go.etcd.io/etcd/client/pkg/v3 v3.5.17 h1:XxnDXAWq2pnxqx76ljWwiQ9jylbpC4rvkAeRVOUKKVw=
go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w=
go.etcd.io/etcd/client/v3 v3.5.17 h1:o48sINNeWz5+pjy/Z0+HKpj/xSnBkuVhVvXkjEXbqZY=
go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a h1:4iLhBPcpqFmylhnkbY3W0ONLUYYkDAW9xMFLfxgsvCw=
golang.org/x/exp v0.0.0-20221208152030-732eee02a75a/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
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/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

220
mqtt/client/mqtt.go Normal file
View File

@ -0,0 +1,220 @@
package client
import (
"context"
"fmt"
"sync"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/gogf/gf/v2/os/glog"
)
// Client MQTT客户端结构
type Client struct {
client mqtt.Client
opts *mqtt.ClientOptions
ctx context.Context
subscribed map[string]byte
subMutex sync.RWMutex
callback mqtt.MessageHandler
// 错误处理相关
onConnectionLost func(error)
onReconnect func()
onConnect func()
onSubscriptionError func(error)
onPublishError func(error)
}
// NewClientWithAuth 创建带用户名密码认证的MQTT客户端
func NewClientWithAuth(ctx context.Context, broker, clientId, username, password string) *Client {
c := &Client{
ctx: ctx,
subscribed: make(map[string]byte),
}
opts := mqtt.NewClientOptions()
opts.AddBroker(broker)
opts.SetClientID(clientId)
opts.SetUsername(username)
opts.SetPassword(password)
// 设置连接丢失处理函数
opts.SetConnectionLostHandler(func(client mqtt.Client, err error) {
glog.Error(ctx, "MQTT连接断开:", err)
if c.onConnectionLost != nil {
c.onConnectionLost(err)
}
})
// 设置重连处理函数
opts.SetReconnectingHandler(func(client mqtt.Client, opts *mqtt.ClientOptions) {
glog.Info(ctx, "MQTT客户端正在尝试重连...")
if c.onReconnect != nil {
c.onReconnect()
}
})
// 设置连接成功处理函数,重连成功后重新订阅主题
opts.SetOnConnectHandler(func(client mqtt.Client) {
glog.Info(ctx, "MQTT客户端重新连接成功")
if c.onConnect != nil {
c.onConnect()
}
// 重连成功后重新订阅主题
go c.resubscribe()
})
// 设置其他选项...
mqttClient := mqtt.NewClient(opts)
c.client = mqttClient
c.opts = opts
return c
}
// Connect 连接到MQTT服务器
func (c *Client) Connect() error {
glog.Info(c.ctx, "开始连接到MQTT服务器...")
token := c.client.Connect()
glog.Info(c.ctx, "等待连接完成...")
// 使用更长的超时时间,避免网络延迟导致连接失败
if token.WaitTimeout(30 * time.Second) {
glog.Info(c.ctx, "连接操作完成")
if token.Error() != nil {
err := fmt.Errorf("连接到MQTT代理时发生错误: %w", token.Error())
glog.Error(c.ctx, "连接MQTT服务器时发生错误:", token.Error())
return err
}
} else {
// 连接超时
err := fmt.Errorf("连接到MQTT服务器超时")
glog.Error(c.ctx, "连接MQTT服务器超时")
return err
}
glog.Info(c.ctx, "成功连接到MQTT服务器")
return nil
}
// Disconnect 断开MQTT连接
func (c *Client) Disconnect() {
if c.client.IsConnected() {
c.client.Disconnect(250)
glog.Info(c.ctx, "已断开MQTT连接")
}
}
// SubscribeMultiple 同时订阅多个主题
func (c *Client) SubscribeMultiple(topics map[string]byte, callback mqtt.MessageHandler) error {
// 保存订阅信息
c.subMutex.Lock()
for topic, qos := range topics {
c.subscribed[topic] = qos
}
c.callback = callback
c.subMutex.Unlock()
token := c.client.SubscribeMultiple(topics, callback)
// 增加订阅超时时间
if token.WaitTimeout(30*time.Second) && token.Error() != nil {
err := fmt.Errorf("同时订阅多个主题出现错误: %w", token.Error())
glog.Error(c.ctx, "订阅主题时发生错误:", token.Error())
if c.onSubscriptionError != nil {
c.onSubscriptionError(err)
}
return err
}
// 检查订阅是否成功
if token.WaitTimeout(30 * time.Second) {
glog.Info(c.ctx, "成功订阅主题:", topics)
} else {
err := fmt.Errorf("订阅主题超时: %v", topics)
glog.Error(c.ctx, "订阅主题超时:", topics)
if c.onSubscriptionError != nil {
c.onSubscriptionError(err)
}
return err
}
return nil
}
// Publish 发布消息
func (c *Client) Publish(topic string, qos byte, retained bool, payload interface{}) error {
token := c.client.Publish(topic, qos, retained, payload)
// 增加发布超时时间
if token.WaitTimeout(30*time.Second) && token.Error() != nil {
err := fmt.Errorf("发送消息到主题%s出现错误: %w", topic, token.Error())
glog.Error(c.ctx, "发布消息到主题", topic, "时发生错误:", token.Error())
if c.onPublishError != nil {
c.onPublishError(err)
}
return err
}
// 检查发布是否成功
if token.WaitTimeout(30 * time.Second) {
glog.Info(c.ctx, "成功发布消息到主题:", topic)
} else {
err := fmt.Errorf("发布消息到主题%s超时", topic)
glog.Error(c.ctx, "发布消息到主题超时:", topic)
if c.onPublishError != nil {
c.onPublishError(err)
}
return err
}
return nil
}
// resubscribe 重新订阅主题
func (c *Client) resubscribe() {
c.subMutex.RLock()
defer c.subMutex.RUnlock()
if len(c.subscribed) == 0 {
glog.Info(c.ctx, "没有需要重新订阅的主题")
return
}
// 复制订阅信息避免并发问题
topics := make(map[string]byte)
for topic, qos := range c.subscribed {
topics[topic] = qos
}
glog.Info(c.ctx, "开始重新订阅主题:", topics)
// 增加重新订阅的超时时间
token := c.client.SubscribeMultiple(topics, c.callback)
if token.WaitTimeout(30 * time.Second) {
if token.Error() != nil {
err := fmt.Errorf("重新订阅主题时发生错误: %w", token.Error())
glog.Error(c.ctx, "重新订阅主题时发生错误:", token.Error())
if c.onSubscriptionError != nil {
c.onSubscriptionError(err)
}
} else {
glog.Info(c.ctx, "重新订阅主题成功")
}
} else {
err := fmt.Errorf("重新订阅主题超时")
glog.Error(c.ctx, "重新订阅主题超时")
if c.onSubscriptionError != nil {
c.onSubscriptionError(err)
}
}
}
// IsConnected 检查是否连接
func (c *Client) IsConnected() bool {
isConnected := c.client.IsConnected()
glog.Debug(c.ctx, "检查连接状态:", isConnected)
return isConnected
}
// Subscribe 单独订阅一个主题
func (c *Client) Subscribe(topic string, qos byte, callback mqtt.MessageHandler) error {
topics := map[string]byte{topic: qos}
return c.SubscribeMultiple(topics, callback)
}

189
server/config.go Normal file
View File

@ -0,0 +1,189 @@
package server
import (
"fmt"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gyaml"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/util/gconv"
)
type Config struct {
Server ServiceConfig `yaml:"server"`
Database *DatabaseConfig `yaml:"database"`
SkipUrl string `yaml:"skipUrl"`
OpenAPITitle string `yaml:"openAPITitle"`
OpenAPIDescription string `yaml:"openAPIDescription"`
OpenAPIUrl string `yaml:"openAPIUrl"`
OpenAPIName string `yaml:"openAPIName"`
DoMain []string `yaml:"doMain"`
OpenAPIVersion string `yaml:"openAPIVersion"`
Logger LoggerConfig `yaml:"logger"`
Dns string `yaml:"dns"`
}
type ServiceConfig struct {
Default ServiceDefault `yaml:"default"`
}
type ServiceDefault struct {
Address string `yaml:"address"`
LogPath string `yaml:"logPath"`
LogStdout bool `yaml:"logStdout"`
ErrorStack bool `yaml:"errorStack"`
ErrorLogEnabled bool `yaml:"errorLogEnabled"`
ErrorLogPattern string `yaml:"errorLogPattern"`
AccessLogEnable bool `yaml:"accessLogEnable"`
AccessLogPattern string `yaml:"accessLogPattern"`
FileServerEnabled bool `yaml:"fileServerEnabled"`
}
type DatabaseConfig struct {
Default DatabaseDefault `yaml:"default"`
}
type DatabaseDefault struct {
Host string `yaml:"host" json:"host"`
Link string `yaml:"link" dc:"数据库连接字符串" json:"link"`
Port string `yaml:"port" json:"port"`
User string `yaml:"user" json:"user"`
Pass string `yaml:"pass" json:"pass"`
Name string `yaml:"name" json:"name"`
Type string `yaml:"type" json:"type"`
Timezone string `yaml:"timezone" json:"timezone"`
Debug bool `yaml:"debug" json:"debug"`
Charset string `yaml:"charset" json:"charset"`
CreatedAt string `yaml:"createdAt" json:"createdAt"`
UpdatedAt string `yaml:"updatedAt" json:"updatedAt"`
}
type LoggerConfig struct {
Path string `yaml:"path" json:"path"`
File string `yaml:"file" json:"file"`
Level string `yaml:"level" json:"level"`
TimeFormat string `yaml:"timeFormat" json:"timeFormat"`
CtxKeys []string `yaml:"ctxKeys" json:"ctxKeys"`
Header bool `yaml:"header" json:"header"`
Stdout bool `yaml:"stdout" json:"stdout"`
RotateSize string `yaml:"rotateSize" json:"rotateSize"`
RotateBackupLimit int `yaml:"rotateBackupLimit" json:"rotateBackupLimit"`
StdoutColorDisabled bool `yaml:"stdoutColorDisabled" json:"stdoutColorDisabled"`
WriterColorEnable bool `yaml:"writerColorEnable" json:"writerColorEnable"`
}
var DefaultConfig = Config{
Server: ServiceConfig{
Default: ServiceDefault{
Address: ":8080",
LogPath: "./log/",
LogStdout: true,
ErrorStack: true,
ErrorLogEnabled: true,
ErrorLogPattern: "error-{Ymd}.log",
AccessLogEnable: false,
FileServerEnabled: true,
},
},
OpenAPITitle: "",
OpenAPIDescription: "Api列表 包含各端接口信息 字段注释 枚举说明",
OpenAPIUrl: "https://panel.magicany.cc:8888/btpanel",
OpenAPIName: "",
DoMain: []string{"localhost", "127.0.0.1"},
OpenAPIVersion: "v1.0",
Dns: "root:123456@tcp(127.0.0.1:3306)/test",
Logger: LoggerConfig{
Path: "./log/",
File: "access-{Ymd}.log",
Level: "all",
TimeFormat: "2006-01-02 15:04:05",
CtxKeys: []string{},
Header: true,
Stdout: true,
RotateSize: "1M",
RotateBackupLimit: 10,
},
}
func DefaultConfigInit() {
database := &DatabaseConfig{Default: DatabaseDefault{
Host: "127.0.0.1",
Port: "3306",
User: "root",
Pass: "123456",
Name: "database",
Type: "mysql",
Timezone: "Local",
Debug: true,
Charset: "utf8",
CreatedAt: "create_time",
UpdatedAt: "update_time",
}}
DefaultConfig.Database = database
yaml, err := gyaml.Encode(DefaultConfig)
if err != nil {
g.Log().Error(gctx.New(), "转换yaml失败", err)
}
if !gfile.IsDir(uploadPath) {
_ = gfile.Mkdir(uploadPath)
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "template"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "scripts"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "public", "html"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "public", "resource", "css"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "public", "resource", "image"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "public", "resource", "js"))
}
g.Log().Info(gctx.New(), "正在检查配置文件", gfile.IsFile(ConfigPath))
if !gfile.IsFile(ConfigPath) {
g.Log().Info(gctx.New(), "正在创建配置文件", ConfigPath)
_, _ = gfile.Create(ConfigPath)
g.Log().Info(gctx.New(), "正在写入配置文件", ConfigPath)
_ = gfile.PutContents(ConfigPath, gconv.String(yaml))
g.Log().Info(gctx.New(), "配置文件创建成功!")
} else {
gcfg.Instance().GetAdapter().(*gcfg.AdapterFile).SetFileName(ConfigPath)
}
}
// DefaultSqliteConfigInit 创建默认的sqlite数据库配置 不会再生成配置文件
// @param path sqlite数据库路径
// @param autoTime 自动时间字段[]string{"create_time","update_time"}
// @param debug 数据库调试模式
// @param prefix 表前缀可空
func DefaultSqliteConfigInit(path string, autoTime []string, debug bool, prefix ...string) {
glog.Info(gctx.New(), "正在检查文件夹", gfile.IsFile(uploadPath))
if !gfile.IsDir(uploadPath) {
_ = gfile.Mkdir(uploadPath)
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "template"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "scripts"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "public", "html"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "public", "resource", "css"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "public", "resource", "image"))
_ = gfile.Mkdir(gfile.Join(gfile.Pwd(), "resource", "public", "resource", "js"))
}
g.Log().Info(gctx.New(), "正在设置数据库配置")
node := gdb.ConfigNode{
Link: fmt.Sprintf("sqlite::@file(%s)", path),
Timezone: "Local",
Charset: "utf8",
CreatedAt: autoTime[0],
UpdatedAt: autoTime[1],
Debug: debug,
}
if len(prefix) > 0 {
node.Prefix = prefix[0]
}
err := gdb.SetConfig(gdb.Config{
"default": gdb.ConfigGroup{
node,
}})
if err != nil {
g.Log().Error(gctx.New(), "设置数据库配置失败", err)
}
g.Log().Info(gctx.New(), "设置数据库配置成功")
}

401
server/v2.go Normal file
View File

@ -0,0 +1,401 @@
package server
import (
"context"
"fmt"
"net/http"
"path/filepath"
"time"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/net/goai"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
type Json struct {
Code int `json:"code" d:"1"`
Data any `json:"data"`
Msg string `json:"msg" d:"操作成功"`
}
type ApiRes struct {
ctx context.Context
json *Json
}
func Success(ctx context.Context) *ApiRes {
json := Json{
Code: 1,
}
var a = ApiRes{
ctx: ctx,
json: &json,
}
return &a
}
func Error(ctx context.Context) *ApiRes {
json := Json{
Code: 0,
}
var a = ApiRes{
ctx: ctx,
json: &json,
}
return &a
}
func (a *ApiRes) SetCode(code int) *ApiRes {
a.json.Code = code
return a
}
func (a *ApiRes) SetData(data interface{}) *ApiRes {
a.json.Data = data
return a
}
func (a *ApiRes) SetMsg(msg string) *ApiRes {
a.json.Msg = msg
return a
}
func (a *ApiRes) End() {
from := g.RequestFromCtx(a.ctx)
from.Header.Set("Access-Control-Expose-Headers", "Set-Cookie")
from.Response.Status = 200
from.Response.WriteJson(a.json)
return
}
func (a *ApiRes) FileDownload(path, name string) {
from := g.RequestFromCtx(a.ctx)
from.Response.ServeFileDownload(path)
return
}
func (a *ApiRes) FileSelect(path string) {
from := g.RequestFromCtx(a.ctx)
from.Response.ServeFile(path)
return
}
// LoginJson 返回登录json数据
/*
* @param ctx
* @param msg
* @param data
*/
func LoginJson(r *ghttp.Request, msg string, data ...interface{}) {
var info interface{}
if len(data) > 0 {
info = data[0]
} else {
info = nil
}
r.Response.WriteJsonExit(Json{
Code: 1,
Data: info,
Msg: msg,
})
}
// ResponseJson 返回json数据
/*
* @param ctx
* @param data
*/
func ResponseJson(ctx context.Context, data interface{}) {
g.RequestFromCtx(ctx).Response.WriteJson(data)
return
}
type PageSize struct {
CurrentPage int `json:"currentPage"`
Data interface{} `json:"data"`
LastPage int `json:"lastPage"`
PerPage int `json:"per_page"`
Total int `json:"total"`
}
// SetPage 设置分页
/*
* @param page
* @param limit
* @param total
* @param data
* @return PageSize
*/
func SetPage(page, limit, total int, data interface{}) *PageSize {
var size = new(PageSize)
if page == 1 {
size.LastPage = 1
} else {
size.LastPage = page - 1
}
size.PerPage = limit
size.Total = total
size.CurrentPage = page
size.Data = data
return size
}
// MiddlewareError 异常处理中间件
func MiddlewareError(r *ghttp.Request) {
r.Middleware.Next()
var (
err = r.GetError()
msg string
res = r.GetHandlerResponse()
status = r.Response.Status
)
json := new(Json)
json.Code = 1
json.Data = res
json.Msg = "操作成功"
if err != nil {
bo := gstr.Contains(err.Error(), ": ")
if bo {
msg = gstr.SubStrFromEx(err.Error(), ": ")
} else {
msg = err.Error()
}
r.Response.ClearBuffer()
json.Code = 0
json.Msg = msg
r.Response.Status = http.StatusInternalServerError
}
if r.Response.BufferLength() > 0 {
return
}
if status == 401 {
json.Code = 0
json.Msg = "请登录后操作"
}
r.Response.WriteJson(json)
}
// AuthBase 鉴权中间件,只有前端或者后端登录成功之后才能通过
func AuthBase(r *ghttp.Request, name string) {
info, err := r.Session.Get(name, nil)
if err != nil {
panic(err.Error())
}
if !info.IsEmpty() {
r.Middleware.Next()
} else {
NoLogin(r)
}
}
// AuthAdmin 鉴权中间件,只有后端登录成功之后才能通过
func AuthAdmin(r *ghttp.Request) {
AuthBase(r, "admin")
}
// AuthIndex 鉴权中间件,只有前端登录成功之后才能通过
func AuthIndex(r *ghttp.Request) {
AuthBase(r, "user")
}
// NoLogin 未登录返回
func NoLogin(r *ghttp.Request) {
r.Response.Status = 401
r.Response.WriteJsonExit(Json{
Code: 401,
Data: nil,
Msg: "请登录后操作",
})
}
// CreateFileDir 创建文件目录
func CreateFileDir() error {
path := gfile.Pwd() + "/resource"
if !gfile.IsDir(path) {
if err := gfile.Mkdir(path); err != nil {
return err
}
if err := gfile.Mkdir(path + "/public/upload"); err != nil {
return err
}
}
return nil
}
func AuthLoginSession(ctx context.Context, sessionKey string) {
ti, err := g.RequestFromCtx(ctx).Session.Get(sessionKey+"LoginTime", "")
if err != nil {
panic(err.Error())
}
if !ti.IsEmpty() {
now := gtime.Now().Timestamp()
if now-gconv.Int64(ti) <= 300 {
number, err := g.RequestFromCtx(ctx).Session.Get(sessionKey+"LoginNum", 0)
if err != nil {
panic(err.Error())
}
if !number.IsEmpty() {
count := gconv.Int(number)
if count == 3 {
panic("请等待5分钟后再次尝试或修改后尝试登录")
}
}
}
}
}
func LoginCountSession(ctx context.Context, sessionKey string) {
ti, err := g.RequestFromCtx(ctx).Session.Get(sessionKey+"LoginTime", "")
if err != nil {
panic(err.Error())
}
if ti.IsEmpty() {
_ = g.RequestFromCtx(ctx).Session.Set(sessionKey+"LoginTime", gtime.Now().Timestamp())
}
now := gtime.Now().Timestamp()
if now-gconv.Int64(ti) <= 300 {
number, err := g.RequestFromCtx(ctx).Session.Get(sessionKey+"LoginNum", 0)
if err != nil {
panic(err.Error())
}
if number.IsEmpty() {
_ = g.RequestFromCtx(ctx).Session.Set(sessionKey+"LoginNum", 1)
} else {
count := gconv.Int(number)
if count == 3 {
panic("尝试登录已超过限制请等待5分钟后再次尝试或修改后尝试登录")
}
_ = g.RequestFromCtx(ctx).Session.Set(sessionKey+"LoginNum", count+1)
}
} else {
_ = g.RequestFromCtx(ctx).Session.Set(sessionKey+"LoginTime", gtime.Now().Timestamp())
_ = g.RequestFromCtx(ctx).Session.Set(sessionKey+"LoginNum", 1)
}
}
func enhanceOpenAPIDoc(s *ghttp.Server) {
openapi := s.GetOpenApi()
openapi.Config.CommonResponse = ghttp.DefaultHandlerResponse{}
openapi.Config.CommonResponseDataField = `Data`
// API description.
openapi.Info = goai.Info{
Title: "Api列表",
Description: "Api列表 包含各端接口信息 字段注释 枚举说明",
Contact: &goai.Contact{
Name: "Api列表",
URL: "https://panel.magicany.cc:8888/btpanel",
},
License: &goai.License{
Name: "马国栋",
URL: "https://panel.magicany.cc:8888/btpanel",
},
Version: "Api列表",
}
}
var ConfigPath = filepath.Join(gfile.Pwd(), "manifest", "config", "config.yaml")
var uploadPath = filepath.Join(gfile.Pwd(), "resource")
// Start 启动服务
/*
* @param agent string
* @param maxSessionTime time.Duration session
* @param isApi bool api
* @param maxBody ...int64 200M
* @return *ghttp.Server
*/
func Start(agent string, maxSessionTime time.Duration, isApi bool, maxBody ...int64) *ghttp.Server {
// var s *ghttp.Server
s := g.Server()
s.SetDumpRouterMap(false)
s.AddStaticPath(fmt.Sprintf("%vstatic", gfile.Separator), uploadPath)
err := s.SetLogPath(gfile.Join(gfile.Pwd(), "resource", "log"))
if err != nil {
fmt.Println(err)
}
s.SetLogLevel("all")
s.SetLogStdout(false)
if len(maxBody) > 0 {
s.SetClientMaxBodySize(maxBody[0])
} else {
s.SetClientMaxBodySize(200 * 1024 * 1024)
}
s.SetFormParsingMemory(50 * 1024 * 1024)
if isApi {
s.SetOpenApiPath("/api.json")
s.SetSwaggerPath("/swagger")
}
s.SetMaxHeaderBytes(1024 * 20)
s.SetErrorStack(true)
s.SetSessionIdName("zrSession")
s.SetAccessLogEnabled(true)
s.SetSessionMaxAge(maxSessionTime)
err = s.SetConfigWithMap(g.Map{
"sessionPath": gfile.Join(gfile.Pwd(), "resource", "session"),
"serverAgent": agent,
})
if err != nil {
fmt.Println(err)
}
s.Use(MiddlewareError)
enhanceOpenAPIDoc(s)
return s
}
// SetConfigAndRun 设置配置并运行服务
// @param s *ghttp.Server 服务实例
// @param address string 监听地址
func SetConfigAndRun(s *ghttp.Server, address string) {
g.Log().Info(gctx.New(), "正在设置日志配置")
g.Log().File("{Y-m-d}.log")
g.Log().Path(gfile.Join(gfile.Pwd(), "log"))
g.Log().Level(glog.LEVEL_ALL)
g.Log().SetWriterColorEnable(false)
g.Log().SetTimeFormat("2006-01-02 15:04:05")
g.Log().Stdout(false)
cfg := g.Log().GetConfig()
cfg.RotateBackupLimit = 10
cfg.RotateSize = 1024 * 1024 * 2
err := g.Log().SetConfig(cfg)
if err != nil {
panic(fmt.Sprintf("设置日志配置失败: %+v", err))
}
s.SetAccessLogEnabled(false)
s.SetErrorLogEnabled(true)
sLog := s.Logger()
sLog.Level(glog.LEVEL_ERRO)
err = sLog.SetPath(gfile.Join(gfile.Pwd(), "resource", "log"))
if err != nil {
panic(fmt.Sprintf("添加服务日志路径失败: %+v", err))
}
sLog.SetLevelPrefix(glog.LEVEL_ERRO, "error")
s.SetLogger(sLog)
g.Log().Info(gctx.New(), "设置日志配置完成")
g.Log().Info(gctx.New(), "正在设置服务监听")
s.SetAddr(address)
s.SetFileServerEnabled(true)
s.SetCookieDomain(fmt.Sprintf("http://%s", address))
g.Log().Info(gctx.New(), "设置服务监听完成,执行自动服务")
s.Run()
}
func CORSMiddleware(r *ghttp.Request) {
corsOptions := r.Response.DefaultCORSOptions()
cfg, _ := gcfg.Instance().Get(r.Context(), "doMain", nil)
if !cfg.IsNil() {
corsOptions.AllowDomain = cfg.Strings()
}
r.Response.CORS(corsOptions)
r.Middleware.Next()
}

73
server/ws/example.go Normal file
View File

@ -0,0 +1,73 @@
package ws
import (
"log"
"net/http"
"time"
"github.com/gogf/gf/v2/util/gconv"
)
var manager = NewWs()
func NewWs() *Manager {
// 1. 自定义配置(可选,也可使用默认配置)
customConfig := &Config{
AllowAllOrigins: true,
HeartbeatInterval: 20 * time.Second, // 20秒发一次心跳
HeartbeatTimeout: 40 * time.Second, // 40秒超时
}
// 2. 创建管理器
m := NewManager(customConfig)
// 3. 覆盖业务回调(核心:自定义消息处理逻辑)
// 连接建立回调
m.OnConnect = func(connID string) {
log.Printf("业务回调:连接[%s]上线,当前在线数:%d", connID, m.GetOnlineCount())
// 欢迎消息
_ = m.SendToConn(connID, []byte("欢迎连接WebSocket服务"))
}
// 收到消息回调
m.OnMessage = func(connID string, msgType int, data any) {
log.Printf("业务回调:收到连接[%s]消息:%s", connID, gconv.String(data))
// 示例echo回复
reply := []byte("服务端回复:" + gconv.String(data))
_ = m.SendToConn(connID, reply)
// 示例:广播消息给所有连接
_ = m.Broadcast([]byte("广播:" + connID + "说:" + gconv.String(data)))
}
// 连接断开回调
m.OnDisconnect = func(connID string, err error) {
log.Printf("业务回调:连接[%s]下线,原因:%v当前在线数%d", connID, err, m.GetOnlineCount())
}
return m
}
func Upgrade(w http.ResponseWriter, r *http.Request, connID string) {
_, err := manager.Upgrade(w, r, connID)
if err != nil {
log.Printf("升级连接失败:%v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func main() {
// 4. 注册WebSocket路由
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
// 自定义连接ID示例使用请求参数中的user_id
connID := r.URL.Query().Get("user_id")
if connID == "" {
http.Error(w, "user_id不能为空", http.StatusBadRequest)
return
}
// 升级连接
Upgrade(w, r, connID)
})
// 5. 启动服务
log.Println("WebSocket服务启动http://localhost:8080/ws")
log.Fatal(http.ListenAndServe(":8080", nil))
}

488
server/ws/websocket.go Normal file
View File

@ -0,0 +1,488 @@
package ws
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/os/gtimer"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gorilla/websocket"
)
// 常量定义:默认配置
const (
// 默认读写缓冲区大小(字节)
DefaultReadBufferSize = 1024
DefaultWriteBufferSize = 1024
// 默认心跳间隔每30秒发送一次心跳
DefaultHeartbeatInterval = 30 * time.Second
// 默认心跳超时60秒未收到客户端心跳响应则关闭连接
DefaultHeartbeatTimeout = 60 * time.Second
// 默认读写超时(秒)
DefaultReadTimeout = 60 * time.Second
DefaultWriteTimeout = 10 * time.Second
// 消息类型
MessageTypeText = websocket.TextMessage
MessageTypeBinary = websocket.BinaryMessage
// 心跳最大重试次数
HeartbeatMaxRetry = 3
)
// Config WebSocket服务端配置
type Config struct {
// 读写缓冲区大小
ReadBufferSize int
WriteBufferSize int
// 跨域配置是否允许所有跨域生产环境建议指定Origin
AllowAllOrigins bool
// 允许的跨域Origin列表AllowAllOrigins=false时生效
AllowedOrigins []string
// 心跳配置
HeartbeatInterval time.Duration // 心跳发送间隔
HeartbeatTimeout time.Duration // 心跳超时时间
// 读写超时
ReadTimeout time.Duration
WriteTimeout time.Duration
MsgType int // 发送消息的默认类型
HeartbeatValue string // 心跳消息的标识字段值(如"heartbeat"、"pong"
HeartbeatKey string // 心跳消息的标识字段名(如"type"
}
// 默认配置
func DefaultConfig() *Config {
return &Config{
ReadBufferSize: DefaultReadBufferSize,
WriteBufferSize: DefaultWriteBufferSize,
AllowAllOrigins: true,
AllowedOrigins: []string{},
HeartbeatInterval: DefaultHeartbeatInterval,
HeartbeatTimeout: DefaultHeartbeatTimeout,
ReadTimeout: DefaultReadTimeout,
WriteTimeout: DefaultWriteTimeout,
MsgType: MessageTypeText,
HeartbeatValue: "heartbeat",
HeartbeatKey: "type", // 心跳消息的标识字段名,默认"type"
}
}
// Connection WebSocket连接结构体
type Connection struct {
conn *websocket.Conn // 底层连接
connID string // 唯一连接ID
manager *Manager // 所属管理器
createTime time.Time // 连接创建时间
heartbeatChan time.Time // 心跳通道(用于检测客户端响应)
heartbeatTime *gtimer.Entry
ctx context.Context // 上下文
cancel context.CancelFunc // 上下文取消函数
writeMutex sync.Mutex // 写消息互斥锁(防止并发写)
heartbeatRetry int // 心跳发送重试次数
}
// Manager WebSocket连接管理器
type Manager struct {
config *Config // 配置
upgrader *websocket.Upgrader // HTTP升级器
connections map[string]*Connection // 所有在线连接connID -> Connection
mutex sync.RWMutex // 读写锁保护connections
// 业务回调:收到消息时触发(用户自定义处理逻辑)
OnMessage func(connID string, msgType int, data any)
// 业务回调:连接建立时触发
OnConnect func(connID string)
// 业务回调:连接关闭时触发
OnDisconnect func(connID string, err error)
}
// Merge 合并配置,用传入的配置覆盖非零值部分
func (c *Config) Merge(other *Config) *Config {
result := *c // 复制当前配置
if other == nil {
return &result
}
if other.ReadBufferSize > 0 {
result.ReadBufferSize = other.ReadBufferSize
}
if other.WriteBufferSize > 0 {
result.WriteBufferSize = other.WriteBufferSize
}
if other.HeartbeatInterval > 0 {
result.HeartbeatInterval = other.HeartbeatInterval
}
if other.HeartbeatTimeout > 0 {
result.HeartbeatTimeout = other.HeartbeatTimeout
}
if other.ReadTimeout > 0 {
result.ReadTimeout = other.ReadTimeout
}
if other.WriteTimeout > 0 {
result.WriteTimeout = other.WriteTimeout
}
if other.AllowAllOrigins {
result.AllowAllOrigins = other.AllowAllOrigins
}
if other.HeartbeatValue != "" {
result.HeartbeatValue = other.HeartbeatValue
}
if other.HeartbeatKey != "" {
result.HeartbeatKey = other.HeartbeatKey
}
if len(other.AllowedOrigins) > 0 {
result.AllowedOrigins = other.AllowedOrigins
}
if other.MsgType != 0 {
result.MsgType = other.MsgType
}
return &result
}
// NewManager 创建连接管理器
func NewManager(config *Config) *Manager {
defaultConfig := DefaultConfig()
finalConfig := defaultConfig.Merge(config)
// 初始化升级器
upgrader := &websocket.Upgrader{
ReadBufferSize: config.ReadBufferSize,
WriteBufferSize: config.WriteBufferSize,
CheckOrigin: func(r *http.Request) bool {
// 跨域检查
if config.AllowAllOrigins {
return true
}
origin := r.Header.Get("Origin")
for _, allowed := range finalConfig.AllowedOrigins {
if origin == allowed {
return true
}
}
return false
},
}
return &Manager{
config: finalConfig,
upgrader: upgrader,
connections: make(map[string]*Connection),
mutex: sync.RWMutex{},
// 默认回调(用户可覆盖)
OnMessage: func(connID string, msgType int, data any) {
log.Printf("[默认回调] 收到连接[%s]消息:%s", connID, gconv.String(data))
},
OnConnect: func(connID string) {
log.Printf("[默认回调] 连接[%s]已建立", connID)
},
OnDisconnect: func(connID string, err error) {
log.Printf("[默认回调] 连接[%s]已关闭:%v", connID, err)
},
}
}
// Upgrade HTTP升级为WebSocket连接
// connID自定义连接唯一ID如用户ID、设备ID
func (m *Manager) Upgrade(w http.ResponseWriter, r *http.Request, connID string) (*Connection, error) {
if connID == "" {
return nil, errors.New("连接ID不能为空")
}
// 检查连接ID是否已存在
m.mutex.RLock()
_, exists := m.connections[connID]
m.mutex.RUnlock()
if exists {
return nil, fmt.Errorf("连接ID[%s]已存在", connID)
}
// 升级HTTP连接
conn, err := m.upgrader.Upgrade(w, r, nil)
if err != nil {
return nil, fmt.Errorf("升级WebSocket失败%w", err)
}
// 创建上下文(用于优雅关闭)
ctx, cancel := context.WithCancel(context.Background())
// 创建连接实例
wsConn := &Connection{
conn: conn,
connID: connID,
manager: m,
createTime: time.Now(),
heartbeatChan: time.Now(), // 缓冲1防止阻塞
ctx: ctx,
cancel: cancel,
writeMutex: sync.Mutex{},
heartbeatRetry: 0,
}
wsConn.heartbeatTime = gtimer.AddSingleton(gctx.New(), m.config.HeartbeatTimeout, func(ctx context.Context) {
log.Printf("[心跳检测] 连接[%s]已关闭:心跳超时", wsConn.connID)
wsConn.heartbeatTime.Close()
wsConn.heartbeatTime.Stop()
wsConn.heartbeatTime = nil
wsConn.ctx.Done()
wsConn.Close(fmt.Errorf("心跳超时"))
})
// 添加到管理器
m.mutex.Lock()
m.connections[connID] = wsConn
m.mutex.Unlock()
// 触发连接建立回调
m.OnConnect(connID)
// 启动读消息协程
go wsConn.ReadPump()
// 启动写消息协程(处理异步发送)
go wsConn.WritePump()
// 启动心跳检测协程
go wsConn.Heartbeat()
return wsConn, nil
}
// ReadPump 读取客户端消息(持续运行)
func (c *Connection) ReadPump() {
defer func() {
// 发生panic时关闭连接
if err := recover(); err != nil {
log.Printf("连接[%s]读消息协程panic%v", c.connID, err)
}
// 关闭连接并清理
c.Close(fmt.Errorf("读消息协程退出"))
}()
// 循环读取消息
for {
select {
case <-c.ctx.Done():
return // 上下文已取消,退出
default:
// 设置读超时(每次读取前重置,防止长时间无消息超时)
c.conn.SetReadDeadline(time.Now().Add(c.manager.config.ReadTimeout))
// 读取客户端消息
msgType, data, err := c.conn.ReadMessage()
if err != nil {
// 区分正常关闭和异常错误
var closeErr *websocket.CloseError
if errors.As(err, &closeErr) {
c.Close(fmt.Errorf("客户端主动关闭:%s代码%d", closeErr.Text, closeErr.Code))
} else {
c.Close(fmt.Errorf("读取消息失败:%w", err))
}
return
}
// 尝试解析JSON格式的心跳消息精准判断替代包含判断
isHeartbeat := false
// 先尝试解析为JSON对象
var msgMap map[string]interface{}
if err := gjson.DecodeTo(data, &msgMap); err == nil {
// 获取心跳标识字段的值
heartbeatValue := gconv.String(msgMap[c.manager.config.HeartbeatKey])
if heartbeatValue == c.manager.config.HeartbeatValue {
isHeartbeat = true
}
} else {
// 非JSON格式降级为包含判断兼容纯文本心跳
str := gconv.String(data)
if gstr.Contains(str, c.manager.config.HeartbeatValue) {
isHeartbeat = true
}
}
if isHeartbeat {
log.Printf("[心跳] 收到连接[%s]心跳消息:%s", c.connID, string(data))
// 心跳消息:重置重试次数 + 发送心跳信号 + 重置读超时
js, err := gjson.Encode(&Msg[any]{c.manager.config.HeartbeatValue, nil, gtime.Timestamp()})
if err != nil {
log.Printf("[心跳] 客户端[%s]json编码失败", c.connID)
continue
}
err = c.Send(js)
if err != nil {
log.Printf("[心跳] 客户端[%s]发送心跳消息失败", c.connID)
continue
}
c.heartbeatTime.Reset()
continue // 跳过业务回调
}
// 非心跳消息:触发业务回调
c.manager.OnMessage(c.connID, msgType, data)
}
}
}
type Msg[T any] struct {
Type string `json:"type"`
Data T `json:"data"`
Timestamp int64 `json:"timestamp"`
}
// WritePump 处理异步写消息(持续运行)
// 扩展为监听写队列,防止消息丢失
func (c *Connection) WritePump() {
defer func() {
if err := recover(); err != nil {
log.Printf("连接[%s]写消息协程panic%v", c.connID, err)
}
}()
// 暂时保持简化,实际可扩展为带缓冲的写队列
<-c.ctx.Done()
}
// Heartbeat 心跳检测(持续运行)
func (c *Connection) Heartbeat() {
defer func() {
if err := recover(); err != nil {
log.Printf("连接[%s]心跳协程panic%v", c.connID, err)
}
}()
c.heartbeatTime.Start()
}
// Send 发送消息到客户端(线程安全)
func (c *Connection) Send(data []byte) error {
select {
case <-c.ctx.Done():
return errors.New("连接已关闭,无法发送消息")
default:
// 加锁防止并发写
c.writeMutex.Lock()
defer c.writeMutex.Unlock()
// 设置写超时
c.conn.SetWriteDeadline(time.Now().Add(c.manager.config.WriteTimeout))
// 发送消息(使用连接的默认类型,支持动态调整)
err := c.conn.WriteMessage(c.manager.config.MsgType, data)
if err != nil {
return fmt.Errorf("发送消息失败:%w", err)
}
return nil
}
}
// Close 关闭连接(优雅清理)
func (c *Connection) Close(err error) {
// 防止重复关闭
select {
case <-c.ctx.Done():
return
default:
}
// 取消上下文(终止所有协程)
c.cancel()
// 关闭底层连接(友好关闭)
_ = c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
_ = c.conn.Close()
// 从管理器移除
c.manager.mutex.Lock()
delete(c.manager.connections, c.connID)
c.manager.mutex.Unlock()
// 触发断开回调
c.manager.OnDisconnect(c.connID, err)
log.Printf("连接[%s]已关闭,当前在线数:%d原因%v", c.connID, c.manager.GetOnlineCount(), err)
}
// GetOnlineCount 获取在线连接数
func (m *Manager) GetOnlineCount() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.connections)
}
// Broadcast 广播消息到所有在线连接
func (m *Manager) Broadcast(data []byte) error {
m.mutex.RLock()
defer m.mutex.RUnlock()
if len(m.connections) == 0 {
return errors.New("无在线连接")
}
// 并发发送(非阻塞)
var wg sync.WaitGroup
var errMsg string
for _, conn := range m.connections {
wg.Add(1)
go func(c *Connection) {
defer wg.Done()
if err := c.Send(data); err != nil {
errMsg += fmt.Sprintf("连接[%s]广播失败:%v", c.connID, err)
}
}(conn)
}
wg.Wait()
if errMsg != "" {
return errors.New(errMsg)
}
return nil
}
// SendToConn 定向发送消息到指定连接
func (m *Manager) SendToConn(connID string, data []byte) error {
m.mutex.RLock()
conn, exists := m.connections[connID]
m.mutex.RUnlock()
if !exists {
return fmt.Errorf("连接[%s]不存在", connID)
}
return conn.Send(data)
}
func (m *Manager) GetAllConn() map[string]*Connection {
m.mutex.RLock()
defer m.mutex.RUnlock()
// 返回副本,防止外部修改
connCopy := make(map[string]*Connection, len(m.connections))
for k, v := range m.connections {
connCopy[k] = v
}
return connCopy
}
func (m *Manager) GetConn(connID string) *Connection {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.connections[connID]
}
// CloseAll 关闭所有连接
func (m *Manager) CloseAll() {
m.mutex.RLock()
connIDs := make([]string, 0, len(m.connections))
for connID := range m.connections {
connIDs = append(connIDs, connID)
}
m.mutex.RUnlock()
for _, connID := range connIDs {
m.mutex.RLock()
conn := m.connections[connID]
m.mutex.RUnlock()
if conn != nil {
conn.Close(errors.New("服务端主动关闭所有连接"))
}
}
}

10
task/consts.go Normal file
View File

@ -0,0 +1,10 @@
package task
// TaskStatus 任务状态
type TaskStatus string
const (
TaskStatusProcessing TaskStatus = "processing"
TaskStatusCompleted TaskStatus = "completed"
TaskStatusFailed TaskStatus = "failed"
)

10
task/model.go Normal file
View File

@ -0,0 +1,10 @@
package task
// Task 通用任务结构体
type Task struct {
Processed int `json:"processed" dc:"已处理数量"`
Total int `json:"total" dc:"总计数量"`
Status TaskStatus `json:"status" dc:"状态processing, completed, failed"`
Message string `json:"message" dc:"消息"`
Path string `json:"path" dc:"文件路径"`
}

76
task/taskManager.go Normal file
View File

@ -0,0 +1,76 @@
package task
import "sync"
// TaskManager 任务管理器
type sTaskManager struct {
tasks map[string]*Task
mutex sync.RWMutex
}
var Manager = &sTaskManager{}
func init() {
Manager = &sTaskManager{
tasks: make(map[string]*Task),
}
}
// CreateTask 创建任务
func (m *sTaskManager) CreateTask(token string, total int) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.tasks[token] = &Task{
Processed: 0,
Total: total,
Status: TaskStatusProcessing,
Message: "开始处理...",
Path: "",
}
}
// UpdateProgress 更新任务进度
func (m *sTaskManager) UpdateProgress(token string, processed int, message string) {
m.mutex.Lock()
defer m.mutex.Unlock()
if task, exists := m.tasks[token]; exists {
task.Processed = processed
task.Message = message
}
}
// CompleteTask 完成任务
func (m *sTaskManager) CompleteTask(token string, message string, path string) {
m.mutex.Lock()
defer m.mutex.Unlock()
if task, exists := m.tasks[token]; exists {
task.Status = TaskStatusCompleted
task.Message = message
task.Path = path
}
}
// FailTask 失败任务
func (m *sTaskManager) FailTask(token string, message string) {
m.mutex.Lock()
defer m.mutex.Unlock()
if task, exists := m.tasks[token]; exists {
task.Status = TaskStatusFailed
task.Message = message
}
}
// GetTask 获取任务
func (m *sTaskManager) GetTask(token string) (*Task, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
task, exists := m.tasks[token]
return task, exists
}
// RemoveTask 移除任务
func (m *sTaskManager) RemoveTask(token string) {
m.mutex.Lock()
defer m.mutex.Unlock()
delete(m.tasks, token)
}

47
tcp/example.go Normal file
View File

@ -0,0 +1,47 @@
package tcp
import (
"fmt"
"time"
)
// Example 展示如何使用TCP服务
func Example() {
// 创建配置
config := &TcpPoolConfig{
BufferSize: 2048,
MaxConnections: 100000,
ConnectTimeout: time.Second * 5,
ReadTimeout: time.Second * 30,
WriteTimeout: time.Second * 10,
MaxIdleTime: time.Minute * 5,
}
// 创建TCP服务器
server := NewTCPServer("0.0.0.0:8888", config)
// 设置消息处理函数
server.SetMessageHandler(func(conn *TcpConnection, msg *TcpMessage) error {
fmt.Printf("Received message from %s: %s\n", conn.Id, string(msg.Data))
// 回显消息
return server.SendTo(conn.Id, []byte(fmt.Sprintf("Echo: %s", msg.Data)))
})
// 启动服务器
if err := server.Start(); err != nil {
fmt.Printf("Failed to start server: %v\n", err)
return
}
// 运行10秒后停止
fmt.Println("TCP server started. Running for 10 seconds...")
time.Sleep(time.Second * 10)
// 停止服务器
if err := server.Stop(); err != nil {
fmt.Printf("Failed to stop server: %v\n", err)
}
fmt.Println("TCP server stopped.")
}

280
tcp/tcp.go Normal file
View File

@ -0,0 +1,280 @@
package tcp
import (
"context"
"fmt"
"sync"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gtcp"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/grpool"
"github.com/gogf/gf/v2/os/gtime"
)
// MessageHandler 消息处理函数类型
type MessageHandler func(conn *TcpConnection, msg *TcpMessage) error
// TCPServer TCP服务器结构
type TCPServer struct {
Address string
Config *TcpPoolConfig
Listener *gtcp.Server
Connection *ConnectionPool
Logger *glog.Logger
MessageHandler MessageHandler
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
// ConnectionPool 连接池结构
type ConnectionPool struct {
connections map[string]*TcpConnection
mutex sync.RWMutex
config *TcpPoolConfig
logger *glog.Logger
}
// NewTCPServer 创建一个新的TCP服务器
func NewTCPServer(address string, config *TcpPoolConfig) *TCPServer {
logger := g.Log(address)
ctx, cancel := context.WithCancel(context.Background())
pool := &ConnectionPool{
connections: make(map[string]*TcpConnection),
config: config,
logger: logger,
}
server := &TCPServer{
Address: address,
Config: config,
Connection: pool,
Logger: logger,
ctx: ctx,
cancel: cancel,
}
server.Listener = gtcp.NewServer(address, server.handleConnection)
return server
}
// SetMessageHandler 设置消息处理函数
func (s *TCPServer) SetMessageHandler(handler MessageHandler) {
s.MessageHandler = handler
}
// Start 启动TCP服务器
func (s *TCPServer) Start() error {
s.Logger.Info(s.ctx, fmt.Sprintf("TCP server starting on %s", s.Address))
go func() {
s.wg.Add(1)
defer s.wg.Done()
if err := s.Listener.Run(); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("TCP server stopped with error: %v", err))
}
}()
return nil
}
// Stop 停止TCP服务器
func (s *TCPServer) Stop() error {
s.Logger.Info(s.ctx, "TCP server stopping...")
s.cancel()
s.Listener.Close()
s.wg.Wait()
s.Connection.Clear()
s.Logger.Info(s.ctx, "TCP server stopped")
return nil
}
// handleConnection 处理新连接
func (s *TCPServer) handleConnection(conn *gtcp.Conn) {
// 生成连接ID
connID := fmt.Sprintf("%s_%d", conn.RemoteAddr().String(), gtime.TimestampNano())
// 创建连接对象
tcpConn := &TcpConnection{
Id: connID,
Address: conn.RemoteAddr().String(),
Server: *conn,
IsActive: true,
LastUsed: time.Now(),
CreatedAt: time.Now(),
}
// 添加到连接池
s.Connection.Add(tcpConn)
s.Logger.Info(s.ctx, fmt.Sprintf("New connection established: %s", connID))
// 启动消息接收协程
go s.receiveMessages(tcpConn)
}
// receiveMessages 接收消息
func (s *TCPServer) receiveMessages(conn *TcpConnection) {
defer func() {
if err := recover(); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Panic in receiveMessages: %v", err))
}
s.Connection.Remove(conn.Id)
conn.Server.Close()
s.Logger.Info(s.ctx, fmt.Sprintf("Connection closed: %s", conn.Id))
}()
buffer := make([]byte, s.Config.BufferSize)
for {
select {
case <-s.ctx.Done():
return
default:
// 设置读取超时
conn.Server.SetReadDeadline(time.Now().Add(s.Config.ReadTimeout))
// 读取数据
n, err := conn.Server.Read(buffer)
if err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Read error from %s: %v", conn.Id, err))
return
}
if n > 0 {
// 更新最后使用时间
conn.Mutex.Lock()
conn.LastUsed = time.Now()
conn.Mutex.Unlock()
// 处理消息
data := make([]byte, n)
copy(data, buffer[:n])
msg := &TcpMessage{
Id: fmt.Sprintf("msg_%d", gtime.TimestampNano()),
ConnId: conn.Id,
Data: data,
Timestamp: time.Now(),
IsSend: false,
}
// 使用协程池处理消息,避免阻塞
grpool.AddWithRecover(s.ctx, func(ctx context.Context) {
if s.MessageHandler != nil {
if err := s.MessageHandler(conn, msg); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Message handling error: %v", err))
}
}
}, func(ctx context.Context, err error) {
s.Logger.Error(ctx, fmt.Sprintf("Message handling error: %v", err))
})
}
}
}
}
// SendTo 发送消息到指定连接
func (s *TCPServer) SendTo(connID string, data []byte) error {
conn := s.Connection.Get(connID)
if conn == nil {
return fmt.Errorf("connection not found: %s", connID)
}
return s.sendMessage(conn, data)
}
// SendToAll 发送消息到所有连接
func (s *TCPServer) SendToAll(data []byte) error {
conns := s.Connection.GetAll()
for _, conn := range conns {
if err := s.sendMessage(conn, data); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Send to %s failed: %v", conn.Id, err))
// 继续发送给其他连接
}
}
return nil
}
// sendMessage 发送消息
func (s *TCPServer) sendMessage(conn *TcpConnection, data []byte) error {
conn.Mutex.Lock()
defer conn.Mutex.Unlock()
// 设置写入超时
conn.Server.SetWriteDeadline(time.Now().Add(s.Config.WriteTimeout))
// 发送数据
_, err := conn.Server.Write(data)
if err != nil {
return err
}
// 更新最后使用时间
conn.LastUsed = time.Now()
return nil
}
// Kick 强制退出客户端
func (s *TCPServer) Kick(connID string) error {
conn := s.Connection.Get(connID)
if conn == nil {
return fmt.Errorf("connection not found: %s", connID)
}
// 关闭连接
conn.Server.Close()
// 从连接池移除
s.Connection.Remove(connID)
s.Logger.Info(s.ctx, fmt.Sprintf("Kicked connection: %s", connID))
return nil
}
// Add 添加连接到连接池
func (p *ConnectionPool) Add(conn *TcpConnection) {
p.mutex.Lock()
defer p.mutex.Unlock()
p.connections[conn.Id] = conn
}
// Get 获取连接
func (p *ConnectionPool) Get(connID string) *TcpConnection {
p.mutex.RLock()
defer p.mutex.RUnlock()
return p.connections[connID]
}
// GetAll 获取所有连接
func (p *ConnectionPool) GetAll() []*TcpConnection {
p.mutex.RLock()
defer p.mutex.RUnlock()
conns := make([]*TcpConnection, 0, len(p.connections))
for _, conn := range p.connections {
conns = append(conns, conn)
}
return conns
}
// Remove 从连接池移除连接
func (p *ConnectionPool) Remove(connID string) {
p.mutex.Lock()
defer p.mutex.Unlock()
delete(p.connections, connID)
}
// Clear 清空连接池
func (p *ConnectionPool) Clear() {
p.mutex.Lock()
defer p.mutex.Unlock()
for connID, conn := range p.connections {
conn.Server.Close()
delete(p.connections, connID)
}
}
// Count 获取连接数量
func (p *ConnectionPool) Count() int {
p.mutex.RLock()
defer p.mutex.RUnlock()
return len(p.connections)
}

38
tcp/tcpConfig.go Normal file
View File

@ -0,0 +1,38 @@
package tcp
import (
"sync"
"time"
"github.com/gogf/gf/v2/net/gtcp"
)
// TcpPoolConfig TCP连接池配置
type TcpPoolConfig struct {
BufferSize int `json:"bufferSize"` // 缓冲区大小
MaxConnections int `json:"maxConnections"` // 最大连接数
ConnectTimeout time.Duration `json:"connectTimeout"` // 连接超时时间
ReadTimeout time.Duration `json:"readTimeout"` // 读取超时时间
WriteTimeout time.Duration `json:"writeTimeout"` // 写入超时时间
MaxIdleTime time.Duration `json:"maxIdleTime"` // 最大空闲时间
}
// TcpConnection TCP连接结构
type TcpConnection struct {
Id string `json:"id"` // 连接ID
Address string `json:"address"` // 连接地址
Server gtcp.Conn `json:"server"` // 实际连接
IsActive bool `json:"isActive"` // 是否活跃
LastUsed time.Time `json:"lastUsed"` // 最后使用时间
CreatedAt time.Time `json:"createdAt"` // 创建时间
Mutex sync.RWMutex `json:"-"` // 读写锁
}
// TcpMessage TCP消息结构
type TcpMessage struct {
Id string `json:"id"` // 消息ID
ConnId string `json:"connId"` // 连接ID
Data []byte `json:"data"` // 消息数据
Timestamp time.Time `json:"timestamp"` // 时间戳
IsSend bool `json:"isSend"` // 是否是发送的消息
}

102
utils/base64Image.go Normal file
View File

@ -0,0 +1,102 @@
package utils
import (
"encoding/base64"
"fmt"
"image"
"image/jpeg"
"image/png"
"io/ioutil"
"os"
"strings"
)
// Base64ToImage 将Base64编码的数据转换为图片并保存到指定路径
func Base64ToImage(base64String, outputPath string) error {
// 移除Base64数据URI前缀如果有
if strings.Contains(base64String, ",") {
base64String = strings.Split(base64String, ",")[1]
}
// 解码Base64字符串
imageData, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return fmt.Errorf("解码Base64数据时出错: %v", err)
}
// 将解码后的数据写入文件
err = ioutil.WriteFile(outputPath, imageData, 0644)
if err != nil {
return fmt.Errorf("保存图片文件时出错: %v", err)
}
return nil
}
// Base64ToImageWithFormat 将Base64编码的数据转换为指定格式的图片并保存
func Base64ToImageWithFormat(base64String, outputPath, format string) error {
// 移除Base64数据URI前缀如果有
if strings.Contains(base64String, ",") {
base64String = strings.Split(base64String, ",")[1]
}
// 解码Base64字符串
imageData, err := base64.StdEncoding.DecodeString(base64String)
if err != nil {
return fmt.Errorf("解码Base64数据时出错: %v", err)
}
// 创建文件
file, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("创建图片文件时出错: %v", err)
}
defer file.Close()
// 根据指定格式编码图片
switch strings.ToLower(format) {
case "jpeg", "jpg":
// 解码图片
img, _, err := image.Decode(strings.NewReader(string(imageData)))
if err != nil {
return fmt.Errorf("解码图片时出错: %v", err)
}
// 编码为JPEG格式
err = jpeg.Encode(file, img, &jpeg.Options{Quality: 90})
if err != nil {
return fmt.Errorf("编码JPEG图片时出错: %v", err)
}
case "png":
// 解码图片
img, _, err := image.Decode(strings.NewReader(string(imageData)))
if err != nil {
return fmt.Errorf("解码图片时出错: %v", err)
}
// 编码为PNG格式
err = png.Encode(file, img)
if err != nil {
return fmt.Errorf("编码PNG图片时出错: %v", err)
}
default:
// 直接写入原始数据
_, err = file.Write(imageData)
if err != nil {
return fmt.Errorf("写入图片数据时出错: %v", err)
}
}
return nil
}
// GetImageFormatFromBase64 从Base64数据中获取图片格式
func GetImageFormatFromBase64(base64String string) string {
// 检查数据URI前缀
if strings.HasPrefix(base64String, "data:image/") {
// 提取格式信息
format := strings.Split(strings.Split(base64String, ";")[0], "/")[1]
return format
}
return "unknown"
}

32
utils/common.go Normal file
View File

@ -0,0 +1,32 @@
package utils
type NormalRes[T any] struct {
Code int `json:"code" dc:"code"`
Data T `json:"data" dc:"data 可null"`
Msg string `json:"msg" dc:"return msg"`
}
type ListRes[T any] struct {
Rows []T
Total int `json:"total"`
}
func NewNormalRes[T any](data T, msg ...string) *NormalRes[T] {
message := ""
if len(msg) == 0 {
message = "操作成功"
} else {
message = msg[0]
}
return &NormalRes[T]{
Code: 1,
Data: data,
Msg: message,
}
}
func NewListRes[T any](data []T, total int) *ListRes[T] {
return &ListRes[T]{
Rows: data,
Total: total,
}
}

346
utils/curd.go Normal file
View File

@ -0,0 +1,346 @@
package utils
import (
"context"
"github.com/gogf/gf/v2/container/gmap"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
type ctx = context.Context
type IDao interface {
DB() gdb.DB
Table() string
Group() string
Ctx(ctx context.Context) *gdb.Model
Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error)
}
type Curd[R any] struct {
Dao IDao
}
var pageInfo = []string{
"page",
"size",
"num",
"limit",
"pagesize",
"pageSize",
"page_size",
"pageNum",
"pagenum",
"page_num",
}
func (c Curd[R]) BuildWhere(req any, changeWhere any, subWhere any, removeFields []string, isSnake ...bool) map[string]any {
// 默认使用小写下划线方式
caseTypeValue := gstr.Snake
if len(isSnake) > 0 && isSnake[0] == false {
caseTypeValue = gstr.CamelLower
}
// 转换req为map
reqMap := gconv.Map(req)
// 清理空值和分页信息
ctx := gctx.New()
cleanedReq := make(map[string]any)
for k, v := range reqMap {
// 清理空值
if g.IsEmpty(v) {
glog.Debugf(ctx, "清理空值:%s", k)
continue
}
// 清理分页信息
if gstr.InArray(pageInfo, k) {
glog.Debugf(ctx, "清理分页信息:%s", k)
continue
}
if len(removeFields) > 0 && gstr.InArray(removeFields, k) {
glog.Debugf(ctx, "清理字段:%s", k)
continue
}
cleanedReq[gstr.CaseConvert(k, caseTypeValue)] = v
}
// 处理changeWhere
if changeWhere != nil {
changeMap := gconv.Map(changeWhere)
for k, v := range changeMap {
if _, hasKey := cleanedReq[k]; !hasKey {
glog.Debugf(ctx, "处理changeWhere%s", k)
continue
}
if len(removeFields) > 0 && gstr.InArray(removeFields, k) {
glog.Debugf(ctx, "清理应删除字段:%s", k)
continue
}
// 转换v为map
vMap := gconv.Map(v)
value, hasValue := vMap["value"]
op, hasOp := vMap["op"]
if hasValue {
glog.Debugf(ctx, "变更字段存在value%s", k)
// 构建新的键名
newKey := k
if hasOp && op != "" {
glog.Debugf(ctx, "变更字段存在op%s", k)
newKey = k + " " + gconv.String(op)
delete(cleanedReq, k)
}
cleanedReq[newKey] = value
}
}
}
// 变量名切换
resultMap := make(map[string]any)
for k, v := range cleanedReq {
// 提取原始键名去掉op部分
originalKey := k
opStr := ""
if opIndex := gstr.Pos(k, " "); opIndex > 0 {
originalKey = k[:opIndex]
opStr = k[opIndex+1:]
}
// 转换键名
convertedKey := originalKey
convertedKey = gstr.CaseConvert(convertedKey, caseTypeValue)
// 如果有op重新构建键名
if opStr != "" {
convertedKey = convertedKey + " " + opStr
}
resultMap[convertedKey] = v
}
// 合并subWhere
if subWhere != nil {
subMap := gconv.Map(subWhere)
resultM := gmap.NewStrAnyMapFrom(resultMap)
resultM.Merge(gmap.NewStrAnyMapFrom(subMap))
resultMap = resultM.Map()
}
return resultMap
}
func (c Curd[R]) BuildMap(op string, value any, field ...string) map[string]any {
if len(field) > 0 {
return map[string]any{
"op": op,
"field": field[0],
"value": value,
}
}
return map[string]any{
"op": op,
"field": "",
"value": value,
}
}
func (c Curd[R]) Builder(ctx context.Context) *gdb.WhereBuilder {
return c.Dao.Ctx(ctx).Builder()
}
func (c Curd[R]) ClearField(req any, delField []string, subField ...map[string]any) map[string]any {
m := gmap.NewStrAnyMapFrom(gconv.Map(req))
if delField != nil && len(delField) > 0 {
m.Iterator(func(k string, v any) bool {
if g.IsEmpty(v) {
m.Remove(k)
return true
}
if gstr.InArray(delField, k) {
m.Remove(k)
return true
}
if gstr.InArray(pageInfo, k) {
m.Remove(k)
return true
}
return true
})
}
if subField != nil && len(subField) > 0 {
m.Merge(gmap.NewStrAnyMapFrom(subField[0]))
}
return m.Map()
}
func (c Curd[R]) ClearFieldPage(ctx ctx, req any, delField []string, where any, page *Paginate, order any, with bool) (items []*R, total int, err error) {
db := c.Dao.Ctx(ctx)
m := c.ClearField(req, delField)
if with {
db = db.WithAll()
}
db = db.Where(m)
if !g.IsNil(where) {
db = db.Where(where)
}
if order != nil {
db = db.Order(order)
}
if !g.IsNil(page) {
db = db.Page(page.Page, page.Limit)
}
err = db.ScanAndCount(&items, &total, false)
return
}
func (c Curd[R]) ClearFieldList(ctx ctx, req any, delField []string, where any, order any, with bool) (items []*R, err error) {
db := c.Dao.Ctx(ctx)
m := c.ClearField(req, delField)
db = db.Where(m)
if !g.IsNil(where) {
db = db.Where(where)
}
if with {
db = db.WithAll()
}
if !g.IsNil(order) {
db = db.Order(order)
}
err = db.Scan(&items)
return
}
func (c Curd[R]) ClearFieldOne(ctx ctx, req any, delField []string, where any, order any, with bool) (items *R, err error) {
db := c.Dao.Ctx(ctx)
m := c.ClearField(req, delField)
db = db.Where(m)
if !g.IsNil(where) {
db = db.Where(where)
}
if with {
db = db.WithAll()
}
if !g.IsNil(order) {
db = db.Order(order)
}
err = db.Scan(&items)
return
}
func (c Curd[R]) Value(ctx ctx, where any, field any) (*gvar.Var, error) {
return c.Dao.Ctx(ctx).Where(where).Fields(field).Value()
}
func (c Curd[R]) DeletePri(ctx ctx, primaryKey any) error {
_, err := c.Dao.Ctx(ctx).WherePri(primaryKey).Delete()
return err
}
func (c Curd[R]) DeleteWhere(ctx ctx, where any) error {
_, err := c.Dao.Ctx(ctx).Where(where).Delete()
return err
}
func (c Curd[R]) Sum(ctx ctx, where any, field string) (float64, error) {
return c.Dao.Ctx(ctx).Where(where).Sum(field)
}
func (c Curd[R]) ArrayField(ctx ctx, where any, field any) ([]*gvar.Var, error) {
if field == nil {
field = "*"
}
return c.Dao.Ctx(ctx).Where(where).Fields(field).Array()
}
func (c Curd[R]) FindPri(ctx ctx, primaryKey any, with bool) (model *R, err error) {
db := c.Dao.Ctx(ctx).WherePri(primaryKey)
if with {
db = db.WithAll()
}
err = db.Scan(&model)
if err != nil {
return
}
return
}
func (c Curd[R]) First(ctx ctx, where any, order any, with bool) (model *R, err error) {
db := c.Dao.Ctx(ctx).Where(where)
if with {
db = db.WithAll()
}
if !g.IsNil(order) {
db = db.Order(order)
}
err = db.Scan(&model)
if err != nil {
return
}
return
}
func (c Curd[R]) Exists(ctx ctx, where any) (exists bool, err error) {
return c.Dao.Ctx(ctx).Where(where).Exist()
}
func (c Curd[R]) All(ctx ctx, where any, order any, with bool) (items []*R, err error) {
db := c.Dao.Ctx(ctx)
if with {
db = db.WithAll()
}
if !g.IsNil(order) {
db = db.Order(order)
}
err = db.Where(where).Scan(&items)
if err != nil {
return nil, err
}
return
}
func (c Curd[R]) Count(ctx ctx, where any) (count int, err error) {
count, err = c.Dao.Ctx(ctx).Where(where).Count()
return
}
func (c Curd[R]) Save(ctx ctx, data any) (id int64, err error) {
result, err := c.Dao.Ctx(ctx).Save(data)
if err != nil {
return
}
id, err = result.LastInsertId()
return
}
func (c Curd[R]) Update(ctx ctx, where any, data any) (count int64, err error) {
result, err := c.Dao.Ctx(ctx).Where(where).Data(data).Update()
if err != nil {
return
}
count, err = result.RowsAffected()
return
}
func (c Curd[R]) UpdatePri(ctx ctx, primaryKey any, data any) (count int64, err error) {
result, err := c.Dao.Ctx(ctx).WherePri(primaryKey).Data(data).Update()
if err != nil {
return
}
count, err = result.RowsAffected()
return
}
func (c Curd[R]) Paginate(ctx context.Context, where any, p Paginate, with bool, order any) (items []*R, total int, err error) {
query := c.Dao.Ctx(ctx)
if where != nil {
query = query.Where(where)
}
query = query.Page(p.Page, p.Limit)
if order != nil {
query = query.Order(order)
}
if with == true {
query = query.WithAll()
}
err = query.Order(order).ScanAndCount(&items, &total, false)
if err != nil {
return
}
return
}

12
utils/model.go Normal file
View File

@ -0,0 +1,12 @@
package utils
type Paginator[I any] struct {
Items []I
Total int
IsSimple bool
}
type Paginate struct {
Limit int `d:"20" json:"limit" v:"max:50"`
Page int `d:"1" dc:"页码" json:"page"`
}

102
utils/sql.go Normal file
View File

@ -0,0 +1,102 @@
package utils
import (
"context"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
// GetCapitalPass MD5化并转换为大写
func GetCapitalPass(val string) string {
md5, err := gmd5.Encrypt(val)
if err != nil {
panic(err.Error())
}
return gstr.CaseCamel(md5)
}
// Transaction 简单封装事务操作
func Transaction(function func() error) {
err := g.DB().Transaction(context.TODO(), func(ctx context.Context, tx gdb.TX) error {
return function()
})
if err != nil {
panic(err.Error())
}
}
type SClient[R any] struct {
client *gclient.Client
request any
header map[string]string
url string
}
func NewClient[R any](request any, url string, header map[string]string) *SClient[R] {
s := &SClient[R]{}
if header != nil {
s.client = g.Client().ContentJson().SetHeaderMap(header)
} else {
s.client = g.Client().ContentJson()
}
s.header = header
s.url = url
s.request = request
return s
}
func (w *SClient[R]) Post(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "post", w.request)
resp := w.client.PostVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
return nil, err
}
return
}
func (w *SClient[R]) Get(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "get", w.request)
resp := w.client.GetVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
return nil, err
}
return
}
func (w *SClient[R]) Put(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "put", w.request)
resp := w.client.PutVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
return nil, err
}
return
}
func (w *SClient[R]) Delete(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "delete", w.request)
resp := w.client.DeleteVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
return nil, err
}
return
}
func (w *SClient[R]) Patch(ctx context.Context) (res *R, err error) {
g.Log().Infof(ctx, "请求Url:%s,请求头:%v,请求方法:%s,请求内容:%s", w.url, w.header, "patch", w.request)
resp := w.client.PatchVar(ctx, w.url, w.request)
err = gconv.Struct(resp, &res)
if err != nil {
g.Log().Errorf(ctx, "解析响应体异常:%s", err)
return nil, err
}
return
}

197
utils/utils.go Normal file
View File

@ -0,0 +1,197 @@
package utils
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"image/jpeg"
"math/big"
"os"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gres"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/nfnt/resize"
)
// Compress 图片压缩
/*
* @param filePath string
* @return string
*
*/
func Compress(filePath string) string {
file, err := os.Open(filePath)
if err != nil {
panic(err.Error())
}
img, err := jpeg.Decode(file)
if err != nil {
panic(err.Error())
}
err = file.Close()
if err != nil {
panic(err.Error())
}
m := resize.Resize(960, 0, img, resize.Lanczos2)
str := gstr.Split(filePath, "/")
sta := gstr.Split(str[len(str)-1], ".")
paths := gfile.Pwd() + "/resource/public/upload/" + sta[0] + "-cop." + sta[1]
out, err := os.Create(paths)
defer out.Close()
err = jpeg.Encode(out, m, nil)
if err != nil {
panic(err.Error())
}
_ = gfile.RemoveFile(filePath)
return sta[0] + "-cop." + sta[1]
}
// Sha256 sha256加密
/*
* @param src string
* @return string
* hash
*/
func Sha256(src string) string {
m := sha256.New()
m.Write([]byte(src))
res := hex.EncodeToString(m.Sum(nil))
return res
}
// InStrArray 判断是否在数组中
/*
* @param ext string
* @param code int
*
*/
func InStrArray(ext string, code int) bool {
if code == 1 {
arr := garray.NewStrArrayFrom(g.SliceStr{".jpg", ".jpeg", ".png"})
return arr.Contains(ext)
} else {
arr := garray.NewStrArrayFrom(g.SliceStr{".xlsx"})
return arr.Contains(ext)
}
}
// ResAddFile 添加文件到资源包
/*
* @param onePath string
* gf pack resource/dist internal/boot/boot_resource.go -n boot
* boot main.goboot
*/
func ResAddFile(onePath string) {
if gfile.Exists(gfile.Pwd() + gfile.Separator + onePath) {
err := gfile.RemoveFile(onePath)
if err != nil {
panic(err)
}
}
g.Log().Debug(gctx.GetInitCtx(), onePath)
gres.Dump()
if gres.IsEmpty() {
return
}
if gstr.Contains(onePath, "/") {
strs := gstr.Split(onePath, "/")
err := gres.Export(strs[1], strs[0])
if err != nil {
panic(err)
}
} else {
err := gres.Export(onePath, onePath)
if err != nil {
panic(err)
}
}
}
const (
CharsetLower = "abcdefghijklmnopqrstuvwxyz"
CharsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
CharsetNumber = "0123456789"
CharsetDefault = CharsetLower + CharsetUpper + CharsetNumber
)
// GenerateString 生成安全随机字符串
func GenerateString(length int) (str string) {
bytes := make([]byte, length)
charsetLen := big.NewInt(int64(len(CharsetDefault)))
for i := range bytes {
n, err := rand.Int(rand.Reader, charsetLen)
if err != nil {
panic(err)
}
bytes[i] = CharsetDefault[n.Int64()]
}
str = string(bytes)
return
}
// GetFileList 获取文件列表
/*
* 访
* 访static访resource
*/
func GetFileList(path string) []string {
if path == "" {
path = "/"
}
filePath := fmt.Sprintf("%s%sresource", gfile.Pwd(), gfile.Separator)
if path != "/" {
filePath += gfile.Separator + path + gfile.Separator
}
paths, _ := gfile.DirNames(filePath)
pathArr := garray.NewStrArray()
for _, v := range paths {
if gstr.Contains(v, ".") {
if path != "/" {
pathArr.Append("/static/" + path + "/" + v)
} else {
pathArr.Append("/static/" + v)
}
} else {
pathArr.Append(v)
}
}
return pathArr.Slice()
}
func Float64Trans(value string) *float64 {
if value == "" {
return nil
}
f := gconv.Float64(value)
return &f
}
func IntTrans(value string) *int {
if value == "" {
return nil
}
i := gconv.Int(value)
return &i
}
func StrTrans(value string) *string {
if value == "" {
return nil
}
i := gconv.String(value)
return &i
}
func TimeTrans(value string) *gtime.Time {
if value == "" {
return nil
}
return gconv.GTime(value)
}