This commit is contained in:
孟帅
2022-11-24 23:37:34 +08:00
parent 4ffe54b6ac
commit 29bda0dcdd
1487 changed files with 97869 additions and 96539 deletions

24
server/internal/library/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,24 @@
// Package cache
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package cache
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcache"
)
func New() *gcache.Cache {
c := gcache.New()
//redis
adapter := gcache.NewAdapterRedis(g.Redis())
//内存
//adapter := gcache.NewAdapterMemory()
c.SetAdapter(adapter)
return c
}

View File

@@ -0,0 +1,44 @@
// Package captcha
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package captcha
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/text/gstr"
"github.com/mojocn/base64Captcha"
)
// GetVerifyImgString 生成验证码
func GetVerifyImgString(ctx context.Context) (idKeyC string, base64stringC string) {
driver := &base64Captcha.DriverString{
Height: 80,
Width: 240,
//NoiseCount: 50,
//ShowLineOptions: 20,
Length: 4,
Source: "abcdefghjkmnpqrstuvwxyz23456789",
Fonts: []string{"chromohv.ttf"},
}
driver = driver.ConvertFonts()
store := base64Captcha.DefaultMemStore
c := base64Captcha.NewCaptcha(driver, store)
idKeyC, base64stringC, err := c.Generate()
if err != nil {
g.Log().Error(ctx, err)
}
return
}
// VerifyString 验证输入的验证码是否正确
func VerifyString(id, answer string) bool {
driver := new(base64Captcha.DriverString)
store := base64Captcha.DefaultMemStore
c := base64Captcha.NewCaptcha(driver, store)
answer = gstr.ToLower(answer)
return c.Verify(id, answer, true)
}

View File

@@ -0,0 +1,324 @@
// Package casbin
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package casbin
import (
"context"
"errors"
"fmt"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
"github.com/gogf/gf/v2/database/gdb"
"log"
"math"
"strings"
)
const (
defaultTableName = "hg_admin_role_casbin"
dropPolicyTableSql = `DROP TABLE IF EXISTS %s`
createPolicyTableSql = `
CREATE TABLE IF NOT EXISTS %s (
id bigint(20) NOT NULL AUTO_INCREMENT,
p_type varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
v0 varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
v1 varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
v2 varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
v3 varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
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;
`
)
type (
adapter struct {
db gdb.DB
table string
}
policyColumns struct {
ID string // ID
PType string // PType
V0 string // V0
V1 string // V1
V2 string // V2
V3 string // V3
V4 string // V4
V5 string // V5
}
// policy rule entity
policyRule struct {
ID int64 `orm:"id" json:"id"`
PType string `orm:"p_type" json:"p_type"`
V0 string `orm:"v0" json:"v0"`
V1 string `orm:"v1" json:"v1"`
V2 string `orm:"v2" json:"v2"`
V3 string `orm:"v3" json:"v3"`
V4 string `orm:"v4" json:"v4"`
V5 string `orm:"v5" json:"v5"`
}
)
var (
errInvalidDatabaseLink = errors.New("invalid database link")
policyColumnsName = policyColumns{
ID: "id",
PType: "p_type",
V0: "v0",
V1: "v1",
V2: "v2",
V3: "v3",
V4: "v4",
V5: "v5",
}
)
// NewAdapter Create a casbin adapter
func NewAdapter(link string) (adp *adapter, err error) {
adp = &adapter{table: defaultTableName}
config := strings.SplitN(link, ":", 2)
if len(config) != 2 {
err = errInvalidDatabaseLink
return
}
if adp.db, err = gdb.New(gdb.ConfigNode{Type: config[0], Link: config[1]}); err != nil {
return
}
err = adp.createPolicyTable()
return
}
func (a *adapter) model() *gdb.Model {
return a.db.Model(a.table).Safe().Ctx(context.TODO())
}
// create a policy table when it's not exists.
func (a *adapter) createPolicyTable() (err error) {
_, err = a.db.Exec(context.TODO(), fmt.Sprintf(createPolicyTableSql, a.table))
return
}
// drop policy table from the storage.
func (a *adapter) dropPolicyTable() (err error) {
_, err = a.db.Exec(context.TODO(), fmt.Sprintf(dropPolicyTableSql, a.table))
return
}
// LoadPolicy loads all policy rules from the storage.
func (a *adapter) LoadPolicy(model model.Model) (err error) {
log.Println("LoadPolicy...")
var rules []policyRule
if err = a.model().Scan(&rules); err != nil {
return
}
for _, rule := range rules {
a.loadPolicyRule(rule, model)
}
return
}
// SavePolicy Saves all policy rules to the storage.
func (a *adapter) SavePolicy(model model.Model) (err error) {
if err = a.dropPolicyTable(); err != nil {
return
}
if err = a.createPolicyTable(); err != nil {
return
}
policyRules := make([]policyRule, 0)
for ptype, ast := range model["p"] {
for _, rule := range ast.Policy {
policyRules = append(policyRules, a.buildPolicyRule(ptype, rule))
}
}
for ptype, ast := range model["g"] {
for _, rule := range ast.Policy {
policyRules = append(policyRules, a.buildPolicyRule(ptype, rule))
}
}
if count := len(policyRules); count > 0 {
if _, err = a.model().Insert(policyRules); err != nil {
return
}
}
return
}
// AddPolicy adds a policy rule to the storage.
func (a *adapter) AddPolicy(sec string, ptype string, rule []string) (err error) {
_, err = a.model().Insert(a.buildPolicyRule(ptype, rule))
return
}
// AddPolicies adds policy rules to the storage.
func (a *adapter) AddPolicies(sec string, ptype string, rules [][]string) (err error) {
if len(rules) == 0 {
return
}
policyRules := make([]policyRule, 0, len(rules))
for _, rule := range rules {
policyRules = append(policyRules, a.buildPolicyRule(ptype, rule))
}
_, err = a.model().Insert(policyRules)
return
}
// RemovePolicy removes a policy rule from the storage.
func (a *adapter) RemovePolicy(sec string, ptype string, rule []string) (err error) {
db := a.model()
db = db.Where(policyColumnsName.PType, ptype)
for index := 0; index < len(rule); index++ {
db = db.Where(fmt.Sprintf("v%d", index), rule[index])
}
_, err = db.Delete()
return err
}
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
func (a *adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) (err error) {
db := a.model()
db = db.Where(policyColumnsName.PType, ptype)
for index := 0; index <= 5; index++ {
if fieldIndex <= index && index < fieldIndex+len(fieldValues) {
db = db.Where(fmt.Sprintf("v%d", index), fieldValues[index-fieldIndex])
}
}
_, err = db.Delete()
return
}
// RemovePolicies removes policy rules from the storage (implements the persist.BatchAdapter interface).
func (a *adapter) RemovePolicies(sec string, ptype string, rules [][]string) (err error) {
db := a.model()
for _, rule := range rules {
where := map[string]interface{}{policyColumnsName.PType: ptype}
for i := 0; i <= 5; i++ {
if len(rule) > i {
where[fmt.Sprintf("v%d", i)] = rule[i]
}
}
db = db.WhereOr(where)
}
_, err = db.Delete()
return
}
// UpdatePolicy updates a policy rule from storage.
func (a *adapter) UpdatePolicy(sec string, ptype string, oldRule, newRule []string) (err error) {
_, err = a.model().Update(a.buildPolicyRule(ptype, newRule), a.buildPolicyRule(ptype, oldRule))
return
}
// UpdatePolicies updates some policy rules to storage, like db, redis.
func (a *adapter) UpdatePolicies(sec string, ptype string, oldRules, newRules [][]string) (err error) {
if len(oldRules) == 0 || len(newRules) == 0 {
return
}
err = a.db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error {
for i := 0; i < int(math.Min(float64(len(oldRules)), float64(len(newRules)))); i++ {
if _, err = tx.Model(a.table).Update(a.buildPolicyRule(ptype, newRules[i]), a.buildPolicyRule(ptype, oldRules[i])); err != nil {
return err
}
}
return nil
})
return
}
// 加载策略规则
func (a *adapter) loadPolicyRule(rule policyRule, model model.Model) {
ruleText := rule.PType
if rule.V0 != "" {
ruleText += ", " + rule.V0
}
if rule.V1 != "" {
ruleText += ", " + rule.V1
}
if rule.V2 != "" {
ruleText += ", " + rule.V2
}
if rule.V3 != "" {
ruleText += ", " + rule.V3
}
if rule.V4 != "" {
ruleText += ", " + rule.V4
}
if rule.V5 != "" {
ruleText += ", " + rule.V5
}
persist.LoadPolicyLine(ruleText, model)
}
// 构建策略规则
func (a *adapter) buildPolicyRule(ptype string, data []string) policyRule {
rule := policyRule{PType: ptype}
if len(data) > 0 {
rule.V0 = data[0]
}
if len(data) > 1 {
rule.V1 = data[1]
}
if len(data) > 2 {
rule.V2 = data[2]
}
if len(data) > 3 {
rule.V3 = data[3]
}
if len(data) > 4 {
rule.V4 = data[4]
}
if len(data) > 5 {
rule.V5 = data[5]
}
return rule
}

