Ver código fonte

1、增加登录时发送登录总线报文;
2、增加登出接口;
3、增加管理端强制用户下线通知接收逻辑。

zhou.xiaoning 2 anos atrás
pai
commit
cde9e1c34f

+ 16 - 0
api/v1/account/login.go

@@ -66,3 +66,19 @@ func TokenCheck(c *gin.Context) {
 
 	response.Ok(c)
 }
+
+// Loginout 用户登出请求
+// @Summary  用户登出请求
+// @Security ApiKeyAuth
+// @accept   application/json
+// @Produce  application/json
+// @Success  200 {object} response.Response{msg=string} "出参"
+// @Router   /Account/Loginout [get]
+// @Tags     账户服务
+func Loginout(c *gin.Context) {
+	if err := accountService.Logout(c); err != nil {
+		response.FailWithMessage(err.Error(), c)
+		return
+	}
+	response.OkWithMessage("登出成功", c)
+}

+ 1 - 1
api/v1/quote/quote.go

@@ -17,7 +17,7 @@ func Quote(c *gin.Context) {
 	// response.OkWithMessage("连接成功", c)
 }
 
-// SendMsgToMQ 订阅商品实时行情请求
+// QuoteSubscribe 订阅商品实时行情请求
 // @Summary  订阅商品实时行情请求
 // @Security ApiKeyAuth
 // @accept   application/json

+ 1 - 0
client/client.go

@@ -381,6 +381,7 @@ type LoginRedis struct {
 	Token     string `json:"token" redis:"token"`         // 令牌
 	Group     string `json:"group" redis:"group"`         // 终端分组
 	Addr      string `json:"addr" redis:"addr"`           // 客户端地址信息 // FIXME: 由于本服务改用短连,所以每次提交请交请求可能会不一样,后期可判断是否在中间件中进行拦截
+	OldToken  string `json:"-" redis:"-"`                 // 旧接入Token
 }
 
 // FromMap Map to Struct

+ 39 - 0
docs/docs.go

