mirror of
https://gitee.com/dromara/mayfly-go
synced 2025-11-02 23:40:24 +08:00
feat: message notify
This commit is contained in:
@@ -3,7 +3,7 @@ module mayfly-go
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
gitee.com/chunanyong/dm v1.8.18
|
||||
gitee.com/chunanyong/dm v1.8.19
|
||||
gitee.com/liuzongyang/libpq v1.10.11
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
@@ -14,8 +14,8 @@ require (
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.25.0
|
||||
github.com/go-sql-driver/mysql v1.9.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/go-sql-driver/mysql v1.9.2
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20241220152942-06eb5c6e8230
|
||||
@@ -23,18 +23,18 @@ require (
|
||||
github.com/microsoft/go-mssqldb v1.8.0
|
||||
github.com/mojocn/base64Captcha v1.3.8 // 验证码
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.7
|
||||
github.com/pkg/sftp v1.13.9
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/redis/go-redis/v9 v9.7.1
|
||||
github.com/redis/go-redis/v9 v9.7.3
|
||||
github.com/robfig/cron/v3 v3.0.1 // 定时任务
|
||||
github.com/sijms/go-ora/v2 v2.8.24
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/veops/go-ansiterm v0.0.5
|
||||
go.mongodb.org/mongo-driver v1.16.0 // mongo
|
||||
golang.org/x/crypto v0.36.0 // ssh
|
||||
golang.org/x/oauth2 v0.26.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/crypto v0.37.0 // ssh
|
||||
golang.org/x/oauth2 v0.29.0
|
||||
golang.org/x/sync v0.13.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
// gorm
|
||||
@@ -93,8 +93,8 @@ require (
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||
golang.org/x/image v0.23.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
|
||||
@@ -16,12 +16,12 @@ var (
|
||||
initFuncs = make([]InitFunc, 0)
|
||||
)
|
||||
|
||||
// 添加初始化ioc函数,由各个默认自行添加(直接init方法中ioc.Register注册不会打印ioc相关日志)
|
||||
// 添加初始化ioc函数,由各个模块自行添加(直接init方法中ioc.Register注册不会打印ioc相关日志)
|
||||
func AddInitIocFunc(initIocFunc InitIocFunc) {
|
||||
initIocFuncs = append(initIocFuncs, initIocFunc)
|
||||
}
|
||||
|
||||
// 添加初始化函数,由各个默认自行添加
|
||||
// 添加初始化函数,由各个模块自行添加
|
||||
func AddInitFunc(initFunc InitFunc) {
|
||||
initFuncs = append(initFuncs, initFunc)
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ const (
|
||||
ImsgNumDb = 60000
|
||||
ImsgNumRedis = 70000
|
||||
ImsgNumMongo = 80000
|
||||
ImsgNumMsg = 90000
|
||||
)
|
||||
|
||||
@@ -226,9 +226,10 @@ var (
|
||||
SQLValue: SQLValueNumeric,
|
||||
}
|
||||
|
||||
// 使用string进行转换,避免长度过长导致精度丢失等
|
||||
DTNumeric = &DataType{
|
||||
Name: "numeric",
|
||||
Valuer: ValuerFloat64,
|
||||
Valuer: ValuerString,
|
||||
SQLValue: SQLValueNumeric,
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ func (c *commonTypeConverter) Blob(col *dbi.Column) *dbi.DbDataType {
|
||||
return Blob
|
||||
}
|
||||
func (c *commonTypeConverter) Longblob(col *dbi.Column) *dbi.DbDataType {
|
||||
col.CharMaxLength = 0
|
||||
return Longblob
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package event
|
||||
|
||||
const (
|
||||
EventTopicDeleteMachine = "machine:delete" // 删除机器的事件主题名
|
||||
EventTopicResourceOp = "resource:op" // 资源操作主题
|
||||
EventTopicResourceOp = "resource:op" // 资源操作主题
|
||||
EventTopicBizMsgTmplSend = "biz:msgtmpl:send" // 发送业务关联的消息模板
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ type Procdef struct {
|
||||
Status entity.ProcdefStatus `json:"status" binding:"required"`
|
||||
Condition string `json:"condition"`
|
||||
Remark string `json:"remark"`
|
||||
MsgTmplId uint64 `json:"msgTmplId"`
|
||||
|
||||
CodePaths []string `json:"codePaths"`
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
"mayfly-go/internal/flow/application/dto"
|
||||
"mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/internal/flow/imsg"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
msgentity "mayfly-go/internal/msg/domain/entity"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
@@ -20,12 +23,15 @@ import (
|
||||
type Procdef struct {
|
||||
procdefApp application.Procdef `inject:"T"`
|
||||
tagTreeRelateApp tagapp.TagTreeRelate `inject:"T"`
|
||||
msgTmplBizApp msgapp.MsgTmplBiz `inject:"T"`
|
||||
}
|
||||
|
||||
func (p *Procdef) ReqConfs() *req.Confs {
|
||||
reqs := [...]*req.Conf{
|
||||
req.NewGet("", p.GetProcdefPage),
|
||||
|
||||
req.NewGet("/detail/:id", p.GetProcdefDetail),
|
||||
|
||||
req.NewGet("/:resourceType/:resourceCode", p.GetProcdef),
|
||||
|
||||
req.NewPost("", p.Save).Log(req.NewLogSaveI(imsg.LogProcdefSave)).RequiredPermissionCode("flow:procdef:save"),
|
||||
@@ -49,6 +55,25 @@ func (p *Procdef) GetProcdefPage(rc *req.Ctx) {
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (p *Procdef) GetProcdefDetail(rc *req.Ctx) {
|
||||
def, err := p.procdefApp.GetById(cast.ToUint64(rc.PathParamInt("id")))
|
||||
biz.ErrIsNil(err)
|
||||
res := new(vo.Procdef)
|
||||
biz.ErrIsNil(structx.Copy(res, def))
|
||||
|
||||
p.tagTreeRelateApp.FillTagInfo(tagentity.TagRelateTypeFlowDef, res)
|
||||
|
||||
bizMsgTmpl := &msgentity.MsgTmplBiz{
|
||||
BizId: res.Id,
|
||||
BizType: application.FlowTaskNotifyBizKey,
|
||||
}
|
||||
if p.msgTmplBizApp.GetByCond(bizMsgTmpl) == nil {
|
||||
res.MsgTmplId = &bizMsgTmpl.TmplId
|
||||
}
|
||||
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (p *Procdef) GetProcdef(rc *req.Ctx) {
|
||||
resourceType := rc.PathParamInt("resourceType")
|
||||
resourceCode := rc.PathParam("resourceCode")
|
||||
@@ -61,6 +86,7 @@ func (a *Procdef) Save(rc *req.Ctx) {
|
||||
rc.ReqParam = form
|
||||
biz.ErrIsNil(a.procdefApp.SaveProcdef(rc.MetaCtx, &dto.SaveProcdef{
|
||||
Procdef: procdef,
|
||||
MsgTmplId: form.MsgTmplId,
|
||||
CodePaths: form.CodePaths,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
type Procdef struct {
|
||||
tagentity.RelateTags // 标签信息
|
||||
entity.Procdef
|
||||
|
||||
MsgTmplId *uint64 `json:"msgTmplId" gorm:"-"` // 消息模板ID
|
||||
}
|
||||
|
||||
func (p *Procdef) GetRelateId() uint64 {
|
||||
|
||||
5
server/internal/flow/application/const.go
Normal file
5
server/internal/flow/application/const.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package application
|
||||
|
||||
const (
|
||||
FlowTaskNotifyBizKey = "flow:task:notify" // 工单任务处理通知
|
||||
)
|
||||
@@ -4,6 +4,7 @@ import "mayfly-go/internal/flow/domain/entity"
|
||||
|
||||
type SaveProcdef struct {
|
||||
Procdef *entity.Procdef
|
||||
MsgTmplId uint64 // 消息模板id
|
||||
CodePaths []string
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/internal/flow/domain/repository"
|
||||
"mayfly-go/internal/flow/imsg"
|
||||
msgapp "mayfly-go/internal/msg/application"
|
||||
msgdto "mayfly-go/internal/msg/application/dto"
|
||||
tagapp "mayfly-go/internal/tag/application"
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
@@ -36,6 +38,7 @@ type procdefAppImpl struct {
|
||||
|
||||
procinstApp Procinst `inject:"T"`
|
||||
|
||||
msgTmplBizApp msgapp.MsgTmplBiz `inject:"T"`
|
||||
tagTreeApp tagapp.TagTree `inject:"T"`
|
||||
tagTreeRelateApp tagapp.TagTreeRelate `inject:"T"`
|
||||
}
|
||||
@@ -67,6 +70,14 @@ func (p *procdefAppImpl) SaveProcdef(ctx context.Context, defParam *dto.SaveProc
|
||||
return p.Tx(ctx, func(ctx context.Context) error {
|
||||
return p.Save(ctx, def)
|
||||
}, func(ctx context.Context) error {
|
||||
// 保存通知消息模板
|
||||
if err := p.msgTmplBizApp.SaveBizTmpl(ctx, msgdto.MsgTmplBizSave{
|
||||
TmplId: defParam.MsgTmplId,
|
||||
BizType: FlowTaskNotifyBizKey,
|
||||
BizId: def.Id,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.tagTreeRelateApp.RelateTag(ctx, tagentity.TagRelateTypeFlowDef, def.Id, defParam.CodePaths...)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,19 +3,24 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/event"
|
||||
"mayfly-go/internal/flow/application/dto"
|
||||
"mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/internal/flow/domain/repository"
|
||||
"mayfly-go/internal/flow/imsg"
|
||||
msgdto "mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/contextx"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/i18n"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/anyx"
|
||||
"mayfly-go/pkg/utils/jsonx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type Procinst interface {
|
||||
@@ -140,6 +145,7 @@ func (p *procinstAppImpl) CompleteTask(ctx context.Context, instTaskId uint64, r
|
||||
procinst.SetEnd()
|
||||
} else {
|
||||
procinst.TaskKey = task.TaskKey
|
||||
|
||||
}
|
||||
|
||||
return p.Tx(ctx, func(ctx context.Context) error {
|
||||
@@ -287,7 +293,26 @@ func (p *procinstAppImpl) createProcinstTask(ctx context.Context, procinst *enti
|
||||
TaskName: task.Name,
|
||||
Assignee: task.UserId,
|
||||
}
|
||||
return p.procinstTaskRepo.Insert(ctx, procinstTask)
|
||||
|
||||
if err := p.procinstTaskRepo.Insert(ctx, procinstTask); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 发送通知消息
|
||||
global.EventBus.Publish(ctx, event.EventTopicBizMsgTmplSend, msgdto.BizMsgTmplSend{
|
||||
BizType: FlowTaskNotifyBizKey,
|
||||
BizId: procinst.ProcdefId,
|
||||
Params: map[string]any{
|
||||
"creator": procinst.Creator,
|
||||
"procdefName": procinst.ProcdefName,
|
||||
"bizKey": procinst.BizKey,
|
||||
"taskName": task.Name,
|
||||
"procinstRemark": procinst.Remark,
|
||||
},
|
||||
ReceiverIds: []uint64{cast.ToUint64(task.UserId)},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取下一审批节点任务
|
||||
|
||||
@@ -15,6 +15,7 @@ type ProcinstTaskQuery struct {
|
||||
ProcinstId uint64 `json:"procinstId"` // 流程实例id
|
||||
ProcinstName string `json:"procinstName"` // 流程实例名称
|
||||
BizType string `json:"bizType" form:"bizType"`
|
||||
BizKey string `json:"bizKey" form:"bizKey"` // 业务key
|
||||
Assignee string `json:"assignee"` // 分配到该任务的用户
|
||||
Status ProcinstTaskStatus `json:"status" form:"status"` // 状态
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"mayfly-go/internal/flow/domain/entity"
|
||||
"mayfly-go/internal/flow/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/gormx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,14 @@ func newProcinstTaskRepo() repository.ProcinstTask {
|
||||
}
|
||||
|
||||
func (p *procinstTaskImpl) GetPageList(condition *entity.ProcinstTaskQuery, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
qd := model.NewModelCond(condition)
|
||||
return p.PageByCondToAny(qd, pageParam, toEntity)
|
||||
qd := gormx.NewQueryWithTableName("t_flow_procinst_task t").
|
||||
Joins("JOIN t_flow_procinst tp ON t.procinst_id = tp.id ").
|
||||
WithCond(model.NewCond().Columns("t.*, tp.biz_key").
|
||||
Eq("tp.biz_key", condition.BizKey).
|
||||
Eq0("tp.is_deleted", model.ModelUndeleted).
|
||||
Eq("tp.biz_type", condition.BizType).
|
||||
Eq0("t.is_deleted", model.ModelUndeleted).
|
||||
Eq("t.status", condition.Status).
|
||||
OrderByDesc("t.id"))
|
||||
return gormx.PageQuery(qd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ func (m *MachineScript) RunMachineScript(rc *req.Ctx) {
|
||||
script := ms.Script
|
||||
// 如果有脚本参数,则用脚本参数替换脚本中的模板占位符参数
|
||||
if params := rc.Query("params"); params != "" {
|
||||
script, err = stringx.TemplateParse(ms.Script, jsonx.ToMap(params))
|
||||
p, err := jsonx.ToMap(params)
|
||||
biz.ErrIsNil(err)
|
||||
script, err = stringx.TemplateParse(ms.Script, p)
|
||||
biz.ErrIsNilAppendErr(err, "failed to parse the script template parameter: %s")
|
||||
}
|
||||
cli, err := m.machineApp.GetCliByAc(ac)
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/event"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/pkg/eventbus"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/ioc"
|
||||
"sync"
|
||||
)
|
||||
@@ -26,16 +21,6 @@ func Init() {
|
||||
GetMachineApp().TimerUpdateStats()
|
||||
|
||||
GetMachineTermOpApp().TimerDeleteTermOp()
|
||||
|
||||
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineFile", func(ctx context.Context, event *eventbus.Event) error {
|
||||
me := event.Val.(*entity.Machine)
|
||||
return GetMachineFileApp().DeleteByCond(ctx, &entity.MachineFile{MachineId: me.Id})
|
||||
})
|
||||
|
||||
global.EventBus.Subscribe(event.EventTopicDeleteMachine, "machineScript", func(ctx context.Context, event *eventbus.Event) error {
|
||||
me := event.Val.(*entity.Machine)
|
||||
return GetMachineScriptApp().DeleteByCond(ctx, &entity.MachineScript{MachineId: me.Id})
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package application
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mayfly-go/internal/event"
|
||||
"mayfly-go/internal/machine/application/dto"
|
||||
"mayfly-go/internal/machine/domain/entity"
|
||||
"mayfly-go/internal/machine/domain/repository"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
tagentity "mayfly-go/internal/tag/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/scheduler"
|
||||
@@ -65,6 +63,9 @@ type machineAppImpl struct {
|
||||
|
||||
tagApp tagapp.TagTree `inject:"T"`
|
||||
resourceAuthCertApp tagapp.ResourceAuthCert `inject:"T"`
|
||||
|
||||
machineScriptApp MachineScript `inject:"T"`
|
||||
machineFileApp MachineFile `inject:"T"`
|
||||
}
|
||||
|
||||
var _ (Machine) = (*machineAppImpl)(nil)
|
||||
@@ -198,12 +199,15 @@ func (m *machineAppImpl) Delete(ctx context.Context, id uint64) error {
|
||||
// 关闭连接
|
||||
mcm.DeleteCli(id)
|
||||
|
||||
// 发布机器删除事件
|
||||
global.EventBus.Publish(ctx, event.EventTopicDeleteMachine, machine)
|
||||
|
||||
resourceType := tagentity.TagTypeMachine
|
||||
return m.Tx(ctx,
|
||||
func(ctx context.Context) error {
|
||||
if err := m.machineFileApp.DeleteByCond(ctx, &entity.MachineFile{MachineId: id}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.machineScriptApp.DeleteByCond(ctx, &entity.MachineScript{MachineId: id}); err != nil {
|
||||
return err
|
||||
}
|
||||
return m.DeleteById(ctx, id)
|
||||
}, func(ctx context.Context) error {
|
||||
return m.tagApp.SaveResourceTag(ctx, &tagdto.SaveResourceTag{
|
||||
|
||||
@@ -4,4 +4,6 @@ import "mayfly-go/pkg/ioc"
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(new(Msg))
|
||||
ioc.Register(new(MsgChannel))
|
||||
ioc.Register(new(MsgTmpl))
|
||||
}
|
||||
|
||||
35
server/internal/msg/api/form/msg.go
Normal file
35
server/internal/msg/api/form/msg.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgChannel struct {
|
||||
model.ExtraData
|
||||
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Type string `json:"type" binding:"required"`
|
||||
Url string `json:"url"`
|
||||
Remark string `json:"remark"`
|
||||
Status int8 `json:"status" binding:"required"`
|
||||
}
|
||||
|
||||
type MsgTmpl struct {
|
||||
model.ExtraData
|
||||
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Title string `json:"title"`
|
||||
Tmpl string `json:"tmpl" binding:"required"`
|
||||
MsgType msgx.MsgType `json:"msgType" binding:"required"`
|
||||
Remark string `json:"remark"`
|
||||
Status int8 `json:"status" binding:"required"`
|
||||
ChannelIds []uint64 `json:"channelIds"`
|
||||
}
|
||||
|
||||
type SendMsg struct {
|
||||
Parmas string `json:"params"`
|
||||
ReceiverIds []uint64 `json:"receiverIds"`
|
||||
}
|
||||
54
server/internal/msg/api/msg_channel.go
Normal file
54
server/internal/msg/api/msg_channel.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/api/form"
|
||||
"mayfly-go/internal/msg/application"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/imsg"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/req"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type MsgChannel struct {
|
||||
msgChannelApp application.MsgChannel `inject:"T"`
|
||||
}
|
||||
|
||||
func (m *MsgChannel) ReqConfs() *req.Confs {
|
||||
basePermCode := "msg:channel:base"
|
||||
|
||||
reqs := [...]*req.Conf{
|
||||
req.NewGet("", m.GetMsgChannels).RequiredPermissionCode(basePermCode),
|
||||
req.NewPost("", m.SaveMsgChannels).Log(req.NewLogSaveI(imsg.LogMsgChannelSave)).RequiredPermissionCode("msg:channel:save"),
|
||||
req.NewDelete("", m.DelMsgChannels).Log(req.NewLogSaveI(imsg.LogMsgChannelDelete)).RequiredPermissionCode("msg:channel:del"),
|
||||
}
|
||||
|
||||
return req.NewConfs("/msg/channels", reqs[:]...)
|
||||
}
|
||||
|
||||
func (m *MsgChannel) GetMsgChannels(rc *req.Ctx) {
|
||||
condition := &entity.MsgChannel{}
|
||||
res, err := m.msgChannelApp.GetPageList(condition, rc.GetPageParam(), new([]entity.MsgChannel))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (m *MsgChannel) SaveMsgChannels(rc *req.Ctx) {
|
||||
form := &form.MsgChannel{}
|
||||
rc.ReqParam = form
|
||||
channel := req.BindJsonAndCopyTo(rc, form, new(entity.MsgChannel))
|
||||
err := m.msgChannelApp.SaveChannel(rc.MetaCtx, channel)
|
||||
biz.ErrIsNil(err)
|
||||
}
|
||||
|
||||
func (m *MsgChannel) DelMsgChannels(rc *req.Ctx) {
|
||||
idsStr := rc.Query("id")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Split(idsStr, ",")
|
||||
|
||||
for _, v := range ids {
|
||||
biz.ErrIsNil(m.msgChannelApp.DeleteChannel(rc.MetaCtx, cast.ToUint64(v)))
|
||||
}
|
||||
}
|
||||
86
server/internal/msg/api/msg_tmpl.go
Normal file
86
server/internal/msg/api/msg_tmpl.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/api/form"
|
||||
"mayfly-go/internal/msg/application"
|
||||
"mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/imsg"
|
||||
"mayfly-go/pkg/biz"
|
||||
"mayfly-go/pkg/req"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/jsonx"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type MsgTmpl struct {
|
||||
msgTmplApp application.MsgTmpl `inject:"T"`
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) ReqConfs() *req.Confs {
|
||||
basePermCode := "msg:tmpl:base"
|
||||
|
||||
reqs := [...]*req.Conf{
|
||||
req.NewGet("", m.GetMsgTmpls).RequiredPermissionCode(basePermCode),
|
||||
req.NewGet(":id/channels", m.GetMsgTmplChannels).RequiredPermissionCode(basePermCode),
|
||||
req.NewPost("", m.SaveMsgTmpl).Log(req.NewLogSaveI(imsg.LogMsgTmplSave)).RequiredPermissionCode("msg:tmpl:save"),
|
||||
req.NewDelete("", m.DelMsgTmpls).Log(req.NewLogSaveI(imsg.LogMsgTmplDelete)).RequiredPermissionCode("msg:tmpl:del"),
|
||||
req.NewPost(":code/send", m.SendMsg).Log(req.NewLogSaveI(imsg.LogMsgTmplSave)).RequiredPermissionCode("msg:tmpl:send"),
|
||||
}
|
||||
|
||||
return req.NewConfs("/msg/tmpls", reqs[:]...)
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) GetMsgTmpls(rc *req.Ctx) {
|
||||
condition := &entity.MsgTmpl{
|
||||
Code: rc.Query("code"),
|
||||
}
|
||||
condition.Id = cast.ToUint64(rc.QueryInt("id"))
|
||||
res, err := m.msgTmplApp.GetPageList(condition, rc.GetPageParam(), new([]entity.MsgTmpl))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = res
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) GetMsgTmplChannels(rc *req.Ctx) {
|
||||
channels, err := m.msgTmplApp.GetTmplChannels(rc.MetaCtx, cast.ToUint64(rc.PathParamInt("id")))
|
||||
biz.ErrIsNil(err)
|
||||
rc.ResData = collx.ArrayMap(channels, func(val *entity.MsgChannel) collx.M {
|
||||
return collx.M{
|
||||
"id": val.Id,
|
||||
"name": val.Name,
|
||||
"type": val.Type,
|
||||
"code": val.Code,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) SaveMsgTmpl(rc *req.Ctx) {
|
||||
form := &form.MsgTmpl{}
|
||||
rc.ReqParam = form
|
||||
channel := req.BindJsonAndCopyTo(rc, form, new(dto.MsgTmplSave))
|
||||
biz.ErrIsNil(m.msgTmplApp.SaveTmpl(rc.MetaCtx, channel))
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) DelMsgTmpls(rc *req.Ctx) {
|
||||
idsStr := rc.Query("id")
|
||||
rc.ReqParam = idsStr
|
||||
ids := strings.Split(idsStr, ",")
|
||||
|
||||
for _, v := range ids {
|
||||
biz.ErrIsNil(m.msgTmplApp.DeleteTmpl(rc.MetaCtx, cast.ToUint64(v)))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MsgTmpl) SendMsg(rc *req.Ctx) {
|
||||
code := rc.PathParam("code")
|
||||
form := &form.SendMsg{}
|
||||
req.BindJsonAndValid(rc, form)
|
||||
|
||||
rc.ReqParam = form
|
||||
|
||||
params, err := jsonx.ToMap(form.Parmas)
|
||||
biz.ErrIsNil(err)
|
||||
biz.ErrIsNil(m.msgTmplApp.Send(rc.MetaCtx, code, params, form.ReceiverIds...))
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
_ "mayfly-go/internal/msg/msgx/sender" // 注册消息发送器
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(new(msgAppImpl), ioc.WithComponentName("MsgApp"))
|
||||
ioc.Register(new(msgChannelAppImpl), ioc.WithComponentName("MsgChannelApp"))
|
||||
ioc.Register(new(msgTmplAppImpl), ioc.WithComponentName("MsgTmplApp"))
|
||||
ioc.Register(new(msgTmplBizAppImpl), ioc.WithComponentName("MsgTmplBizApp"))
|
||||
}
|
||||
|
||||
func GetMsgApp() Msg {
|
||||
|
||||
36
server/internal/msg/application/dto/msg.go
Normal file
36
server/internal/msg/application/dto/msg.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgTmplSave struct {
|
||||
model.ExtraData
|
||||
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
Status entity.MsgTmplStatus `json:"status" `
|
||||
Title string `json:"title"`
|
||||
Tmpl string `json:"type"`
|
||||
MsgType msgx.MsgType `json:"msgType"`
|
||||
|
||||
ChannelIds []uint64 `json:"channelIds"`
|
||||
}
|
||||
|
||||
// MsgTmplBizSave 消息模板关联业务信息
|
||||
type MsgTmplBizSave struct {
|
||||
TmplId uint64 // 消息模板id
|
||||
BizId uint64 // 业务id
|
||||
BizType string
|
||||
}
|
||||
|
||||
// BizMsgTmplSend 业务消息模板发送消息
|
||||
type BizMsgTmplSend struct {
|
||||
BizId uint64 // 业务id
|
||||
BizType string
|
||||
Params map[string]any // 模板占位符参数
|
||||
ReceiverIds []uint64 // 接收人id
|
||||
}
|
||||
52
server/internal/msg/application/msg_channel.go
Normal file
52
server/internal/msg/application/msg_channel.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
)
|
||||
|
||||
type MsgChannel interface {
|
||||
base.App[*entity.MsgChannel]
|
||||
|
||||
GetPageList(condition *entity.MsgChannel, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
SaveChannel(ctx context.Context, msgChannel *entity.MsgChannel) error
|
||||
|
||||
DeleteChannel(ctx context.Context, id uint64) error
|
||||
}
|
||||
|
||||
type msgChannelAppImpl struct {
|
||||
base.AppImpl[*entity.MsgChannel, repository.MsgChannel]
|
||||
|
||||
msgTempApp MsgTmpl `inject:"T"`
|
||||
}
|
||||
|
||||
var _ (MsgChannel) = (*msgChannelAppImpl)(nil)
|
||||
|
||||
func (m *msgChannelAppImpl) GetPageList(condition *entity.MsgChannel, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return m.Repo.GetPageList(condition, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (m *msgChannelAppImpl) SaveChannel(ctx context.Context, msgChannel *entity.MsgChannel) error {
|
||||
if msgChannel.Id == 0 {
|
||||
msgChannel.Code = stringx.Rand(8)
|
||||
}
|
||||
return m.Save(ctx, msgChannel)
|
||||
}
|
||||
|
||||
func (m *msgChannelAppImpl) DeleteChannel(ctx context.Context, id uint64) error {
|
||||
return m.Tx(ctx, func(ctx context.Context) error {
|
||||
if err := m.DeleteById(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
// 删除渠道关联的模板
|
||||
if err := m.msgTempApp.DeleteTmplChannel(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
208
server/internal/msg/application/msg_tmpl.go
Normal file
208
server/internal/msg/application/msg_tmpl.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
sysapp "mayfly-go/internal/sys/application"
|
||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"mayfly-go/pkg/utils/structx"
|
||||
)
|
||||
|
||||
type MsgTmpl interface {
|
||||
base.App[*entity.MsgTmpl]
|
||||
|
||||
GetPageList(condition *entity.MsgTmpl, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
|
||||
SaveTmpl(ctx context.Context, msgTmpl *dto.MsgTmplSave) error
|
||||
|
||||
DeleteTmpl(ctx context.Context, id uint64) error
|
||||
|
||||
GetTmplChannels(ctx context.Context, tmplId uint64) ([]*entity.MsgChannel, error)
|
||||
|
||||
// Send 发送消息
|
||||
Send(ctx context.Context, tmplCode string, params map[string]any, receiverId ...uint64) error
|
||||
|
||||
// DeleteTmplChannel 删除指定渠道关联的模板
|
||||
DeleteTmplChannel(ctx context.Context, channelId uint64) error
|
||||
}
|
||||
|
||||
type msgTmplAppImpl struct {
|
||||
base.AppImpl[*entity.MsgTmpl, repository.MsgTmpl]
|
||||
|
||||
msgTmplChannelRepo repository.MsgTmplChannel `inject:"T"`
|
||||
|
||||
msgChannelApp MsgChannel `inject:"T"`
|
||||
msgTmplBizApp MsgTmplBiz `inject:"T"`
|
||||
accountApp sysapp.Account `inject:"T"`
|
||||
}
|
||||
|
||||
var _ (MsgTmpl) = (*msgTmplAppImpl)(nil)
|
||||
|
||||
func (m *msgTmplAppImpl) GetPageList(condition *entity.MsgTmpl, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
return m.Repo.GetPageList(condition, pageParam, toEntity)
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) SaveTmpl(ctx context.Context, msgTmpl *dto.MsgTmplSave) error {
|
||||
return m.Tx(ctx, func(ctx context.Context) error {
|
||||
mt := &entity.MsgTmpl{}
|
||||
structx.Copy(mt, msgTmpl)
|
||||
isCreate := mt.Id == 0
|
||||
if isCreate {
|
||||
mt.Code = stringx.Rand(8)
|
||||
}
|
||||
|
||||
if err := m.Save(ctx, mt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldTemplChannelIds := []uint64{}
|
||||
if !isCreate {
|
||||
oldTemplChannels, err := m.msgTmplChannelRepo.SelectByCond(&entity.MsgTmplChannel{TmplId: mt.Id}, "channel_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldTemplChannelIds = collx.ArrayMap(oldTemplChannels, func(c *entity.MsgTmplChannel) uint64 {
|
||||
return c.ChannelId
|
||||
})
|
||||
}
|
||||
|
||||
add, del, _ := collx.ArrayCompare(msgTmpl.ChannelIds, oldTemplChannelIds)
|
||||
if len(add) > 0 {
|
||||
tmplChannels := collx.ArrayMap(msgTmpl.ChannelIds, func(channelId uint64) *entity.MsgTmplChannel {
|
||||
return &entity.MsgTmplChannel{
|
||||
ChannelId: channelId,
|
||||
TmplId: mt.Id,
|
||||
}
|
||||
})
|
||||
if err := m.msgTmplChannelRepo.BatchInsert(ctx, tmplChannels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(del) > 0 {
|
||||
if err := m.msgTmplChannelRepo.DeleteByCond(ctx, model.NewCond().Eq("tmpl_id", mt.Id).In("channel_id", del)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) DeleteTmpl(ctx context.Context, id uint64) error {
|
||||
return m.Tx(ctx, func(ctx context.Context) error {
|
||||
if err := m.DeleteById(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := m.msgTmplBizApp.DeleteByTmplId(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.msgTmplChannelRepo.DeleteByCond(ctx, &entity.MsgTmplChannel{TmplId: id})
|
||||
})
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) GetTmplChannels(ctx context.Context, tmplId uint64) ([]*entity.MsgChannel, error) {
|
||||
tmplChannels, err := m.msgTmplChannelRepo.SelectByCond(&entity.MsgTmplChannel{TmplId: tmplId}, "channel_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tmplChannels) == 0 {
|
||||
return []*entity.MsgChannel{}, nil
|
||||
}
|
||||
|
||||
return m.msgChannelApp.GetByIds(collx.ArrayMap(tmplChannels, func(c *entity.MsgTmplChannel) uint64 {
|
||||
return c.ChannelId
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) Send(ctx context.Context, tmplCode string, params map[string]any, receiverId ...uint64) error {
|
||||
tmpl := &entity.MsgTmpl{Code: tmplCode}
|
||||
err := m.GetByCond(tmpl)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("message template does not exist")
|
||||
}
|
||||
if tmpl.Status != entity.TmplStatusEnable {
|
||||
return errorx.NewBiz("message template is disabled")
|
||||
}
|
||||
|
||||
tmplChannels, err := m.msgTmplChannelRepo.SelectByCond(&entity.MsgTmplChannel{TmplId: tmpl.Id}, "channel_id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tmplChannels) == 0 {
|
||||
return errorx.NewBiz("message template is not associated with any channel")
|
||||
}
|
||||
|
||||
channels, err := m.msgChannelApp.GetByIds(collx.ArrayMap(tmplChannels, func(c *entity.MsgTmplChannel) uint64 {
|
||||
return c.ChannelId
|
||||
}))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// content, err := stringx.TemplateParse(tmpl.Tmpl, params)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// toAll := len(receiverId) == 0
|
||||
accounts, err := m.accountApp.GetByIds(receiverId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := &msgx.Msg{
|
||||
Content: tmpl.Tmpl,
|
||||
Params: params,
|
||||
Title: tmpl.Title,
|
||||
Type: tmpl.MsgType,
|
||||
ExtraData: tmpl.ExtraData,
|
||||
}
|
||||
|
||||
if len(accounts) > 0 {
|
||||
msg.Receivers = collx.ArrayMap(accounts, func(account *sysentity.Account) msgx.Receiver {
|
||||
return msgx.Receiver{
|
||||
ExtraData: account.ExtraData,
|
||||
Email: account.Email,
|
||||
Mobile: account.Mobile,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
if channel.Status != entity.ChannelStatusEnable {
|
||||
logx.Warnf("channel is disabled => %s", channel.Code)
|
||||
continue
|
||||
}
|
||||
|
||||
go func(channel *entity.MsgChannel) {
|
||||
if err := msgx.Send(&msgx.Channel{
|
||||
Type: channel.Type,
|
||||
Name: channel.Name,
|
||||
URL: channel.Url,
|
||||
ExtraData: channel.ExtraData,
|
||||
}, msg); err != nil {
|
||||
logx.Errorf("send msg error => channel=%s, msg=%s, err -> %v", channel.Code, msg.Content, err)
|
||||
}
|
||||
}(channel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *msgTmplAppImpl) DeleteTmplChannel(ctx context.Context, channelId uint64) error {
|
||||
return m.msgTmplChannelRepo.DeleteByCond(ctx, &entity.MsgTmplChannel{ChannelId: channelId})
|
||||
}
|
||||
102
server/internal/msg/application/msg_tmpl_biz.go
Normal file
102
server/internal/msg/application/msg_tmpl_biz.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/errorx"
|
||||
)
|
||||
|
||||
type MsgTmplBiz interface {
|
||||
base.App[*entity.MsgTmplBiz]
|
||||
|
||||
// SaveBizTmpl 保存消息模板关联业务信息
|
||||
SaveBizTmpl(ctx context.Context, bizTmpl dto.MsgTmplBizSave) error
|
||||
|
||||
// DeleteByBiz 根据业务删除消息模板业务关联
|
||||
DeleteByBiz(ctx context.Context, bizType string, bizId uint64) error
|
||||
|
||||
// DeleteByTmplId 根据模板ID删除消息模板业务关联
|
||||
DeleteByTmplId(ctx context.Context, tmplId uint64) error
|
||||
|
||||
// Send 发送消息
|
||||
Send(ctx context.Context, sendParam dto.BizMsgTmplSend) error
|
||||
}
|
||||
|
||||
type msgTmplBizAppImpl struct {
|
||||
base.AppImpl[*entity.MsgTmplBiz, repository.MsgTmplBiz]
|
||||
|
||||
msgTmplApp MsgTmpl `inject:"T"`
|
||||
}
|
||||
|
||||
var _ (MsgTmplBiz) = (*msgTmplBizAppImpl)(nil)
|
||||
|
||||
func (m *msgTmplBizAppImpl) SaveBizTmpl(ctx context.Context, bizTmpl dto.MsgTmplBizSave) error {
|
||||
msgTmplId := bizTmpl.TmplId
|
||||
bizId := bizTmpl.BizId
|
||||
bizType := bizTmpl.BizType
|
||||
if bizId == 0 {
|
||||
return errorx.NewBiz("business ID cannot be empty")
|
||||
}
|
||||
if bizType == "" {
|
||||
return errorx.NewBiz("business type cannot be empty")
|
||||
}
|
||||
|
||||
msgTmplBiz := &entity.MsgTmplBiz{
|
||||
BizId: bizId,
|
||||
BizType: bizType,
|
||||
}
|
||||
// exist
|
||||
if err := m.GetByCond(msgTmplBiz); err == nil {
|
||||
// tmplId不变,直接返回即可
|
||||
if msgTmplBiz.TmplId == msgTmplId {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果模板ID为0,表示删除业务关联
|
||||
if msgTmplId == 0 {
|
||||
return m.DeleteByBiz(ctx, bizTmpl.BizType, bizTmpl.BizId)
|
||||
}
|
||||
|
||||
update := &entity.MsgTmplBiz{
|
||||
TmplId: msgTmplId,
|
||||
}
|
||||
update.Id = msgTmplBiz.Id
|
||||
return m.UpdateById(ctx, update)
|
||||
}
|
||||
|
||||
if msgTmplId == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
msgTmplBiz.TmplId = msgTmplId
|
||||
return m.Save(ctx, msgTmplBiz)
|
||||
}
|
||||
|
||||
func (m *msgTmplBizAppImpl) DeleteByBiz(ctx context.Context, bizType string, bizId uint64) error {
|
||||
return m.DeleteByCond(ctx, &entity.MsgTmplBiz{BizId: bizId, BizType: bizType})
|
||||
}
|
||||
|
||||
func (m *msgTmplBizAppImpl) DeleteByTmplId(ctx context.Context, tmplId uint64) error {
|
||||
return m.DeleteByCond(ctx, &entity.MsgTmplBiz{TmplId: tmplId})
|
||||
}
|
||||
|
||||
func (m *msgTmplBizAppImpl) Send(ctx context.Context, sendParam dto.BizMsgTmplSend) error {
|
||||
// 获取业务关联的消息模板
|
||||
msgTmplBiz := &entity.MsgTmplBiz{
|
||||
BizId: sendParam.BizId,
|
||||
BizType: sendParam.BizType,
|
||||
}
|
||||
if err := m.GetByCond(msgTmplBiz); err != nil {
|
||||
return errorx.NewBiz("message tmplate association business information does not exist")
|
||||
}
|
||||
|
||||
mstTmpl, err := m.msgTmplApp.GetById(msgTmplBiz.TmplId)
|
||||
if err != nil {
|
||||
return errorx.NewBiz("message template does not exist")
|
||||
}
|
||||
|
||||
return m.msgTmplApp.Send(ctx, mstTmpl.Code, sendParam.Params, sendParam.ReceiverIds...)
|
||||
}
|
||||
29
server/internal/msg/domain/entity/msg_channel.go
Normal file
29
server/internal/msg/domain/entity/msg_channel.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgChannel struct {
|
||||
model.Model
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name" gorm:"size:50;not null;"` // 渠道名称
|
||||
Code string `json:"code" gorm:"size:50;not null;"` // 渠道编码
|
||||
Type msgx.ChannelType `json:"type" gorm:"size:30;not null;"` // 渠道类型
|
||||
Url string `json:"url" gorm:"size:200;"` // 渠道url
|
||||
Status MsgChannelStatus `json:"status" gorm:"not null;"` // 状态
|
||||
Remark *string `json:"remark" gorm:"size:200;"` // 备注
|
||||
}
|
||||
|
||||
func (a *MsgChannel) TableName() string {
|
||||
return "t_msg_channel"
|
||||
}
|
||||
|
||||
type MsgChannelStatus int8
|
||||
|
||||
const (
|
||||
ChannelStatusEnable MsgChannelStatus = 1 // 启用状态
|
||||
ChannelStatusDisable MsgChannelStatus = -1 // 禁用状态
|
||||
)
|
||||
42
server/internal/msg/domain/entity/msg_tmpl.go
Normal file
42
server/internal/msg/domain/entity/msg_tmpl.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
// MsgTmpl 消息模板
|
||||
type MsgTmpl struct {
|
||||
model.Model
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name" gorm:"size:50;not null;"` // 模板名称
|
||||
Code string `json:"code" gorm:"size:32;not null;"` // 模板编码
|
||||
Title string `json:"title" gorm:"size:100;"` // 标题
|
||||
Tmpl string `json:"tmpl" gorm:"size:2000;not null;"` // 消息模板
|
||||
MsgType msgx.MsgType `json:"msgType" gorm:"not null;"` // 消息类型
|
||||
Status MsgTmplStatus `json:"status" gorm:"not null;"` // 状态
|
||||
Remark *string `json:"remark" gorm:"size:200;"` // 备注
|
||||
}
|
||||
|
||||
func (a *MsgTmpl) TableName() string {
|
||||
return "t_msg_tmpl"
|
||||
}
|
||||
|
||||
type MsgTmplStatus int8
|
||||
|
||||
const (
|
||||
TmplStatusEnable MsgTmplStatus = 1 // 启用状态
|
||||
TmplStatusDisable MsgTmplStatus = -1 // 禁用状态
|
||||
)
|
||||
|
||||
// MsgTmplChannel 消息模板渠道关联
|
||||
type MsgTmplChannel struct {
|
||||
model.CreateModelNLD
|
||||
TmplId uint64 `json:"tmplId" gorm:"not null;"` // 模板id
|
||||
ChannelId uint64 `json:"channelId" gorm:"not null;"` // 渠道id
|
||||
}
|
||||
|
||||
func (a *MsgTmplChannel) TableName() string {
|
||||
return "t_msg_tmpl_channel"
|
||||
}
|
||||
18
server/internal/msg/domain/entity/msg_tmpl_biz.go
Normal file
18
server/internal/msg/domain/entity/msg_tmpl_biz.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
// MsgTmplBiz 消息模板关联业务信息
|
||||
type MsgTmplBiz struct {
|
||||
model.Model
|
||||
|
||||
TmplId uint64 `json:"tmplId" gorm:"not null;"` // 模板id
|
||||
BizId uint64 `json:"bizId" gorm:"not null;"` // 业务id
|
||||
BizType string `json:"bizType" gorm:"size:32;not null;"` // 业务类型
|
||||
}
|
||||
|
||||
func (a *MsgTmplBiz) TableName() string {
|
||||
return "t_msg_tmpl_biz"
|
||||
}
|
||||
13
server/internal/msg/domain/repository/msg_channel.go
Normal file
13
server/internal/msg/domain/repository/msg_channel.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgChannel interface {
|
||||
base.Repo[*entity.MsgChannel]
|
||||
|
||||
GetPageList(condition *entity.MsgChannel, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
17
server/internal/msg/domain/repository/msg_tmpl.go
Normal file
17
server/internal/msg/domain/repository/msg_tmpl.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgTmpl interface {
|
||||
base.Repo[*entity.MsgTmpl]
|
||||
|
||||
GetPageList(condition *entity.MsgTmpl, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error)
|
||||
}
|
||||
|
||||
type MsgTmplChannel interface {
|
||||
base.Repo[*entity.MsgTmplChannel]
|
||||
}
|
||||
10
server/internal/msg/domain/repository/msg_tmpl_biz.go
Normal file
10
server/internal/msg/domain/repository/msg_tmpl_biz.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/pkg/base"
|
||||
)
|
||||
|
||||
type MsgTmplBiz interface {
|
||||
base.Repo[*entity.MsgTmplBiz]
|
||||
}
|
||||
12
server/internal/msg/imsg/en.go
Normal file
12
server/internal/msg/imsg/en.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package imsg
|
||||
|
||||
import "mayfly-go/pkg/i18n"
|
||||
|
||||
var En = map[i18n.MsgId]string{
|
||||
LogMsgChannelSave: "Message channel- save",
|
||||
LogMsgChannelDelete: "Message channel- delete",
|
||||
|
||||
LogMsgTmplSave: "Message template- save",
|
||||
LogMsgTmplDelete: "Message template- delete",
|
||||
LogMsgTmplSend: "Message template- send",
|
||||
}
|
||||
20
server/internal/msg/imsg/imsg.go
Normal file
20
server/internal/msg/imsg/imsg.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package imsg
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/common/consts"
|
||||
"mayfly-go/pkg/i18n"
|
||||
)
|
||||
|
||||
func init() {
|
||||
i18n.AppendLangMsg(i18n.Zh_CN, Zh_CN)
|
||||
i18n.AppendLangMsg(i18n.En, En)
|
||||
}
|
||||
|
||||
const (
|
||||
LogMsgChannelSave = iota + consts.ImsgNumMsg
|
||||
LogMsgChannelDelete
|
||||
|
||||
LogMsgTmplSave
|
||||
LogMsgTmplDelete
|
||||
LogMsgTmplSend
|
||||
)
|
||||
12
server/internal/msg/imsg/zh_cn.go
Normal file
12
server/internal/msg/imsg/zh_cn.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package imsg
|
||||
|
||||
import "mayfly-go/pkg/i18n"
|
||||
|
||||
var Zh_CN = map[i18n.MsgId]string{
|
||||
LogMsgChannelSave: "消息渠道-保存",
|
||||
LogMsgChannelDelete: "消息渠道-删除",
|
||||
|
||||
LogMsgTmplSave: "消息模板-保存",
|
||||
LogMsgTmplDelete: "消息模板-删除",
|
||||
LogMsgTmplSend: "消息模板-发送",
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type msgChannelRepoImpl struct {
|
||||
base.RepoImpl[*entity.MsgChannel]
|
||||
}
|
||||
|
||||
func newMsgChannelRepo() repository.MsgChannel {
|
||||
return &msgChannelRepoImpl{}
|
||||
}
|
||||
|
||||
func (m *msgChannelRepoImpl) GetPageList(condition *entity.MsgChannel, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
pd := model.NewCond().
|
||||
Eq("id", condition.Id).
|
||||
Like("code", condition.Code).
|
||||
OrderBy(orderBy...)
|
||||
return m.PageByCondToAny(pd, pageParam, toEntity)
|
||||
}
|
||||
32
server/internal/msg/infrastructure/persistence/msg_tmpl.go
Normal file
32
server/internal/msg/infrastructure/persistence/msg_tmpl.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type msgTmplRepoImpl struct {
|
||||
base.RepoImpl[*entity.MsgTmpl]
|
||||
}
|
||||
|
||||
func newMsgTmplRepo() repository.MsgTmpl {
|
||||
return &msgTmplRepoImpl{}
|
||||
}
|
||||
|
||||
func (m *msgTmplRepoImpl) GetPageList(condition *entity.MsgTmpl, pageParam *model.PageParam, toEntity any, orderBy ...string) (*model.PageResult[any], error) {
|
||||
pd := model.NewCond().
|
||||
Eq("id", condition.Id).
|
||||
Like("code", condition.Code).
|
||||
OrderBy(orderBy...)
|
||||
return m.PageByCondToAny(pd, pageParam, toEntity)
|
||||
}
|
||||
|
||||
type msgTmplChannelRepoImpl struct {
|
||||
base.RepoImpl[*entity.MsgTmplChannel]
|
||||
}
|
||||
|
||||
func newMsgTmplChannelRepo() repository.MsgTmplChannel {
|
||||
return &msgTmplChannelRepoImpl{}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"mayfly-go/internal/msg/domain/entity"
|
||||
"mayfly-go/internal/msg/domain/repository"
|
||||
"mayfly-go/pkg/base"
|
||||
)
|
||||
|
||||
type msgTmplBizRepoImpl struct {
|
||||
base.RepoImpl[*entity.MsgTmplBiz]
|
||||
}
|
||||
|
||||
func newMsgTmplBizRepo() repository.MsgTmplBiz {
|
||||
return &msgTmplBizRepoImpl{}
|
||||
}
|
||||
@@ -6,4 +6,8 @@ import (
|
||||
|
||||
func InitIoc() {
|
||||
ioc.Register(newMsgRepo(), ioc.WithComponentName("MsgRepo"))
|
||||
ioc.Register(newMsgChannelRepo(), ioc.WithComponentName("MsgChannelRepo"))
|
||||
ioc.Register(newMsgTmplRepo(), ioc.WithComponentName("MsgTmplRepo"))
|
||||
ioc.Register(newMsgTmplChannelRepo(), ioc.WithComponentName("MsgTmplChannelRepo"))
|
||||
ioc.Register(newMsgTmplBizRepo(), ioc.WithComponentName("MsgTmplBizRepo"))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package init
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mayfly-go/initialize"
|
||||
"mayfly-go/internal/event"
|
||||
"mayfly-go/internal/msg/api"
|
||||
"mayfly-go/internal/msg/application"
|
||||
"mayfly-go/internal/msg/application/dto"
|
||||
"mayfly-go/internal/msg/infrastructure/persistence"
|
||||
"mayfly-go/pkg/eventbus"
|
||||
"mayfly-go/pkg/global"
|
||||
"mayfly-go/pkg/ioc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -13,4 +19,14 @@ func init() {
|
||||
application.InitIoc()
|
||||
api.InitIoc()
|
||||
})
|
||||
|
||||
initialize.AddInitFunc(Init)
|
||||
}
|
||||
|
||||
func Init() {
|
||||
msgTmplBizApp := ioc.Get[application.MsgTmplBiz]("MsgTmplBizApp")
|
||||
|
||||
global.EventBus.SubscribeAsync(event.EventTopicBizMsgTmplSend, "BizMsgTmplSend", func(ctx context.Context, event *eventbus.Event) error {
|
||||
return msgTmplBizApp.Send(ctx, event.Val.(dto.BizMsgTmplSend))
|
||||
}, false)
|
||||
}
|
||||
|
||||
84
server/internal/msg/msgx/msgx.go
Normal file
84
server/internal/msg/msgx/msgx.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package msgx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mayfly-go/pkg/model"
|
||||
)
|
||||
|
||||
type MsgType int8
|
||||
type ChannelType string
|
||||
|
||||
const (
|
||||
MsgTypeText MsgType = 1
|
||||
MsgTypeMarkdown MsgType = 2
|
||||
MsgTypeHtml MsgType = 3
|
||||
)
|
||||
|
||||
const (
|
||||
ChannelTypeEmail ChannelType = "email"
|
||||
ChannelTypeDingBot ChannelType = "dingBot"
|
||||
ChannelTypeQywxBot ChannelType = "qywxBot"
|
||||
ChannelTypeFeishuBot ChannelType = "feishuBot"
|
||||
)
|
||||
|
||||
const (
|
||||
ReceiverKey = "receiver"
|
||||
)
|
||||
|
||||
// Send 发送消息
|
||||
func Send(channel *Channel, msg *Msg) error {
|
||||
sender, err := GetMsgSender(channel.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sender.Send(channel, msg)
|
||||
}
|
||||
|
||||
type Receiver struct {
|
||||
model.ExtraData
|
||||
|
||||
Mobile string
|
||||
Email string
|
||||
}
|
||||
|
||||
type Msg struct {
|
||||
model.ExtraData
|
||||
|
||||
Title string // 消息title
|
||||
Type MsgType // 消息类型
|
||||
Content string // 消息内容
|
||||
Params map[string]any // 消息参数(替换消息中的占位符)
|
||||
|
||||
Receivers []Receiver // 消息接收人
|
||||
}
|
||||
|
||||
// Channel 消息发送渠道信息
|
||||
type Channel struct {
|
||||
model.ExtraData
|
||||
|
||||
Type ChannelType // 渠道类型
|
||||
Name string
|
||||
URL string
|
||||
}
|
||||
|
||||
// MsgSender 定义消息发送接口
|
||||
type MsgSender interface {
|
||||
// Send 发送消息
|
||||
Send(channel *Channel, msg *Msg) error
|
||||
}
|
||||
|
||||
var messageSenders = make(map[ChannelType]MsgSender)
|
||||
|
||||
// RegisterMsgSender 注册消息发送器
|
||||
func RegisterMsgSender(channel ChannelType, sender MsgSender) {
|
||||
messageSenders[channel] = sender
|
||||
}
|
||||
|
||||
// GetMsgSender 获取消息发送器
|
||||
func GetMsgSender(channel ChannelType) (MsgSender, error) {
|
||||
sender, ok := messageSenders[channel]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported message channel %s", channel)
|
||||
}
|
||||
return sender, nil
|
||||
}
|
||||
107
server/internal/msg/msgx/sender/ding_bot.go
Normal file
107
server/internal/msg/msgx/sender/ding_bot.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/httpx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strings"
|
||||
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type dingBotMsgReq struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Text struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"text"`
|
||||
Markdown struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
} `json:"markdown"`
|
||||
At struct {
|
||||
// AtUserIds []string `json:"atUserIds"`
|
||||
AtMobiles []string `json:"atMobiles"`
|
||||
IsAtAll bool `json:"isAtAll"`
|
||||
} `json:"at"`
|
||||
}
|
||||
|
||||
type dingBotMsgResp struct {
|
||||
Code int `json:"errcode"`
|
||||
Message string `json:"errmsg"`
|
||||
}
|
||||
|
||||
// DingBotSender 钉钉机器人消息发送
|
||||
type DingBotSender struct{}
|
||||
|
||||
func (d DingBotSender) Send(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
// https://open.dingtalk.com/document/robots/custom-robot-access#title-72m-8ag-pqw
|
||||
msgReq := dingBotMsgReq{}
|
||||
|
||||
params := msg.Params
|
||||
receiver := collx.ArrayMapFilter(msg.Receivers, func(a msgx.Receiver) (string, bool) {
|
||||
return a.Mobile, a.Mobile != ""
|
||||
})
|
||||
|
||||
if len(receiver) > 0 {
|
||||
msgReq.At.AtMobiles = receiver
|
||||
// 替换文本中的receiver,使用@mobile用于@指定用户
|
||||
params[msgx.ReceiverKey] = strings.Join(collx.ArrayMap(receiver, func(a string) string { return "@" + a }), "")
|
||||
} else {
|
||||
msgReq.At.IsAtAll = true
|
||||
params[msgx.ReceiverKey] = ""
|
||||
}
|
||||
|
||||
content, err := stringx.TemplateResolve(msg.Content, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if msg.Type == msgx.MsgTypeMarkdown {
|
||||
msgReq.MsgType = "markdown"
|
||||
msgReq.Markdown.Title = msg.Title
|
||||
msgReq.Markdown.Text = content
|
||||
} else {
|
||||
msgReq.MsgType = "text"
|
||||
msgReq.Text.Content = content
|
||||
}
|
||||
|
||||
timestamp := time.Now().UnixMilli()
|
||||
sign, err := d.sign(channel.GetExtraString("secret"), timestamp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var res dingBotMsgResp
|
||||
err = httpx.NewReq(fmt.Sprintf("%s×tamp=%d&sign=%s", channel.URL, timestamp, sign)).
|
||||
PostObj(msgReq).
|
||||
BodyTo(&res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return errors.New(res.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d DingBotSender) sign(secret string, timestamp int64) (string, error) {
|
||||
// https://open.dingtalk.com/document/robots/customize-robot-security-settings
|
||||
// timestamp + key -> sha256 -> URL encode
|
||||
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
_, err := h.Write([]byte(stringToSign))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
signature = url.QueryEscape(signature)
|
||||
return signature, nil
|
||||
}
|
||||
100
server/internal/msg/msgx/sender/email.go
Normal file
100
server/internal/msg/msgx/sender/email.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type EmailSender struct{}
|
||||
|
||||
func (e EmailSender) Send(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
return e.SendEmail(channel, msg)
|
||||
}
|
||||
|
||||
func (e EmailSender) SendEmail(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
subject := msg.Title
|
||||
content, err := stringx.TemplateResolve(msg.Content, msg.Params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
to := collx.ArrayMapFilter(msg.Receivers, func(a msgx.Receiver) (string, bool) {
|
||||
return a.Email, a.Email != ""
|
||||
})
|
||||
|
||||
if len(to) == 0 {
|
||||
return errors.New("no receiver")
|
||||
}
|
||||
|
||||
systemName := "mayfly-go"
|
||||
|
||||
serverAndPort := strings.Split(channel.URL, ":")
|
||||
smtpServer := serverAndPort[0]
|
||||
smtpPort := 465
|
||||
if len(serverAndPort) == 2 {
|
||||
smtpPort = cast.ToInt(serverAndPort[1])
|
||||
}
|
||||
|
||||
smtpAccount := channel.GetExtraString("smtpAccount")
|
||||
smtpPassword := channel.GetExtraString("smtpPassword")
|
||||
|
||||
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
|
||||
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
||||
"From: %s<%s>\r\n"+
|
||||
"Subject: %s\r\n"+
|
||||
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
||||
strings.Join(to, ";"), systemName, smtpAccount, encodedSubject, content))
|
||||
auth := smtp.PlainAuth("", smtpAccount, smtpPassword, smtpServer)
|
||||
addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort)
|
||||
|
||||
if smtpPort == 465 {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: smtpServer,
|
||||
}
|
||||
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", smtpServer, smtpPort), tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := smtp.NewClient(conn, smtpServer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
if err = client.Auth(auth); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = client.Mail(smtpAccount); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, receiver := range to {
|
||||
if err = client.Rcpt(receiver); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(mail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = smtp.SendMail(addr, auth, smtpAccount, to, mail)
|
||||
}
|
||||
return err
|
||||
}
|
||||
100
server/internal/msg/msgx/sender/feishu_bot.go
Normal file
100
server/internal/msg/msgx/sender/feishu_bot.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/httpx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strings"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/may-fly/cast"
|
||||
)
|
||||
|
||||
type feishuBotMsgReq struct {
|
||||
MsgType string `json:"msg_type"`
|
||||
Content struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type feishuBotMsgResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"msg"`
|
||||
}
|
||||
|
||||
// FeishuBotSender 发送飞书机器人消息
|
||||
type FeishuBotSender struct{}
|
||||
|
||||
func (f FeishuBotSender) Send(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
|
||||
msgReq := feishuBotMsgReq{
|
||||
MsgType: "text",
|
||||
}
|
||||
|
||||
params := msg.Params
|
||||
receiver := `<at user_id="all"></at>`
|
||||
// 使用receiver参数替换消息内容中可能存在的接收人信息
|
||||
if len(msg.Receivers) > 0 {
|
||||
if to := collx.ArrayMapFilter(msg.Receivers, func(a msgx.Receiver) (string, bool) {
|
||||
if uid := a.GetExtraString("feishuUserId"); uid != "" {
|
||||
// 使用<at user_id="userId"></at>
|
||||
return fmt.Sprintf(`<at user_id="%s"></at>`, uid), true
|
||||
}
|
||||
return "", false
|
||||
}); len(to) > 0 {
|
||||
receiver = strings.Join(to, "")
|
||||
}
|
||||
}
|
||||
params[msgx.ReceiverKey] = receiver
|
||||
content, err := stringx.TemplateResolve(msg.Content, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msgReq.Content.Text = content
|
||||
|
||||
if secret := channel.GetExtraString("secret"); secret != "" {
|
||||
timestamp := time.Now().Unix()
|
||||
if sign, err := f.sign(secret, timestamp); err != nil {
|
||||
return err
|
||||
} else {
|
||||
msgReq.Sign = sign
|
||||
}
|
||||
msgReq.Timestamp = cast.ToString(timestamp)
|
||||
}
|
||||
|
||||
var res feishuBotMsgResp
|
||||
err = httpx.NewReq(channel.URL).
|
||||
PostObj(msgReq).
|
||||
BodyTo(&res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return errors.New(res.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e FeishuBotSender) sign(secret string, timestamp int64) (string, error) {
|
||||
//timestamp + key 做sha256, 再进行base64 encode
|
||||
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
|
||||
}
|
||||
82
server/internal/msg/msgx/sender/qywx_bot.go
Normal file
82
server/internal/msg/msgx/sender/qywx_bot.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"mayfly-go/internal/msg/msgx"
|
||||
"mayfly-go/pkg/httpx"
|
||||
"mayfly-go/pkg/utils/collx"
|
||||
"mayfly-go/pkg/utils/stringx"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type qywxBotMsgReq struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Text struct {
|
||||
Content string `json:"content"`
|
||||
MentionedMobileList []string `json:"mentioned_mobile_list"`
|
||||
} `json:"text"`
|
||||
Markdown struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"markdown"`
|
||||
}
|
||||
|
||||
type qywxBotMsgResp struct {
|
||||
Code int `json:"errcode"`
|
||||
Message string `json:"errmsg"`
|
||||
}
|
||||
|
||||
// QywxBotSender 企业微信机器人消息发送
|
||||
type QywxBotSender struct{}
|
||||
|
||||
func (e QywxBotSender) Send(channel *msgx.Channel, msg *msgx.Msg) error {
|
||||
// https://developer.work.weixin.qq.com/document/path/91770
|
||||
msgReq := qywxBotMsgReq{}
|
||||
|
||||
params := msg.Params
|
||||
receiver := ""
|
||||
// 使用receiver参数替换消息内容中可能存在的接收人信息
|
||||
if len(msg.Receivers) > 0 {
|
||||
if to := collx.ArrayMapFilter(msg.Receivers, func(a msgx.Receiver) (string, bool) {
|
||||
if uid := a.GetExtraString("qywxUserId"); uid != "" {
|
||||
// 使用<@userId>用于@指定用户
|
||||
return fmt.Sprintf("<@%s>", uid), true
|
||||
}
|
||||
return "", false
|
||||
}); len(to) > 0 {
|
||||
receiver = strings.Join(to, "")
|
||||
}
|
||||
}
|
||||
params[msgx.ReceiverKey] = receiver
|
||||
content, err := stringx.TemplateResolve(msg.Content, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if msg.Type == msgx.MsgTypeMarkdown {
|
||||
msgReq.MsgType = "markdown"
|
||||
msgReq.Markdown.Content = content
|
||||
// msgReq.Markdown.MentionedMobileList = receivers // markdown不支持@人,需要使用<@userId>
|
||||
} else {
|
||||
msgReq.MsgType = "text"
|
||||
msgReq.Text.Content = content
|
||||
|
||||
// receivers := msg.Receivers
|
||||
// if len(msg.Receivers) == 0 {
|
||||
// receivers = []string{"@all"}
|
||||
// }
|
||||
|
||||
// msgReq.Text.MentionedMobileList = receivers
|
||||
}
|
||||
|
||||
var res qywxBotMsgResp
|
||||
err = httpx.NewReq(channel.URL).PostObj(msgReq).BodyTo(&res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.Code != 0 {
|
||||
return errors.New(res.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
10
server/internal/msg/msgx/sender/sender.go
Normal file
10
server/internal/msg/msgx/sender/sender.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package sender
|
||||
|
||||
import "mayfly-go/internal/msg/msgx"
|
||||
|
||||
func init() {
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeEmail, EmailSender{})
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeDingBot, DingBotSender{})
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeQywxBot, QywxBotSender{})
|
||||
msgx.RegisterMsgSender(msgx.ChannelTypeFeishuBot, FeishuBotSender{})
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
package form
|
||||
|
||||
import "mayfly-go/pkg/model"
|
||||
|
||||
type AccountCreateForm struct {
|
||||
model.ExtraData
|
||||
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name" binding:"required,max=16" msg:"required=name cannot be blank,max=The maximum length of a name cannot exceed 16 characters"`
|
||||
Username string `json:"username" binding:"pattern=account_username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type AccountUpdateForm struct {
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name" binding:"max=16"` // 姓名
|
||||
Username string `json:"username" binding:"omitempty,pattern=account_username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email" binding:"omitempty,email"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,12 @@ import (
|
||||
|
||||
type AccountManageVO struct {
|
||||
model.Model
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
Status entity.AccountStatus `json:"status"`
|
||||
LastLoginTime *time.Time `json:"lastLoginTime"`
|
||||
OtpSecret string `json:"otpSecret"`
|
||||
@@ -19,6 +23,8 @@ type SimpleAccountVO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Mobile string `json:"mobile"`
|
||||
Email string `json:"email"`
|
||||
|
||||
Roles []*AccountRoleVO `json:"roles" gorm:"-"`
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ func (m *syslogAppImpl) AppendLog(logId uint64, appendLog *AppendLogReq) {
|
||||
syslog.Resp = fmt.Sprintf("%s\n%s", syslog.Resp, appendLogMsg)
|
||||
syslog.Type = appendLog.Type
|
||||
if len(appendLog.Extra) > 0 {
|
||||
existExtra := jsonx.ToMap(syslog.Extra)
|
||||
existExtra, _ := jsonx.ToMap(syslog.Extra)
|
||||
syslog.Extra = jsonx.ToStr(collx.MapMerge(existExtra, appendLog.Extra))
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (m *syslogAppImpl) SetExtra(logId uint64, key string, val any) {
|
||||
syslog = sl
|
||||
}
|
||||
|
||||
extraMap := jsonx.ToMap(syslog.Extra)
|
||||
extraMap, _ := jsonx.ToMap(syslog.Extra)
|
||||
if extraMap == nil {
|
||||
extraMap = make(map[string]any)
|
||||
}
|
||||
|
||||
@@ -10,9 +10,12 @@ import (
|
||||
|
||||
type Account struct {
|
||||
model.Model
|
||||
model.ExtraData
|
||||
|
||||
Name string `json:"name" gorm:"size:30;not null;"`
|
||||
Username string `json:"username" gorm:"size:30;not null;"`
|
||||
Mobile string `json:"mobile" gorm:"size:20;"`
|
||||
Email string `json:"email" gorm:"size:100;"`
|
||||
Password string `json:"-" gorm:"size:64;not null;"`
|
||||
Status AccountStatus `json:"status" gorm:"not null;"`
|
||||
LastLoginTime *time.Time `json:"lastLoginTime"`
|
||||
|
||||
@@ -22,10 +22,8 @@ func init() {
|
||||
}
|
||||
|
||||
func Init() {
|
||||
|
||||
global.EventBus.SubscribeAsync(event.EventTopicResourceOp, "ResourceOpLogApp", func(ctx context.Context, event *eventbus.Event) error {
|
||||
codePath := event.Val.(string)
|
||||
return application.GetResourceOpLogApp().AddResourceOpLog(ctx, codePath)
|
||||
}, false)
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func run(db *gorm.DB, fs ...func() []*gormigrate.Migration) error {
|
||||
IDColumnName: "id",
|
||||
IDColumnSize: 300,
|
||||
UseTransaction: true,
|
||||
ValidateUnknownMigrations: true,
|
||||
ValidateUnknownMigrations: false,
|
||||
}, ms)
|
||||
if err := m.Migrate(); err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,7 +2,11 @@ package migrations
|
||||
|
||||
import (
|
||||
machineentity "mayfly-go/internal/machine/domain/entity"
|
||||
msgentity "mayfly-go/internal/msg/domain/entity"
|
||||
sysentity "mayfly-go/internal/sys/domain/entity"
|
||||
"mayfly-go/pkg/logx"
|
||||
"mayfly-go/pkg/model"
|
||||
"time"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"gorm.io/gorm"
|
||||
@@ -11,6 +15,7 @@ import (
|
||||
func V1_9() []*gormigrate.Migration {
|
||||
var migrations []*gormigrate.Migration
|
||||
migrations = append(migrations, V1_9_3()...)
|
||||
migrations = append(migrations, V1_9_4()...)
|
||||
return migrations
|
||||
}
|
||||
|
||||
@@ -40,3 +45,167 @@ func V1_9_3() []*gormigrate.Migration {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func V1_9_4() []*gormigrate.Migration {
|
||||
return []*gormigrate.Migration{
|
||||
{
|
||||
ID: "20250213-v1.9.4-addMsg",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
tx.AutoMigrate(&sysentity.Account{})
|
||||
tx.AutoMigrate(&msgentity.MsgTmpl{}, &msgentity.MsgTmplChannel{}, &msgentity.MsgChannel{}, &msgentity.MsgTmplBiz{})
|
||||
|
||||
la := &model.LoginAccount{Id: 1, Username: "admin"}
|
||||
// 创建审批默认消息模板
|
||||
processMsgTmplCode := "7u2MRCaB"
|
||||
if err := tx.Where("code = ?", processMsgTmplCode).First(&msgentity.MsgTmpl{}).Error; err != nil {
|
||||
tmplRemark := "工单审批通知模板"
|
||||
msgTmpl := &msgentity.MsgTmpl{
|
||||
Code: processMsgTmplCode,
|
||||
Name: "工单审批通知",
|
||||
Tmpl: `{{.receiver}}
|
||||
您有新的工单需要审批
|
||||
发起人:{{.creator}}
|
||||
工单标题:{{.procdefName}}
|
||||
备注:{{.procinstRemark}}
|
||||
业务编号:{{.bizKey}}`,
|
||||
Title: "工单审批",
|
||||
MsgType: 1,
|
||||
Status: 1,
|
||||
Remark: &tmplRemark,
|
||||
}
|
||||
msgTmpl.FillBaseInfo(model.IdGenTypeNone, la)
|
||||
if err := tx.Create(msgTmpl).Error; err != nil {
|
||||
logx.ErrorTrace("create msg tmpl error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
resources := []*sysentity.Resource{
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742816076}}}},
|
||||
Pid: 0,
|
||||
UiPath: "ckg5ICnd/",
|
||||
Name: "menu.msgManage",
|
||||
Code: "/msg",
|
||||
Type: 1,
|
||||
Meta: `{"icon":"Message","isKeepAlive":true,"routeName":"msg"}`,
|
||||
Weight: 60000000,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742816279}}}},
|
||||
Pid: 1742816076,
|
||||
UiPath: "ckg5ICnd/eKQ8qAlH/",
|
||||
Name: "menu.channel",
|
||||
Code: "channels",
|
||||
Type: 1,
|
||||
Meta: `{"component":"msg/channel/ChannelList","icon":"Message","isKeepAlive":true,"routeName":"ChannelList"}`,
|
||||
Weight: 1742816279,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876893}}}},
|
||||
Pid: 1742816279,
|
||||
UiPath: "ckg5ICnd/eKQ8qAlH/p2Xi8asv/",
|
||||
Name: "menu.msgChannelBase",
|
||||
Code: "msg:channel:base",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742823660,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742823661}}}},
|
||||
Pid: 1742816279,
|
||||
UiPath: "ckg5ICnd/eKQ8qAlH/Iu82rFKW/",
|
||||
Name: "menu.saveMsgChannel",
|
||||
Code: "msg:channel:save",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742823661,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742826138}}}},
|
||||
Pid: 1742816279,
|
||||
UiPath: "ckg5ICnd/eKQ8qAlH/Y4kRzNJp/",
|
||||
Name: "menu.delMsgChannel",
|
||||
Code: "msg:channel:del",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742826138,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876469}}}},
|
||||
Pid: 1742816076,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/",
|
||||
Name: "menu.msgTmpl",
|
||||
Code: "tmpls",
|
||||
Type: 1,
|
||||
Meta: `{"component":"msg/tmpl/TmplList","icon":"Message","isKeepAlive":true,"routeName":"TmplList"}`,
|
||||
Weight: 1742876469,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876795}}}},
|
||||
Pid: 1742876469,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/ExV9tz2l/",
|
||||
Name: "menu.saveMsgTmpl",
|
||||
Code: "msg:tmpl:save",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742876795,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876813}}}},
|
||||
Pid: 1742876469,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/2y7drhga/",
|
||||
Name: "menu.delMsgTmpl",
|
||||
Code: "msg:tmpl:del",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742876813,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742876922}}}},
|
||||
Pid: 1742876469,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/VRX9YtM3/",
|
||||
Name: "menu.msgTmplBase",
|
||||
Code: "msg:tmpl:base",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742876794,
|
||||
},
|
||||
{
|
||||
Model: model.Model{CreateModel: model.CreateModel{DeletedModel: model.DeletedModel{IdModel: model.IdModel{Id: 1742912893}}}},
|
||||
Pid: 1742876469,
|
||||
UiPath: "ckg5ICnd/XiJf38uW/42PkAmLB/",
|
||||
Name: "menu.sendMsg",
|
||||
Code: "msg:tmpl:send",
|
||||
Type: 2,
|
||||
Meta: ``,
|
||||
Weight: 1742912893,
|
||||
},
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, r := range resources {
|
||||
if err := tx.Where("ui_path = ?", r.UiPath).First(&sysentity.Resource{}).Error; err == nil {
|
||||
continue
|
||||
}
|
||||
r.Status = 1
|
||||
r.CreateTime = &now
|
||||
r.UpdateTime = &now
|
||||
r.Creator = la.Username
|
||||
r.Modifier = la.Username
|
||||
r.CreatorId = la.Id
|
||||
r.ModifierId = la.Id
|
||||
if err := tx.Create(r).Error; err != nil {
|
||||
logx.ErrorTrace("create msg resource menu error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import "fmt"
|
||||
|
||||
const (
|
||||
AppName = "mayfly-go"
|
||||
Version = "v1.9.3"
|
||||
Version = "v1.9.4"
|
||||
)
|
||||
|
||||
func GetAppInfo() string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package httpclient
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// 默认超时
|
||||
const DefTimeout = 60
|
||||
|
||||
type RequestWrapper struct {
|
||||
type Req struct {
|
||||
client http.Client
|
||||
url string
|
||||
method string
|
||||
@@ -37,16 +37,16 @@ type MultipartFile struct {
|
||||
}
|
||||
|
||||
// 创建一个请求
|
||||
func NewRequest(url string) *RequestWrapper {
|
||||
return &RequestWrapper{url: url, client: http.Client{}}
|
||||
func NewReq(url string) *Req {
|
||||
return &Req{url: url, client: http.Client{}}
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) Url(url string) *RequestWrapper {
|
||||
func (r *Req) Url(url string) *Req {
|
||||
r.url = url
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) Header(name, value string) *RequestWrapper {
|
||||
func (r *Req) Header(name, value string) *Req {
|
||||
if r.header == nil {
|
||||
r.header = make(map[string]string)
|
||||
}
|
||||
@@ -54,12 +54,12 @@ func (r *RequestWrapper) Header(name, value string) *RequestWrapper {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) Timeout(timeout int) *RequestWrapper {
|
||||
func (r *Req) Timeout(timeout int) *Req {
|
||||
r.timeout = timeout
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) GetByQuery(queryMap collx.M) *ResponseWrapper {
|
||||
func (r *Req) GetByQuery(queryMap collx.M) *Resp {
|
||||
var params string
|
||||
for k, v := range queryMap {
|
||||
if params != "" {
|
||||
@@ -71,13 +71,13 @@ func (r *RequestWrapper) GetByQuery(queryMap collx.M) *ResponseWrapper {
|
||||
return r.Get()
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) Get() *ResponseWrapper {
|
||||
func (r *Req) Get() *Resp {
|
||||
r.method = "GET"
|
||||
r.body = nil
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) PostJson(body string) *ResponseWrapper {
|
||||
func (r *Req) PostJson(body string) *Resp {
|
||||
buf := bytes.NewBufferString(body)
|
||||
r.method = "POST"
|
||||
r.body = buf
|
||||
@@ -88,15 +88,15 @@ func (r *RequestWrapper) PostJson(body string) *ResponseWrapper {
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) PostObj(body any) *ResponseWrapper {
|
||||
func (r *Req) PostObj(body any) *Resp {
|
||||
marshal, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return &ResponseWrapper{err: errors.New("解析json obj错误")}
|
||||
return &Resp{err: errors.New("解析json obj错误")}
|
||||
}
|
||||
return r.PostJson(string(marshal))
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) PostForm(params string) *ResponseWrapper {
|
||||
func (r *Req) PostForm(params string) *Resp {
|
||||
buf := bytes.NewBufferString(params)
|
||||
r.method = "POST"
|
||||
r.body = buf
|
||||
@@ -107,7 +107,7 @@ func (r *RequestWrapper) PostForm(params string) *ResponseWrapper {
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams collx.M) *ResponseWrapper {
|
||||
func (r *Req) PostMulipart(files []MultipartFile, reqParams collx.M) *Resp {
|
||||
buf := &bytes.Buffer{}
|
||||
// 文件写入 buf
|
||||
writer := multipart.NewWriter(buf)
|
||||
@@ -117,7 +117,7 @@ func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams collx.M)
|
||||
if uploadFile.FilePath != "" {
|
||||
file, err := os.Open(uploadFile.FilePath)
|
||||
if err != nil {
|
||||
return &ResponseWrapper{err: err}
|
||||
return &Resp{err: err}
|
||||
}
|
||||
defer file.Close()
|
||||
reader = file
|
||||
@@ -127,18 +127,18 @@ func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams collx.M)
|
||||
|
||||
part, err := writer.CreateFormFile(uploadFile.FieldName, uploadFile.FileName)
|
||||
if err != nil {
|
||||
return &ResponseWrapper{err: err}
|
||||
return &Resp{err: err}
|
||||
}
|
||||
io.Copy(part, reader)
|
||||
}
|
||||
// 如果有其他参数,则写入body
|
||||
for k, v := range reqParams {
|
||||
if err := writer.WriteField(k, cast.ToString(v)); err != nil {
|
||||
return &ResponseWrapper{err: err}
|
||||
return &Resp{err: err}
|
||||
}
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return &ResponseWrapper{err: err}
|
||||
return &Resp{err: err}
|
||||
}
|
||||
|
||||
r.method = "POST"
|
||||
@@ -150,8 +150,8 @@ func (r *RequestWrapper) PostMulipart(files []MultipartFile, reqParams collx.M)
|
||||
return sendRequest(r)
|
||||
}
|
||||
|
||||
func sendRequest(rw *RequestWrapper) *ResponseWrapper {
|
||||
respWrapper := &ResponseWrapper{}
|
||||
func sendRequest(rw *Req) *Resp {
|
||||
respWrapper := &Resp{}
|
||||
timeout := rw.timeout
|
||||
if timeout > 0 {
|
||||
rw.client.Timeout = time.Duration(timeout) * time.Second
|
||||
@@ -166,37 +166,37 @@ func sendRequest(rw *RequestWrapper) *ResponseWrapper {
|
||||
}
|
||||
setRequestHeader(req, rw.header)
|
||||
resp, err := rw.client.Do(req)
|
||||
return &ResponseWrapper{resp: resp, err: err}
|
||||
return &Resp{resp: resp, err: err}
|
||||
}
|
||||
|
||||
func setRequestHeader(req *http.Request, header map[string]string) {
|
||||
req.Header.Set("User-Agent", "golang/mayfly")
|
||||
req.Header.Set("User-Agent", "golang/mayfly-go")
|
||||
for k, v := range header {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
type ResponseWrapper struct {
|
||||
type Resp struct {
|
||||
resp *http.Response
|
||||
err error
|
||||
}
|
||||
|
||||
// 将响应体通过json解析转为指定结构体
|
||||
func (r *ResponseWrapper) BodyToObj(objPtr any) error {
|
||||
// BodyTo 将响应体通过json解析转为指定结构体
|
||||
func (r *Resp) BodyTo(ptr any) error {
|
||||
bodyBytes, err := r.BodyBytes()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bodyBytes, &objPtr)
|
||||
err = json.Unmarshal(bodyBytes, &ptr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析响应体-json解析失败-%s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 将响应体转为strings
|
||||
func (r *ResponseWrapper) BodyToString() (string, error) {
|
||||
// BodyToString 将响应体转为strings
|
||||
func (r *Resp) BodyToString() (string, error) {
|
||||
bodyBytes, err := r.BodyBytes()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -204,21 +204,20 @@ func (r *ResponseWrapper) BodyToString() (string, error) {
|
||||
return string(bodyBytes), nil
|
||||
}
|
||||
|
||||
// 将响应体通过json解析转为map
|
||||
func (r *ResponseWrapper) BodyToMap() (map[string]any, error) {
|
||||
// BodyToMap 将响应体通过json解析转为map
|
||||
func (r *Resp) BodyToMap() (map[string]any, error) {
|
||||
var res map[string]any
|
||||
return res, r.BodyToObj(&res)
|
||||
return res, r.BodyTo(&res)
|
||||
}
|
||||
|
||||
// 获取响应体的字节数组
|
||||
func (r *ResponseWrapper) BodyBytes() ([]byte, error) {
|
||||
resp, err := r.GetHttpResp()
|
||||
// BodyBytes 获取响应体的字节数组
|
||||
func (r *Resp) BodyBytes() ([]byte, error) {
|
||||
bodyReader, err := r.BodyReader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
defer resp.Body.Close()
|
||||
defer bodyReader.Close()
|
||||
body, err := io.ReadAll(bodyReader)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应体数据失败-%s", err.Error())
|
||||
@@ -226,8 +225,18 @@ func (r *ResponseWrapper) BodyBytes() ([]byte, error) {
|
||||
return body, err
|
||||
}
|
||||
|
||||
// 获取http响应结果结构体
|
||||
func (r *ResponseWrapper) GetHttpResp() (*http.Response, error) {
|
||||
// BodyReader 获取响应体的reader
|
||||
func (r *Resp) BodyReader() (io.ReadCloser, error) {
|
||||
resp, err := r.GetHttpResp()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// GetHttpResp 获取http响应结果结构体
|
||||
func (r *Resp) GetHttpResp() (*http.Response, error) {
|
||||
if r.err != nil {
|
||||
return nil, fmt.Errorf("请求失败-%s", r.err.Error())
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package httpclient
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,20 +12,20 @@ type TestStruct struct {
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
res, err := NewRequest("www.baidu.com").Get().BodyToString()
|
||||
res, err := NewReq("www.baidu.com").Get().BodyToString()
|
||||
fmt.Println(err)
|
||||
fmt.Println(res)
|
||||
}
|
||||
|
||||
func TestGetBodyToMap(t *testing.T) {
|
||||
res, err := NewRequest("http://go.mayfly.run/api/syslogs?pageNum=1&pageSize=10").Get().BodyToMap()
|
||||
res, err := NewReq("http://go.mayfly.run/api/syslogs?pageNum=1&pageSize=10").Get().BodyToMap()
|
||||
fmt.Println(err)
|
||||
fmt.Println(res["msg"])
|
||||
fmt.Println(res["code"])
|
||||
}
|
||||
|
||||
func TestGetQueryBodyToMap(t *testing.T) {
|
||||
res, err := NewRequest("http://go.mayfly.run/api/syslogs").
|
||||
res, err := NewReq("http://go.mayfly.run/api/syslogs").
|
||||
Header("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTUzOTQ5NTIsImlkIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.pGrczVZqk5nlId-FZPkjW_O5Sw3-2yjgzACp_j4JEXY").
|
||||
GetByQuery(collx.M{"pageNum": 1, "pageSize": 10}).
|
||||
BodyToMap()
|
||||
@@ -217,7 +217,7 @@ func (m Map[K, V]) Value() (driver.Value, error) {
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
type Slice[T int | string | Map[string, any]] []T
|
||||
type Slice[T int | uint64 | string | Map[string, any]] []T
|
||||
|
||||
func (s *Slice[T]) Scan(value any) error {
|
||||
if v, ok := value.([]byte); ok && len(v) > 0 {
|
||||
@@ -254,3 +254,19 @@ func (e ExtraData) GetExtraString(key string) string {
|
||||
}
|
||||
return cast.ToString(e.Extra[key])
|
||||
}
|
||||
|
||||
// GetExtraInt 获取额外信息中的int类型字段值
|
||||
func (e ExtraData) GetExtraInt(key string) int {
|
||||
if e.Extra == nil {
|
||||
return 0
|
||||
}
|
||||
return cast.ToInt(e.Extra[key])
|
||||
}
|
||||
|
||||
// GetExtraBool 获取额外信息中的bool类型字段值
|
||||
func (e ExtraData) GetExtraBool(key string) bool {
|
||||
if e.Extra == nil {
|
||||
return false
|
||||
}
|
||||
return cast.ToBool(e.Extra[key])
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ func ArrayContains[T comparable](arr []T, el T) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 数组转为map
|
||||
// @param keyFunc key的主键
|
||||
// ArrayToMap 数组转为map
|
||||
//
|
||||
// keyFunc key的主键
|
||||
func ArrayToMap[T any, K comparable](arr []T, keyFunc func(val T) K) map[K]T {
|
||||
res := make(map[K]T, len(arr))
|
||||
for _, val := range arr {
|
||||
@@ -70,7 +71,7 @@ func ArrayToMap[T any, K comparable](arr []T, keyFunc func(val T) K) map[K]T {
|
||||
return res
|
||||
}
|
||||
|
||||
// 数组映射,即将一数组元素通过映射函数转换为另一数组
|
||||
// ArrayMap 数组映射,即将一数组元素通过映射函数转换为另一数组
|
||||
func ArrayMap[T any, K any](arr []T, mapFunc func(val T) K) []K {
|
||||
res := make([]K, len(arr))
|
||||
for i, val := range arr {
|
||||
@@ -79,6 +80,18 @@ func ArrayMap[T any, K any](arr []T, mapFunc func(val T) K) []K {
|
||||
return res
|
||||
}
|
||||
|
||||
// ArrayMapFilter 数组映射并过滤,若mapFunc返回false,则不映射该元素到新数组。
|
||||
func ArrayMapFilter[T any, K any](arr []T, mapFilterFunc func(val T) (K, bool)) []K {
|
||||
res := make([]K, 0)
|
||||
for _, val := range arr {
|
||||
mapRes, needMap := mapFilterFunc(val)
|
||||
if needMap {
|
||||
res = append(res, mapRes)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 将数组或切片按固定大小分割成小数组
|
||||
func ArrayChunk[T any](arr []T, chunkSize int) [][]T {
|
||||
var chunks [][]T
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
)
|
||||
|
||||
// json字符串转map
|
||||
func ToMap(jsonStr string) map[string]any {
|
||||
func ToMap(jsonStr string) (map[string]any, error) {
|
||||
if jsonStr == "" {
|
||||
return map[string]any{}
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
return ToMapByBytes([]byte(jsonStr))
|
||||
}
|
||||
@@ -21,13 +21,10 @@ func To[T any](jsonStr string, res T) (T, error) {
|
||||
}
|
||||
|
||||
// json字节数组转map
|
||||
func ToMapByBytes(bytes []byte) map[string]any {
|
||||
func ToMapByBytes(bytes []byte) (map[string]any, error) {
|
||||
var res map[string]any
|
||||
err := json.Unmarshal(bytes, &res)
|
||||
if err != nil {
|
||||
logx.ErrorTrace("json字符串转map失败", err)
|
||||
}
|
||||
return res
|
||||
return res, err
|
||||
}
|
||||
|
||||
// 转换为json字符串
|
||||
|
||||
Reference in New Issue
Block a user