From 892509b5db146be196acb9c709da59f0bac1b545 Mon Sep 17 00:00:00 2001 From: laodaming <11058467+laudamine@user.noreply.gitee.com> Date: Thu, 9 Nov 2023 14:30:06 +0800 Subject: [PATCH] fix --- server/feishu-sync/etc/feishu-sync.yaml | 10 ++ server/feishu-sync/feishu-sync.go | 37 +++++ server/feishu-sync/internal/config/config.go | 18 +++ server/feishu-sync/internal/handler/routes.go | 22 +++ .../internal/handler/webhookhandler.go | 15 ++ .../internal/logic/user_webhook.go | 138 ++++++++++++++++ .../internal/logic/webhooklogic.go | 150 ++++++++++++++++++ .../internal/svc/servicecontext.go | 27 ++++ server/feishu-sync/internal/types/types.go | 75 +++++++++ server_api/feishu-sync.api | 16 ++ 10 files changed, 508 insertions(+) create mode 100644 server/feishu-sync/etc/feishu-sync.yaml create mode 100644 server/feishu-sync/feishu-sync.go create mode 100644 server/feishu-sync/internal/config/config.go create mode 100644 server/feishu-sync/internal/handler/routes.go create mode 100644 server/feishu-sync/internal/handler/webhookhandler.go create mode 100644 server/feishu-sync/internal/logic/user_webhook.go create mode 100644 server/feishu-sync/internal/logic/webhooklogic.go create mode 100644 server/feishu-sync/internal/svc/servicecontext.go create mode 100644 server/feishu-sync/internal/types/types.go create mode 100644 server_api/feishu-sync.api diff --git a/server/feishu-sync/etc/feishu-sync.yaml b/server/feishu-sync/etc/feishu-sync.yaml new file mode 100644 index 00000000..03a43b25 --- /dev/null +++ b/server/feishu-sync/etc/feishu-sync.yaml @@ -0,0 +1,10 @@ +Name: feishu-sync +Host: 0.0.0.0 +Port: 9925 +Timeout: 15000 #服务超时时间(毫秒) +SourceMysql: fsreaderwriter:XErSYmLELKMnf3Dh@tcp(fusen.cdmigcvz3rle.us-east-2.rds.amazonaws.com:3306)/fusen +SourceRabbitMq: "" +Auth: + AccessSecret: fusen2023 + AccessExpire: 2592000 + RefreshAfter: 1592000 diff --git a/server/feishu-sync/feishu-sync.go b/server/feishu-sync/feishu-sync.go new file mode 100644 index 00000000..140e58b8 --- /dev/null +++ b/server/feishu-sync/feishu-sync.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "fmt" + "net/http" + "time" + + "fusenapi/utils/auth" + "fusenapi/utils/fsconfig" + + "fusenapi/server/feishu-sync/internal/config" + "fusenapi/server/feishu-sync/internal/handler" + "fusenapi/server/feishu-sync/internal/svc" + + "github.com/zeromicro/go-zero/rest" +) + +var configFile = flag.String("f", "etc/feishu-sync.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + fsconfig.StartNacosConfig(*configFile, &c, nil) + + c.Timeout = int64(time.Second * 15) + server := rest.MustNewServer(c.RestConf, rest.WithCustomCors(auth.FsCors, func(w http.ResponseWriter) { + })) + defer server.Stop() + + ctx := svc.NewServiceContext(c) + handler.RegisterHandlers(server, ctx) + + fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) + server.Start() +} diff --git a/server/feishu-sync/internal/config/config.go b/server/feishu-sync/internal/config/config.go new file mode 100644 index 00000000..1224b8b8 --- /dev/null +++ b/server/feishu-sync/internal/config/config.go @@ -0,0 +1,18 @@ +package config + +import ( + "fusenapi/server/feishu-sync/internal/types" + "github.com/zeromicro/go-zero/rest" +) + +type Config struct { + rest.RestConf + SourceMysql string + Auth types.Auth + SourceRabbitMq string + FeiShu struct { + ApiHost string + EncryptKey string + VerificationToken string + } +} diff --git a/server/feishu-sync/internal/handler/routes.go b/server/feishu-sync/internal/handler/routes.go new file mode 100644 index 00000000..db0e1b4c --- /dev/null +++ b/server/feishu-sync/internal/handler/routes.go @@ -0,0 +1,22 @@ +// Code generated by goctl. DO NOT EDIT. +package handler + +import ( + "net/http" + + "fusenapi/server/feishu-sync/internal/svc" + + "github.com/zeromicro/go-zero/rest" +) + +func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodPost, + Path: "/api/feishu/webhook", + Handler: WebhookHandler(serverCtx), + }, + }, + ) +} diff --git a/server/feishu-sync/internal/handler/webhookhandler.go b/server/feishu-sync/internal/handler/webhookhandler.go new file mode 100644 index 00000000..5ed0e568 --- /dev/null +++ b/server/feishu-sync/internal/handler/webhookhandler.go @@ -0,0 +1,15 @@ +package handler + +import ( + "fusenapi/server/feishu-sync/internal/logic" + "fusenapi/server/feishu-sync/internal/svc" + "net/http" +) + +func WebhookHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // 创建一个业务逻辑层实例 + l := logic.NewWebhookLogic(r.Context(), svcCtx) + l.Webhook(w, r) + } +} diff --git a/server/feishu-sync/internal/logic/user_webhook.go b/server/feishu-sync/internal/logic/user_webhook.go new file mode 100644 index 00000000..a4f45e67 --- /dev/null +++ b/server/feishu-sync/internal/logic/user_webhook.go @@ -0,0 +1,138 @@ +package logic + +type UserWebhookMsg struct { + Schema string `json:"schema"` + Header struct { + EventId string `json:"event_id"` + EventType string `json:"event_type"` + CreateTime string `json:"create_time"` + Token string `json:"token"` + AppId string `json:"app_id"` + TenantKey string `json:"tenant_key"` + } `json:"header"` + Event struct { + Object struct { + OpenId string `json:"open_id"` + UnionId string `json:"union_id"` + UserId string `json:"user_id"` + Name string `json:"name"` + EnName string `json:"en_name"` + Nickname string `json:"nickname"` + Email string `json:"email"` + EnterpriseEmail string `json:"enterprise_email"` + JobTitle string `json:"job_title"` + Mobile string `json:"mobile"` + Gender int64 `json:"gender"` + Avatar struct { + Avatar72 string `json:"avatar_72"` + Avatar240 string `json:"avatar_240"` + Avatar640 string `json:"avatar_640"` + AvatarOrigin string `json:"avatar_origin"` + } `json:"avatar"` + Status struct { + IsFrozen bool `json:"is_frozen"` + IsResigned bool `json:"is_resigned"` + IsActivated bool `json:"is_activated"` + IsExited bool `json:"is_exited"` + IsUnjoin bool `json:"is_unjoin"` + } `json:"status"` + DepartmentIds []string `json:"department_ids"` + LeaderUserId string `json:"leader_user_id"` + City string `json:"city"` + Country string `json:"country"` + WorkStation string `json:"work_station"` + Joint64ime int64 `json:"join_time"` + EmployeeNo string `json:"employee_no"` + EmployeeType int64 `json:"employee_type"` + Orders []struct { + DepartmentId string `json:"department_id"` + UserOrder int64 `json:"user_order"` + DepartmentOrder int64 `json:"department_order"` + IsPrimaryDept bool `json:"is_primary_dept"` + } `json:"orders"` + CustomAttrs []struct { + Type string `json:"type"` + Id string `json:"id"` + Value struct { + Text string `json:"text"` + Url string `json:"url"` + PcUrl string `json:"pc_url"` + OptionId string `json:"option_id"` + OptionValue string `json:"option_value"` + Name string `json:"name"` + PictureUrl string `json:"picture_url"` + GenericUser struct { + Id string `json:"id"` + Type int64 `json:"type"` + } `json:"generic_user"` + } `json:"value"` + } `json:"custom_attrs"` + JobLevelId string `json:"job_level_id"` + JobFamilyId string `json:"job_family_id"` + DottedLineLeaderUserIds []string `json:"dotted_line_leader_user_ids"` + } `json:"object"` + } `json:"event"` +} + +// 员工增删改信息 +func (l *WebhookLogic) OnUserChange(data []byte) error { + return nil + /*var msg UserWebhookMsg + if err := json.Unmarshal(data, &msg); err != nil { + return err + } + avatar, _ := json.Marshal(msg.Event.Object.Avatar) + isFrozen := int64(0) + if msg.Event.Object.Status.IsFrozen { + isFrozen = 1 + } + isResigned := int64(0) + if msg.Event.Object.Status.IsResigned { + isResigned = 1 + } + isActivated := int64(0) + if msg.Event.Object.Status.IsActivated { + isActivated = 1 + } + isExited := int64(0) + if msg.Event.Object.Status.IsExited { + isExited = 1 + } + isUnjoin := int64(0) + if msg.Event.Object.Status.IsUnjoin { + isUnjoin = 1 + } + departmentIds, _ := json.Marshal(msg.Event.Object.DepartmentIds) + orders, _ := json.Marshal(msg.Event.Object.Orders) + feiShuMsgCreateTimeInt64, err := strconv.ParseInt(msg.Header.CreateTime, 10, 64) + if err != nil { + return err + } + feiShuMsgCreateTime := time.UnixMilli(feiShuMsgCreateTimeInt64) + return l.svcCtx.AllModels.FsFeishuUser.CreateOrUpdate(l.ctx, msg.Header.AppId, msg.Event.Object.OpenId, &gmodel.FsFeishuUser{ + AppId: &msg.Header.AppId, + OpenId: &msg.Event.Object.OpenId, + UnionId: &msg.Event.Object.UnionId, + Name: &msg.Event.Object.Name, + EnName: &msg.Event.Object.EnName, + Nickname: &msg.Event.Object.Nickname, + Email: &msg.Event.Object.Email, + EnterpriseEmail: &msg.Event.Object.EnterpriseEmail, + JobTitle: &msg.Event.Object.JobTitle, + Mobile: &msg.Event.Object.Mobile, + Gender: &msg.Event.Object.Gender, + Avatar: &avatar, + IsFrozen: &isFrozen, + IsResigned: &isResigned, + IsActivated: &isActivated, + IsExited: &isExited, + IsUnjoin: &isUnjoin, + DepartmentIds: &departmentIds, + WorkStation: &msg.Event.Object.WorkStation, + EmployeeNo: &msg.Event.Object.EmployeeNo, + EmployeeType: &msg.Event.Object.EmployeeType, + Orders: &orders, + Ctime: &feiShuMsgCreateTime, + Utime: &feiShuMsgCreateTime, + })*/ +} diff --git a/server/feishu-sync/internal/logic/webhooklogic.go b/server/feishu-sync/internal/logic/webhooklogic.go new file mode 100644 index 00000000..9977575f --- /dev/null +++ b/server/feishu-sync/internal/logic/webhooklogic.go @@ -0,0 +1,150 @@ +package logic + +import ( + "context" + "encoding/json" + "fusenapi/model/gmodel" + "fusenapi/server/feishu-sync/internal/svc" + "fusenapi/utils/feishu" + "github.com/zeromicro/go-zero/core/logx" + "io" + "net/http" + "strconv" + "time" +) + +type WebhookLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewWebhookLogic(ctx context.Context, svcCtx *svc.ServiceContext) *WebhookLogic { + return &WebhookLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +type EncryptWebhookMsg struct { + Encrypt string `json:"encrypt"` //加密的消息 +} +type WebhookMsg struct { + Type string `json:"type"` + Challenge string `json:"challenge"` + Header map[string]interface{} `json:"header"` +} + +// webhook消息事件header(body参数)基础信息 +type BaseWebhookMsgHeaderType struct { + EventId string `json:"event_id"` //事件id(可作为消息唯一性确认) + EventType string `json:"event_type"` //事件类型 + CreateTime string `json:"create_time"` //创建时间 + Token string `json:"token"` //事件token + AppId string `json:"app_id"` //app id + TenantKey string `json:"tenant_key"` //租户key +} + +func (l *WebhookLogic) Webhook(w http.ResponseWriter, r *http.Request) { + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + logx.Error("读取请求body失败", err) + return + } + defer r.Body.Close() + //计算签名 + timestamp := r.Header.Get("X-Lark-Request-Timestamp") + nonce := r.Header.Get("X-Lark-Request-Nonce") + signature := r.Header.Get("X-Lark-Signature") + sign := feishu.CalculateFeiShuWebhookSignature(timestamp, nonce, l.svcCtx.Config.FeiShu.EncryptKey, bodyBytes) + if signature != sign { + logx.Error("非法的消息,签名验证不通过", sign, "====", signature) + return + } + var encryptMsg EncryptWebhookMsg + if err = json.Unmarshal(bodyBytes, &encryptMsg); err != nil { + logx.Error("反序列化body失败", err, "body数据:", string(bodyBytes)) + return + } + if encryptMsg.Encrypt == "" { + logx.Error("消息加密信息是空的") + return + } + //解密 + realMsgBytes, err := feishu.DecryptFeiShuWebhookMsg(encryptMsg.Encrypt, l.svcCtx.Config.FeiShu.EncryptKey) + if err != nil { + logx.Error(err) + return + } + //如果只是验证http连接的消息 + var webhookMsg WebhookMsg + if err = json.Unmarshal(realMsgBytes, &webhookMsg); err != nil { + logx.Error("反序列化请求body失败", err, "解密数据:", string(realMsgBytes)) + return + } + //验证连接(直接返回) + if webhookMsg.Type == "url_verification" { + challengeRsp := map[string]string{ + "challenge": webhookMsg.Challenge, + } + b, _ := json.Marshal(challengeRsp) + w.Write(b) + return + } + bodyHeaderByte, err := json.Marshal(webhookMsg.Header) + if err != nil { + logx.Error("序列化请求体header失败:", err) + return + } + var msgHeader BaseWebhookMsgHeaderType + if err = json.Unmarshal(bodyHeaderByte, &msgHeader); err != nil { + logx.Error("反序列化请求体中的header失败", err) + return + } + httpHeaderBytes, _ := json.Marshal(r.Header) + httpHeaderStr := string(httpHeaderBytes) + //解密后的数据 + decryptMsgStr := string(realMsgBytes) + feiShuMsgCreateTimeInt64, err := strconv.ParseInt(msgHeader.CreateTime, 10, 64) + if err != nil { + logx.Error("解析消息时间错误:", err) + return + } + feiShuMsgCreateTime := time.UnixMilli(feiShuMsgCreateTimeInt64) + now := time.Now().UTC() + //把事件加入日志 + err = l.svcCtx.AllModels.FsFeishuWebhookLog.Create(l.ctx, &gmodel.FsFeishuWebhookLog{ + AppId: &msgHeader.AppId, + EventId: &msgHeader.EventId, + EventType: &msgHeader.EventType, + HttpHeader: &httpHeaderStr, + Data: &encryptMsg.Encrypt, + DecryptData: &decryptMsgStr, + MsgCtime: &feiShuMsgCreateTime, + Ctime: &now, + }) + if err != nil { + logx.Error("保存webhook消息日志失败:", err) + } + switch msgHeader.EventType { + case "contact.department.created_v3": //部门新建 + case "contact.department.deleted_v3": //部门删除 + case "contact.department.updated_v3": //部门信息变化 + case "contact.employee_type_enum.actived_v3": //启动人员类型事件 + case "contact.employee_type_enum.created_v3": //新建人员类型事件 + case "contact.employee_type_enum.deactivated_v3": //停用人员类型事件 + case "contact.employee_type_enum.deleted_v3": //删除人员类型事件 + case "contact.employee_type_enum.updated_v3": //修改人员类型名称事件 + case "contact.user.created_v3": //员工入职 + err = l.OnUserChange(realMsgBytes) + case "contact.user.deleted_v3": //员工离职 + err = l.OnUserChange(realMsgBytes) + case "contact.user.updated_v3": //员工信息变化 + err = l.OnUserChange(realMsgBytes) + } + if err != nil { + logx.Error("处理事件错误:", err) + } + return +} diff --git a/server/feishu-sync/internal/svc/servicecontext.go b/server/feishu-sync/internal/svc/servicecontext.go new file mode 100644 index 00000000..6e5b8149 --- /dev/null +++ b/server/feishu-sync/internal/svc/servicecontext.go @@ -0,0 +1,27 @@ +package svc + +import ( + "fusenapi/initalize" + "fusenapi/model/gmodel" + "fusenapi/server/feishu-sync/internal/config" + "gorm.io/gorm" +) + +type ServiceContext struct { + Config config.Config + + MysqlConn *gorm.DB + AllModels *gmodel.AllModelsGen + RabbitMq *initalize.RabbitMqHandle +} + +func NewServiceContext(c config.Config) *ServiceContext { + conn := initalize.InitMysql(c.SourceMysql) + + return &ServiceContext{ + Config: c, + MysqlConn: conn, + AllModels: gmodel.NewAllModels(initalize.InitMysql(c.SourceMysql)), + RabbitMq: initalize.InitRabbitMq(c.SourceRabbitMq, nil), + } +} diff --git a/server/feishu-sync/internal/types/types.go b/server/feishu-sync/internal/types/types.go new file mode 100644 index 00000000..b5fd43e5 --- /dev/null +++ b/server/feishu-sync/internal/types/types.go @@ -0,0 +1,75 @@ +// Code generated by goctl. DO NOT EDIT. +package types + +import ( + "fusenapi/utils/basic" +) + +type Request struct { +} + +type Response struct { + Code int `json:"code"` + Message string `json:"msg"` + Data interface{} `json:"data"` +} + +type Auth struct { + AccessSecret string `json:"accessSecret"` + AccessExpire int64 `json:"accessExpire"` + RefreshAfter int64 `json:"refreshAfter"` +} + +type File struct { + Filename string `fsfile:"filename"` + Header map[string][]string `fsfile:"header"` + Size int64 `fsfile:"size"` + Data []byte `fsfile:"data"` +} + +type Meta struct { + TotalCount int64 `json:"total_count"` + PageCount int64 `json:"page_count"` + CurrentPage int `json:"current_page"` + PerPage int `json:"per_page"` +} + +// Set 设置Response的Code和Message值 +func (resp *Response) Set(Code int, Message string) *Response { + return &Response{ + Code: Code, + Message: Message, + } +} + +// Set 设置整个Response +func (resp *Response) SetWithData(Code int, Message string, Data interface{}) *Response { + return &Response{ + Code: Code, + Message: Message, + Data: Data, + } +} + +// SetStatus 设置默认StatusResponse(内部自定义) 默认msg, 可以带data, data只使用一个参数 +func (resp *Response) SetStatus(sr *basic.StatusResponse, data ...interface{}) *Response { + newResp := &Response{ + Code: sr.Code, + } + if len(data) == 1 { + newResp.Data = data[0] + } + return newResp +} + +// SetStatusWithMessage 设置默认StatusResponse(内部自定义) 非默认msg, 可以带data, data只使用一个参数 +func (resp *Response) SetStatusWithMessage(sr *basic.StatusResponse, msg string, data ...interface{}) *Response { + newResp := &Response{ + Code: sr.Code, + Message: msg, + } + if len(data) == 1 { + newResp.Data = data[0] + } + return newResp +} diff --git a/server_api/feishu-sync.api b/server_api/feishu-sync.api new file mode 100644 index 00000000..31db4bea --- /dev/null +++ b/server_api/feishu-sync.api @@ -0,0 +1,16 @@ +syntax = "v1" + +info ( + title: "飞书同步服务"// TODO: add title + desc: // TODO: add description + author: "" + email: "" +) + +import "basic.api" + +service feishu-sync { + //飞书ticket webhook事件接口 + @handler WebhookHandler + post /api/feishu/webhook(request) returns (response); +} \ No newline at end of file