View File

@@ -0,0 +1,68 @@
// Package casbin
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package casbin
import (
"context"
"net/http"
"testing"
)
// TestNew description
func TestNew(t *testing.T) {
InitEnforcer(context.TODO())
user := "admin"
path := "/"
method := http.MethodGet
t.Logf("\nuser:%v\npath:%v\nmethod:%v", user, path, method)
ok, err := Enforcer.DeletePermissionsForUser(user)
if err != nil {
t.Error(err)
}
t.Logf("delete user premission:%v", ok)
CheckPremission(t, user, path, method)
AddPremission(t, user, "*", ActionAll)
CheckPremission(t, user, path, method)
user1 := "user1"
path1 := "/api/v1/*"
checkPathTrue := "/api/v1/user/list"
checkPathFalse := "/api/v2/user/list"
AddPremission(t, user1, path1, ActionAll)
CheckPremission(t, user1, checkPathTrue, ActionPost)
CheckPremission(t, user1, checkPathFalse, http.MethodGet)
CheckPremission(t, user1, checkPathTrue, http.MethodGet)
CheckPremission(t, user1, "/api/v1/user/list2", http.MethodGet)
ok, err = Enforcer.DeletePermissionsForUser(user1)
if err != nil {
t.Error(err)
}
t.Logf("delete user premission:%v", ok)
CheckPremission(t, user1, "/api/v1/user1/list", http.MethodGet)
}
// CheckPremission description
func CheckPremission(t *testing.T, user string, path string, method string) {
ok, err := Enforcer.Enforce(user, path, method)
if err != nil {
t.Error(err)
}
t.Logf("check \tuser[%s] \tpremission[%s] \tpath[%s] \tallow[%v]", user, method, path, ok)
}
// Add description
func AddPremission(t *testing.T, user string, path string, method string) {
ok, err := Enforcer.AddPolicy(user, path, method)
if err != nil {
t.Error(err)
}
t.Logf("add \tuser[%s] \tpremission[%s] \tpath[%s] \tresult[%v]", user, method, path, ok)
}

View File

@@ -0,0 +1,113 @@
// Package casbin
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package casbin
import (
"context"
"github.com/casbin/casbin/v2"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
"github.com/gogf/gf/v2/frame/g"
"hotgo/internal/consts"
"net/http"
"strings"
)
const (
ActionGet = http.MethodGet
ActionPost = http.MethodPost
ActionPut = http.MethodPut
ActionDelete = http.MethodDelete
ActionAll = "GET|POST|PUT|DELETE|PATCH|OPTIONS|HEAD"
)
var Enforcer *casbin.Enforcer
// InitEnforcer 初始化
func InitEnforcer(ctx context.Context) {
var (
link, _ = g.Cfg().Get(ctx, "database.default.link")
a, err = NewAdapter(link.String())
)
if err != nil {
g.Log().Panicf(ctx, "casbin.NewAdapter err . %v", err)
return
}
Enforcer, err = casbin.NewEnforcer("./manifest/config/casbin.conf", a)
if err != nil {
g.Log().Panicf(ctx, "casbin.NewEnforcer err . %v", err)
return
}
loadPermissions(ctx)
}
func loadPermissions(ctx context.Context) {
type Policy struct {
Key string `json:"key"`
Permissions string `json:"permissions"`
}
var (
rules [][]string
polices []*Policy
err error
superRoleKey, _ = g.Cfg().Get(ctx, "hotgo.admin.superRoleKey")
)
err = g.Model("hg_admin_role r").
LeftJoin("hg_admin_role_menu rm", "r.id=rm.role_id").
LeftJoin("hg_admin_menu m", "rm.menu_id=m.id").
Fields("r.key,m.permissions").
Where("r.status", consts.StatusEnabled).
Where("m.status", consts.StatusEnabled).
Where("m.permissions !=?", "").
Where("r.key !=?", superRoleKey.String()).
Scan(&polices)
if err != nil {
g.Log().Fatalf(ctx, "loadPermissions Scan err:%v", err)
return
}
for _, policy := range polices {
if strings.Contains(policy.Permissions, ",") {
lst := strings.Split(policy.Permissions, ",")
for _, permissions := range lst {
rules = append(rules, []string{policy.Key, permissions, ActionAll})
}
} else {
rules = append(rules, []string{policy.Key, policy.Permissions, ActionAll})
}
}
if _, err = Enforcer.AddPolicies(rules); err != nil {
g.Log().Fatalf(ctx, "loadPermissions AddPolicies err:%v", err)
return
}
}
func Clear(ctx context.Context) (err error) {
_, err = Enforcer.RemovePolicies(Enforcer.GetPolicy())
if err != nil {
g.Log().Warningf(ctx, "Enforcer RemovePolicies err:%+v", err)
return
}
// 检查是否清理干净
if len(Enforcer.GetPolicy()) > 0 {
return Clear(ctx)
}
return
}
func Refresh(ctx context.Context) (err error) {
if err = Clear(ctx); err != nil {
return err
}
loadPermissions(ctx)
return
}

View File

