项目梳理

353 阅读10分钟

整个魔飞业务流程图如上所示,以打开用户卧室的一盏灯为例,详细阐述业务处理流程。 用户在家里对着魔飞说一句“魔飞,魔飞,打开主卧的灯”,魔飞将语音命令转化成文字上传到AIUI,AIUI将对应语义上传到魔飞的APPID对应的后处理地址,语义格式为json格式

AIUI通过post方式向语义后处理服务传递数据

Request

  • URL

    POST http://172.31.0.0:4050/aiui/post-processor
    
  • 输入参数

参数 类型 说明 获取方法 举例
msgsignature string 存储消息的签名 Query "5497eae7ff44a5a9b6f93fe07a2702e7f1575016"
timestamp string 由平台生成时间戳 Query "1453366222359"
rand string 平台随机生成的随机串 Query "56789"
encrypttype string 用户设置是否加密,取值{"raw","aes"}当为"aes"时,需要用到aesKey Query "aes"
  • requestBody(具体详见https://aiui.xfyun.cn/docs/protocol)
{
    "MsgId":"123",
    "CreateTime":12345,
    "AppId":"111",
    "UserId":"222",
    "SessionParams":"ffddd",
    "UserParams":"PG5hbWU",
    "FromSub":"iat",
    "Msg":{"Type":"text",
          "ContentType":"Json",
  "Content":"eyJpbnRlbnQiOnsic2F2ZV9oaXN0b3J5Ijp0cnVlLCJvcGVyYXRpb24iOiJTRVQiLCJyYyI6MCwic2VtYW50aWMiOnsic2xvdHMiOnsiYXR0ciI6IuW8gOWFsyIsImF0dHJUeXBlIjoiU3RyaW5nIiwiYXR0clZhbHVlIjoi5byAIn19LCJzZXJ2aWNlIjoiY3VydGFpbl9zbWFydEhvbWUiLCJzdGF0ZSI6eyJmZzo6Y3VydGFpbl9zbWFydEhvbWU6OmRlZmF1bHQ6OmRlZmF1bHQiOnsic3RhdGUiOiJkZWZhdWx0In19LCJ0ZXh0Ijoi5omT5byA56qX5biYIiwidXVpZCI6ImNpZGExNWVkOTU3QGR4MDBjNzBmYzk1MjY4MDEwNjQ0IiwidXNlZF9zdGF0ZSI6eyJzdGF0ZV9rZXkiOiJmZzo6Y3VydGFpbl9zbWFydEhvbWU6OmRlZmF1bHQ6OmRlZmF1bHQiLCJzdGF0ZSI6ImRlZmF1bHQifSwiYW5zd2VyIjp7InRleHQiOiLlt7LkuLrmgqjmiZPlvIDnqpfluJgiLCJ0eXBlIjoiVCJ9LCJkaWFsb2dfc3RhdCI6ImRhdGFJbnZhbGlkIiwic2lkIjoiY2lkYTE1ZWQ5NTdAZHgwMGM3MGZjOTUyNjgwMTA2NDQifX0="
}
}

参数 类型 说明
MsgId string 消息id
CreateTime int 消息创建时间
AppId string 开发者应用Id
UserId string AIUI唯一用户标注
SessionParams Base64格式字符串 本次会话交互参数
UserParams Base64格式字符串 开发者自定义参数,通过客户端的userparams参数上传
FromSub string 上游业务类型,目前包括两种(iat:听写结果,kc:语义结果)
Msg json 消息内容
Type string text
ContentType string 消息格式,可以为Json plain xml等
Content Base64格式字符串 内容

语义后处理服务在收到AIUI发送过来的消息后,经过必要的检验后,对上述请求内容进行json解析,json数据格式如下

// AIUI消息
type AIUIMessage struct {
	MsgId         string  `json:"MsgId"`
	CreateTime    float64 `json:"CreatedAt"`
	AppId         string  `json:"AppId"`
	UserId        string  `json:"UserId"`
	SessionParams string  `json:"SessionParams"`
	UserParams    string  `json:"UserParams"`
	FromSub       string  `json:"FromSub"`
	Msg           struct {
		ContentType string `json:"ContentType"`
		Type        string `json:"Type"`
		Content     string `json:"Content"`
	} `json:"Msg"`
}

在经过必要字段检验之后,对content字段在base64解码,解码得到的字符串内容做json解析,格式如下

// AIUI控制指令
type AIUIControl struct {
	Intent struct {
		Answer struct {
			Text string `json:"text"`
		} `json:"answer"`
		DialogStat     string      `json:"dialog_stat"`
		RC             int         `json:"rc"`
		Operation      string      `json:"operation"`
		DemandSemantic interface{} `json:"demand_semantic"`
		OrigSemantic   interface{} `json:"orig_semantic"`
		SearchSemantic interface{} `json:"search_semantic"`
		Semantic       interface{} `json:"semantic"`
		Service        string      `json:"service"`
		Text           string      `json:"text"`
		Sid            string      `json:"sid"`
		Uuid           string      `json:"uuid"`
	} `json:"intent"`
}

将base64格式content字段解码内部示例如下

{
    "answer": {
        "text": "已为您打开灯",
        "type": "T"
    },
    "uuid": "atn0e3b1dc4@dx000710ef83c5a11001",
    "dialog_stat": "dataInvalid",
    "operation": "SET",
    "rc": 0,
    "save_history": true,
    "semantic": {
        "slots": {
            "attr": "开关",
            "attrType": "String",
            "attrValue": "开",
            "location": {
                "room": "主卧",
                "type": "LOC_HOUSE"
            }
        }
    },
    "service": "light_smartHome",
    "state": {
        "fg::light_smartHome::default::default": {
            "state": "default"
        }
    },
    "text": "打开主卧的灯",
    "used_state": {
        "state": "default",
        "state_key": "fg::light_smartHome::default::default"
    },
    "sid": "atn0e3b1dc4@dx000710ef83c5a11001"
}

重点关注在service字段,后缀为smartHome表示是智能家居语义需要处理,其他的直接返回,对智能家居语义消息通过rabbitmq传递给下一服务-语义转换服务, 通过rabbitmq传递的消息json格式如下

// AIOT RabbitMQ消息结构体
type AiotRabbit struct {
	MsgId string `json:"msgId"`
	Did   string `json:"did"`
	Sid  string      `json:"sid"`
	Uid  string      `json:"uid"`
	Data AIUIControl `json:"data"`
}

MsgId Did Sid Uid Data均通过AIUI传递的消息得到,如did是通过AIUI传过来的sn号(AIUI从魔飞传过来的消息中得到sn号)在数据库中找到对应的did。Data就是上面content字段base64解码得到的json数据 语义转换服务主要任务是将AIUI智能家居语义消息转换成我们平台标准控制指令,服务监听rabbitmq消息,得到用户请求开灯消息后,经过对内容处理,如果是场景信息(service为group_smartHome)整合消息通过rabbitmq传给下一服务(oauth-proxy),消息的json格式如下

	var sceneControl struct {
   	Uid  string                `json:"uid"`
   	Sid  string                `json:"sid"`
   	Data structure.AiotControl `json:"data"`
   }
   ```
   其中structure.AiotControl格式如下
   ```json
   type AiotControl struct {
   Base
   AIOTBase
   State   PropertyDesired `json:"state"`
   Service ServiceInput    `json:"service"`
}
type ServiceInput struct {
   Name  string      `json:"name"`
   Input interface{} `json:"input"`
}

将service.name赋值为scene,input 赋值为map[set]=AIUIControl.Intent.Semantic.Slots.Attr,消息整合后传给rabbitmq的队列cmd_scene_external oauth-proxy服务收到消息后,在oauth2_scenes数据库中根据(uid用户名 name场景名)把所有符合条件的场景数据找出来,数据库数据字段如下

//场景存储
type Scene struct {
	Name       string      `json:"name"`                   //场景名称
	Uid        string      `json:"uid"`                    //用户id
	UserId     string      `json:"user_id" bson:"user_id"` //第三方用户id
	OrgId      string      `json:"org_id" bson:"org_id"`   //第三方厂商id
	Extensions interface{} `json:"extensions"`             //扩展字段
	Flag       bool        `json:"-"`
	Deleted    bool        `json:"-"`
}

对每一个场景数据,根据(uid orgid)得到accesstoken,如果是第三方厂商则调用通用厂商oauth服务(oauth-generic),如果是海尔或者晾霸则调用大厂商oauth服务(oauth-uhome),这些服务会通过http接口向厂商报告控制指令。 如果得到的消息不是场景消息,而是对设备的控制指令,则根据控制指令中的uid region room device作为selector 去个性化数据库查找所有符合条件的用户数据,这些数据组成一个数组,每条数据格式如下

type PersOfUid struct {
	Device string
	Did    string
	Uid    string
	Name   string
	Room   string
	Region string
}

将每条数据中的设备号did放在切片中,装填到AIUIControl.Intent.Semantic.Slots.DeviceInstance里面,这里面就是要控制的设备的id,服务下推消息格式

type AiotRabbit struct {
	Sid  string `json:"sid"`
	Uid  string `json:"uid"`
	Data []byte `json:"data"`
}

其中Data是结构体的字节形式,结构体如下

	type AiotControl struct {
	Base
	AIOTBase
	State   PropertyDesired `json:"state"`
	Service ServiceInput    `json:"service"`
}
type ServiceInput struct {
	Name  string      `json:"name"`
	Input interface{} `json:"input"`
}
type AIOTBase struct {
	MessageId string `json:"messageId"`
	Device    struct {
		Did string `json:"did"`
	} `json:"device"`
}

主要把device.did赋值(通过上面的DeviceInstance),一些属性值改变比如“打开开关”换成“set 1”等。通过rabbitmq的队列cmd_push传递给下一服务指令处理服务core_cmd_process,如果涉及多个设备发多条消息。 指令控制服务对传过来的消息检查规范,是否重复(根据messageid),将指令存储到数据库,指令鉴权(设备did是否属于用户uid),如果是直连设备,通过rabbitmq队列cmd_push_direct将消息发给下一服务设备指令交互服务(core-cmd-transfer),这里消息和接收到的一样没有做改动,如果是第三方设备则通过rabbitmq的队列cmd_push_third下发给oauth代理服务(oauth-proxy),oauth-proxy服务将接收到的消息解析,利用did在数据库oauth2_devices中查找设备信息,数据库中设备信息字段如下

//设备列表存储
type Device struct {
	Did        string `json:"did"`                          //设备ID
	ProductId  string `json:"product_id" bson:"product_id"` //产品id
	ProductKey string `json:"ProductKey" bson:"ProductKey"` //产品key

	Name string `json:"name"` //设备名称
	Type string `json:"type"` //设备类型
	Room string `json:"room"` //房间
	Uid  string `json:"uid"`  //用户id

	UserId   string `json:"user_id" bson:"user_id"`     //第三方用户id
	DeviceId string `json:"device_id" bson:"device_id"` //第三方设备id
	OrgId    string `json:"org_id" bson:"org_id"`       //第三方厂商id

	Extensions interface{} `json:"extensions"` //扩展字段

	Deleted bool `json:"-"`
	Flag    bool `json:"-"`
}

其中比较重要的有厂商id(orgid),第三方设备id(deviceid),将设备控制信息封装好,根据厂商id判断,如果是通用厂商,调用oauth-generic服务,否则调用大厂商oauth服务,通过http接口向厂商发送设备控制指令。 第三方设备调用oauth-proxy http接口上报场景信息,oauth-proxy将信息存储,整理通过rabbitmq的队列persync发送给个性化服务persync,消息的结构如下

type SceneData struct {
	Uid   string `json:"uid"`
	Scene string `json:"scene"`
}

type SceneReport struct {
	Action string    `json:"action"`
	Sid    string    `json:"sid"`
	Data   SceneData `json:"data"`
}

action 为update时是增加个性化信息,delete为删除个性化信息,个性化服务先去个性化数据库将数据更新(用户级别),update的增加数据,delete删除数据,等数据库更新完成,向AIUI上传该用户更新后所有的个性化数据,覆盖AIUI之前的个性化信息,通过AIUI提供的http接口,向AIUI传递的信息有appid(应用id),uid(用户id)和具体的个性化信息,如设备别名,设备房间信息等等。

其他服务还有与魔飞交互的服务 ota 和 cota ,分别对魔飞固件升级管理和远程设备配置管理,通过mqtt和魔飞交互。 关于app还有一个服务app-setting 用来提供魔飞app版本更新接口。 core-thing提供grpc接口供其他服务查询不属于自己服务的数据库的信息,如根据uid did查找设备详细信息

下面是服务之间通过rabbitmq传递消息的图示

个性化项目难点: 遇到问题:第一次向数据库更新数据,然后通过http的post方法将更新后用户的所有数据传给AIUI,紧接着第二次向数据库更新该用户个性化信息,在post的时候,第二次先上传成功,然后第一次的数据才上传成功,由于数据是全面覆盖的,所以第一次的数据将第二次数据覆盖,第二次更新的信息就丢失了 解决方法: 利用通道技术,数据未插入数据库前往通道+1,数据库更新完成-1,接收通道数据并求和,如果和为0说明数据库操作已经完成,可以上传数据,上传的时候加锁,这个用户下面同一时刻只能有这一个上传(上传的时候查找数据库搜刮所有数据),等上传完成解锁,用户下一次上传才能开始,这样能确保最后一次上传肯定是全部更新完成的数据 用到技术

  • sync.Map map是并发不安全的,同时读写会报错fatal error: concurrent map read and map write写的时候会设置hashWriting标志,读的时候会检查hashWriting标志,如果有这个标志,就会报并发错误
    sync.Map是并发安全的,有几种方法 1.func (m *Map) Load(key interface{}) (value interface{}, ok bool) 加载方法,也就是提供一个键key,查找对应的值value,如果不存在,通过ok反映 2.func (m *Map) Store(key, value interface{}) 增加或者更新一条数据

项目中用这种结构存储用户的数据库操作求和情况,键为uid,值为cnt 和锁的结构体

  • 通道中元素为如下结构体

type DbLock struct { Flag int json:"flag" Uid string json:"uid" }

设备配置管理平台难点: 操作人员通过管理平台的http接口想要获取设备端的设备配置信息,服务通过mqtt和设备交互,mqtt是发布订阅模式,服务向代理服务器请求配置信息的主题发布信息,设备端并不会立即向该主题返回信息,然而http请求需要返回设备配置信息 解决方法: 通过通道技术,在发布消息后,创建一个通道,设备端收到设备配置查询消息后,会在设备配置回应主题发布消息,服务端监听消息,得到消息后将消息放进以messageid(每一条发布消息都有唯一的messageid)为标识的通道中,服务端采用select语句,一个case中等待通道中数据进来,另一个case 设置延迟时间,如果5秒内通道内没有收到消息则返回查询超时错误。

用到技术: select语句 map 键是messageid 值是通道,收到设备端消息就会解析放进通道里

Linux服务器环境搭建难点: 在系统上安装k8s集群,用公司的一个脚本,写上master地址,node地址,自动化部署k8s,docker,安装kong下载rpm包用系统服务的方式安装systemctl命令,konga(docker安装),和需要的数据库,rabbitmq安装在k8s里面,两个副本,node机各一个

算了一下这一年写的代码大概13668,一万五左右