This commit is contained in:
孟帅
2023-05-10 23:54:50 +08:00
parent bbe655a4d8
commit 49a96750bf
314 changed files with 15138 additions and 6244 deletions

View File

@@ -35,7 +35,7 @@ func ScanInstall(m Module) (record *InstallRecord, err error) {
func IsInstall(m Module) bool {
record, err := ScanInstall(m)
if err != nil {
g.Log().Debug(m.Ctx(), err.Error())
g.Log().Debugf(m.Ctx(), "addons.IsInstall err:%+v", err)
return false
}
if record == nil {

View File

@@ -51,7 +51,7 @@ func SetAdapter(ctx context.Context) {
if !gfile.Exists(conf.FileDir) {
if err := gfile.Mkdir(conf.FileDir); err != nil {
g.Log().Fatalf(ctx, "Failed to create the cache directory. Procedure, err:%+v", err)
g.Log().Fatalf(ctx, "failed to create the cache directory. procedure, err:%+v", err)
return
}
}

View File

@@ -9,9 +9,11 @@ import (
"fmt"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/util/gconv"
"io/ioutil"
"os"
"path/filepath"
"time"
@@ -181,7 +183,12 @@ func (c *AdapterFile) createName(key string) string {
}
func (c *AdapterFile) read(key string) (*fileContent, error) {
value, err := ioutil.ReadFile(c.createName(key))
rp := gfile.RealPath(c.createName(key))
if rp == "" {
return nil, nil
}
value, err := os.ReadFile(rp)
if err != nil {
return nil, err
}
@@ -236,6 +243,10 @@ func (c *AdapterFile) Fetch(key string) (interface{}, error) {
return "", err
}
if content == nil {
return "", nil
}
return content.Data, nil
}
@@ -286,5 +297,8 @@ func (c *AdapterFile) Save(key string, value string, lifeTime time.Duration) err
return err
}
return ioutil.WriteFile(c.createName(key), data, perm)
err = os.WriteFile(c.createName(key), data, perm)
g.Log().Warningf(gctx.New(), "Save err:%+v", err)
return err
}

View File

@@ -3,7 +3,6 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package casbin
import (
@@ -31,7 +30,7 @@ CREATE TABLE IF NOT EXISTS %s (
v4 varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
v5 varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'casbin权限表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '管理员_casbin权限表' ROW_FORMAT = Dynamic;
`
)

View File

@@ -3,7 +3,6 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package casbin
import (

View File

@@ -34,7 +34,7 @@ func Get(ctx context.Context) *model.Context {
func SetUser(ctx context.Context, user *model.Identity) {
c := Get(ctx)
if c == nil {
g.Log().Warning(ctx, "contexts.SetUser, c == nil ")
g.Log().Warning(ctx, "contexts.SetUser, c == nil ")
return
}
c.User = user
@@ -44,7 +44,7 @@ func SetUser(ctx context.Context, user *model.Identity) {
func SetResponse(ctx context.Context, response *model.Response) {
c := Get(ctx)
if c == nil {
g.Log().Warning(ctx, "contexts.SetResponse, c == nil ")
g.Log().Warning(ctx, "contexts.SetResponse, c == nil ")
return
}
c.Response = response
@@ -54,22 +54,12 @@ func SetResponse(ctx context.Context, response *model.Response) {
func SetModule(ctx context.Context, module string) {
c := Get(ctx)
if c == nil {
g.Log().Warning(ctx, "contexts.SetModule, c == nil ")
g.Log().Warning(ctx, "contexts.SetModule, c == nil ")
return
}
c.Module = module
}
// SetTakeUpTime 设置请求耗时
func SetTakeUpTime(ctx context.Context, takeUpTime int64) {
c := Get(ctx)
if c == nil {
g.Log().Warning(ctx, "contexts.SetTakeUpTime, c == nil ")
return
}
c.TakeUpTime = takeUpTime
}
// GetUser 获取用户信息
func GetUser(ctx context.Context) *model.Identity {
c := Get(ctx)
@@ -120,7 +110,7 @@ func GetModule(ctx context.Context) string {
func SetAddonName(ctx context.Context, name string) {
c := Get(ctx)
if c == nil {
g.Log().Warning(ctx, "contexts.SetAddonName, c == nil ")
g.Log().Warning(ctx, "contexts.SetAddonName, c == nil ")
return
}
Get(ctx).AddonName = name
@@ -143,3 +133,35 @@ func GetAddonName(ctx context.Context) string {
}
return Get(ctx).AddonName
}
// SetData 设置额外数据
func SetData(ctx context.Context, k string, v interface{}) {
c := Get(ctx)
if c == nil {
g.Log().Warning(ctx, "contexts.SetData, c == nil ")
return
}
Get(ctx).Data[k] = v
}
// SetDataMap 设置额外数据
func SetDataMap(ctx context.Context, vs g.Map) {
c := Get(ctx)
if c == nil {
g.Log().Warning(ctx, "contexts.SetData, c == nil ")
return
}
for k, v := range vs {
Get(ctx).Data[k] = v
}
}
// GetData 获取额外数据
func GetData(ctx context.Context) g.Map {
c := Get(ctx)
if c == nil {
return nil
}
return c.Data
}

View File

@@ -0,0 +1,30 @@
package contexts
import (
"context"
"time"
)
type detached struct {
ctx context.Context
}
func (detached) Deadline() (time.Time, bool) {
return time.Time{}, false
}
func (detached) Done() <-chan struct{} {
return nil
}
func (detached) Err() error {
return nil
}
func (d detached) Value(key interface{}) interface{} {
return d.ctx.Value(key)
}
func Detach(ctx context.Context) context.Context {
return detached{ctx: ctx}
}

View File

@@ -292,7 +292,7 @@ buildDone:
return
}
// getBuildInVarMapJson retrieves and returns the custom build-in variables in configuration
// getBuildInVarStr retrieves and returns the custom build-in variables in configuration
// file as json.
func (c cBuild) getBuildInVarStr(ctx context.Context, in cBuildInput) string {
buildInVarMap := in.VarMap

View File

@@ -12,6 +12,7 @@ var (
type cGen struct {
g.Meta `name:"gen" brief:"{cGenBrief}" dc:"{cGenDc}"`
cGenDao
cGenEnums
cGenPb
cGenPbEntity
cGenService

View File

@@ -0,0 +1,15 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package cmd
import (
"hotgo/internal/library/hggen/internal/cmd/genenums"
)
type (
cGenEnums = genenums.CGenEnums
)

View File

@@ -3,6 +3,8 @@ package cmd
import (
"context"
"fmt"
"github.com/gogf/gf/v2/text/gstr"
"os"
"strings"
"github.com/gogf/gf/v2/frame/g"
@@ -37,6 +39,10 @@ gf init my-mono-repo -m
name for the project. It will create a folder with NAME in current directory.
The NAME will also be the module name for the project.
`
// cInitGitDir the git directory
cInitGitDir = ".git"
// cInitGitignore the gitignore file
cInitGitignore = ".gitignore"
)
func init() {
@@ -57,17 +63,22 @@ type cInitInput struct {
type cInitOutput struct{}
func (c cInit) Index(ctx context.Context, in cInitInput) (out *cInitOutput, err error) {
var (
overwrote = false
)
if !gfile.IsEmpty(in.Name) && !allyes.Check() {
s := gcmd.Scanf(`the folder "%s" is not empty, files might be overwrote, continue? [y/n]: `, in.Name)
if strings.EqualFold(s, "n") {
return
}
overwrote = true
}
mlog.Print("initializing...")
// Create project folder and files.
var (
templateRepoName string
gitignoreFile = in.Name + "/" + cInitGitignore
)
if in.Mono {
templateRepoName = cInitMonoRepo
@@ -81,14 +92,35 @@ func (c cInit) Index(ctx context.Context, in cInitInput) (out *cInitOutput, err
return
}
// build ignoreFiles from the .gitignore file
ignoreFiles := make([]string, 0, 10)
ignoreFiles = append(ignoreFiles, cInitGitDir)
if overwrote {
err = gfile.ReadLines(gitignoreFile, func(line string) error {
// Add only hidden files or directories
// If other directories are added, it may cause the entire directory to be ignored
// such as 'main' in the .gitignore file, but the path is 'D:\main\my-project'
if line != "" && strings.HasPrefix(line, ".") {
ignoreFiles = append(ignoreFiles, line)
}
return nil
})
// if not found the .gitignore file will skip os.ErrNotExist error
if err != nil && !os.IsNotExist(err) {
return
}
}
// Replace template name to project name.
err = gfile.ReplaceDir(
cInitRepoPrefix+templateRepoName,
gfile.Basename(gfile.RealPath(in.Name)),
in.Name,
"*",
true,
)
err = gfile.ReplaceDirFunc(func(path, content string) string {
for _, ignoreFile := range ignoreFiles {
if strings.Contains(path, ignoreFile) {
return content
}
}
return gstr.Replace(gfile.GetContents(path), cInitRepoPrefix+templateRepoName, gfile.Basename(gfile.RealPath(in.Name)))
}, in.Name, "*", true)
if err != nil {
return
}

View File

@@ -33,7 +33,7 @@ like json/xml/yaml/toml/ini.
cTplParseEg = `
gf tpl parse -p ./template -v values.json -r
gf tpl parse -p ./template -v values.json -n *.tpl -r
gf tpl parse -p ./template -v values.json -d '${,}}' -r
gf tpl parse -p ./template -v values.json -d '${{,}}' -r
gf tpl parse -p ./template -v values.json -o ./template.parsed
`
cTplSupportValuesFilePattern = `*.json,*.xml,*.yaml,*.yml,*.toml,*.ini`
@@ -63,7 +63,7 @@ func init() {
}
func (c *cTpl) Parse(ctx context.Context, in cTplParseInput) (out *cTplParseOutput, err error) {
if in.Output == "" && in.Replace == false {
if in.Output == "" && !in.Replace {
return nil, gerror.New(`parameter output and replace should not be both empty`)
}
delimiters := gstr.SplitAndTrim(in.Delimiters, ",")

View File

@@ -50,7 +50,7 @@ func generateStructDefinition(ctx context.Context, in generateStructDefinitionIn
return buffer.String()
}
// generateStructFieldForModel generates and returns the attribute definition for specified field.
// generateStructFieldDefinition generates and returns the attribute definition for specified field.
func generateStructFieldDefinition(
ctx context.Context, field *gdb.TableField, in generateStructDefinitionInput,
) []string {

View File

@@ -0,0 +1,83 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package genenums
import (
"context"
"golang.org/x/tools/go/packages"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gtag"
"hotgo/internal/library/hggen/internal/consts"
"hotgo/internal/library/hggen/internal/utility/mlog"
)
type (
CGenEnums struct{}
CGenEnumsInput struct {
g.Meta `name:"enums" config:"{CGenEnumsConfig}" brief:"{CGenEnumsBrief}" eg:"{CGenEnumsEg}"`
Src string `name:"src" short:"s" dc:"source folder path to be parsed" d:"."`
Path string `name:"path" short:"p" dc:"output go file path storing enums content" d:"internal/boot/boot_enums.go"`
Prefix string `name:"prefix" short:"x" dc:"only exports packages that starts with specified prefix"`
}
CGenEnumsOutput struct{}
)
const (
CGenEnumsConfig = `gfcli.gen.enums`
CGenEnumsBrief = `parse go files in current project and generate enums go file`
CGenEnumsEg = `
gf gen enums
gf gen enums -p internal/boot/boot_enums.go
gf gen enums -p internal/boot/boot_enums.go -s .
gf gen enums -x github.com/gogf
`
)
func init() {
gtag.Sets(g.MapStrStr{
`CGenEnumsEg`: CGenEnumsEg,
`CGenEnumsBrief`: CGenEnumsBrief,
`CGenEnumsConfig`: CGenEnumsConfig,
})
}
func (c CGenEnums) Enums(ctx context.Context, in CGenEnumsInput) (out *CGenEnumsOutput, err error) {
realPath := gfile.RealPath(in.Src)
if realPath == "" {
mlog.Fatalf(`source folder path "%s" does not exist`, in.Src)
}
err = gfile.Chdir(realPath)
if err != nil {
mlog.Fatal(err)
}
mlog.Printf(`scanning: %s`, realPath)
cfg := &packages.Config{
Dir: realPath,
Mode: pkgLoadMode,
Tests: false,
}
pkgs, err := packages.Load(cfg)
if err != nil {
mlog.Fatal(err)
}
p := NewEnumsParser(in.Prefix)
p.ParsePackages(pkgs)
var enumsContent = gstr.ReplaceByMap(consts.TemplateGenEnums, g.MapStrStr{
"{PackageName}": gfile.Basename(gfile.Dir(in.Path)),
"{EnumsJson}": "`" + p.Export() + "`",
})
enumsContent = gstr.Trim(enumsContent)
if err = gfile.PutContents(in.Path, enumsContent); err != nil {
return
}
mlog.Printf(`generated: %s`, in.Path)
mlog.Print("done!")
return
}

View File

@@ -0,0 +1,140 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package genenums
import (
"go/constant"
"go/types"
"golang.org/x/tools/go/packages"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
const pkgLoadMode = 0xffffff
type EnumsParser struct {
enums []EnumItem
parsedPkg map[string]struct{}
prefix string
}
type EnumItem struct {
Name string
Value string
Kind constant.Kind // String/Int/Bool/Float/Complex/Unknown
Type string // Pkg.ID + TypeName
}
var standardPackages = make(map[string]struct{})
func init() {
stdPackages, err := packages.Load(nil, "std")
if err != nil {
panic(err)
}
for _, p := range stdPackages {
standardPackages[p.ID] = struct{}{}
}
}
func NewEnumsParser(prefix string) *EnumsParser {
return &EnumsParser{
enums: make([]EnumItem, 0),
parsedPkg: make(map[string]struct{}),
prefix: prefix,
}
}
func (p *EnumsParser) ParsePackages(pkgs []*packages.Package) {
for _, pkg := range pkgs {
p.ParsePackage(pkg)
}
}
func (p *EnumsParser) ParsePackage(pkg *packages.Package) {
// Ignore std packages.
if _, ok := standardPackages[pkg.ID]; ok {
return
}
// Ignore pared packages.
if _, ok := p.parsedPkg[pkg.ID]; ok {
return
}
p.parsedPkg[pkg.ID] = struct{}{}
// Only parse specified prefix.
if p.prefix != "" {
if !gstr.HasPrefix(pkg.ID, p.prefix) {
return
}
}
var (
scope = pkg.Types.Scope()
names = scope.Names()
)
for _, name := range names {
con, ok := scope.Lookup(name).(*types.Const)
if !ok {
// Only constants can be enums.
continue
}
if !con.Exported() {
// Ignore unexported values.
continue
}
var enumType = con.Type().String()
if !gstr.Contains(enumType, "/") {
// Ignore std types.
continue
}
var (
enumName = con.Name()
enumValue = con.Val().ExactString()
enumKind = con.Val().Kind()
)
if con.Val().Kind() == constant.String {
enumValue = constant.StringVal(con.Val())
}
p.enums = append(p.enums, EnumItem{
Name: enumName,
Value: enumValue,
Type: enumType,
Kind: enumKind,
})
}
for _, im := range pkg.Imports {
p.ParsePackage(im)
}
}
func (p *EnumsParser) Export() string {
var typeEnumMap = make(map[string][]interface{})
for _, enum := range p.enums {
if typeEnumMap[enum.Type] == nil {
typeEnumMap[enum.Type] = make([]interface{}, 0)
}
var value interface{}
switch enum.Kind {
case constant.Int:
value = gconv.Int64(enum.Value)
case constant.String:
value = enum.Value
case constant.Float:
value = gconv.Float64(enum.Value)
case constant.Bool:
value = gconv.Bool(enum.Value)
default:
value = enum.Value
}
typeEnumMap[enum.Type] = append(typeEnumMap[enum.Type], value)
}
return gjson.MustEncodeString(typeEnumMap)
}

View File

@@ -0,0 +1,23 @@
// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package consts
const TemplateGenEnums = `
// ================================================================================
// Code generated by GoFrame CLI tool. DO NOT EDIT.
// ================================================================================
package {PackageName}
import (
"github.com/gogf/gf/v2/util/gtag"
)
func init() {
gtag.SetGlobalEnums({EnumsJson})
}
`

View File

@@ -3,7 +3,6 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package views
import (
@@ -21,7 +20,7 @@ const (
LogicWhereNoSupport = "\t// TODO 暂不支持生成[ %s ]查询方式,请自行补充此处代码!"
LogicListSimpleSelect = "\tfields, err := hgorm.GenSelect(ctx, sysin.%sListModel{}, dao.%s)\n\tif err != nil {\n\t\treturn\n\t}"
LogicListJoinSelect = "\t//关联表select\n\tfields, err := hgorm.GenJoinSelect(ctx, %sin.%sListModel{}, dao.%s, []*hgorm.Join{\n%v\t})"
LogicListJoinOnRelation = "\t// 关联表%s\n\tmod = mod.%s(hgorm.GenJoinOnRelation(\n\t\tdao.%s.Table(), dao.%s.Columns().%s, // 主表表名,关联条件\n\t\tdao.%s.Table(), \"%s\", dao.%s.Columns().%s, // 关联表表名,别名,关联条件\n\t)...)\n\n"
LogicListJoinOnRelation = "\t// 关联表%s\n\tmod = mod.%s(hgorm.GenJoinOnRelation(\n\t\tdao.%s.Table(), dao.%s.Columns().%s, // 主表表名,关联字段\n\t\tdao.%s.Table(), \"%s\", dao.%s.Columns().%s, // 关联表表名,别名,关联字段\n\t)...)\n\n"
LogicEditUpdate = "\t\t_, err = s.Model(ctx).\n\t\t\tFieldsEx(\n%s\t\t\t).\n\t\t\tWhere(dao.%s.Columns().%s, in.%s).Data(in).Update()\n\t\treturn "
LogicEditInsert = "\t_, err = s.Model(ctx, &handler.Option{FilterAuth: false}).\n\t\tFieldsEx(\n%s\t\t).\n\t\tData(in).Insert()"
LogicSwitchUpdate = "g.Map{\n\t\tin.Key: in.Value,\n%s}"
@@ -136,10 +135,11 @@ func (l *gCurd) generateLogicListJoin(ctx context.Context, in *CurdPreviewInput)
selectBuffer.WriteString(fmt.Sprintf(LogicListJoinSelect, in.options.TemplateGroup, in.In.VarName, in.In.DaoName, joinSelectRows))
data["select"] = selectBuffer.String()
data["fields"] = "fields"
data["link"] = linkBuffer.String()
} else {
data["select"] = fmt.Sprintf(LogicListSimpleSelect, in.In.VarName, in.In.DaoName)
data["fields"] = fmt.Sprintf("%sin.%sListModel{}", in.options.TemplateGroup, in.In.VarName)
}
return data

View File

@@ -9,6 +9,7 @@ import (
"bytes"
"context"
"fmt"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"hotgo/internal/model/input/sysin"
"hotgo/utility/convert"
@@ -24,7 +25,9 @@ func (l *gCurd) webModelTplData(ctx context.Context, in *CurdPreviewInput) (data
data["defaultState"] = l.generateWebModelDefaultState(ctx, in)
data["rules"] = l.generateWebModelRules(ctx, in)
data["formSchema"] = l.generateWebModelFormSchema(ctx, in)
data["columns"] = l.generateWebModelColumns(ctx, in)
if data["columns"], err = l.generateWebModelColumns(ctx, in); err != nil {
return nil, err
}
return
}
@@ -214,12 +217,14 @@ func (l *gCurd) generateWebModelFormSchemaEach(buffer *bytes.Buffer, fields []*s
}
}
func (l *gCurd) generateWebModelColumns(ctx context.Context, in *CurdPreviewInput) string {
func (l *gCurd) generateWebModelColumns(ctx context.Context, in *CurdPreviewInput) (string, error) {
buffer := bytes.NewBuffer(nil)
buffer.WriteString("export const columns = [\n")
// 主表
l.generateWebModelColumnsEach(buffer, in, in.masterFields)
if err := l.generateWebModelColumnsEach(buffer, in, in.masterFields); err != nil {
return "", err
}
// 关联表
if len(in.options.Join) > 0 {
@@ -227,15 +232,17 @@ func (l *gCurd) generateWebModelColumns(ctx context.Context, in *CurdPreviewInpu
if !isEffectiveJoin(v) {
continue
}
l.generateWebModelColumnsEach(buffer, in, v.Columns)
if err := l.generateWebModelColumnsEach(buffer, in, v.Columns); err != nil {
return "", err
}
}
}
buffer.WriteString("];\n")
return buffer.String()
return buffer.String(), nil
}
func (l *gCurd) generateWebModelColumnsEach(buffer *bytes.Buffer, in *CurdPreviewInput, fields []*sysin.GenCodesColumnListModel) {
func (l *gCurd) generateWebModelColumnsEach(buffer *bytes.Buffer, in *CurdPreviewInput, fields []*sysin.GenCodesColumnListModel) (err error) {
for _, field := range fields {
if !field.IsList {
continue
@@ -251,9 +258,17 @@ func (l *gCurd) generateWebModelColumnsEach(buffer *bytes.Buffer, in *CurdPrevie
component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n render(row) {\n return formatToDate(row.%s);\n },\n },\n", field.Dc, field.TsName, field.TsName)
case FormModeSelect:
if g.IsEmpty(in.options.dictMap[field.TsName]) {
err = gerror.Newf("设置单选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name)
return
}
component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n render(row) {\n if (isNullObject(row.%s)) {\n return ``;\n }\n return h(\n NTag,\n {\n style: {\n marginRight: '6px',\n },\n type: getOptionTag(options.value.%s, row.%s),\n bordered: false,\n },\n {\n default: () => getOptionLabel(options.value.%s, row.%s),\n }\n );\n },\n },\n", field.Dc, field.TsName, field.TsName, in.options.dictMap[field.TsName], field.TsName, in.options.dictMap[field.TsName], field.TsName)
case FormModeSelectMultiple:
if g.IsEmpty(in.options.dictMap[field.TsName]) {
err = gerror.Newf("设置多选下拉框选项时,必须选择字典类型,字段名称:%v", field.Name)
return
}
component = fmt.Sprintf(" {\n title: '%s',\n key: '%s',\n render(row) {\n if (isNullObject(row.%s) || !isArray(row.%s)) {\n return ``;\n }\n return row.%s.map((tagKey) => {\n return h(\n NTag,\n {\n style: {\n marginRight: '6px',\n },\n type: getOptionTag(options.value.%s, tagKey),\n bordered: false,\n },\n {\n default: () => getOptionLabel(options.value.%s, tagKey),\n }\n );\n });\n },\n },\n", field.Dc, field.TsName, field.TsName, field.TsName, field.TsName, in.options.dictMap[field.TsName], in.options.dictMap[field.TsName])
case FormModeUploadImage:
@@ -280,4 +295,6 @@ func (l *gCurd) generateWebModelColumnsEach(buffer *bytes.Buffer, in *CurdPrevie
buffer.WriteString(component)
}
return
}

View File

@@ -3,7 +3,6 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package views
import (
@@ -45,7 +44,10 @@ func (l *gCurd) generateWebViewItem(ctx context.Context, in *CurdPreviewInput) s
case FormModeTime:
component = defaultComponent
case FormModeRadio, FormModeCheckbox, FormModeSelect, FormModeSelectMultiple:
case FormModeRadio, FormModeSelect:
component = fmt.Sprintf("<n-descriptions-item label=\"%s\">\n <n-tag\n :type=\"getOptionTag(options.%s, formValue?.%s)\"\n size=\"small\"\n class=\"min-left-space\"\n >{{ getOptionLabel(options.%s, formValue?.%s) }}</n-tag\n >\n </n-descriptions-item>", field.Dc, in.options.dictMap[field.TsName], field.TsName, in.options.dictMap[field.TsName], field.TsName)
case FormModeCheckbox, FormModeSelectMultiple:
component = fmt.Sprintf("<n-descriptions-item label=\"%s\">\n <template v-for=\"(item, key) in formValue?.%s\" :key=\"key\">\n <n-tag\n :type=\"getOptionTag(options.%s, item)\"\n size=\"small\"\n class=\"min-left-space\"\n >{{ getOptionLabel(options.%s, item) }}</n-tag\n >\n </template>\n </n-descriptions-item>", field.Dc, field.TsName, in.options.dictMap[field.TsName], in.options.dictMap[field.TsName])
case FormModeUploadImage:

View File

@@ -107,3 +107,7 @@ func GetAuthorization(r *ghttp.Request) string {
return gstr.Replace(authorization, "Bearer ", "")
}
func GenAuthKey(token string) string {
return gmd5.MustEncryptString(token)
}

View File

@@ -16,7 +16,7 @@ import (
"github.com/gogf/gf/v2/util/gconv"
"github.com/kayon/iploc"
"hotgo/utility/validate"
"io/ioutil"
"io"
"net"
"net/http"
"strings"
@@ -169,7 +169,7 @@ func GetPublicIP(ctx context.Context) (ip string, err error) {
var data *WhoisRegionData
err = g.Client().Timeout(10*time.Second).GetVar(ctx, whoisApi).Scan(&data)
if err != nil {
g.Log().Infof(ctx, "GetPublicIP alternatives are being tried err:%+v", err)
g.Log().Info(ctx, "GetPublicIP fail, alternatives are being tried.")
return GetPublicIP2()
}
@@ -187,7 +187,7 @@ func GetPublicIP2() (ip string, err error) {
}
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
body, err := io.ReadAll(response.Body)
if err != nil {
return
}
@@ -219,6 +219,9 @@ func GetLocalIP() (ip string, err error) {
// GetClientIp 获取客户端IP
func GetClientIp(r *ghttp.Request) string {
if r == nil {
return ""
}
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = r.GetClientIp()

View File

@@ -3,7 +3,6 @@ package tcp
import (
"context"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gtcp"
@@ -36,6 +35,7 @@ type Client struct {
IsLogin bool // 是否已登录
addr string
auth *AuthMeta
rpc *Rpc
timeout time.Duration
connectInterval time.Duration
maxConnectCount uint
@@ -103,6 +103,7 @@ func NewClient(config *ClientConfig) (client *Client, err error) {
client.timeout = config.Timeout
}
client.rpc = NewRpc(client.Ctx)
return
}
@@ -248,7 +249,31 @@ func (client *Client) read() {
client.Logger.Debugf(client.Ctx, "client RecvPkg invalid message: %+v", msg)
continue
}
f(msg.Data, client.conn)
switch msg.Router {
case "ResponseServerLogin", "ResponseServerHeartbeat": // 服务登录、心跳无需验证签名
ctx, cancel := initCtx(gctx.New(), &Context{})
doHandleRouterMsg(f, ctx, cancel, msg.Data)
default: // 通用路由消息处理
in, err := VerifySign(msg.Data, client.auth.AppId, client.auth.SecretKey)
if err != nil {
client.Logger.Warningf(client.Ctx, "client read VerifySign err:%+v message: %+v", err, msg)
continue
}
ctx, cancel := initCtx(gctx.New(), &Context{
Conn: client.conn,
Auth: client.auth,
TraceID: in.TraceID,
})
// 响应rpc消息
if client.rpc.HandleMsg(ctx, cancel, msg.Data) {
return
}
doHandleRouterMsg(f, ctx, cancel, msg.Data)
}
}
})
}
@@ -307,16 +332,45 @@ func (client *Client) Write(data interface{}) error {
return gerror.New("client Write message is nil")
}
// 签名
SetSign(data, gctx.CtxId(client.Ctx), client.auth.AppId, client.auth.SecretKey)
msgType := reflect.TypeOf(data)
if msgType == nil || msgType.Kind() != reflect.Ptr {
return gerror.Newf("client json message pointer required: %+v", data)
}
msg := &Message{Router: msgType.Elem().Name(), Data: data}
client.Logger.Debugf(client.Ctx, "client Write Router:%v, data:%+v", msg.Router, gjson.New(data).String())
return SendPkg(client.conn, msg)
}
// Send 发送消息
func (client *Client) Send(ctx context.Context, data interface{}) error {
MsgPkg(data, client.auth, gctx.CtxId(ctx))
return client.Write(data)
}
// Reply 回复消息
func (client *Client) Reply(ctx context.Context, data interface{}) (err error) {
user := GetCtx(ctx)
if user == nil {
err = gerror.New("获取回复用户信息失败")
return
}
MsgPkg(data, client.auth, user.TraceID)
return client.Write(data)
}
// RpcRequest 发送消息并等待响应结果
func (client *Client) RpcRequest(ctx context.Context, data interface{}) (res interface{}, err error) {
var (
traceID = MsgPkg(data, client.auth, gctx.CtxId(ctx))
key = client.rpc.GetCallId(client.conn, traceID)
)
if traceID == "" {
err = gerror.New("traceID is required")
return
}
return client.rpc.Request(key, func() {
client.Write(data)
})
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/gogf/gf/v2/os/gcron"
"github.com/gogf/gf/v2/os/gtime"
"hotgo/internal/consts"
)
func (client *Client) getCronKey(s string) string {
@@ -19,19 +20,19 @@ func (client *Client) stopCron() {
func (client *Client) startCron() {
// 心跳超时检查
if gcron.Search(client.getCronKey(cronHeartbeatVerify)) == nil {
if gcron.Search(client.getCronKey(consts.TCPCronHeartbeatVerify)) == nil {
gcron.AddSingleton(client.Ctx, "@every 600s", func(ctx context.Context) {
if client.heartbeat < gtime.Timestamp()-600 {
client.Logger.Debugf(client.Ctx, "client heartbeat timeout, about to reconnect..")
client.Logger.Debugf(client.Ctx, "client heartbeat timeout, about to reconnect..")
client.Destroy()
}
}, client.getCronKey(cronHeartbeatVerify))
}, client.getCronKey(consts.TCPCronHeartbeatVerify))
}
// 心跳
if gcron.Search(client.getCronKey(cronHeartbeat)) == nil {
if gcron.Search(client.getCronKey(consts.TCPCronHeartbeat)) == nil {
gcron.AddSingleton(client.Ctx, "@every 120s", func(ctx context.Context) {
client.serverHeartbeat()
}, client.getCronKey(cronHeartbeat))
}, client.getCronKey(consts.TCPCronHeartbeat))
}
}

View File

@@ -1,16 +1,19 @@
package tcp
import (
"github.com/gogf/gf/v2/errors/gcode"
"context"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"hotgo/internal/consts"
"hotgo/internal/model/input/msgin"
)
// serverLogin 心跳
func (client *Client) serverHeartbeat() {
if err := client.Write(&msgin.ServerHeartbeat{}); err != nil {
client.Logger.Debugf(client.Ctx, "client WriteMsg ServerHeartbeat err:%+v", err)
ctx := gctx.New()
if err := client.Send(ctx, &msgin.ServerHeartbeat{}); err != nil {
client.Logger.Debugf(ctx, "client WriteMsg ServerHeartbeat err:%+v", err)
return
}
}
@@ -22,40 +25,45 @@ func (client *Client) serverLogin() {
Name: client.auth.Name,
}
if err := client.Write(data); err != nil {
client.Logger.Debugf(client.Ctx, "client WriteMsg ServerLogin err:%+v", err)
ctx := gctx.New()
if err := client.Send(ctx, data); err != nil {
client.Logger.Debugf(ctx, "client WriteMsg ServerLogin err:%+v", err)
return
}
}
func (client *Client) onResponseServerLogin(ctx context.Context, args ...interface{}) {
var in *msgin.ResponseServerLogin
if err := gconv.Scan(args[0], &in); err != nil {
client.Logger.Infof(ctx, "onResponseServerLogin message Scan failed:%+v, args:%+v", err, args[0])
return
}
if in.Code != consts.TCPMsgCodeSuccess {
client.IsLogin = false
client.Logger.Warningf(ctx, "onResponseServerLogin quit err:%v", in.Message)
client.Destroy()
return
}
client.IsLogin = true
if client.loginEvent != nil {
client.loginEvent()
}
}
func (client *Client) onResponseServerLogin(args ...interface{}) {
var in *msgin.ResponseServerLogin
if err := gconv.Scan(args[0], &in); err != nil {
client.Logger.Infof(client.Ctx, "onResponseServerLogin message Scan failed:%+v, args:%+v", err, args[0])
return
}
client.Logger.Infof(client.Ctx, "onResponseServerLogin in:%+v", *in)
if in.Code != gcode.CodeOK.Code() {
client.IsLogin = false
client.Logger.Warningf(client.Ctx, "onResponseServerLogin quit err:%v", in.Message)
client.Destroy()
return
}
client.IsLogin = true
}
func (client *Client) onResponseServerHeartbeat(args ...interface{}) {
func (client *Client) onResponseServerHeartbeat(ctx context.Context, args ...interface{}) {
var in *msgin.ResponseServerHeartbeat
if err := gconv.Scan(args[0], &in); err != nil {
client.Logger.Infof(client.Ctx, "onResponseServerHeartbeat message Scan failed:%+v, args:%+v", err, args)
client.Logger.Infof(ctx, "onResponseServerHeartbeat message Scan failed:%+v, args:%+v", err, args)
return
}
if in.Code != consts.TCPMsgCodeSuccess {
client.Logger.Warningf(ctx, "onResponseServerHeartbeat err:%v", in.Message)
return
}
client.heartbeat = gtime.Timestamp()
client.Logger.Infof(client.Ctx, "onResponseServerHeartbeat in:%+v", *in)
}

View File

@@ -0,0 +1,57 @@
package tcp
import (
"context"
"github.com/gogf/gf/v2/net/gtcp"
"github.com/gogf/gf/v2/net/gtrace"
"hotgo/internal/consts"
)
// initCtx 初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改
func initCtx(ctx context.Context, model *Context) (newCtx context.Context, cancel context.CancelFunc) {
if model.TraceID != "" {
newCtx, _ = gtrace.WithTraceID(ctx, model.TraceID)
} else {
newCtx = ctx
}
newCtx = context.WithValue(newCtx, consts.ContextTCPKey, model)
newCtx, cancel = context.WithCancel(newCtx)
return
}
// SetCtx 设置上下文变量
func SetCtx(ctx context.Context, model *Context) {
context.WithValue(ctx, consts.ContextTCPKey, model)
}
// GetCtx 获得上下文变量如果没有设置那么返回nil
func GetCtx(ctx context.Context) *Context {
value := ctx.Value(consts.ContextTCPKey)
if value == nil {
return nil
}
if localCtx, ok := value.(*Context); ok {
return localCtx
}
return nil
}
// GetCtxConn .
func GetCtxConn(ctx context.Context) *gtcp.Conn {
c := GetCtx(ctx)
if c == nil {
return nil
}
return c.Conn
}
// GetCtxAuth 认证元数据
func GetCtxAuth(ctx context.Context) *AuthMeta {
c := GetCtx(ctx)
if c == nil {
return nil
}
return c.Auth
}

View File

@@ -1,19 +1,8 @@
package tcp
import "github.com/gogf/gf/v2/os/gtime"
// 定时任务
const (
cronHeartbeatVerify = "tcpHeartbeatVerify"
cronHeartbeat = "tcpHeartbeat"
cronAuthVerify = "tcpAuthVerify"
)
// 认证分组
const (
ClientGroupCron = "cron" // 定时任务
ClientGroupQueue = "queue" // 消息队列
ClientGroupAuth = "auth" // 服务授权
import (
"github.com/gogf/gf/v2/net/gtcp"
"github.com/gogf/gf/v2/os/gtime"
)
// AuthMeta 认证元数据
@@ -25,5 +14,11 @@ type AuthMeta struct {
EndAt *gtime.Time `json:"-"`
}
type Context struct {
Conn *gtcp.Conn `json:"conn"`
Auth *AuthMeta `json:"auth"` // 认证元数据
TraceID string `json:"traceID"` // 链路ID
}
// CallbackEvent 回调事件
type CallbackEvent func()

View File

@@ -0,0 +1,22 @@
package tcp
type Response interface {
PkgResponse()
GetError() (err error)
}
// PkgResponse 打包响应消息
func PkgResponse(data interface{}) {
if c, ok := data.(Response); ok {
c.PkgResponse()
return
}
}
// GetResponseError 解析响应消息中的错误
func GetResponseError(data interface{}) (err error) {
if c, ok := data.(Response); ok {
return c.GetError()
}
return
}

View File

@@ -1,13 +1,17 @@
package tcp
import (
"context"
"encoding/json"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/gtcp"
"github.com/gogf/gf/v2/os/grpool"
"github.com/gogf/gf/v2/util/gconv"
)
type RouterHandler func(args ...interface{})
var GoPool = grpool.New(100)
type RouterHandler func(ctx context.Context, args ...interface{})
// Message 路由消息
type Message struct {
@@ -37,3 +41,26 @@ func RecvPkg(conn *gtcp.Conn) (*Message, error) {
return msg, err
}
}
// MsgPkg 打包消息
func MsgPkg(data interface{}, auth *AuthMeta, traceID string) string {
// 打包签名
msg := PkgSign(data, auth.AppId, auth.SecretKey, traceID)
// 打包响应消息
PkgResponse(data)
if msg == nil {
return ""
}
return msg.TraceID
}
// doHandleRouterMsg 处理路由消息
func doHandleRouterMsg(fun RouterHandler, ctx context.Context, cancel context.CancelFunc, args ...interface{}) {
GoPool.Add(ctx, func(ctx context.Context) {
fun(ctx, args...)
cancel()
})
}

View File

@@ -0,0 +1,103 @@
package tcp
import (
"context"
"fmt"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/gtcp"
"hotgo/internal/consts"
"hotgo/utility/simple"
"sync"
"time"
)
type Rpc struct {
ctx context.Context
mutex sync.Mutex
callbacks map[string]RpcRespFunc
}
type RpcResp struct {
res interface{}
err error
}
type RpcRespFunc func(resp interface{}, err error)
func NewRpc(ctx context.Context) *Rpc {
return &Rpc{
ctx: ctx,
callbacks: make(map[string]RpcRespFunc),
}
}
// GetCallId 获取回调id
func (r *Rpc) GetCallId(client *gtcp.Conn, traceID string) string {
return fmt.Sprintf("%v.%v", client.LocalAddr().String(), traceID)
}
// HandleMsg 处理rpc消息
func (r *Rpc) HandleMsg(ctx context.Context, cancel context.CancelFunc, data interface{}) bool {
user := GetCtx(ctx)
callId := r.GetCallId(user.Conn, user.TraceID)
if call, ok := r.callbacks[callId]; ok {
r.mutex.Lock()
delete(r.callbacks, callId)
r.mutex.Unlock()
simple.SafeGo(ctx, func(ctx context.Context) {
call(data, nil)
cancel()
})
return true
}
return false
}
// Request 发起rpc请求
func (r *Rpc) Request(callId string, send func()) (res interface{}, err error) {
var (
waitCh = make(chan struct{})
resCh = make(chan RpcResp, 1)
isClose = false
)
defer func() {
isClose = true
close(resCh)
// 移除消息
if _, ok := r.callbacks[callId]; ok {
r.mutex.Lock()
delete(r.callbacks, callId)
r.mutex.Unlock()
}
}()
simple.SafeGo(r.ctx, func(ctx context.Context) {
close(waitCh)
// 加入回调
r.mutex.Lock()
r.callbacks[callId] = func(res interface{}, err error) {
if !isClose {
resCh <- RpcResp{res: res, err: err}
}
}
r.mutex.Unlock()
// 发送消息
send()
})
<-waitCh
select {
case <-time.After(consts.TCPRpcTimeout):
err = gerror.New("rpc response timeout")
return
case got := <-resCh:
return got.res, got.err
}
}

View File

@@ -3,12 +3,12 @@ package tcp
import (
"context"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gtcp"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/glog"
"hotgo/internal/consts"
"reflect"
"sync"
"time"
@@ -31,6 +31,7 @@ type Server struct {
Logger *glog.Logger
addr string
name string
rpc *Rpc
ln *gtcp.Server
wgLn sync.WaitGroup
mutex sync.Mutex
@@ -72,6 +73,7 @@ func NewServer(config *ServerConfig) (server *Server, err error) {
return
}
server.Logger = logger
server.rpc = NewRpc(server.Ctx)
server.startCron()
@@ -100,13 +102,19 @@ func (server *Server) accept(conn *gtcp.Conn) {
switch msg.Router {
case "ServerLogin": // 服务登录
server.onServerLogin(msg.Data, conn)
// 初始化上下文
ctx, cancel := initCtx(gctx.New(), &Context{
Conn: conn,
})
doHandleRouterMsg(server.onServerLogin, ctx, cancel, msg.Data)
case "ServerHeartbeat": // 心跳
if client == nil {
server.Logger.Infof(server.Ctx, "conn not connected, ignore the heartbeat, msg:%+v", msg)
continue
}
server.onServerHeartbeat(msg, client)
// 初始化上下文
ctx, cancel := initCtx(gctx.New(), &Context{})
doHandleRouterMsg(server.onServerHeartbeat, ctx, cancel, msg.Data, client)
default: // 通用路由消息处理
if client == nil {
server.Logger.Warningf(server.Ctx, "conn is not logged in but sends a routing message. actively conn disconnect, msg:%+v", msg)
@@ -121,14 +129,25 @@ func (server *Server) accept(conn *gtcp.Conn) {
// handleRouterMsg 处理路由消息
func (server *Server) handleRouterMsg(msg *Message, client *ClientConn) {
// 验证签名
err := VerifySign(msg.Data, client.Auth.AppId, client.Auth.SecretKey)
in, err := VerifySign(msg.Data, client.Auth.AppId, client.Auth.SecretKey)
if err != nil {
server.Logger.Warningf(server.Ctx, "handleRouterMsg VerifySign err:%+v message: %+v", err, msg)
return
}
// 初始化上下文
ctx, cancel := initCtx(gctx.New(), &Context{
Conn: client.Conn,
Auth: client.Auth,
TraceID: in.TraceID,
})
// 响应rpc消息
if server.rpc.HandleMsg(ctx, cancel, msg.Data) {
return
}
handle := func(routers map[string]RouterHandler, group string) {
if routers == nil {
server.Logger.Debugf(server.Ctx, "handleRouterMsg route is not initialized %v message: %+v", group, msg)
@@ -139,15 +158,16 @@ func (server *Server) handleRouterMsg(msg *Message, client *ClientConn) {
server.Logger.Debugf(server.Ctx, "handleRouterMsg invalid %v message: %+v", group, msg)
return
}
f(msg.Data, client)
doHandleRouterMsg(f, ctx, cancel, msg.Data)
}
switch client.Auth.Group {
case ClientGroupCron:
case consts.TCPClientGroupCron:
handle(server.cronRouters, client.Auth.Group)
case ClientGroupQueue:
case consts.TCPClientGroupQueue:
handle(server.queueRouters, client.Auth.Group)
case ClientGroupAuth:
case consts.TCPClientGroupAuth:
handle(server.authRouters, client.Auth.Group)
default:
server.Logger.Warningf(server.Ctx, "group is not registered: %+v", client.Auth.Group)
@@ -173,6 +193,16 @@ func (server *Server) getAppIdClients(appid string) (list []*ClientConn) {
return
}
// GetGroupClients 获取指定分组的所有连接
func (server *Server) GetGroupClients(group string) (list []*ClientConn) {
for _, v := range server.clients {
if v.Auth.Group == group {
list = append(list, v)
}
}
return
}
// RegisterAuthRouter 注册授权路由
func (server *Server) RegisterAuthRouter(routers map[string]RouterHandler) {
server.mutex.Lock()
@@ -272,7 +302,39 @@ func (server *Server) Write(conn *gtcp.Conn, data interface{}) (err error) {
msg := &Message{Router: msgType.Elem().Name(), Data: data}
server.Logger.Debugf(server.Ctx, "server Write Router:%v, data:%+v", msg.Router, gjson.New(data).String())
return SendPkg(conn, msg)
}
// Send 发送消息
func (server *Server) Send(ctx context.Context, client *ClientConn, data interface{}) (err error) {
MsgPkg(data, client.Auth, gctx.CtxId(ctx))
return server.Write(client.Conn, data)
}
// Reply 回复消息
func (server *Server) Reply(ctx context.Context, data interface{}) (err error) {
user := GetCtx(ctx)
if user == nil {
err = gerror.New("获取回复用户信息失败")
return
}
MsgPkg(data, user.Auth, user.TraceID)
return server.Write(user.Conn, data)
}
// RpcRequest 向指定客户端发送消息并等待响应结果
func (server *Server) RpcRequest(ctx context.Context, client *ClientConn, data interface{}) (res interface{}, err error) {
var (
traceID = MsgPkg(data, client.Auth, gctx.CtxId(ctx))
key = server.rpc.GetCallId(client.Conn, traceID)
)
if traceID == "" {
err = gerror.New("traceID is required")
return
}
return server.rpc.Request(key, func() {
server.Write(client.Conn, data)
})
}

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/gogf/gf/v2/os/gcron"
"github.com/gogf/gf/v2/os/gtime"
"hotgo/internal/consts"
)
func (server *Server) getCronKey(s string) string {
@@ -19,7 +20,7 @@ func (server *Server) stopCron() {
func (server *Server) startCron() {
// 心跳超时检查
if gcron.Search(server.getCronKey(cronHeartbeatVerify)) == nil {
if gcron.Search(server.getCronKey(consts.TCPCronHeartbeatVerify)) == nil {
gcron.AddSingleton(server.Ctx, "@every 300s", func(ctx context.Context) {
if server.clients == nil {
return
@@ -30,11 +31,11 @@ func (server *Server) startCron() {
server.Logger.Debugf(server.Ctx, "client heartbeat timeout, close conn. auth:%+v", client.Auth)
}
}
}, server.getCronKey(cronHeartbeatVerify))
}, server.getCronKey(consts.TCPCronHeartbeatVerify))
}
// 认证检查
if gcron.Search(server.getCronKey(cronAuthVerify)) == nil {
if gcron.Search(server.getCronKey(consts.TCPCronAuthVerify)) == nil {
gcron.AddSingleton(server.Ctx, "@every 300s", func(ctx context.Context) {
if server.clients == nil {
return
@@ -45,6 +46,6 @@ func (server *Server) startCron() {
server.Logger.Debugf(server.Ctx, "client auth expired, close conn. auth:%+v", client.Auth)
}
}
}, server.getCronKey(cronAuthVerify))
}, server.getCronKey(consts.TCPCronAuthVerify))
}
}

View File

@@ -1,8 +1,8 @@
package tcp
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gtcp"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
@@ -12,75 +12,73 @@ import (
"hotgo/utility/convert"
)
func (server *Server) onServerLogin(args ...interface{}) {
func (server *Server) onServerLogin(ctx context.Context, args ...interface{}) {
var (
in = new(msgin.ServerLogin)
conn = args[1].(*gtcp.Conn)
user = GetCtx(ctx)
res = new(msgin.ResponseServerLogin)
models *entity.SysServeLicense
)
if err := gconv.Scan(args[0], &in); err != nil {
server.Logger.Infof(server.Ctx, "onServerLogin message Scan failed:%+v, args:%+v", err, args)
server.Logger.Warningf(ctx, "onServerLogin message Scan failed:%+v, args:%+v", err, args)
return
}
server.Logger.Infof(server.Ctx, "onServerLogin in:%+v", *in)
err := g.Model("sys_serve_license").
Ctx(server.Ctx).
err := g.Model("sys_serve_license").Ctx(ctx).
Where("appid = ?", in.AppId).
Scan(&models)
if err != nil {
res.Code = 1
res.Message = err.Error()
server.Write(conn, res)
server.Write(user.Conn, res)
return
}
if models == nil {
res.Code = 2
res.Message = "授权信息不存在"
server.Write(conn, res)
server.Write(user.Conn, res)
return
}
// 验证签名
if err = VerifySign(in, models.Appid, models.SecretKey); err != nil {
if _, err = VerifySign(in, models.Appid, models.SecretKey); err != nil {
res.Code = 3
res.Message = "签名错误,请联系管理员"
server.Write(conn, res)
server.Write(user.Conn, res)
return
}
if models.Status != consts.StatusEnabled {
res.Code = 4
res.Message = "授权已禁用,请联系管理员"
server.Write(conn, res)
server.Write(user.Conn, res)
return
}
if models.Group != in.Group {
res.Code = 5
res.Message = "你登录的授权分组未得到授权,请联系管理员"
server.Write(conn, res)
server.Write(user.Conn, res)
return
}
if models.EndAt.Before(gtime.Now()) {
res.Code = 6
res.Message = "授权已过期,请联系管理员"
server.Write(conn, res)
server.Write(user.Conn, res)
return
}
allowedIps := convert.IpFilterStrategy(models.AllowedIps)
if _, ok := allowedIps["*"]; !ok {
ip := gstr.StrTillEx(conn.RemoteAddr().String(), ":")
ip := gstr.StrTillEx(user.Conn.RemoteAddr().String(), ":")
if _, ok2 := allowedIps[ip]; !ok2 {
res.Code = 7
res.Message = "IP(" + ip + ")未授权,请联系管理员"
server.Write(conn, res)
server.Write(user.Conn, res)
return
}
}
@@ -99,14 +97,14 @@ func (server *Server) onServerLogin(args ...interface{}) {
}
// 当前连接也踢掉
server.Write(conn, res2)
conn.Close()
server.Write(user.Conn, res2)
user.Conn.Close()
return
}
server.mutexConns.Lock()
server.clients[conn.RemoteAddr().String()] = &ClientConn{
Conn: conn,
server.clients[user.Conn.RemoteAddr().String()] = &ClientConn{
Conn: user.Conn,
Auth: &AuthMeta{
Group: in.Group,
Name: in.Name,
@@ -118,39 +116,46 @@ func (server *Server) onServerLogin(args ...interface{}) {
}
server.mutexConns.Unlock()
server.Write(conn, res)
_, err = g.Model("sys_serve_license").
Ctx(server.Ctx).
_, err = g.Model("sys_serve_license").Ctx(ctx).
Where("id = ?", models.Id).Data(g.Map{
"online": online,
"login_times": models.LoginTimes + 1,
"last_login_at": gtime.Now(),
"last_active_at": gtime.Now(),
"remote_addr": conn.RemoteAddr().String(),
"remote_addr": user.Conn.RemoteAddr().String(),
}).Update()
if err != nil {
server.Logger.Warningf(server.Ctx, "onServerLogin Update err:%+v", err)
server.Logger.Warningf(ctx, "onServerLogin Update err:%+v", err)
}
res.AppId = in.AppId
res.Code = consts.TCPMsgCodeSuccess
server.Write(user.Conn, res)
}
func (server *Server) onServerHeartbeat(args ...interface{}) {
var in *msgin.ServerHeartbeat
if err := gconv.Scan(args, &in); err != nil {
server.Logger.Infof(server.Ctx, "onServerHeartbeat message Scan failed:%+v, args:%+v", err, args)
func (server *Server) onServerHeartbeat(ctx context.Context, args ...interface{}) {
var (
in *msgin.ServerHeartbeat
res = new(msgin.ResponseServerHeartbeat)
)
if err := gconv.Scan(args[0], &in); err != nil {
server.Logger.Warningf(ctx, "onServerHeartbeat message Scan failed:%+v, args:%+v", err, args)
return
}
client := args[1].(*ClientConn)
client.heartbeat = gtime.Timestamp()
server.Write(client.Conn, &msgin.ResponseServerHeartbeat{})
_, err := g.Model("sys_serve_license").
Ctx(server.Ctx).
_, err := g.Model("sys_serve_license").Ctx(ctx).
Where("appid = ?", client.Auth.AppId).Data(g.Map{
"last_active_at": gtime.Now(),
}).Update()
if err != nil {
server.Logger.Warningf(server.Ctx, "onServerHeartbeat Update err:%+v", err)
server.Logger.Warningf(ctx, "onServerHeartbeat Update err:%+v", err)
}
res.Code = consts.TCPMsgCodeSuccess
server.Write(client.Conn, res)
}

View File

@@ -7,35 +7,38 @@ import (
)
type Sign interface {
SetSign(traceID, appId, secretKey string)
SetSign(appId, secretKey string) *msgin.RpcMsg
SetTraceID(traceID string)
}
// SetSign 设置签名
func SetSign(data interface{}, traceID, appId, secretKey string) {
// PkgSign 打包签名
func PkgSign(data interface{}, appId, secretKey, traceID string) *msgin.RpcMsg {
if c, ok := data.(Sign); ok {
c.SetSign(traceID, appId, secretKey)
return
c.SetTraceID(traceID)
return c.SetSign(appId, secretKey)
}
return nil
}
// VerifySign 验证签名
func VerifySign(data interface{}, appId, secretKey string) (err error) {
func VerifySign(data interface{}, appId, secretKey string) (in *msgin.RpcMsg, err error) {
// 无密钥,无需签名
if secretKey == "" {
return
}
var in *msgin.Request
if err = gconv.Scan(data, &in); err != nil {
return
}
if appId != in.AppId {
return gerror.New("appId invalid")
err = gerror.New("appId invalid")
return
}
if in.Sign != in.GetSign(secretKey) {
return gerror.New("sign invalid")
err = gerror.New("sign invalid")
return
}
return
}

View File

@@ -3,10 +3,11 @@ package feishu
import (
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"strconv"
"time"
"github.com/go-resty/resty/v2"
"hotgo/internal/library/notify/feishu/internal/security"
)
@@ -59,23 +60,18 @@ func (d *Client) Send(message Message) (string, *Response, error) {
if err != nil {
return "", res, err
}
reqString := string(reqBytes)
client := resty.New()
URL := fmt.Sprintf("%v%v", feishuAPI, d.AccessToken)
resp, err := client.SetRetryCount(3).R().
SetBody(body).
var (
result *Response
URL = fmt.Sprintf("%v%v", feishuAPI, d.AccessToken)
reqString = string(reqBytes)
)
g.Client().
Retry(3, time.Second).
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetResult(&Response{}).
ForceContentType("application/json").
Post(URL)
if err != nil {
return reqString, nil, err
}
result := resp.Result().(*Response)
PostVar(gctx.New(), URL, &result)
if result.Code != 0 {
return reqString, result, fmt.Errorf("send message to feishu error = %s", result.Msg)
}

View File

@@ -0,0 +1,197 @@
package alipay
import (
"context"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/util/gconv"
"hotgo/internal/consts"
"hotgo/internal/model"
"hotgo/internal/model/input/payin"
)
func New(config *model.PayConfig) *aliPay {
return &aliPay{
config: config,
}
}
type aliPay struct {
config *model.PayConfig
}
// Refund 订单退款
func (h *aliPay) Refund(ctx context.Context, in payin.RefundInp) (res *payin.RefundModel, err error) {
client, err := GetClient(h.config)
if err != nil {
return
}
bm := make(gopay.BodyMap)
bm.Set("out_trade_no", in.Pay.OutTradeNo).
Set("refund_amount", in.RefundMoney).
Set("out_request_no", in.RefundSn).
Set("refund_reason", in.Remark)
refund, err := client.TradeRefund(ctx, bm)
if err != nil {
return
}
if refund.Response.FundChange != "Y" {
err = gerror.New("支付宝本次退款未发生资金变化!")
return
}
return
}
// Notify 异步通知
func (h *aliPay) Notify(ctx context.Context, in payin.NotifyInp) (res *payin.NotifyModel, err error) {
notifyReq, err := alipay.ParseNotifyToBodyMap(ghttp.RequestFromCtx(ctx).Request)
if err != nil {
return
}
// 支付宝异步通知验签(公钥证书模式)
ok, err := alipay.VerifySignWithCert(h.config.AliPayCertPublicKeyRSA2, notifyReq)
if err != nil {
return
}
if !ok {
err = gerror.New("支付宝验签不通过!")
return
}
var notify *NotifyRequest
if err = gconv.Scan(notifyReq, &notify); err != nil {
return
}
if notify == nil {
err = gerror.New("解析订单参数失败!")
return
}
if notify.TradeStatus != "TRADE_SUCCESS" {
err = gerror.New("非交易支付成功状态,无需处理!")
// 这里如果相对非交易支付成功状态进行处理,可自行调整此处逻辑
// ...
return
}
if notify.OutTradeNo == "" {
err = gerror.New("订单中没有找到商户单号!")
return
}
res = new(payin.NotifyModel)
res.TransactionId = notify.TradeNo
res.OutTradeNo = notify.OutTradeNo
res.PayAt = notify.GmtPayment
res.ActualAmount = gconv.Float64(notify.ReceiptAmount)
return
}
// CreateOrder 创建订单
func (h *aliPay) CreateOrder(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
client, err := GetClient(h.config)
if err != nil {
return nil, err
}
// 设置回调地址
client.SetReturnUrl(in.Pay.ReturnUrl).SetNotifyUrl(in.Pay.NotifyUrl)
switch in.Pay.TradeType {
case consts.TradeTypeAliScan, consts.TradeTypeAliWeb:
return h.scan(ctx, in)
case consts.TradeTypeAliWap:
return h.wap(ctx, in)
default:
err = gerror.Newf("暂未支持的交易方式:%v", in.Pay.TradeType)
}
return
}
func GetClient(config *model.PayConfig) (client *alipay.Client, err error) {
client, err = alipay.NewClient(config.AliPayAppId, gfile.GetContents(config.AliPayPrivateKey), true)
if err != nil {
err = gerror.Newf("创建支付宝客户端失败:%+v", err.Error())
return
}
// 打开Debug开关输出日志默认关闭
if config.Debug {
client.DebugSwitch = gopay.DebugOn
}
client.SetLocation(alipay.LocationShanghai) // 设置时区,不设置或出错均为默认服务器时间
// 证书路径
err = client.SetCertSnByPath(config.AliPayAppCertPublicKey, config.AliPayRootCert, config.AliPayCertPublicKeyRSA2)
return
}
// scan 扫码支付
func (h *aliPay) scan(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
client, err := GetClient(h.config)
if err != nil {
return nil, err
}
// 设置回调地址
client.SetReturnUrl(in.Pay.ReturnUrl).SetNotifyUrl(in.Pay.NotifyUrl)
bm := make(gopay.BodyMap)
bm.Set("subject", in.Pay.Subject).
Set("out_trade_no", in.Pay.OutTradeNo).
Set("total_amount", in.Pay.PayAmount)
payUrl, err := client.TradePagePay(ctx, bm)
if err != nil {
if bizErr, ok := alipay.IsBizError(err); ok {
return nil, bizErr
}
return nil, err
}
res = new(payin.CreateOrderModel)
res.TradeType = in.Pay.TradeType
res.PayURL = payUrl
res.OutTradeNo = in.Pay.OutTradeNo
return
}
func (h *aliPay) wap(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
client, err := GetClient(h.config)
if err != nil {
return nil, err
}
// 设置回调地址
client.SetReturnUrl(in.Pay.ReturnUrl).SetNotifyUrl(in.Pay.NotifyUrl)
bm := make(gopay.BodyMap)
bm.Set("subject", in.Pay.Subject).
Set("out_trade_no", in.Pay.OutTradeNo).
Set("total_amount", in.Pay.PayAmount).
Set("product_code", "QUICK_WAP_WAY")
// 手机网站支付请求
payUrl, err := client.TradeWapPay(ctx, bm)
if err != nil {
return
}
res = new(payin.CreateOrderModel)
res.TradeType = in.Pay.TradeType
res.PayURL = payUrl
res.OutTradeNo = in.Pay.OutTradeNo
return
}

View File

@@ -0,0 +1,61 @@
package alipay
import "github.com/gogf/gf/v2/os/gtime"
// NotifyRequest 支付宝异步通知参数
// 文档https://opendocs.alipay.com/open/203/105286
type NotifyRequest struct {
NotifyTime string `json:"notify_time,omitempty"`
NotifyType string `json:"notify_type,omitempty"`
NotifyId string `json:"notify_id,omitempty"`
AppId string `json:"app_id,omitempty"`
Charset string `json:"charset,omitempty"`
Version string `json:"version,omitempty"`
SignType string `json:"sign_type,omitempty"`
Sign string `json:"sign,omitempty"`
AuthAppId string `json:"auth_app_id,omitempty"`
TradeNo string `json:"trade_no,omitempty"`
OutTradeNo string `json:"out_trade_no,omitempty"`
OutBizNo string `json:"out_biz_no,omitempty"`
BuyerId string `json:"buyer_id,omitempty"`
BuyerLogonId string `json:"buyer_logon_id,omitempty"`
SellerId string `json:"seller_id,omitempty"`
SellerEmail string `json:"seller_email,omitempty"`
TradeStatus string `json:"trade_status,omitempty"`
TotalAmount string `json:"total_amount,omitempty"`
ReceiptAmount string `json:"receipt_amount,omitempty"`
InvoiceAmount string `json:"invoice_amount,omitempty"`
BuyerPayAmount string `json:"buyer_pay_amount,omitempty"`
PointAmount string `json:"point_amount,omitempty"`
RefundFee string `json:"refund_fee,omitempty"`
Subject string `json:"subject,omitempty"`
Body string `json:"body,omitempty"`
GmtCreate string `json:"gmt_create,omitempty"`
GmtPayment *gtime.Time `json:"gmt_payment,omitempty"`
GmtRefund string `json:"gmt_refund,omitempty"`
GmtClose string `json:"gmt_close,omitempty"`
FundBillList []*FundBillListInfo `json:"fund_bill_list,omitempty"`
PassbackParams string `json:"passback_params,omitempty"`
VoucherDetailList []*VoucherDetail `json:"voucher_detail_list,omitempty"`
Method string `json:"method,omitempty"` // 电脑网站支付 支付宝请求 return_url 同步返回参数
Timestamp string `json:"timestamp,omitempty"` // 电脑网站支付 支付宝请求 return_url 同步返回参数
}
type FundBillListInfo struct {
Amount string `json:"amount,omitempty"`
FundChannel string `json:"fundChannel,omitempty"` // 异步通知里是 fundChannel
}
type VoucherDetail struct {
Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
Amount string `json:"amount,omitempty"`
MerchantContribute string `json:"merchant_contribute,omitempty"`
OtherContribute string `json:"other_contribute,omitempty"`
Memo string `json:"memo,omitempty"`
TemplateId string `json:"template_id,omitempty"`
PurchaseBuyerContribute string `json:"purchase_buyer_contribute,omitempty"`
PurchaseMerchantContribute string `json:"purchase_merchant_contribute,omitempty"`
PurchaseAntContribute string `json:"purchase_ant_contribute,omitempty"`
}

View File

@@ -0,0 +1,13 @@
package payment
import "hotgo/internal/model"
var config *model.PayConfig
func SetConfig(c *model.PayConfig) {
config = c
}
func GetConfig() *model.PayConfig {
return config
}

View File

@@ -0,0 +1,44 @@
package payment
import (
"context"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"hotgo/internal/library/contexts"
"hotgo/internal/model/input/payin"
"hotgo/utility/simple"
"sync"
)
// 异步回调
type NotifyCallFunc func(ctx context.Context, pay payin.NotifyCallFuncInp) (err error)
var (
notifyCall = make(map[string]NotifyCallFunc)
ncLock sync.Mutex
)
// RegisterNotifyCall 注册支付成功回调方法
func RegisterNotifyCall(group string, f NotifyCallFunc) {
ncLock.Lock()
defer ncLock.Unlock()
if _, ok := notifyCall[group]; ok {
panic("notifyCall repeat registration, group:" + group)
}
notifyCall[group] = f
}
// NotifyCall 执行订单分组的异步回调
func NotifyCall(ctx context.Context, in payin.NotifyCallFuncInp) {
f, ok := notifyCall[in.Pay.OrderGroup]
if ok {
ctx = contexts.Detach(ctx)
simple.SafeGo(ctx, func(ctx context.Context) {
if err := f(ctx, in); err != nil {
g.Log().Warningf(ctx, "payment.NotifyCall in:%+v exec err:%+v", gjson.New(in.Pay).String(), err)
}
})
}
return
}

View File

@@ -0,0 +1,121 @@
package payment
import (
"context"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/grand"
"hotgo/internal/consts"
"hotgo/internal/library/payment/alipay"
"hotgo/internal/library/payment/qqpay"
"hotgo/internal/library/payment/wxpay"
"hotgo/internal/model/input/payin"
"hotgo/utility/validate"
)
// PayClient 支付客户端
type PayClient interface {
// CreateOrder 创建订单
CreateOrder(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error)
// Notify 异步通知
Notify(ctx context.Context, in payin.NotifyInp) (res *payin.NotifyModel, err error)
// Refund 订单退款
Refund(ctx context.Context, in payin.RefundInp) (res *payin.RefundModel, err error)
}
func New(name ...string) PayClient {
var (
payType = consts.PayTypeWxPay
client PayClient
)
if len(name) > 0 && name[0] != "" {
payType = name[0]
}
switch payType {
case consts.PayTypeAliPay:
client = alipay.New(config)
case consts.PayTypeWxPay:
client = wxpay.New(config)
case consts.PayTypeQQPay:
client = qqpay.New(config)
default:
panic(fmt.Sprintf("暂不支持的支付方式:%v", payType))
}
return client
}
// GenOrderSn 生成业务订单号
func GenOrderSn() string {
orderSn := fmt.Sprintf("HG@%v%v", gtime.Now().Format("YmdHis"), grand.S(4))
count, err := g.Model("pay_log").Where("order_sn", orderSn).Count()
if err != nil {
panic(fmt.Sprintf("payment.GenOrderSn err:%+v", err))
}
if count > 0 {
return GenOrderSn()
}
return orderSn
}
// GenOutTradeNo 生成商户订单号
func GenOutTradeNo() string {
outTradeNo := fmt.Sprintf("%v%v", gtime.Now().Format("YmdHis"), grand.N(10000000, 99999999))
count, err := g.Model("pay_log").Where("out_trade_no", outTradeNo).Count()
if err != nil {
panic(fmt.Sprintf("payment.GenOutTradeNo err:%+v", err))
}
if count > 0 {
return GenOutTradeNo()
}
return outTradeNo
}
// GenRefundSn 生成退款订单号
func GenRefundSn() string {
outTradeNo := fmt.Sprintf("%v%v", gtime.Now().Format("YmdHis"), grand.N(10000, 99999))
count, err := g.Model("pay_refund").Where("refund_trade_no", outTradeNo).Count()
if err != nil {
panic(fmt.Sprintf("payment.GenRefundSn err:%+v", err))
}
if count > 0 {
return GenRefundSn()
}
return outTradeNo
}
// AutoTradeType 根据userAgent自动识别交易方式在实际支付场景中你可以手动调整识别规则
func AutoTradeType(payType, userAgent string) (tradeType string) {
isMobile := validate.IsMobileVisit(userAgent)
switch payType {
case consts.PayTypeAliPay:
if isMobile {
return consts.TradeTypeAliWap
}
return consts.TradeTypeAliWeb
case consts.PayTypeWxPay:
if isMobile {
if validate.IsWxBrowserVisit(userAgent) {
return consts.TradeTypeWxMP
}
if validate.IsWxMiniProgramVisit(userAgent) {
return consts.TradeTypeWxMini
}
return consts.TradeTypeWxH5
}
return consts.TradeTypeWxScan
case consts.PayTypeQQPay:
if isMobile {
return consts.TradeTypeQQWap
}
return consts.TradeTypeQQWeb
default:
}
return
}

View File

@@ -0,0 +1,134 @@
package qqpay
import (
"context"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/qq"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gogf/gf/v2/util/grand"
"hotgo/internal/consts"
"hotgo/internal/model"
"hotgo/internal/model/input/payin"
)
func New(config *model.PayConfig) *qqPay {
return &qqPay{
config: config,
}
}
type qqPay struct {
config *model.PayConfig
}
// Refund 订单退款
func (h *qqPay) Refund(ctx context.Context, in payin.RefundInp) (res *payin.RefundModel, err error) {
err = gerror.New("暂不支持QQ支付申请退款如有疑问请联系管理员")
return
}
// Notify 异步通知
func (h *qqPay) Notify(ctx context.Context, in payin.NotifyInp) (res *payin.NotifyModel, err error) {
notifyReq, err := qq.ParseNotifyToBodyMap(ghttp.RequestFromCtx(ctx).Request)
if err != nil {
return
}
// 验签操作
ok, err := qq.VerifySign(h.config.QQPayApiKey, qq.SignType_MD5, notifyReq)
if err != nil {
return
}
if !ok {
err = gerror.New("QQ支付验签不通过")
return
}
var notify *NotifyRequest
if err = gconv.Scan(notifyReq, &notify); err != nil {
return
}
if notify == nil {
err = gerror.New("解析订单参数失败!")
return
}
if notify.TradeState != "SUCCESS" {
err = gerror.New("非交易支付成功状态,无需处理!")
// 这里如果相对非交易支付成功状态进行处理,可自行调整此处逻辑
// ...
return
}
if notify.OutTradeNo == "" {
err = gerror.New("订单中没有找到商户单号!")
return
}
res = new(payin.NotifyModel)
res.TransactionId = notify.TransactionId
res.OutTradeNo = notify.OutTradeNo
res.PayAt = gtime.New(notify.TimeEnd)
res.ActualAmount = gconv.Float64(notify.CouponFee) / 100 // 用户本次交易中,实际支付的金额 转为元,和系统内保持一至
return
}
// CreateOrder 创建订单
func (h *qqPay) CreateOrder(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
client := GetClient(h.config)
switch in.Pay.TradeType {
case consts.TradeTypeQQWeb, consts.TradeTypeQQWap:
bm := make(gopay.BodyMap)
bm.
Set("mch_id", h.config.QQPayMchId).
Set("body", in.Pay.Subject).
Set("out_trade_no", in.Pay.OutTradeNo).
Set("notify_url", in.Pay.NotifyUrl).
Set("nonce_str", grand.Letters(32)).
Set("spbill_create_ip", in.Pay.CreateIp).
Set("trade_type", "NATIVE"). // MICROPAY、APP、JSAPI、NATIVE
Set("total_fee", int64(in.Pay.PayAmount*100))
qqRsp, err := client.UnifiedOrder(ctx, bm)
if err != nil {
return nil, err
}
if qqRsp.ReturnCode != "SUCCESS" {
err = gerror.New(qqRsp.ReturnMsg)
return nil, err
}
if qqRsp.ResultCode != "SUCCESS" {
err = gerror.New(qqRsp.ErrCodeDes)
return nil, err
}
res = new(payin.CreateOrderModel)
res.TradeType = in.Pay.TradeType
res.PayURL = qqRsp.CodeUrl
res.OutTradeNo = in.Pay.OutTradeNo
default:
err = gerror.Newf("暂未支持的交易方式:%v", in.Pay.TradeType)
}
return
}
func GetClient(config *model.PayConfig) (client *qq.Client) {
client = qq.NewClient(config.QQPayMchId, config.QQPayApiKey)
// 打开Debug开关输出日志默认关闭
if config.Debug {
client.DebugSwitch = gopay.DebugOn
}
return
}

View File

@@ -0,0 +1,23 @@
package qqpay
// NotifyRequest QQ支付异步通知参数
// 文档https://mp.qpay.tenpay.cn/buss/wiki/38/1204
type NotifyRequest struct {
Appid string `xml:"appid,omitempty" json:"appid,omitempty"`
MchId string `xml:"mch_id,omitempty" json:"mch_id,omitempty"`
NonceStr string `xml:"nonce_str,omitempty" json:"nonce_str,omitempty"`
Sign string `xml:"sign,omitempty" json:"sign,omitempty"`
DeviceInfo string `xml:"device_info,omitempty" json:"device_info,omitempty"`
TradeType string `xml:"trade_type,omitempty" json:"trade_type,omitempty"`
TradeState string `xml:"trade_state,omitempty" json:"trade_state,omitempty"`
BankType string `xml:"bank_type,omitempty" json:"bank_type,omitempty"`
FeeType string `xml:"fee_type,omitempty" json:"fee_type,omitempty"`
TotalFee string `xml:"total_fee,omitempty" json:"total_fee,omitempty"`
CashFee string `xml:"cash_fee,omitempty" json:"cash_fee,omitempty"`
CouponFee string `xml:"coupon_fee,omitempty" json:"coupon_fee,omitempty"`
TransactionId string `xml:"transaction_id,omitempty" json:"transaction_id,omitempty"`
OutTradeNo string `xml:"out_trade_no,omitempty" json:"out_trade_no,omitempty"`
Attach string `xml:"attach,omitempty" json:"attach,omitempty"`
TimeEnd string `xml:"time_end,omitempty" json:"time_end,omitempty"`
Openid string `xml:"openid,omitempty" json:"openid,omitempty"`
}

View File

@@ -0,0 +1,311 @@
package wxpay
import (
"context"
"crypto/rsa"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/pkg/xpem"
"github.com/go-pay/gopay/wechat/v3"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"hotgo/internal/consts"
weOpen "hotgo/internal/library/wechat"
"hotgo/internal/model"
"hotgo/internal/model/input/payin"
"time"
)
func New(config *model.PayConfig) *wxPay {
return &wxPay{
config: config,
}
}
type wxPay struct {
config *model.PayConfig
}
// Refund 订单退款
func (h *wxPay) Refund(ctx context.Context, in payin.RefundInp) (res *payin.RefundModel, err error) {
client, err := GetClient(h.config)
if err != nil {
return nil, err
}
bm := make(gopay.BodyMap)
bm.Set("out_trade_no", in.Pay.OutTradeNo).
Set("out_refund_no", in.RefundSn).
Set("reason", in.Remark).
SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", int64(in.Pay.PayAmount*100)).
Set("currency", "CNY").
Set("refund", int64(in.RefundMoney*100))
})
refund, err := client.V3Refund(ctx, bm)
if err != nil {
return
}
if refund.Error != "" {
err = gerror.Newf("微信支付发起退款失败,原因:%v", refund.Response.Status)
return
}
if refund.Response.Status != "SUCCESS" && refund.Response.Status != "PROCESSING" {
err = gerror.Newf("微信支付发起退款失败,状态码:%v", refund.Response.Status)
return
}
return
}
// Notify 异步通知
func (h *wxPay) Notify(ctx context.Context, in payin.NotifyInp) (res *payin.NotifyModel, err error) {
notifyReq, err := wechat.V3ParseNotify(ghttp.RequestFromCtx(ctx).Request)
if err != nil {
return
}
client, err := GetClient(h.config)
if err != nil {
return
}
// 获取微信平台证书
certMap, err := getPublicKeyMap(client)
if err != nil {
return
}
// 验证异步通知的签名
if err = notifyReq.VerifySignByPKMap(certMap); err != nil {
return
}
notify, err := notifyReq.DecryptCipherText(h.config.WxPayAPIv3Key)
if err != nil {
return
}
if notify.TradeState != "SUCCESS" {
err = gerror.New("非交易支付成功状态,无需处理!")
// 这里如果相对非交易支付成功状态进行处理,可自行调整此处逻辑
// ...
return
}
if notify.OutTradeNo == "" {
err = gerror.New("订单中没有找到商户单号!")
return
}
res = new(payin.NotifyModel)
res.TransactionId = notify.TransactionId
res.OutTradeNo = notify.OutTradeNo
res.PayAt = gtime.New(notify.SuccessTime)
res.ActualAmount = float64(notify.Amount.PayerTotal / 100) // 转为元,和系统内保持一至
return
}
// CreateOrder 创建订单
func (h *wxPay) CreateOrder(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
switch in.Pay.TradeType {
case consts.TradeTypeWxScan:
return h.scan(ctx, in)
case consts.TradeTypeWxMP, consts.TradeTypeWxMini:
return h.jsapi(ctx, in)
case consts.TradeTypeWxH5:
return h.h5(ctx, in)
default:
err = gerror.Newf("暂未支持的交易方式:%v", in.Pay.TradeType)
}
return
}
func GetClient(config *model.PayConfig) (client *wechat.ClientV3, err error) {
client, err = wechat.NewClientV3(config.WxPayMchId, config.WxPaySerialNo, config.WxPayAPIv3Key, config.WxPayPrivateKey)
if err != nil {
return
}
if _, _, err = client.GetAndSelectNewestCertALL(); err != nil {
return nil, err
}
serialNo, snCertMap, err := client.GetAndSelectNewestCert()
if err != nil {
return
}
snPkMap := make(map[string]*rsa.PublicKey)
for sn, cert := range snCertMap {
pubKey, err := xpem.DecodePublicKey([]byte(cert))
if err != nil {
return nil, err
}
snPkMap[sn] = pubKey
}
client.SnCertMap = snPkMap
client.WxSerialNo = serialNo
// 打开Debug开关输出日志默认关闭
if config.Debug {
client.DebugSwitch = gopay.DebugOn
}
return
}
func getPublicKeyMap(client *wechat.ClientV3) (wxPublicKeyMap map[string]*rsa.PublicKey, err error) {
serialNo, snCertMap, err := client.GetAndSelectNewestCert()
if err != nil {
return
}
snPkMap := make(map[string]*rsa.PublicKey)
for sn, cert := range snCertMap {
pubKey, err := xpem.DecodePublicKey([]byte(cert))
if err != nil {
return nil, err
}
snPkMap[sn] = pubKey
}
client.SnCertMap = snPkMap
client.WxSerialNo = serialNo
wxPublicKeyMap = client.WxPublicKeyMap()
return
}
// scan 创建扫码支付订单
func (h *wxPay) scan(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
client, err := GetClient(h.config)
if err != nil {
return
}
bm := make(gopay.BodyMap)
bm.Set("appid", h.config.WxPayAppId).
Set("mchid", h.config.WxPayMchId).
Set("description", in.Pay.Subject).
Set("out_trade_no", in.Pay.OutTradeNo).
Set("time_expire", time.Now().Add(2*time.Hour).Format(time.RFC3339)).
Set("notify_url", in.Pay.NotifyUrl).
SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", int64(in.Pay.PayAmount*100)).
Set("currency", "CNY")
})
wxRsp, err := client.V3TransactionNative(ctx, bm)
if err != nil {
return
}
if wxRsp.Code != 0 {
err = gerror.New(wxRsp.Error)
return
}
res = new(payin.CreateOrderModel)
res.TradeType = in.Pay.TradeType
res.PayURL = wxRsp.Response.CodeUrl
res.OutTradeNo = in.Pay.OutTradeNo
return
}
// h5 创建H5支付订单
func (h *wxPay) h5(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
client, err := GetClient(h.config)
if err != nil {
return
}
// 初始化参数Map
bm := make(gopay.BodyMap)
bm.Set("appid", h.config.WxPayAppId).
Set("mchid", h.config.WxPayMchId).
Set("description", in.Pay.Subject).
Set("out_trade_no", in.Pay.OutTradeNo).
Set("time_expire", time.Now().Add(2*time.Hour).Format(time.RFC3339)).
Set("notify_url", in.Pay.NotifyUrl).
SetBodyMap("amount", func(b gopay.BodyMap) {
b.Set("total", int64(in.Pay.PayAmount*100)).
Set("currency", "CNY")
}).
SetBodyMap("scene_info", func(b gopay.BodyMap) {
b.Set("payer_client_ip", in.Pay.CreateIp).
SetBodyMap("h5_info", func(b gopay.BodyMap) {
b.Set("type", "Wap")
})
})
// 请求支付下单,成功后得到结果
wxRsp, err := client.V3TransactionH5(ctx, bm)
if err != nil {
return
}
if wxRsp.Code != 0 {
err = gerror.New(wxRsp.Error)
return
}
res = new(payin.CreateOrderModel)
res.TradeType = in.Pay.TradeType
res.PayURL = wxRsp.Response.H5Url
res.OutTradeNo = in.Pay.OutTradeNo
return
}
// jsapi 创建jsapi支付订单
func (h *wxPay) jsapi(ctx context.Context, in payin.CreateOrderInp) (res *payin.CreateOrderModel, err error) {
jsApi := new(payin.JSAPI)
jsApi.Config, err = weOpen.GetJsConfig(ctx, in.Pay.ReturnUrl)
if err != nil {
return
}
client, err := GetClient(h.config)
if err != nil {
return
}
bm := make(gopay.BodyMap)
bm.Set("appid", h.config.WxPayAppId).
Set("mchid", h.config.WxPayMchId).
Set("description", in.Pay.Subject).
Set("out_trade_no", in.Pay.OutTradeNo).
Set("time_expire", time.Now().Add(2*time.Hour).Format(time.RFC3339)).
Set("notify_url", in.Pay.NotifyUrl).
SetBodyMap("amount", func(bm gopay.BodyMap) {
bm.Set("total", int64(in.Pay.PayAmount*100)).
Set("currency", "CNY")
}).
SetBodyMap("payer", func(bm gopay.BodyMap) {
bm.Set("openid", in.Pay.Openid)
})
wxRsp, err := client.V3TransactionJsapi(ctx, bm)
if err != nil {
return
}
if wxRsp.Code != 0 {
err = gerror.New(wxRsp.Error)
return
}
js, err := client.PaySignOfJSAPI(h.config.WxPayAppId, wxRsp.Response.PrepayId)
if err != nil {
return
}
jsApi.Params = js
res = new(payin.CreateOrderModel)
res.TradeType = in.Pay.TradeType
res.OutTradeNo = in.Pay.OutTradeNo
res.JsApi = jsApi
return
}

View File

@@ -0,0 +1 @@
package wxpay

View File

@@ -51,7 +51,7 @@ func (q *DiskConsumerMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg M
if mqMsg.MsgId != "" {
receiveDo(mqMsg)
queue.Commit(index, offset)
sleep = time.Millisecond * 1
sleep = time.Millisecond * 10
}
} else {
sleep = time.Second
@@ -102,6 +102,11 @@ func (d *DiskProducerMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, er
return
}
func (d *DiskProducerMq) SendDelayMsg(topic string, body string, delaySecond int64) (mqMsg MqMsg, err error) {
err = gerror.New("implement me")
return
}
func (d *DiskProducerMq) getProducer(topic string) *disk.Queue {
queue, ok := d.producers[topic]
if ok {

View File

@@ -3,7 +3,6 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package queue
import (
@@ -76,6 +75,11 @@ func (r *KafkaMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error
return mqMsg, nil
}
func (r *KafkaMq) SendDelayMsg(topic string, body string, delaySecond int64) (mqMsg MqMsg, err error) {
err = gerror.New("implement me")
return
}
// ListenReceiveMsgDo 消费数据
func (r *KafkaMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
if r.consumerIns == nil {

View File

@@ -3,35 +3,28 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package queue
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"hotgo/utility/charset"
)
const (
ConsumerLogErrFormat = "消费 [%s] 失败, mqMsgId:%+v, mqMsgData:%+v, err:%+v, stack:%+v"
ProducerLogErrFormat = "生产 [%s] 失败, data:%+v, err:%+v, stack:%+v"
ConsumerLogErrFormat = "消费 [%s] 失败, body:%+v, err:%+v"
ProducerLogErrFormat = "生产 [%s] 失败, body:%+v, err:%+v"
)
// ConsumerLog 消费日志
func ConsumerLog(ctx context.Context, topic string, mqMsg MqMsg, err error) {
if err != nil {
g.Log().Printf(ctx, ConsumerLogErrFormat, topic, mqMsg.MsgId, mqMsg.BodyString(), err, charset.ParseErrStack(err))
} else {
g.Log().Print(ctx, "消费 ["+topic+"] 成功", mqMsg.MsgId)
g.Log().Errorf(ctx, ConsumerLogErrFormat, topic, string(mqMsg.Body), err)
}
}
// ProducerLog 生产日志
func ProducerLog(ctx context.Context, topic string, data interface{}, err error) {
func ProducerLog(ctx context.Context, topic string, mqMsg MqMsg, err error) {
if err != nil {
g.Log().Printf(ctx, ProducerLogErrFormat, topic, gconv.String(data), err, charset.ParseErrStack(err))
} else {
g.Log().Print(ctx, "生产 ["+topic+"] 成功", gconv.String(data))
g.Log().Errorf(ctx, ProducerLogErrFormat, topic, string(mqMsg.Body), err)
}
}

View File

@@ -11,6 +11,17 @@ func Push(topic string, data interface{}) (err error) {
return
}
mqMsg, err := q.SendMsg(topic, gconv.String(data))
ProducerLog(ctx, topic, mqMsg.MsgId, err)
return err
ProducerLog(ctx, topic, mqMsg, err)
return
}
// DelayPush 推送延迟队列
func DelayPush(topic string, data interface{}, second int64) (err error) {
q, err := InstanceProducer()
if err != nil {
return
}
mqMsg, err := q.SendDelayMsg(topic, gconv.String(data), second)
ProducerLog(ctx, topic, mqMsg, err)
return
}

View File

@@ -18,6 +18,7 @@ import (
type MqProducer interface {
SendMsg(topic string, body string) (mqMsg MqMsg, err error)
SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error)
SendDelayMsg(topic string, body string, delaySecond int64) (mqMsg MqMsg, err error)
}
type MqConsumer interface {
@@ -42,10 +43,7 @@ type Config struct {
}
type RedisConf struct {
Address string `json:"address"`
Db int `json:"db"`
Pass string `json:"pass"`
IdleTimeout int `json:"idleTimeout"`
Timeout int64 `json:"timeout"`
}
type RocketmqConf struct {
Address []string `json:"address"`
@@ -124,19 +122,12 @@ func NewProducer(groupName string) (mqClient MqProducer, err error) {
Version: config.Kafka.Version,
})
case "redis":
address := g.Cfg().MustGet(ctx, "queue.redis.address", nil).String()
if len(address) == 0 {
err = gerror.New("queue redis address is not support")
return
if _, err = g.Redis().Do(ctx, "ping"); err == nil {
mqClient = RegisterRedisMqProducer(RedisOption{
Timeout: config.Redis.Timeout,
}, groupName)
}
mqClient, err = RegisterRedisMqProducer(RedisOption{
Addr: config.Redis.Address,
Passwd: config.Redis.Pass,
DBnum: config.Redis.Db,
Timeout: config.Redis.IdleTimeout,
}, PoolOption{
5, 50, 5,
}, groupName, config.Retry)
case "disk":
config.Disk.GroupName = groupName
mqClient, err = RegisterDiskMqProducer(config.Disk)
@@ -197,19 +188,11 @@ func NewConsumer(groupName string) (mqClient MqConsumer, err error) {
ClientId: clientId,
})
case "redis":
if len(config.Redis.Address) == 0 {
err = gerror.New("queue redis address is not support")
return
if _, err = g.Redis().Do(ctx, "ping"); err == nil {
mqClient = RegisterRedisMqConsumer(RedisOption{
Timeout: config.Redis.Timeout,
}, groupName)
}
mqClient, err = RegisterRedisMqConsumer(RedisOption{
Addr: config.Redis.Address,
Passwd: config.Redis.Pass,
DBnum: config.Redis.Db,
Timeout: config.Redis.IdleTimeout,
}, PoolOption{
5, 50, 5,
}, groupName)
case "disk":
config.Disk.GroupName = groupName
mqClient, err = RegisterDiskMqConsumer(config.Disk)

View File

@@ -1,42 +1,26 @@
package queue
import (
"context"
"encoding/json"
"fmt"
"github.com/bufanyun/pool"
"github.com/gogf/gf/v2/database/gredis"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gomodule/redigo/redis"
"hotgo/utility/encrypt"
"math/rand"
"strconv"
"time"
)
type RedisMq struct {
poolName string
groupName string
retry int
timeout int
}
type PoolOption struct {
InitCap int
MaxCap int
IdleTimeout int
timeout int64
}
type RedisOption struct {
Addr string
Passwd string
DBnum int
Timeout int
}
var redisPoolMap map[string]pool.Pool
func init() {
redisPoolMap = make(map[string]pool.Pool)
Timeout int64
}
// SendMsg 按字符串类型生产数据
@@ -49,43 +33,88 @@ func (r *RedisMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error
if r.poolName == "" {
return mqMsg, gerror.New("RedisMq producer not register")
}
if topic == "" {
return mqMsg, gerror.New("RedisMq topic is empty")
}
msgId := getRandMsgId()
rdx, put, err := getRedis(r.poolName, r.retry)
defer put()
if err != nil {
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者获取redis实例失败:", err))
}
mqMsg = MqMsg{
RunType: SendMsg,
Topic: topic,
MsgId: msgId,
MsgId: getRandMsgId(),
Body: body,
Timestamp: time.Now(),
}
mqMsgJson, err := json.Marshal(mqMsg)
data, err := json.Marshal(mqMsg)
if err != nil {
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者解析json消息失败:", err))
return
}
queueName := r.genQueueName(r.groupName, topic)
_, err = redis.Int64(rdx.Do("LPUSH", queueName, mqMsgJson))
if err != nil {
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者添加消息失败:", err))
key := r.genKey(r.groupName, topic)
if _, err = g.Redis().Do(ctx, "LPUSH", key, data); err != nil {
return
}
if r.timeout > 0 {
_, err = rdx.Do("EXPIRE", queueName, r.timeout)
if err != nil {
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者设置过期时间失败:", err))
if _, err = g.Redis().Do(ctx, "EXPIRE", key, r.timeout); err != nil {
return
}
}
return
}
func (r *RedisMq) SendDelayMsg(topic string, body string, delaySecond int64) (mqMsg MqMsg, err error) {
if delaySecond < 1 {
return r.SendMsg(topic, body)
}
if r.poolName == "" {
err = gerror.New("SendDelayMsg RedisMq not register")
return
}
if topic == "" {
err = gerror.New("SendDelayMsg RedisMq topic is empty")
return
}
mqMsg = MqMsg{
RunType: SendMsg,
Topic: topic,
MsgId: getRandMsgId(),
Body: []byte(body),
Timestamp: time.Now(),
}
data, err := json.Marshal(mqMsg)
if err != nil {
return
}
var (
conn = g.Redis()
key = r.genKey(r.groupName, "delay:"+topic)
expireSecond = time.Now().Unix() + delaySecond
timePiece = fmt.Sprintf("%s:%d", key, expireSecond)
z = gredis.ZAddMember{Score: float64(expireSecond), Member: timePiece}
)
if _, err = conn.ZAdd(ctx, key, &gredis.ZAddOption{}, z); err != nil {
return
}
if _, err = conn.RPush(ctx, timePiece, data); err != nil {
return
}
// consumer will also delete the item
if r.timeout > 0 {
_, _ = conn.Expire(ctx, timePiece, r.timeout+delaySecond)
_, _ = conn.Expire(ctx, key, r.timeout)
}
return
}
@@ -98,190 +127,148 @@ func (r *RedisMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg))
return gerror.New("RedisMq topic is empty")
}
queueName := r.genQueueName(r.groupName, topic)
var (
key = r.genKey(r.groupName, topic)
key2 = r.genKey(r.groupName, "delay:"+topic)
)
go func() {
for range time.Tick(500 * time.Millisecond) {
mqMsgList := r.loopReadQueue(queueName)
for range time.Tick(300 * time.Millisecond) {
mqMsgList := r.loopReadQueue(key)
for _, mqMsg := range mqMsgList {
receiveDo(mqMsg)
}
}
}()
go func() {
mqMsgCh, errCh := r.loopReadDelayQueue(key2)
for mqMsg := range mqMsgCh {
receiveDo(mqMsg)
}
for err = range errCh {
if err != nil && err != context.Canceled && err != context.DeadlineExceeded {
g.Log().Infof(ctx, "ListenReceiveMsgDo Delay topic:%v, err:%+v", topic, err)
}
}
}()
select {}
}
// 生成队列名称
func (r *RedisMq) genQueueName(groupName string, topic string) string {
// 生成队列key
func (r *RedisMq) genKey(groupName string, topic string) string {
return fmt.Sprintf("queue:%s_%s", groupName, topic)
}
func (r *RedisMq) loopReadQueue(queueName string) (mqMsgList []MqMsg) {
rdx, put, err := getRedis(r.poolName, r.retry)
defer put()
if err != nil {
g.Log().Warningf(ctx, "loopReadQueue getRedis err:%+v", err)
return
}
func (r *RedisMq) loopReadQueue(key string) (mqMsgList []MqMsg) {
conn := g.Redis()
for {
infoByte, err := redis.Bytes(rdx.Do("RPOP", queueName))
if redis.ErrNil == err || len(infoByte) == 0 {
break
}
data, err := conn.Do(ctx, "RPOP", key)
if err != nil {
g.Log().Warningf(ctx, "loopReadQueue redis RPOP err:%+v", err)
break
}
var mqMsg MqMsg
if err = json.Unmarshal(infoByte, &mqMsg); err != nil {
g.Log().Warningf(ctx, "loopReadQueue Unmarshal err:%+v", err)
if data.IsEmpty() {
break
}
var mqMsg MqMsg
if err = data.Scan(&mqMsg); err != nil {
g.Log().Warningf(ctx, "loopReadQueue Scan err:%+v", err)
break
}
if mqMsg.MsgId != "" {
mqMsgList = append(mqMsgList, mqMsg)
}
}
return mqMsgList
}
func RegisterRedisMqProducer(connOpt RedisOption, poolOpt PoolOption, groupName string, retry int) (client MqProducer, err error) {
client, err = RegisterRedisMq(connOpt, poolOpt, groupName, retry)
if err != nil {
err = gerror.Newf("RegisterRedisMqProducer err:%+v", err)
return
}
return
func RegisterRedisMqProducer(connOpt RedisOption, groupName string) (client MqProducer) {
return RegisterRedisMq(connOpt, groupName)
}
// RegisterRedisMqConsumer 注册消费者
func RegisterRedisMqConsumer(connOpt RedisOption, poolOpt PoolOption, groupName string) (client MqConsumer, err error) {
client, err = RegisterRedisMq(connOpt, poolOpt, groupName, 0)
if err != nil {
err = gerror.Newf("RegisterRedisMqConsumer err:%+v", err)
return
}
return
func RegisterRedisMqConsumer(connOpt RedisOption, groupName string) (client MqConsumer) {
return RegisterRedisMq(connOpt, groupName)
}
// RegisterRedisMq 注册redis实例
func RegisterRedisMq(connOpt RedisOption, poolOpt PoolOption, groupName string, retry int) (mqIns *RedisMq, err error) {
poolName, err := registerRedis(connOpt.Addr, connOpt.Passwd, connOpt.DBnum, poolOpt)
if err != nil {
return
}
if retry <= 0 {
retry = 0
}
mqIns = &RedisMq{
poolName: poolName,
func RegisterRedisMq(connOpt RedisOption, groupName string) *RedisMq {
return &RedisMq{
poolName: encrypt.Md5ToString(fmt.Sprintf("%s-%d", groupName, time.Now().UnixNano())),
groupName: groupName,
retry: retry,
timeout: connOpt.Timeout,
}
return mqIns, nil
}
// RegisterRedis 注册一个redis配置
func registerRedis(host, pass string, dbNum int, opt PoolOption) (poolName string, err error) {
poolName = encrypt.Md5ToString(fmt.Sprintf("%s-%s-%d", host, pass, dbNum))
if _, ok := redisPoolMap[poolName]; ok {
return poolName, nil
}
connRedis := func() (interface{}, error) {
conn, err := redis.Dial("tcp", host)
if err != nil {
return nil, err
}
if pass != "" {
if _, err = conn.Do("AUTH", pass); err != nil {
return nil, err
}
}
if dbNum > 0 {
if _, err = conn.Do("SELECT", dbNum); err != nil {
return nil, err
}
}
return conn, err
}
// closeRedis 关闭连接
closeRedis := func(v interface{}) error {
return v.(redis.Conn).Close()
}
// pingRedis 检测连接连通性
pingRedis := func(v interface{}) error {
conn := v.(redis.Conn)
val, err := redis.String(conn.Do("PING"))
if err != nil {
return err
}
if val != "PONG" {
return gerror.New("queue redis ping is error ping => " + val)
}
return nil
}
p, err := pool.NewChannelPool(&pool.Config{
InitialCap: opt.InitCap,
MaxCap: opt.MaxCap,
Factory: connRedis,
Close: closeRedis,
Ping: pingRedis,
IdleTimeout: time.Duration(opt.IdleTimeout) * time.Second,
})
if err != nil {
return poolName, err
}
mutex.Lock()
defer mutex.Unlock()
redisPoolMap[poolName] = p
return poolName, nil
}
// getRedis 获取一个redis db连接
func getRedis(poolName string, retry int) (db redis.Conn, put func(), err error) {
put = func() {}
if _, ok := redisPoolMap[poolName]; ok == false {
return nil, put, gerror.New("db connect is nil")
}
redisPool := redisPoolMap[poolName]
conn, err := redisPool.Get()
for i := 0; i < retry; i++ {
if err == nil {
break
}
conn, err = redisPool.Get()
time.Sleep(time.Second)
}
if err != nil {
return nil, put, err
}
put = func() {
if err = redisPool.Put(conn); err != nil {
return
}
}
db = conn.(redis.Conn)
return db, put, nil
}
func getRandMsgId() string {
rand.Seed(time.Now().UnixNano())
rand.NewSource(time.Now().UnixNano())
radium := rand.Intn(999) + 1
timeCode := time.Now().UnixNano()
return fmt.Sprintf("%d%.4d", timeCode, radium)
}
func (r *RedisMq) loopReadDelayQueue(key string) (resCh chan MqMsg, errCh chan error) {
resCh = make(chan MqMsg, 0)
errCh = make(chan error, 1)
go func() {
defer close(resCh)
defer close(errCh)
conn := g.Redis()
for {
now := time.Now().Unix()
do, err := conn.Do(ctx, "zrangebyscore", key, "0", strconv.FormatInt(now, 10), "limit", 0, 1)
if err != nil {
return
}
val := do.Strings()
if len(val) == 0 {
select {
case <-ctx.Done():
errCh <- ctx.Err()
return
case <-time.After(time.Second):
continue
}
}
for _, listK := range val {
for {
pop, err := conn.LPop(ctx, listK)
if err != nil {
errCh <- err
return
} else if pop.IsEmpty() {
conn.ZRem(ctx, key, listK)
conn.Del(ctx, listK)
break
} else {
var mqMsg MqMsg
if err = pop.Scan(&mqMsg); err != nil {
g.Log().Warningf(ctx, "loopReadDelayQueue Scan err:%+v", err)
break
}
if mqMsg.MsgId == "" {
continue
}
select {
case resCh <- mqMsg:
case <-ctx.Done():
errCh <- ctx.Err()
return
}
}
}
}
}
}()
return resCh, errCh
}

View File

@@ -3,7 +3,6 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package queue
import (
@@ -15,6 +14,7 @@ import (
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
"github.com/apache/rocketmq-client-go/v2/rlog"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
)
@@ -81,6 +81,11 @@ func (r *RocketMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err erro
return mqMsg, nil
}
func (r *RocketMq) SendDelayMsg(topic string, body string, delaySecond int64) (mqMsg MqMsg, err error) {
err = gerror.New("implement me")
return
}
// ListenReceiveMsgDo 消费数据
func (r *RocketMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
if r.consumerIns == nil {

View File

@@ -3,7 +3,6 @@
// @Copyright Copyright (c) 2023 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package response
import (
@@ -27,7 +26,6 @@ func JsonExit(r *ghttp.Request, code int, message string, data ...interface{}) {
// @param code 状态码(200:成功,302跳转和http请求状态码一至)
// @param message 请求结果信息
// @param data 请求结果,根据不同接口返回结果的数据结构不同
//
func RJson(r *ghttp.Request, code int, message string, data ...interface{}) {
responseData := interface{}(nil)
if len(data) > 0 {
@@ -84,3 +82,12 @@ func Redirect(r *ghttp.Request, location string, code ...int) {
func Download(r *ghttp.Request, location string, code ...int) {
r.Response.ServeFileDownload("test.txt")
}
// RText 返回成功文本
func RText(r *ghttp.Request, message string) {
// 清空响应
r.Response.ClearBuffer()
// 写入响应
r.Response.Write(message)
}

View File

@@ -10,15 +10,15 @@ import (
"hotgo/internal/model/input/sysin"
)
// SmsDrive 短信驱动
type SmsDrive interface {
// Drive 短信驱动
type Drive interface {
SendCode(ctx context.Context, in sysin.SendCodeInp, config *model.SmsConfig) (err error)
}
func New(name ...string) SmsDrive {
func New(name ...string) Drive {
var (
instanceName = consts.SmsDriveAliYun
drive SmsDrive
drive Drive
)
if len(name) > 0 && name[0] != "" {

View File

@@ -0,0 +1,64 @@
package wechat
import (
"context"
"github.com/gogf/gf/v2/os/gcache"
"hotgo/internal/library/cache"
"time"
)
type Cache struct {
ctx context.Context
cache *gcache.Cache
}
// NewCache 实例化
func NewCache(ctx context.Context, name ...*gcache.Cache) *Cache {
var defaultCache = cache.Instance()
if len(name) > 0 {
defaultCache = name[0]
}
return &Cache{ctx: ctx, cache: defaultCache}
}
// SetCache 设置缓存驱动
func (r *Cache) SetCache(cache *gcache.Cache) {
r.cache = cache
}
// SetCtx 设置 ctx 参数
func (r *Cache) SetCtx(ctx context.Context) {
r.ctx = ctx
}
// Get 获取一个值
func (r *Cache) Get(key string) interface{} {
get, err := r.cache.Get(r.ctx, key)
if err != nil {
return nil
}
return get.Interface()
}
// Set 设置一个值
func (r *Cache) Set(key string, val interface{}, timeout time.Duration) error {
return r.cache.Set(r.ctx, key, val, timeout)
}
// IsExist 判断key是否存在
func (r *Cache) IsExist(key string) bool {
contains, err := r.cache.Contains(r.ctx, key)
if err != nil {
return false
}
return contains
}
// Delete 删除
func (r *Cache) Delete(key string) error {
_, err := r.cache.Remove(r.ctx, key)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,13 @@
package wechat
import "hotgo/internal/model"
var config *model.WechatConfig
func SetConfig(c *model.WechatConfig) {
config = c
}
func GetConfig() *model.WechatConfig {
return config
}

View File

@@ -0,0 +1,108 @@
package wechat
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/silenceper/wechat/v2"
"github.com/silenceper/wechat/v2/officialaccount"
offConfig "github.com/silenceper/wechat/v2/officialaccount/config"
officialJs "github.com/silenceper/wechat/v2/officialaccount/js"
officialOauth "github.com/silenceper/wechat/v2/officialaccount/oauth"
"github.com/silenceper/wechat/v2/openplatform"
openConfig "github.com/silenceper/wechat/v2/openplatform/config"
"hotgo/internal/consts"
)
// NewOfficialAccount 微信公众号实例
func NewOfficialAccount(ctx context.Context) *officialaccount.OfficialAccount {
cfg := &offConfig.Config{
AppID: config.OfficialAppID,
AppSecret: config.OfficialAppSecret,
Token: config.OfficialToken,
EncodingAESKey: config.OfficialEncodingAESKey,
Cache: NewCache(ctx),
}
return wechat.NewWechat().GetOfficialAccount(cfg)
}
// NewOpenPlatform 开放平台实例
func NewOpenPlatform(ctx context.Context) *openplatform.OpenPlatform {
cfg := &openConfig.Config{
AppID: config.OpenAppID,
AppSecret: config.OpenAppSecret,
Token: config.OpenToken,
EncodingAESKey: config.OpenEncodingAESKey,
Cache: NewCache(ctx),
}
return wechat.NewWechat().GetOpenPlatform(cfg)
}
// GetOpenOauthURL 代第三方公众号 - 获取网页授权地址
func GetOpenOauthURL(ctx context.Context, redirectURI, scope, state string) (location string, err error) {
op := NewOpenPlatform(ctx)
appid := config.OfficialAppID // 公众号appid
oauth := op.GetOfficialAccount(appid).PlatformOauth()
if scope == "" {
scope = consts.WechatScopeBase
}
location, err = oauth.GetRedirectURL(redirectURI, scope, state, appid)
return
}
// GetOpenUserAccessToken 代第三方公众号 - 通过网页授权的code 换取access_token
func GetOpenUserAccessToken(ctx context.Context, code string) (accessToken officialOauth.ResAccessToken, err error) {
op := NewOpenPlatform(ctx)
appid := config.OfficialAppID // 公众号appid
officialAccount := op.GetOfficialAccount(appid)
componentAccessToken, err := op.GetComponentAccessToken()
if err != nil {
return
}
accessToken, err = officialAccount.PlatformOauth().GetUserAccessToken(code, appid, componentAccessToken)
if err != nil {
return
}
if accessToken.ErrCode > 0 {
err = gerror.Newf("GetOpenUserAccessToken err:%+v", accessToken.ErrMsg)
return
}
return
}
// GetUserInfo 获取用户信息
func GetUserInfo(ctx context.Context, token officialOauth.ResAccessToken) (info officialOauth.UserInfo, err error) {
oauth := NewOfficialAccount(ctx).GetOauth()
info, err = oauth.GetUserInfo(token.AccessToken, token.OpenID, "")
return
}
// GetOauthURL 获取网页授权地址
func GetOauthURL(ctx context.Context, redirectURI, scope, state string) (location string, err error) {
oauth := NewOfficialAccount(ctx).GetOauth()
location, err = oauth.GetRedirectURL(redirectURI, scope, state)
return
}
// GetUserAccessToken 通过网页授权的code 换取access_token
func GetUserAccessToken(ctx context.Context, code string) (accessToken officialOauth.ResAccessToken, err error) {
oauth := NewOfficialAccount(ctx).GetOauth()
accessToken, err = oauth.GetUserAccessToken(code)
if err != nil {
return
}
if accessToken.ErrCode > 0 {
err = gerror.Newf("GetUserAccessToken err:%+v", accessToken.ErrMsg)
return
}
return
}
// GetJsConfig 获取js配置
func GetJsConfig(ctx context.Context, uri string) (config *officialJs.Config, err error) {
return NewOfficialAccount(ctx).GetJs().GetConfig(uri)
}