@@ -64,6 +64,45 @@ const docTemplate = `{
                 }
             }
         },
+        "/Account/Loginout": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "账户服务"
+                ],
+                "summary": "用户登出请求",
+                "responses": {
+                    "200": {
+                        "description": "出参",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/response.Response"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "msg": {
+                                            "type": "string"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "/Account/TokenCheck": {
             "get": {
                 "security": [

+ 39 - 0
docs/swagger.json

@@ -55,6 +55,45 @@
                 }
             }
         },
+        "/Account/Loginout": {
+            "get": {
+                "security": [
+                    {
+                        "ApiKeyAuth": []
+                    }
+                ],
+                "consumes": [
+                    "application/json"
+                ],
+                "produces": [
+                    "application/json"
+                ],
+                "tags": [
+                    "账户服务"
+                ],
+                "summary": "用户登出请求",
+                "responses": {
+                    "200": {
+                        "description": "出参",
+                        "schema": {
+                            "allOf": [
+                                {
+                                    "$ref": "#/definitions/response.Response"
+                                },
+                                {
+                                    "type": "object",
+                                    "properties": {
+                                        "msg": {
+                                            "type": "string"
+                                        }
+                                    }
+                                }
+                            ]
+                        }
+                    }
+                }
+            }
+        },
         "/Account/TokenCheck": {
             "get": {
                 "security": [

+ 21 - 0
docs/swagger.yaml

@@ -135,6 +135,27 @@ paths:
       summary: 账户登录
       tags:
       - 账户服务
+  /Account/Loginout:
+    get:
+      consumes:
+      - application/json
+      produces:
+      - application/json
+      responses:
+        "200":
+          description: 出参
+          schema:
+            allOf:
+            - $ref: '#/definitions/response.Response'
+            - properties:
+                msg:
+                  type: string
+              type: object
+      security:
+      - ApiKeyAuth: []
+      summary: 用户登出请求
+      tags:
+      - 账户服务
   /Account/TokenCheck:
     get:
       consumes:

+ 5 - 1
global/funcode.go

@@ -118,7 +118,11 @@ var (
 	DeliveryClientOperatorReq   = 196743 // 交收终端操作接口请求
 	DeliveryClientOperatorRsp   = 196744 // 交收终端操作接口响应
 
-	LoginReq = 65537 // 用户登录请求
+	LoginReq  = 65537 // 用户登录请求
+	LoginRsp  = 65538 // 用户登录应答
+	LogoutReq = 65539 // 用户登出请求
+	LogoutRsp = 65540 // 用户登出应答 - 主要用于接收管理端踢上线
+	// CustOfflineNtf = 131074 // 客户离线通知 - 暂时不用
 )
 
 // 通过请求功能码获取对应主题的方法

+ 42 - 4
initialize/rabbitmq.go

@@ -11,6 +11,7 @@ import (
 	"mtp20access/res/pb"
 	accountSrv "mtp20access/service/account"
 	"mtp20access/utils"
+	"strconv"
 
 	// "github.com/golang/protobuf/proto"
 	"github.com/streadway/amqp"
@@ -51,9 +52,9 @@ func (t *MQProc) Process(topic, queuename string, msg *[]byte) {
 	// global.M2A_LOG.Info(info)
 
 	if funcode, sessionId, bytes, serialNumber, err := t.getRspProtobuf(msg); err == nil && bytes != nil {
-		if sessionId == 0 {
-			// 通知类
-			t.onNtf(funcode, bytes)
+		if sessionId == 0 || funcode == uint32(global.LogoutRsp) || funcode == uint32(global.LoginRsp) {
+			// 通知类 或 特殊处理
+			t.onNtf(funcode, sessionId, bytes)
 		} else {
 			// 请求回复W
 			// 尝试获取对应异步任务
@@ -96,11 +97,42 @@ func (t *MQProc) Process(topic, queuename string, msg *[]byte) {
 	}
 }
 
-func (t *MQProc) onNtf(funcode uint32, bytes *[]byte) {
+func (t *MQProc) onNtf(funcode uint32, sessionId uint32, bytes *[]byte) {
 	var clients []*client.Client
 	var err error
 
 	switch int(funcode) {
+	case global.LoginRsp: // 用户登录应答 - 主要记录旧Token
+		var p pb.LoginRsp
+		if err = proto.Unmarshal(*bytes, &p); err != nil {
+			global.M2A_LOG.Error("总线数据反序列化失败", zap.Error(err))
+			return
+		}
+
+		// 获取目标客户
+		for i := range client.Clients {
+			c := client.Clients[i]
+			if strconv.Itoa(int(p.GetUserID())) == c.UserID &&
+				strconv.Itoa(int(sessionId)) == c.SessionID {
+				// 主要记录旧Token
+				c.OldToken = p.GetToken()
+			}
+		}
+	case global.LogoutRsp: // 用户登出应答 - 主要用于接收管理端踢上线
+		var p pb.LogoutRsp
+		if err = proto.Unmarshal(*bytes, &p); err != nil {
+			global.M2A_LOG.Error("总线数据反序列化失败", zap.Error(err))
+			return
+		}
+
+		// 获取目标客户
+		clients = make([]*client.Client, 0)
+		for i := range client.Clients {
+			c := client.Clients[i]
+			if strconv.Itoa(int(p.GetHeader().GetUserID())) == c.UserID {
+				clients = append(clients, c)
+			}
+		}
 	case global.MoneyChangedNtf: // 资金变化通知
 		var p pb.MoneyChangedNtf
 		if err = proto.Unmarshal(*bytes, &p); err != nil {
@@ -182,6 +214,8 @@ func (t *MQProc) getRspProtobuf(msg *[]byte) (funcode uint32, sessionId uint32,
 	case global.MoneyChangedNtf,
 		global.OrderDealedNtf,
 		global.MarketStatusChangeNtf,
+		global.LoginRsp,
+		global.LogoutRsp,
 		global.ListingOrderChangeNtf: // 资金变化通知等
 
 		bytes = &b
@@ -791,6 +825,10 @@ func (t *MQProc) getRspProtobuf(msg *[]byte) (funcode uint32, sessionId uint32,
 // RabbitMQSubscribeTopic 订阅主题
 func RabbitMQSubscribeTopic() (err error) {
 	// 订阅需要的总线响应主题
+	if err = rabbitmq.SubscribeTopic(global.TOPIC_RSP_USER); err != nil {
+		global.M2A_LOG.Error("rabbitmq subscribe topic failed, err:", zap.Error(err))
+		return
+	}
 	if err = rabbitmq.SubscribeTopic(global.TOPIC_RSP_NTF); err != nil {
 		global.M2A_LOG.Error("rabbitmq subscribe topic failed, err:", zap.Error(err))
 		return

+ 282 - 8
res/pb/mtp2.pb.go

@@ -13985,6 +13985,214 @@ func (x *LoginRsp) GetClientID() uint64 {
 	return 0
 }
 
+// 客户离线通知
+type CustOfflineNtf struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Header    *MessageHead `protobuf:"bytes,1,opt,name=Header" json:"Header,omitempty"`        // 消息头
+	SessionID []uint32     `protobuf:"varint,2,rep,name=SessionID" json:"SessionID,omitempty"` // uint32 用户的sessionid(由接入服务分配的),是一数组。该接口是系统内部服务使用的接口
+	LoginID   []uint64     `protobuf:"varint,3,rep,name=LoginID" json:"LoginID,omitempty"`     // uint64 用户登陆的loginid,是一数组。该接口是系统内部服务使用的接口
+}
+
+func (x *CustOfflineNtf) Reset() {
+	*x = CustOfflineNtf{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_mtp2_proto_msgTypes[120]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CustOfflineNtf) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CustOfflineNtf) ProtoMessage() {}
+
+func (x *CustOfflineNtf) ProtoReflect() protoreflect.Message {
+	mi := &file_mtp2_proto_msgTypes[120]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CustOfflineNtf.ProtoReflect.Descriptor instead.
+func (*CustOfflineNtf) Descriptor() ([]byte, []int) {
+	return file_mtp2_proto_rawDescGZIP(), []int{120}
+}
+
+func (x *CustOfflineNtf) GetHeader() *MessageHead {
+	if x != nil {
+		return x.Header
+	}
+	return nil
+}
+
+func (x *CustOfflineNtf) GetSessionID() []uint32 {
+	if x != nil {
+		return x.SessionID
+	}
+	return nil
+}
+
+func (x *CustOfflineNtf) GetLoginID() []uint64 {
+	if x != nil {
+		return x.LoginID
+	}
+	return nil
+}
+
+// 用户登出请求
+type LogoutReq struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Header    *MessageHead `protobuf:"bytes,1,opt,name=Header" json:"Header,omitempty"`
+	LoginID   *uint64      `protobuf:"varint,2,opt,name=LoginID" json:"LoginID,omitempty"`     // 登录ID
+	Token     *string      `protobuf:"bytes,3,opt,name=Token" json:"Token,omitempty"`          // 登录时返回的用户令牌
+	LoginIp   *string      `protobuf:"bytes,4,opt,name=LoginIp" json:"LoginIp,omitempty"`      // 登出IP地址
+	LoginPort *uint32      `protobuf:"varint,5,opt,name=LoginPort" json:"LoginPort,omitempty"` // 登出通信端口
+}
+
+func (x *LogoutReq) Reset() {
+	*x = LogoutReq{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_mtp2_proto_msgTypes[121]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *LogoutReq) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LogoutReq) ProtoMessage() {}
+
+func (x *LogoutReq) ProtoReflect() protoreflect.Message {
+	mi := &file_mtp2_proto_msgTypes[121]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use LogoutReq.ProtoReflect.Descriptor instead.
+func (*LogoutReq) Descriptor() ([]byte, []int) {
+	return file_mtp2_proto_rawDescGZIP(), []int{121}
+}
+
+func (x *LogoutReq) GetHeader() *MessageHead {
+	if x != nil {
+		return x.Header
+	}
+	return nil
+}
+
+func (x *LogoutReq) GetLoginID() uint64 {
+	if x != nil && x.LoginID != nil {
+		return *x.LoginID
+	}
+	return 0
+}
+
+func (x *LogoutReq) GetToken() string {
+	if x != nil && x.Token != nil {
+		return *x.Token
+	}
+	return ""
+}
+
+func (x *LogoutReq) GetLoginIp() string {
+	if x != nil && x.LoginIp != nil {
+		return *x.LoginIp
+	}
+	return ""
+}
+
+func (x *LogoutReq) GetLoginPort() uint32 {
+	if x != nil && x.LoginPort != nil {
+		return *x.LoginPort
+	}
+	return 0
+}
+
+// 用户登出应答
+type LogoutRsp struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Header  *MessageHead `protobuf:"bytes,1,opt,name=Header" json:"Header,omitempty"`    // 消息头
+	RetCode *int32       `protobuf:"varint,2,opt,name=RetCode" json:"RetCode,omitempty"` // 返回码
+	RetDesc *string      `protobuf:"bytes,3,opt,name=RetDesc" json:"RetDesc,omitempty"`  // 描述信息
+}
+
+func (x *LogoutRsp) Reset() {
+	*x = LogoutRsp{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_mtp2_proto_msgTypes[122]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *LogoutRsp) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LogoutRsp) ProtoMessage() {}
+
+func (x *LogoutRsp) ProtoReflect() protoreflect.Message {
+	mi := &file_mtp2_proto_msgTypes[122]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use LogoutRsp.ProtoReflect.Descriptor instead.
+func (*LogoutRsp) Descriptor() ([]byte, []int) {
+	return file_mtp2_proto_rawDescGZIP(), []int{122}
+}
+
+func (x *LogoutRsp) GetHeader() *MessageHead {
+	if x != nil {
+		return x.Header
+	}
+	return nil
+}
+
+func (x *LogoutRsp) GetRetCode() int32 {
+	if x != nil && x.RetCode != nil {
+		return *x.RetCode
+	}
+	return 0
+}
+
+func (x *LogoutRsp) GetRetDesc() string {
+	if x != nil && x.RetDesc != nil {
+		return *x.RetDesc
+	}
+	return ""
+}
+
 var File_mtp2_proto protoreflect.FileDescriptor
 
 var file_mtp2_proto_rawDesc = []byte{
@@ -16572,7 +16780,31 @@ var file_mtp2_proto_rawDesc = []byte{
 	0x49, 0x6e, 0x66, 0x6f, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x10, 0x43, 0x6c, 0x69, 0x65,
 	0x6e, 0x74, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1a, 0x0a, 0x08,
 	0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x12, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08,
-	0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44,
+	0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x22, 0x71, 0x0a, 0x0e, 0x43, 0x75, 0x73, 0x74,
+	0x4f, 0x66, 0x66, 0x6c, 0x69, 0x6e, 0x65, 0x4e, 0x74, 0x66, 0x12, 0x27, 0x0a, 0x06, 0x48, 0x65,
+	0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e,
+	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x65, 0x61, 0x64, 0x52, 0x06, 0x48, 0x65, 0x61,
+	0x64, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x44,
+	0x18, 0x02, 0x20, 0x03, 0x28, 0x0d, 0x52, 0x09, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49,
+	0x44, 0x12, 0x18, 0x0a, 0x07, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x49, 0x44, 0x18, 0x03, 0x20, 0x03,
+	0x28, 0x04, 0x52, 0x07, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x49, 0x44, 0x22, 0x9c, 0x01, 0x0a, 0x09,
+	0x4c, 0x6f, 0x67, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x12, 0x27, 0x0a, 0x06, 0x48, 0x65, 0x61,
+	0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x4d,
+	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x48, 0x65, 0x61, 0x64, 0x52, 0x06, 0x48, 0x65, 0x61, 0x64,
+	0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x49, 0x44, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x04, 0x52, 0x07, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05,
+	0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x54, 0x6f, 0x6b,
+	0x65, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x49, 0x70, 0x18, 0x04, 0x20,
+	0x01, 0x28, 0x09, 0x52, 0x07, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x49, 0x70, 0x12, 0x1c, 0x0a, 0x09,
+	0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52,
+	0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x68, 0x0a, 0x09, 0x4c, 0x6f,
+	0x67, 0x6f, 0x75, 0x74, 0x52, 0x73, 0x70, 0x12, 0x27, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65,
+	0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x70, 0x62, 0x2e, 0x4d, 0x65, 0x73,
+	0x73, 0x61, 0x67, 0x65, 0x48, 0x65, 0x61, 0x64, 0x52, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72,
+	0x12, 0x18, 0x0a, 0x07, 0x52, 0x65, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
+	0x05, 0x52, 0x07, 0x52, 0x65, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x65,
+	0x74, 0x44, 0x65, 0x73, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x52, 0x65, 0x74,
+	0x44, 0x65, 0x73, 0x63,
 }
 
 var (
@@ -16587,7 +16819,7 @@ func file_mtp2_proto_rawDescGZIP() []byte {
 	return file_mtp2_proto_rawDescData
 }
 
-var file_mtp2_proto_msgTypes = make([]protoimpl.MessageInfo, 120)
+var file_mtp2_proto_msgTypes = make([]protoimpl.MessageInfo, 123)
 var file_mtp2_proto_goTypes = []interface{}{
 	(*MessageHead)(nil),                         // 0: pb.MessageHead
 	(*NotifyHead)(nil),                          // 1: pb.NotifyHead
@@ -16709,6 +16941,9 @@ var file_mtp2_proto_goTypes = []interface{}{
 	(*DeliveryClientOperatorRsp)(nil),           // 117: pb.DeliveryClientOperatorRsp
 	(*LoginReq)(nil),                            // 118: pb.LoginReq
 	(*LoginRsp)(nil),                            // 119: pb.LoginRsp
+	(*CustOfflineNtf)(nil),                      // 120: pb.CustOfflineNtf
+	(*LogoutReq)(nil),                           // 121: pb.LogoutReq
+	(*LogoutRsp)(nil),                           // 122: pb.LogoutRsp
 }
 var file_mtp2_proto_depIdxs = []int32{
 	0,   // 0: pb.MoneyChangedNtf.Header:type_name -> pb.MessageHead
@@ -16836,11 +17071,14 @@ var file_mtp2_proto_depIdxs = []int32{
 	0,   // 122: pb.DeliveryClientOperatorRsp.Header:type_name -> pb.MessageHead
 	0,   // 123: pb.LoginReq.Header:type_name -> pb.MessageHead
 	0,   // 124: pb.LoginRsp.Header:type_name -> pb.MessageHead
-	125, // [125:125] is the sub-list for method output_type
-	125, // [125:125] is the sub-list for method input_type
-	125, // [125:125] is the sub-list for extension type_name
-	125, // [125:125] is the sub-list for extension extendee
-	0,   // [0:125] is the sub-list for field type_name
+	0,   // 125: pb.CustOfflineNtf.Header:type_name -> pb.MessageHead
+	0,   // 126: pb.LogoutReq.Header:type_name -> pb.MessageHead
+	0,   // 127: pb.LogoutRsp.Header:type_name -> pb.MessageHead
+	128, // [128:128] is the sub-list for method output_type
+	128, // [128:128] is the sub-list for method input_type
+	128, // [128:128] is the sub-list for extension type_name
+	128, // [128:128] is the sub-list for extension extendee
+	0,   // [0:128] is the sub-list for field type_name
 }
 
 func init() { file_mtp2_proto_init() }
@@ -18289,6 +18527,42 @@ func file_mtp2_proto_init() {
 				return nil
 			}
 		}
+		file_mtp2_proto_msgTypes[120].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CustOfflineNtf); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_mtp2_proto_msgTypes[121].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*LogoutReq); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_mtp2_proto_msgTypes[122].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*LogoutRsp); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
@@ -18296,7 +18570,7 @@ func file_mtp2_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_mtp2_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   120,
+			NumMessages:   123,
 			NumExtensions: 0,
 			NumServices:   0,
 		},

+ 20 - 0
res/pb/mtp2.proto

@@ -1596,4 +1596,24 @@ message LoginRsp {
 		optional string LoginCode = 16; // 登陆码
 		optional bytes ClientSystemInfo = 17; // 终端系统信息
 		optional uint64 ClientID = 18; // 终端ID(登陆服务分配,用于通道交易关联链路)
+}
+// 客户离线通知
+message CustOfflineNtf {
+	optional MessageHead Header = 1; // 消息头
+		repeated uint32 SessionID = 2; // uint32 用户的sessionid(由接入服务分配的),是一数组。该接口是系统内部服务使用的接口
+		repeated uint64 LoginID = 3; // uint64 用户登陆的loginid,是一数组。该接口是系统内部服务使用的接口
+}
+// 用户登出请求
+message LogoutReq {
+	optional MessageHead Header = 1;
+		optional uint64 LoginID = 2; // 登录ID
+		optional string Token = 3; // 登录时返回的用户令牌
+		optional string LoginIp = 4; // 登出IP地址
+		optional uint32 LoginPort = 5; // 登出通信端口
+}
+// 用户登出应答
+message LogoutRsp {
+	optional MessageHead Header = 1; // 消息头
+	optional int32 RetCode = 2; // 返回码
+	optional string RetDesc = 3; // 描述信息
 }

+ 1 - 0
router/account.go

@@ -17,5 +17,6 @@ func InitAccountPrivateRouter(r *gin.RouterGroup) {
 	accountR := r.Group("Account").Use()
 	{
 		accountR.GET("TokenCheck", account.TokenCheck)
+		accountR.GET("Loginout", account.Loginout)
 	}
 }

+ 48 - 2
service/account/login.go

@@ -9,7 +9,6 @@ import (
 	"mtp20access/global"
 	accountModel "mtp20access/model/account"
 	"mtp20access/model/account/request"
-	jwtRequest "mtp20access/model/common/request"
 	"mtp20access/packet"
 	"mtp20access/rabbitmq"
 	"mtp20access/res/pb"
@@ -18,6 +17,9 @@ import (
 	"mtp20access/utils"
 	"strconv"
 
+	commonRequest "mtp20access/model/common/request"
+
+	"github.com/gin-gonic/gin"
 	"github.com/gofrs/uuid"
 	"github.com/golang/protobuf/proto"
 	"go.uber.org/zap"
@@ -168,7 +170,7 @@ func buildRedisLoginInfo(loginaccount accountModel.Loginaccount, addr string, gr
 
 	// 生成本服务Token
 	j := &utils.JWT{SigningKey: []byte(global.M2A_CONFIG.JWT.SigningKey)} // 唯一签名
-	claims := j.CreateClaims(jwtRequest.BaseClaims{
+	claims := j.CreateClaims(commonRequest.BaseClaims{
 		LoginID:   int(loginaccount.LOGINID),
 		Group:     group,
 		SessionID: sessionID,
@@ -296,3 +298,47 @@ func GetClientsByAccountID(accountID uint64) (clients []*client.Client, err erro
 
 	return
 }
+
+func Logout(c *gin.Context) (err error) {
+	// 获取请求账号信息
+	s, exists := c.Get("claims")
+	if !exists {
+		err = errors.New("获取请求账号信息异常")
+		global.M2A_LOG.Error(err.Error(), zap.Error(err))
+		return
+	}
+	claims := s.(*commonRequest.CustomClaims)
+
+	// 获取登录账户信息
+	t, exists := client.Clients[claims.SessionID]
+	if !exists {
+		err = errors.New("获取登录账户信息异常")
+		global.M2A_LOG.Error(err.Error(), zap.Error(err))
+		return
+	}
+
+	// 发送登出报文给总线
+	logoutReq := pb.LogoutReq{}
+	logoutReq.LoginID = utils.SetPointValue(uint64(claims.LoginID))
+	logoutReq.Token = utils.SetPointValue(t.OldToken)
+	logoutReq.LoginIp = utils.SetPointValue(c.ClientIP())
+	port, _ := strconv.Atoi(c.Request.URL.Port())
+	logoutReq.LoginPort = utils.SetPointValue(uint32(port))
+	header := pb.MessageHead{}
+	header.FunCode = utils.SetPointValue(uint32(global.LogoutReq))
+	userID, _ := strconv.Atoi(t.UserID)
+	header.UserID = utils.SetPointValue(uint32(userID))
+	uid, _ := uuid.NewV4()
+	header.UUID = utils.SetPointValue(uid.String())
+	logoutReq.Header = &header
+	if b, e := proto.Marshal(&logoutReq); e == nil {
+		packet := &client.MQPacket{
+			FunCode:   uint32(global.LogoutReq),
+			SessionId: uint32(claims.SessionID),
+			Data:      &b,
+		}
+		go rabbitmq.Publish(global.TOPIC_REQ_USER, packet)
+	}
+
+	return
+}