整个魔飞业务流程图如上所示,以打开用户卧室的一盏灯为例,详细阐述业务处理流程。 用户在家里对着魔飞说一句“魔飞,魔飞,打开主卧的灯”,魔飞将语音命令转化成文字上传到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,一万五左右