@@ -0,0 +1,81 @@
// Package contexts
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package contexts
import (
"context"
"github.com/gogf/gf/v2/net/ghttp"
"hotgo/internal/consts"
"hotgo/internal/model"
)
// Init 初始化上下文对象指针到上下文对象中,以便后续的请求流程中可以修改
func Init(r *ghttp.Request, customCtx *model.Context) {
r.SetCtxVar(consts.ContextKey, customCtx)
}
// Get 获得上下文变量如果没有设置那么返回nil
func Get(ctx context.Context) *model.Context {
value := ctx.Value(consts.ContextKey)
if value == nil {
return nil
}
if localCtx, ok := value.(*model.Context); ok {
return localCtx
}
return nil
}
// SetUser 将上下文信息设置到上下文请求中,注意是完整覆盖
func SetUser(ctx context.Context, user *model.Identity) {
Get(ctx).User = user
}
// SetResponse 设置组件响应 用于全局日志使用
func SetResponse(ctx context.Context, response *model.Response) {
Get(ctx).Response = response
}
// SetModule 设置应用模块
func SetModule(ctx context.Context, module string) {
Get(ctx).Module = module
}
// SetTakeUpTime 设置请求耗时
func SetTakeUpTime(ctx context.Context, takeUpTime int64) {
Get(ctx).TakeUpTime = takeUpTime
}
// GetUserId 获取用户ID
func GetUserId(ctx context.Context) int64 {
user := Get(ctx).User
if user == nil {
return 0
}
return user.Id
}
// GetRoleId 获取用户角色ID
func GetRoleId(ctx context.Context) int64 {
user := Get(ctx).User
if user == nil {
return 0
}
return user.Role
}
// GetRoleKey 获取用户角色唯一编码
func GetRoleKey(ctx context.Context) string {
user := Get(ctx).User
if user == nil {
return ""
}
return user.RoleKey
}

View File

@@ -0,0 +1,70 @@
// Package ems
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package ems
import (
"github.com/gogf/gf/v2/errors/gerror"
"hotgo/internal/model"
"hotgo/utility/validate"
"net/smtp"
"strings"
)
// Send 发送邮件入口
func Send(config *model.EmailConfig, to string, subject string, body string) error {
return sendToMail(config, to, subject, body, "html")
}
// SendTestMail 发送测试邮件
func SendTestMail(config *model.EmailConfig, to string) error {
subject := "这是一封来自HotGo的测试邮件"
body := `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="iso-8859-15">
<title>这是一封来自HotGo的测试邮件</title>
</head>
<body>
当你收到这封邮件的时候,说明已经联调成功了,恭喜你!
</body>
</html>`
return Send(config, to, subject, body)
}
func sendToMail(config *model.EmailConfig, to, subject, body, mailType string) error {
if config == nil {
return gerror.New("邮件配置不能为空")
}
var (
contentType string
auth = smtp.PlainAuth("", config.User, config.Password, config.Host)
sendTo = strings.Split(to, ";")
)
if len(sendTo) == 0 {
return gerror.New("收件人不能为空")
}
for _, em := range sendTo {
if !validate.IsEmail(em) {
return gerror.Newf("邮件格式不正确,请检查:%v", em)
}
}
if mailType == "html" {
contentType = "Content-Type: text/" + mailType + "; charset=UTF-8"
} else {
contentType = "Content-Type: text/plain" + "; charset=UTF-8"
}
msg := []byte("To: " + to + "\r\nFrom: " + config.SendName + "<" + config.User + ">" + "\r\nSubject: " + subject + "\r\n" + contentType + "\r\n\r\n" + body)
return smtp.SendMail(config.Addr, auth, config.User, sendTo, msg)
}

View File

@@ -0,0 +1,112 @@
// Package jwt
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package jwt
import (
"context"
"fmt"
j "github.com/dgrijalva/jwt-go"
"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"hotgo/internal/consts"
"hotgo/internal/library/cache"
"hotgo/internal/model"
"time"
)
// GenerateLoginToken 为指定用户生成token
func GenerateLoginToken(ctx context.Context, user *model.Identity, isRefresh bool) (interface{}, error) {
var (
jwtVersion, _ = g.Cfg().Get(ctx, "jwt.version", "1.0")
jwtSign, _ = g.Cfg().Get(ctx, "jwt.sign", "hotGo")
token = j.NewWithClaims(j.SigningMethodHS256, j.MapClaims{
"id": user.Id,
"username": user.Username,
"realname": user.RealName,
"avatar": user.Avatar,
"email": user.Email,
"mobile": user.Mobile,
"last_time": user.LastTime,
"last_ip": user.LastIp,
"exp": user.Exp,
"expires": user.Expires,
"app": user.App,
"role": user.Role,
"role_key": user.RoleKey,
"visit_count": user.VisitCount,
"is_refresh": isRefresh,
"jwt_version": jwtVersion.String(),
})
)
tokenString, err := token.SignedString(jwtSign.Bytes())
if err != nil {
return nil, gerror.New(err.Error())
}
var (
tokenStringMd5 = gmd5.MustEncryptString(tokenString)
// 绑定登录token
c = cache.New()
key = consts.RedisJwtToken + tokenStringMd5
// 将有效期转为持续时间,单位:秒
expires, _ = time.ParseDuration(fmt.Sprintf("+%vs", user.Expires))
)
err = c.Set(ctx, key, tokenString, expires)
if err != nil {
return nil, gerror.New(err.Error())
}
err = c.Set(ctx, consts.RedisJwtUserBind+user.App+":"+gconv.String(user.Id), key, expires)
if err != nil {
return nil, gerror.New(err.Error())
}
return tokenString, err
}
// ParseToken 解析token
func ParseToken(tokenString string, secret []byte) (j.MapClaims, error) {
if tokenString == "" {
err := gerror.New("token 为空")
return nil, err
}
token, err := j.Parse(tokenString, func(token *j.Token) (interface{}, error) {
if _, ok := token.Method.(*j.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
})
if token == nil {
err := gerror.New("token不存在")
return nil, err
}
if claims, ok := token.Claims.(j.MapClaims); ok && token.Valid {
return claims, nil
} else {
return nil, err
}
}
// GetAuthorization 获取authorization
func GetAuthorization(r *ghttp.Request) string {
// 默认从请求头获取
var authorization = r.Header.Get("Authorization")
// 如果请求头不存在则从get参数获取
if authorization == "" {
return r.Get("authorization").String()
}
return gstr.Replace(authorization, "Bearer ", "")
}

View File

@@ -0,0 +1,197 @@
// Package location
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package location
import (
"context"
"github.com/axgle/mahonia"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/kayon/iploc"
"hotgo/utility/validate"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
)
type IpLocationData struct {
Ip string `json:"ip"`
Country string `json:"country"`
Region string `json:"region"`
Province string `json:"province"`
ProvinceCode int64 `json:"province_code"`
City string `json:"city"`
CityCode int64 `json:"city_code"`
Area string `json:"area"`
AreaCode int64 `json:"area_code"`
}
// WhoisLocation 通过Whois接口查询IP归属地
func WhoisLocation(ctx context.Context, ip string) IpLocationData {
type whoisRegionData struct {
Ip string `json:"ip"`
Pro string `json:"pro" `
ProCode string `json:"proCode" `
City string `json:"city" `
CityCode string `json:"cityCode"`
Region string `json:"region"`
RegionCode string `json:"regionCode"`
Addr string `json:"addr"`
Err string `json:"err"`
}
if !validate.IsIp(ip) {
return IpLocationData{}
}
response, err := g.Client().Timeout(10*time.Second).Get(ctx, "http://whois.pconline.com.cn/ipJson.jsp?ip="+ip+"&json=true")
if err != nil {
err = gerror.New(err.Error())
return IpLocationData{
Ip: ip,
}
}
defer response.Close()
var enc mahonia.Decoder
enc = mahonia.NewDecoder("gbk")
data := enc.ConvertString(response.ReadAllString())
whoisData := whoisRegionData{}
if err := gconv.Struct(data, &whoisData); err != nil {
err = gerror.New(err.Error())
g.Log().Print(ctx, "err:", err)
return IpLocationData{
Ip: ip,
}
}
return IpLocationData{
Ip: whoisData.Ip,
//Country string `json:"country"`
Region: whoisData.Addr,
Province: whoisData.Pro,
ProvinceCode: gconv.Int64(whoisData.ProCode),
City: whoisData.City,
CityCode: gconv.Int64(whoisData.CityCode),
Area: whoisData.Region,
AreaCode: gconv.Int64(whoisData.RegionCode),
}
}
// Cz88Find 通过Cz88的IP库查询IP归属地
func Cz88Find(ctx context.Context, ip string) IpLocationData {
if !validate.IsIp(ip) {
g.Log().Print(ctx, "ip格式错误:", ip)
return IpLocationData{}
}
loc, err := iploc.OpenWithoutIndexes("./storage/ip/qqwry-utf8.dat")
if err != nil {
err = gerror.New(err.Error())
return IpLocationData{
Ip: ip,
}
}
detail := loc.Find(ip)
if detail == nil {
return IpLocationData{
Ip: ip,
}
}
locationData := IpLocationData{
Ip: ip,
Country: detail.Country,
Region: detail.Region,
Province: detail.Province,
City: detail.City,
Area: detail.County,
}
if gstr.LenRune(locationData.Province) == 0 {
return locationData
}
return locationData
}
// IsJurisByIpTitle 判断地区名称是否为直辖市
func IsJurisByIpTitle(title string) bool {
lists := []string{"北京市", "天津市", "重庆市", "上海市"}
for i := 0; i < len(lists); i++ {
if gstr.Contains(lists[i], title) {
return true
}
}
return false
}
// GetLocation 获取IP归属地信息
func GetLocation(ctx context.Context, ip string) IpLocationData {
method, _ := g.Cfg().Get(ctx, "hotgo.ipMethod", "cz88")
if method.String() == "whois" {
return WhoisLocation(ctx, ip)
}
return Cz88Find(ctx, ip)
}
// GetPublicIP 获取公网IP
func GetPublicIP() (ip string, err error) {
response, err := http.Get("http://members.3322.org/dyndns/getip")
if err != nil {
return
}
defer response.Body.Close()
body, _ := ioutil.ReadAll(response.Body)
ip = string(body)
ip = strings.ReplaceAll(ip, "\n", "")
return
}
// GetLocalIP 获取服务器内网IP
func GetLocalIP() (ip string, err error) {
addrs, err := net.InterfaceAddrs()
if err != nil {
return
}
for _, addr := range addrs {
ipAddr, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ipAddr.IP.IsLoopback() {
continue
}
if !ipAddr.IP.IsGlobalUnicast() {
continue
}
return ipAddr.IP.String(), nil
}
return
}
// GetClientIp 获取客户端IP
func GetClientIp(r *ghttp.Request) string {
ip := r.Header.Get("X-Forwarded-For")
if ip == "" {
ip = r.GetClientIp()
}
return ip
}

View File

@@ -0,0 +1,83 @@
package feishu
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/go-resty/resty/v2"
"hotgo/internal/library/notify/feishu/internal/security"
)
const feishuAPI = "https://open.feishu.cn/open-apis/bot/v2/hook/"
// Client feishu client
type Client struct {
AccessToken string
Secret string
}
// NewClient new client
func NewClient(accessToken, secret string) *Client {
return &Client{
AccessToken: accessToken,
Secret: secret,
}
}
// Response response struct
type Response struct {
Code int64 `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
Extra interface{} `json:"Extra"`
StatusCode int64 `json:"StatusCode"`
StatusMessage string `json:"StatusMessage"`
}
// Send send message
func (d *Client) Send(message Message) (string, *Response, error) {
res := &Response{}
if len(d.AccessToken) < 1 {
return "", res, fmt.Errorf("accessToken is empty")
}
timestamp := time.Now().Unix()
sign, err := security.GenSign(d.Secret, timestamp)
if err != nil {
return "", res, err
}
body := message.Body()
body["timestamp"] = strconv.FormatInt(timestamp, 10)
body["sign"] = sign
reqBytes, err := json.Marshal(body)
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).
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)
if result.Code != 0 {
return reqString, result, fmt.Errorf("send message to feishu error = %s", result.Msg)
}
return reqString, result, nil
}

View File

@@ -0,0 +1,24 @@
package feishu
type ImageMessage struct {
MsgType MsgType `json:"msg_type"`
Content ImageContent `json:"content"`
}
type ImageContent struct {
ImageKey string `json:"image_key"`
}
func (m *ImageMessage) Body() map[string]interface{} {
m.MsgType = MsgTypeImage
return structToMap(m)
}
func NewImageMessage() *ImageMessage {
return &ImageMessage{}
}
func (m *ImageMessage) SetImageKey(key string) *ImageMessage {
m.Content.ImageKey = key
return m
}

View File

@@ -0,0 +1,21 @@
package feishu
type InteractiveMessage struct {
MsgType MsgType `json:"msg_type"`
Card string `json:"card"`
}
func (m *InteractiveMessage) Body() map[string]interface{} {
m.MsgType = MsgTypeInteractive
return structToMap(m)
}
func NewInteractiveMessage() *InteractiveMessage {
return &InteractiveMessage{}
}
// SetCard set card with cardbuilder https://open.feishu.cn/tool/cardbuilder?from=custom_bot_doc
func (m *InteractiveMessage) SetCard(card string) *InteractiveMessage {
m.Card = card
return m
}

View File

@@ -0,0 +1,21 @@
package security
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
)
// GenSign generate sign
func GenSign(secret string, timestamp int64) (string, error) {
stringToSign := fmt.Sprintf("%v", timestamp) + "\n" + secret
var data []byte
h := hmac.New(sha256.New, []byte(stringToSign))
_, err := h.Write(data)
if err != nil {
return "", err
}
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return signature, nil
}

View File

@@ -0,0 +1,66 @@
package feishu
import (
"reflect"
"strings"
)
type MsgType string
// MsgType
const (
MsgTypeText MsgType = "text"
MsgTypePost MsgType = "post"
MsgTypeImage MsgType = "image"
MsgTypeShareChat MsgType = "share_chat"
MsgTypeInteractive MsgType = "interactive"
)
// Message interface
type Message interface {
Body() map[string]interface{}
}
func structToMap(item interface{}) map[string]interface{} {
res := map[string]interface{}{}
if item == nil {
return res
}
v := reflect.TypeOf(item)
reflectValue := reflect.ValueOf(item)
reflectValue = reflect.Indirect(reflectValue)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
for i := 0; i < v.NumField(); i++ {
tag := v.Field(i).Tag.Get("json")
// remove omitEmpty
omitEmpty := false
if strings.HasSuffix(tag, "omitempty") {
omitEmpty = true
idx := strings.Index(tag, ",")
if idx > 0 {
tag = tag[:idx]
} else {
tag = ""
}
}
field := reflectValue.Field(i).Interface()
if tag != "" && tag != "-" {
if omitEmpty && reflectValue.Field(i).IsZero() {
continue
}
if v.Field(i).Type.Kind() == reflect.Struct {
res[tag] = structToMap(field)
} else {
res[tag] = field
}
}
}
return res
}

View File

@@ -0,0 +1,170 @@
package feishu
import (
"encoding/json"
"log"
)
type PostMessage struct {
MsgType MsgType `json:"msg_type"`
Content PostContent `json:"content"`
}
func NewPostMessage() *PostMessage {
return &PostMessage{}
}
func (m *PostMessage) Body() map[string]interface{} {
m.MsgType = MsgTypePost
return structToMap(m)
}
func (m *PostMessage) SetZH(u PostUnit) *PostMessage {
m.Content.Post.ZH = u
return m
}
func (m *PostMessage) SetZHTitle(t string) *PostMessage {
m.Content.Post.ZH.Title = t
return m
}
func (m *PostMessage) AppendZHContent(i []PostItem) *PostMessage {
m.Content.Post.ZH.Content = append(m.Content.Post.ZH.Content, i)
return m
}
func (m *PostMessage) SetJA(u PostUnit) *PostMessage {
m.Content.Post.JA = u
return m
}
func (m *PostMessage) SetJATitle(t string) *PostMessage {
m.Content.Post.JA.Title = t
return m
}
func (m *PostMessage) AppendJAContent(i []PostItem) *PostMessage {
m.Content.Post.JA.Content = append(m.Content.Post.JA.Content, i)
return m
}
func (m *PostMessage) SetEN(u PostUnit) *PostMessage {
m.Content.Post.EN = u
return m
}
func (m *PostMessage) SetENTitle(t string) *PostMessage {
m.Content.Post.EN.Title = t
return m
}
func (m *PostMessage) AppendENContent(i []PostItem) *PostMessage {
m.Content.Post.EN.Content = append(m.Content.Post.EN.Content, i)
return m
}
type PostContent struct {
Post PostBody `json:"post"`
}
type PostBody struct {
ZH PostUnit `json:"zh_cn,omitempty"`
JA PostUnit `json:"ja_jp,omitempty"`
EN PostUnit `json:"en_us,omitempty"`
}
type PostUnit struct {
Title string `json:"title,omitempty"`
Content [][]PostItem `json:"content"`
}
type PostItem interface{}
type Text struct {
Tag string `json:"tag"`
Text string `json:"text"`
UnEscape bool `json:"un_escape,omitempty"`
}
func NewText(text string) Text {
t := Text{
Tag: "text",
Text: text,
}
return t
}
type A struct {
Tag string `json:"tag"`
Text string `json:"text"`
Href string `json:"href"`
UnEscape bool `json:"un_escape,omitempty"`
}
func NewA(text, href string) A {
t := A{
Tag: "a",
Text: text,
Href: href,
}
return t
}
type AT struct {
Tag string `json:"tag"`
UserID string `json:"user_id"`
}
func NewAT(userID string) AT {
t := AT{
Tag: "at",
UserID: userID,
}
return t
}
type Image struct {
Tag string `json:"tag"`
ImageKey string `json:"image_key"`
Height int `json:"height"`
Width int `json:"width"`
}
func NewImage(imageKey string, height, width int) Image {
t := Image{
Tag: "image",
ImageKey: imageKey,
Height: height,
Width: width,
}
return t
}
type PostCMDMessage struct {
MsgType MsgType `json:"msg_type"`
Content PostCMDContent `json:"content"`
}
func (m *PostCMDMessage) Body() map[string]interface{} {
m.MsgType = MsgTypePost
return structToMap(m)
}
type PostCMDContent struct {
Post map[string]interface{} `json:"post"`
}
func NewPostCMDMessage() *PostCMDMessage {
return &PostCMDMessage{}
}
func (m *PostCMDMessage) SetPost(post string) *PostCMDMessage {
var result map[string]interface{}
err := json.Unmarshal([]byte(post), &result)
if err != nil {
log.Print("SetPost err: ", err)
}
m.Content.Post = result
return m
}

View File

@@ -0,0 +1,24 @@
package feishu
type ShareChatMessage struct {
MsgType MsgType `json:"msg_type"`
Content ShareChatContent `json:"content"`
}
type ShareChatContent struct {
ShareChatID string `json:"share_chat_id"`
}
func (m *ShareChatMessage) Body() map[string]interface{} {
m.MsgType = MsgTypeShareChat
return structToMap(m)
}
func NewShareChatMessage() *ShareChatMessage {
return &ShareChatMessage{}
}
func (m *ShareChatMessage) SetShareChatID(ID string) *ShareChatMessage {
m.Content.ShareChatID = ID
return m
}

View File

@@ -0,0 +1,24 @@
package feishu
type TextMessage struct {
MsgType MsgType `json:"msg_type"`
Content Content `json:"content"`
}
type Content struct {
Text string `json:"text"`
}
func (m *TextMessage) Body() map[string]interface{} {
m.MsgType = MsgTypeText
return structToMap(m)
}
func NewTextMessage() *TextMessage {
return &TextMessage{}
}
func (m *TextMessage) SetText(text string) *TextMessage {
m.Content.Text = text
return m
}

View File

@@ -0,0 +1,216 @@
// Package queue
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package queue
import (
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"hotgo/utility/charset"
"sync"
"time"
)
type MqProducer interface {
SendMsg(topic string, body string) (mqMsg MqMsg, err error)
SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error)
}
type MqConsumer interface {
ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error)
}
const (
_ = iota
SendMsg
ReceiveMsg
)
type Config struct {
Switch bool `json:"switch"`
Driver string `json:"driver"`
Retry int `json:"retry"`
MultiComsumer bool `json:"multiComsumer"`
GroupName string `json:"groupName"`
Redis RedisConf
Rocketmq RocketmqConf
Kafka KafkaConf
}
type RedisConf struct {
Address string `json:"address"`
Db int `json:"db"`
Pass string `json:"pass"`
Timeout int `json:"timeout"`
}
type RocketmqConf struct {
Address []string `json:"address"`
LogLevel string `json:"logLevel"`
}
type KafkaConf struct {
Address []string `json:"address"`
Version string `json:"version"`
RandClient bool `json:"randClient"`
}
type MqMsg struct {
RunType int `json:"run_type"`
Topic string `json:"topic"`
MsgId string `json:"msg_id"`
Offset int64 `json:"offset"`
Partition int32 `json:"partition"`
Timestamp time.Time `json:"timestamp"`
Body []byte `json:"body"`
}
var (
ctx = gctx.New()
mqProducerInstanceMap map[string]MqProducer
mqConsumerInstanceMap map[string]MqConsumer
mutex sync.Mutex
config Config
)
func init() {
mqProducerInstanceMap = make(map[string]MqProducer)
mqConsumerInstanceMap = make(map[string]MqConsumer)
get, err := g.Cfg().Get(ctx, "queue")
if err != nil {
g.Log().Fatalf(ctx, "queue config load fail, err .%v", err)
return
}
get.Scan(&config)
}
// InstanceConsumer 实例化消费者
func InstanceConsumer() (mqClient MqConsumer, err error) {
return NewConsumer(config.GroupName)
}
// InstanceProducer 实例化生产者
func InstanceProducer() (mqClient MqProducer, err error) {
return NewProducer(config.GroupName)
}
// NewProducer 新建一个生产者实例
func NewProducer(groupName string) (mqClient MqProducer, err error) {
if item, ok := mqProducerInstanceMap[groupName]; ok {
return item, nil
}
if groupName == "" {
return mqClient, gerror.New("mq groupName is empty.")
}
switch config.Driver {
case "rocketmq":
if len(config.Rocketmq.Address) == 0 {
g.Log().Fatal(ctx, "queue rocketmq address is not support")
}
mqClient = RegisterRocketProducerMust(config.Rocketmq.Address, groupName, config.Retry)
case "kafka":
if len(config.Kafka.Address) == 0 {
g.Log().Fatal(ctx, "queue kafka address is not support")
}
mqClient = RegisterKafkaProducerMust(KafkaConfig{
Brokers: config.Kafka.Address,
GroupID: groupName,
Version: config.Kafka.Version,
})
case "redis":
address, _ := g.Cfg().Get(ctx, "queue.redis.address", nil)
if len(address.String()) == 0 {
g.Log().Fatal(ctx, "queue redis address is not support")
}
mqClient = RegisterRedisMqProducerMust(RedisOption{
Addr: config.Redis.Address,
Passwd: config.Redis.Pass,
DBnum: config.Redis.Db,
Timeout: config.Redis.Timeout,
}, PoolOption{
5, 50, 5,
}, groupName, config.Retry)
default:
g.Log().Fatal(ctx, "queue driver is not support")
}
mutex.Lock()
defer mutex.Unlock()
mqProducerInstanceMap[groupName] = mqClient
return mqClient, nil
}
// NewConsumer 新建一个消费者实例
func NewConsumer(groupName string) (mqClient MqConsumer, err error) {
randTag := string(charset.RandomCreateBytes(6))
// 是否支持创建多个消费者
if config.MultiComsumer == false {
randTag = "001"
}
if item, ok := mqConsumerInstanceMap[groupName+"-"+randTag]; ok {
return item, nil
}
if groupName == "" {
return mqClient, gerror.New("mq groupName is empty.")
}
switch config.Driver {
case "rocketmq":
if len(config.Rocketmq.Address) == 0 {
return nil, gerror.New("queue.rocketmq.address is empty.")
}
mqClient = RegisterRocketConsumerMust(config.Rocketmq.Address, groupName)
case "kafka":
if len(config.Kafka.Address) == 0 {
g.Log().Fatal(ctx, "queue kafka address is not support")
}
clientId := "HOTGO-Consumer-" + groupName
if config.Kafka.RandClient {
clientId += "-" + randTag
}
mqClient = RegisterKafkaMqConsumerMust(KafkaConfig{
Brokers: config.Kafka.Address,
GroupID: groupName,
Version: config.Kafka.Version,
ClientId: clientId,
})
case "redis":
if len(config.Redis.Address) == 0 {
g.Log().Fatal(ctx, "queue redis address is not support")
}
mqClient = RegisterRedisMqConsumerMust(RedisOption{
Addr: config.Redis.Address,
Passwd: config.Redis.Pass,
DBnum: config.Redis.Db,
Timeout: config.Redis.Timeout,
}, PoolOption{
5, 50, 5,
}, groupName)
default:
g.Log().Fatal(ctx, "queue driver is not support")
}
mutex.Lock()
defer mutex.Unlock()
mqConsumerInstanceMap[groupName] = mqClient
return mqClient, nil
}
// BodyString 返回消息体
func (m *MqMsg) BodyString() string {
return string(m.Body)
}

View File

@@ -0,0 +1,249 @@
// Package queue
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package queue
import (
"context"
"fmt"
"github.com/Shopify/sarama"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"hotgo/utility/signal"
"time"
)
type KafkaMq struct {
endPoints []string
Partitions int32
producerIns sarama.AsyncProducer
consumerIns sarama.ConsumerGroup
}
type KafkaConfig struct {
ClientId string
Brokers []string
GroupID string
Partitions int32
Replication int16
Version string
UserName string
Password string
}
// SendMsg 按字符串类型生产数据
func (r *KafkaMq) SendMsg(topic string, body string) (mqMsg MqMsg, err error) {
return r.SendByteMsg(topic, []byte(body))
}
// SendByteMsg 生产数据
func (r *KafkaMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error) {
msg := &sarama.ProducerMessage{
Topic: topic,
Value: sarama.ByteEncoder(body),
Timestamp: time.Now(),
}
if r.producerIns == nil {
return mqMsg, gerror.New("queue kafka producerIns is nil")
}
r.producerIns.Input() <- msg
ctx, cancle := context.WithTimeout(context.Background(), 5*time.Second)
defer cancle()
select {
case info := <-r.producerIns.Successes():
return MqMsg{
RunType: SendMsg,
Topic: info.Topic,
Offset: info.Offset,
Partition: info.Partition,
Timestamp: info.Timestamp,
}, nil
case fail := <-r.producerIns.Errors():
if nil != fail {
return mqMsg, fail.Err
}
case <-ctx.Done():
return mqMsg, gerror.New("send mqMst timeout")
}
return mqMsg, nil
}
// ListenReceiveMsgDo 消费数据
func (r *KafkaMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
if r.consumerIns == nil {
return gerror.New("queue kafka consumer not register")
}
consumer := Consumer{
ready: make(chan bool),
receiveDoFun: receiveDo,
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
if err := r.consumerIns.Consume(ctx, []string{topic}, &consumer); err != nil {
FatalLog(ctx, "kafka Error from consumer", err)
}
if ctx.Err() != nil {
Log(ctx, fmt.Sprintf("kafka consoumer stop : %v", ctx.Err()))
return
}
consumer.ready = make(chan bool)
}
}()
<-consumer.ready // Await till the consumer has been set up
Log(ctx, "kafka consumer up and running!...")
signal.AppDefer(func() {
Log(ctx, "kafka consumer close...")
cancel()
if err = r.consumerIns.Close(); err != nil {
FatalLog(ctx, "kafka Error closing client", err)
}
})
return
}
// RegisterKafkaMqConsumerMust 注册消费者
func RegisterKafkaMqConsumerMust(connOpt KafkaConfig) (client MqConsumer) {
mqIns := &KafkaMq{}
kfkVersion, _ := sarama.ParseKafkaVersion(connOpt.Version)
if validateVersion(kfkVersion) == false {
kfkVersion = sarama.V2_4_0_0
}
brokers := connOpt.Brokers
config := sarama.NewConfig()
config.Consumer.Return.Errors = true
config.Version = kfkVersion
if connOpt.UserName != "" {
config.Net.SASL.Enable = true
config.Net.SASL.User = connOpt.UserName
config.Net.SASL.Password = connOpt.Password
}
// 默认按随机方式消费
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
config.Consumer.Offsets.Initial = sarama.OffsetNewest
config.Consumer.Offsets.AutoCommit.Interval = 10 * time.Millisecond
config.ClientID = connOpt.ClientId
consumerClient, err := sarama.NewConsumerGroup(brokers, connOpt.GroupID, config)
if err != nil {
g.Log().Fatal(ctx, err)
}
mqIns.consumerIns = consumerClient
return mqIns
}
// RegisterKafkaProducerMust 注册并启动生产者接口实现
func RegisterKafkaProducerMust(connOpt KafkaConfig) (client MqProducer) {
mqIns := &KafkaMq{}
connOpt.ClientId = "HOTGO-Producer"
RegisterKafkaProducer(connOpt, mqIns) //这里如果使用go程需要处理chan同步问题
return mqIns
}
// RegisterKafkaProducer 注册同步类型实例
func RegisterKafkaProducer(connOpt KafkaConfig, mqIns *KafkaMq) {
kfkVersion, _ := sarama.ParseKafkaVersion(connOpt.Version)
if validateVersion(kfkVersion) == false {
kfkVersion = sarama.V2_4_0_0
}
brokers := connOpt.Brokers
config := sarama.NewConfig()
// 等待服务器所有副本都保存成功后的响应
config.Producer.RequiredAcks = sarama.WaitForAll
// 随机向partition发送消息
config.Producer.Partitioner = sarama.NewRandomPartitioner
// 是否等待成功和失败后的响应,只有上面的RequireAcks设置不是NoReponse这里才有用.
config.Producer.Return.Successes = true
config.Producer.Return.Errors = true
config.Producer.Compression = sarama.CompressionNone
config.ClientID = connOpt.ClientId
config.Version = kfkVersion
if connOpt.UserName != "" {
config.Net.SASL.Enable = true
config.Net.SASL.User = connOpt.UserName
config.Net.SASL.Password = connOpt.Password
}
var err error
mqIns.producerIns, err = sarama.NewAsyncProducer(brokers, config)
if err != nil {
g.Log().Fatal(ctx, err)
}
signal.AppDefer(func() {
Log(ctx, "kafka producer AsyncClose...")
mqIns.producerIns.AsyncClose()
})
}
// validateVersion 验证版本是否有效
func validateVersion(version sarama.KafkaVersion) bool {
for _, item := range sarama.SupportedVersions {
if version.String() == item.String() {
return true
}
}
return false
}
type Consumer struct {
ready chan bool
receiveDoFun func(mqMsg MqMsg)
}
// Setup is run at the beginning of a new session, before ConsumeClaim
func (consumer *Consumer) Setup(sarama.ConsumerGroupSession) error {
// Mark the consumer as ready
close(consumer.ready)
return nil
}
// Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited
func (consumer *Consumer) Cleanup(sarama.ConsumerGroupSession) error {
return nil
}
// ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages().
func (consumer *Consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
// NOTE:
// Do not move the code below to a goroutine.
// The `ConsumeClaim` itself is called within a goroutine, see:
// https://github.com/Shopify/sarama/blob/master/consumer_group.go#L27-L29
// `ConsumeClaim` 方法已经是 goroutine 调用 不要在该方法内进行 goroutine
for message := range claim.Messages() {
consumer.receiveDoFun(MqMsg{
RunType: ReceiveMsg,
Topic: message.Topic,
Body: message.Value,
Offset: message.Offset,
Timestamp: message.Timestamp,
Partition: message.Partition,
})
session.MarkMessage(message, "")
}
return nil
}

View File

@@ -0,0 +1,42 @@
// Package queue
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 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/internal/consts"
)
// ConsumerLog 消费日志
func ConsumerLog(ctx context.Context, topic string, mqMsg MqMsg, err error) {
if err != nil {
g.Log(consts.QueueLogPath).Error(ctx, "消费 ["+topic+"] 失败", mqMsg, err)
} else {
g.Log(consts.QueueLogPath).Debug(ctx, "消费 ["+topic+"] 成功", mqMsg.MsgId)
}
}
// ProducerLog 生产日志
func ProducerLog(ctx context.Context, topic string, data interface{}, err error) {
if err != nil {
g.Log(consts.QueueLogPath).Error(ctx, "生产 ["+topic+"] 失败", gconv.String(data))
} else {
g.Log(consts.QueueLogPath).Debug(ctx, "生产 ["+topic+"] 成功", gconv.String(data))
}
}
// FatalLog 致命日志
func FatalLog(ctx context.Context, text string, err error) {
g.Log(consts.QueueLogPath).Fatal(ctx, text+":", err)
}
// Log 通用日志
func Log(ctx context.Context, text string) {
g.Log(consts.QueueLogPath).Debug(ctx, text)
}

View File

@@ -0,0 +1,289 @@
package queue
import (
"encoding/json"
"fmt"
"github.com/bufanyun/pool"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gomodule/redigo/redis"
"hotgo/internal/consts"
"hotgo/utility/encrypt"
"math/rand"
"time"
)
type RedisMq struct {
poolName string
groupName string
retry int
timeout int
}
type PoolOption struct {
InitCap int
MaxCap int
IdleTimeout int
}
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)
}
// SendMsg 按字符串类型生产数据
func (r *RedisMq) SendMsg(topic string, body string) (mqMsg MqMsg, err error) {
return r.SendByteMsg(topic, []byte(body))
}
// SendByteMsg 生产数据
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,
Body: body,
Timestamp: time.Now(),
}
mqMsgJson, err := json.Marshal(mqMsg)
if err != nil {
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者解析json消息失败:", err))
}
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))
}
if r.timeout > 0 {
_, err = rdx.Do("EXPIRE", queueName, r.timeout)
if err != nil {
return mqMsg, gerror.New(fmt.Sprint("queue redis 生产者设置过期时间失败:", err))
}
}
return
}
// ListenReceiveMsgDo 消费数据
func (r *RedisMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
if r.poolName == "" {
return gerror.New("RedisMq producer not register")
}
if topic == "" {
return gerror.New("RedisMq topic is empty")
}
queueName := r.genQueueName(r.groupName, topic)
go func() {
for range time.Tick(500 * time.Millisecond) {
mqMsgList := r.loopReadQueue(queueName)
for _, mqMsg := range mqMsgList {
receiveDo(mqMsg)
}
}
}()
select {}
}
// 生成队列名称
func (r *RedisMq) genQueueName(groupName string, topic string) string {
return fmt.Sprintf(consts.QueueName+"%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
}
for {
infoByte, err := redis.Bytes(rdx.Do("RPOP", queueName))
if redis.ErrNil == err || len(infoByte) == 0 {
break
}
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)
break
}
if mqMsg.MsgId != "" {
mqMsgList = append(mqMsgList, mqMsg)
}
}
return mqMsgList
}
func RegisterRedisMqProducerMust(connOpt RedisOption, poolOpt PoolOption, groupName string, retry int) (client MqProducer) {
var err error
client, err = RegisterRedisMq(connOpt, poolOpt, groupName, retry)
if err != nil {
g.Log().Fatal(ctx, "RegisterRedisMqProducerMust err:%+v", err)
return
}
return client
}
// RegisterRedisMqConsumerMust 注册消费者
func RegisterRedisMqConsumerMust(connOpt RedisOption, poolOpt PoolOption, groupName string) (client MqConsumer) {
var err error
client, err = RegisterRedisMq(connOpt, poolOpt, groupName, 0)
if err != nil {
g.Log().Fatal(ctx, "RegisterRedisMqConsumerMust err:%+v", err)
return
}
return client
}
// 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,
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() {
redisPool.Put(conn)
}
db = conn.(redis.Conn)
return db, put, nil
}
func getRandMsgId() (msgId string) {
rand.Seed(time.Now().UnixNano())
radium := rand.Intn(999) + 1
timeCode := time.Now().UnixNano()
msgId = fmt.Sprintf("%d%.4d", timeCode, radium)
return msgId
}

View File

@@ -0,0 +1,171 @@
// Package queue
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package queue
import (
"context"
"errors"
"fmt"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/consumer"
"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/frame/g"
)
type RocketMq struct {
endPoints []string
producerIns rocketmq.Producer
consumerIns rocketmq.PushConsumer
}
// rewriteLog 重写日志
func rewriteLog() {
level, _ := g.Cfg().Get(ctx, "queue.rocketmq.logLevel", "debug")
rlog.SetLogger(&RocketMqLogger{Flag: "[rocket_mq]", LevelLog: level.String()})
}
// RegisterRocketProducerMust 注册并启动生产者接口实现
func RegisterRocketProducerMust(endPoints []string, groupName string, retry int) (client MqProducer) {
rewriteLog()
var err error
client, err = RegisterRocketMqProducer(endPoints, groupName, retry)
if err != nil {
panic(err)
}
return client
}
// RegisterRocketConsumerMust 注册消费者
func RegisterRocketConsumerMust(endPoints []string, groupName string) (client MqConsumer) {
rewriteLog()
var err error
client, err = RegisterRocketMqConsumer(endPoints, groupName)
if err != nil {
panic(err)
}
return client
}
// SendMsg 按字符串类型生产数据
func (r *RocketMq) SendMsg(topic string, body string) (mqMsg MqMsg, err error) {
return r.SendByteMsg(topic, []byte(body))
}
// SendByteMsg 生产数据
func (r *RocketMq) SendByteMsg(topic string, body []byte) (mqMsg MqMsg, err error) {
if r.producerIns == nil {
return mqMsg, errors.New("RocketMq producer not register")
}
result, err := r.producerIns.SendSync(context.Background(), &primitive.Message{
Topic: topic,
Body: body,
})
if err != nil {
return
}
if result.Status != primitive.SendOK {
return mqMsg, errors.New(fmt.Sprintf("RocketMq producer send msg error status:%v", result.Status))
}
mqMsg = MqMsg{
RunType: SendMsg,
Topic: topic,
MsgId: result.MsgID,
Body: body,
}
return mqMsg, nil
}
// ListenReceiveMsgDo 消费数据
func (r *RocketMq) ListenReceiveMsgDo(topic string, receiveDo func(mqMsg MqMsg)) (err error) {
if r.consumerIns == nil {
return errors.New("RocketMq consumer not register")
}
err = r.consumerIns.Subscribe(topic, consumer.MessageSelector{}, func(ctx context.Context,
msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
for _, item := range msgs {
go receiveDo(MqMsg{
RunType: ReceiveMsg,
Topic: item.Topic,
MsgId: item.MsgId,
Body: item.Body,
})
}
return consumer.ConsumeSuccess, nil
})
if err != nil {
return err
}
err = r.consumerIns.Start()
if err != nil {
r.consumerIns.Unsubscribe(topic)
return err
}
return
}
// RegisterRocketMqProducer 注册rocketmq生产者
func RegisterRocketMqProducer(endPoints []string, groupName string, retry int) (mqIns *RocketMq, err error) {
addr, err := primitive.NewNamesrvAddr(endPoints...)
if err != nil {
return nil, err
}
mqIns = &RocketMq{
endPoints: endPoints,
}
if retry <= 0 {
retry = 0
}
mqIns.producerIns, err = rocketmq.NewProducer(
producer.WithNameServer(addr),
producer.WithRetry(retry),
producer.WithGroupName(groupName),
)
if err != nil {
return nil, err
}
err = mqIns.producerIns.Start()
if err != nil {
return nil, err
}
return mqIns, nil
}
// RegisterRocketMqConsumer 注册rocketmq消费者
func RegisterRocketMqConsumer(endPoints []string, groupName string) (mqIns *RocketMq, err error) {
addr, err := primitive.NewNamesrvAddr(endPoints...)
if err != nil {
return nil, err
}
mqIns = &RocketMq{
endPoints: endPoints,
}
mqIns.consumerIns, err = rocketmq.NewPushConsumer(
consumer.WithNameServer(addr),
consumer.WithConsumerModel(consumer.Clustering),
consumer.WithGroupName(groupName),
)
if err != nil {
return nil, err
}
return mqIns, nil
}

View File

@@ -0,0 +1,89 @@
// Package queue
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package queue
import (
"fmt"
)
type RocketMqLogger struct {
Flag string
LevelLog string
}
func (l *RocketMqLogger) Debug(msg string, fields map[string]interface{}) {
if l.LevelLog == "close" {
return
}
if msg == "" && len(fields) == 0 {
return
}
if l.LevelLog == "debug" || l.LevelLog == "all" {
Log(ctx, fmt.Sprint(l.Flag, " [debug] ", msg))
}
}
func (l *RocketMqLogger) Level(level string) {
Log(ctx, fmt.Sprint(l.Flag, " [level] ", level))
}
func (l *RocketMqLogger) OutputPath(path string) (err error) {
Log(ctx, fmt.Sprint(l.Flag, " [path] ", path))
return nil
}
func (l *RocketMqLogger) Info(msg string, fields map[string]interface{}) {
if l.LevelLog == "close" {
return
}
if msg == "" && len(fields) == 0 {
return
}
if l.LevelLog == "info" || l.LevelLog == "all" {
Log(ctx, fmt.Sprint(l.Flag, " [info] ", msg))
}
}
func (l *RocketMqLogger) Warning(msg string, fields map[string]interface{}) {
if l.LevelLog == "close" {
return
}
if msg == "" && len(fields) == 0 {
return
}
if l.LevelLog == "warn" || l.LevelLog == "all" {
Log(ctx, fmt.Sprint(l.Flag, " [warn] ", msg))
}
}
func (l *RocketMqLogger) Error(msg string, fields map[string]interface{}) {
if l.LevelLog == "close" {
return
}
if msg == "" && len(fields) == 0 {
return
}
if l.LevelLog == "error" || l.LevelLog == "all" {
Log(ctx, fmt.Sprint(l.Flag, " [error] ", msg))
}
}
func (l *RocketMqLogger) Fatal(msg string, fields map[string]interface{}) {
if l.LevelLog == "close" {
return
}
if msg == "" && len(fields) == 0 {
return
}
if l.LevelLog == "fatal" || l.LevelLog == "all" {
Log(ctx, fmt.Sprint(l.Flag, " [fatal] ", msg))
}
}

View File

@@ -0,0 +1,84 @@
// Package response
// @Link https://github.com/bufanyun/hotgo
// @Copyright Copyright (c) 2022 HotGo CLI
// @Author Ms <133814250@qq.com>
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
//
package response
import (
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gctx"
"hotgo/internal/consts"
"hotgo/internal/library/contexts"
"hotgo/internal/model"
"time"
)
// JsonExit 返回JSON数据并退出当前HTTP执行函数
func JsonExit(r *ghttp.Request, code int, message string, data ...interface{}) {
RJson(r, code, message, data...)
r.Exit()
}
// RJson 标准返回结果数据结构封装
// @Description: 返回固定数据结构的JSON
// @param r
// @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 {
responseData = data[0]
}
Res := &model.Response{
Code: code,
Message: message,
Timestamp: time.Now().Unix(),
TraceID: gctx.CtxId(r.Context()),
}
// 如果不是正常的返回则将data转为error
if consts.CodeOK == code {
Res.Data = responseData
} else {
Res.Error = responseData
}
// 清空响应
r.Response.ClearBuffer()
// 写入响应
r.Response.WriteJson(Res)
// 加入到上下文
contexts.SetResponse(r.Context(), Res)
}
// SusJson 返回成功JSON
func SusJson(isExit bool, r *ghttp.Request, message string, data ...interface{}) {
if isExit {
JsonExit(r, consts.CodeOK, message, data...)
}
RJson(r, consts.CodeOK, message, data...)
}
// FailJson 返回失败JSON
func FailJson(isExit bool, r *ghttp.Request, message string, data ...interface{}) {
if isExit {
JsonExit(r, consts.CodeNil, message, data...)
}
RJson(r, consts.CodeNil, message, data...)
}
// Redirect 重定向
func Redirect(r *ghttp.Request, location string, code ...int) {
r.Response.RedirectTo(location, code...)
}
// Download 下载文件
func Download(r *ghttp.Request, location string, code ...int) {
r.Response.ServeFileDownload("test.txt")
}