# IM服务器 此项目是家庭医生的消息服务器,含医生端与患者端。医生端使用REST API收发消息,患者端使用Web Socket收发消息。 ## 代码管理 项目代码使用git管理,请安装git客户端并fork代码再下载修改。 ## 环境安装 - 开发工具: Node.js的代码可以使用文本编辑器开发,如:Sublime, vi, vim等。也可以使用IDE开发,推荐使用WebStorm,包含大量便捷功能,加速开发。 - 数据库准备: IM服务器使用MySQL数据库,因此需要先安装MySQL数据库。然后执行resources/schema/im_schema.sql与resources/schema/talk_group_schema.sql脚本文件,创建数据模式。第一个创建消息存储所需要的模式,第二个创建业务讨论组数据模式,并包含演示数据。 - 运行环境:Node.js使用社区6.X版本或Joyent的0.10.x版本开发,部署环境也需要使用相应的版本。 - ECMA Script版本:代码需要ECMA Script 6版本的支持,提供对class, arrow function的支持。 ## 数据库结构 数据库设计是将消息按通道分开:系统消息,个人消息及群消息。 - system_messages: 系统消息,通过系统端发出去的消息存储在此表 - p2p_messages: 私信,通过私信端发出去的消息存储在此表 - group_messages: 群消息,通过群消息端发出去的消息存储在此表 - muc_messages:muc患者咨询消息,通过咨询发送出去的消息存储在此表 - app_status: 消息推送获取应用状态的表,当消息需要推送到设备的时候,先从此表获取客户端信息,再用个推服务推送到目标设备 - participants:会话成员表,所有的会话成员创建都创建到此表中 - sessions:所有的消息对话的主体对象,会话表,记录所有会话记录,根据type和business_type来区分对应的信息type=1 muc模式,type=2 p2p模式,type=3 group模式 type = 0 系统会话 - topics:所有的咨询(包括名医咨询都存储在此表中)关联会话信息,每个会话跟议题是一对多的关系。议题的消息根据会话类型不同分别存储在p2p_messages(会话类型type = 2)和muc_messages(会话类型type = 1) ## 运行 对Windows环境,可以预先安装Node.js。对Linux环境,代码包中包含Node.js程序,第一次部署需要使用chmod为此增加执行权限: chmod +x node 之后通过以下命令启动消息服务器: node app.armour.js 启动成功将出现如下日志: [INFO] app.js(125,5): Starting IM server, version 1.0.5.20161107, running on port 3000, 2016-11-22 16:00:17 [INFO] app.js(126,5): Configuration profile: dev ## 测试 测试框架使用mocha。REST客户端使用supertest。should模块提供断言测试。测试用例位于test目录,并根据模块划分。 ## 部署 部署服务之前需要配置环境变量IM_PROFILE,Windows平台直接在系统中配置,Linux平台在profile中配置: export IM_PROFILE=prod IM_PROFILE值将决定应用加载的配置文件,配置文件位于src/resources/config。IM_PROFILE值为prod, test, dev中的一个。若配置错误服务将无法运行。若未配置此参数则默认使用dev配置。 为保证服务正常运行,当服务异常退出时自动重启服务,IM服务器使用app.armour.js脚本,对进程添加保护:进程异常退出时,自动重启。为实现此目标,部署时通过appArmour启动服务即可实现进程的守护功能。 ### 部署到Docker上 未完工 ### 集群 未完工 ## 开发SDK IM提供了开发SDK,一个JS脚本。客户端可以通过引用此脚本或将此脚本打包到资源中。此外,脚本使用jQuery作为基础环境,因此客户端需要预先引用jQuery脚本。客户端通过以下链接引用SDK文件 ## 工程结构 - 消息流: 首先要明白即时消息的流程,有助于理解整个过程。医生端的IM仅关注医生部分,即下图中蓝色标注的内容。 ![IM活动图](./doc/images/activity.png) ### MVC 应用根据MVC模式设计。但REST API只用到Model与Controller两部分,与页面相关的会使用到View层。Model层代码位于model,View层代码位于views,Controller层分两处存放: - REST API相关的放在endpoints - 与页面相关的放在controllers ### 消息发送 实时与延时 ## 错误处理 Node.js支持同步与异步调用,也导致了异常错误处理与众不同。一个给定的函数,它处理异常的方式要么是同步(用 throw方式)要么是异步的(用 callback 或者 EventEmitter),不会两者兼具。 用户可以在回调函数里处理错误,也可以使用 try/catch捕获异常 ,但是不能一起用。 实际上,使用throw并且期望调用者使用 try/catch 是很罕见的,因为 NodeJS里的同步函数通常不会产生运行失败(主要的例外是类似于JSON.parse的用户输入验证函数)。 ## API 本节将简要描述服务所提供的API,包括消息收发,运行状态及成员维护等。API分为两种:REST与WebSocket。 ### REST API REST API遵循REST最佳实践,规范命名URL中的每个部分。注意POST请求是将数据作为请求体发送。下文中的host:port分别表示服务所在的主机地址及端口。 Application: { Base: '/api/v2/application', BadgeNo: '/badge_no' //获取角标 }, Users: { Base: '/api/v2/users', Login: '/login', //用户登录 Logout: '/logout', //用户登出 User: '/:user_id', //获取用户信息 UserStatus: '/:user_id/status',//用户状态 UserConsultTime:'/:user_id/consult/time'//用户咨询时间 }, Sessions: { Base: '/api/v2/sessions', Session: '/:session_id/session', // 获取会话 SessionListByType: '/sessionListByType', // 按会话类型获取会话 SessionCountByType: '/sessionCountByType', // 按会话类型获取会话数量 SessionSticky: '/:session_id/sticky', // 会话置顶,置顶使用PUT,取消置顶使用DELETE SessionStatus: '/:session_id/status', // 更新状态 SessionName: '/:session_id/name', // 更新会话名称 RecentSessions: '/recent', // 最近会话,使用类型过滤出'患者'或'医生'会话 IsExist: '/isExist', // 判断会话是否存在(i健康发送muc的im消息会话前会先调用创建会话的方法,这个方法会更新redis中每个成员的最后获取时间,导致未读消息不准) Topics: '/:topic_id/topics', // 获取单个议题 Topic: '/topics/:topic_id', // 议题,指定ID的议题将返回其信息 TopicEnded: '/:session_id/topics/:topic_id/ended', // 议题是否已结束,若top_id为current,则检查最后一个议题的状态 TopicInto: '/:session_id/topics/:topic_id/into', // 居民进入议题 TopicList:'/topics', //获取议题列表 HealthTopicList:'/healthTopics', //健康咨询 TopicListByType:'/topicListByType', //按类型查找咨询(未回复,进行中,已回复) TopicListCountByType:'/topicListCountByType', //按类型查找咨询(未回复,进行中,已回复)的数量 HealthTeamTopicList:'/healthTeamTopics', //健康咨询(区分团队) TopicReplyCount:"/topics/count/reply", //议题回复数统计 TopicMessages:'/topic/:topic_id/messages', //议题消息 Messages: '/:session_id/messages', // 会话消息 MessagesByTopic: '/:session_id/topics/:topic_id/messages', // 议题消息 Message: '/:session_id/messages/:message_id', // 单条消息 SessionsUnreadMessageCount: '/unread_message_count', // 所有会话的未读消息数 SessionUnreadMessageCount: '/:session_id/unread_message_count', // 指定会话的未读消息数 SessionUnreadMessages: '/:session_id/messages/unread', // 会话未读消息 ParticipantUpdate:'/:session_id/participant/update', //更新成员=删除旧成员,新增新成员 Participants: '/:session_id/participants', // 会话所有成员 ParticipantsAvatar: '/:session_id/participants/avatars', // 会话所有成员头像 Participant: '/:session_id/participants/:participant_id', // 会话单个成员(多个participant_id 用英文逗号间隔) ParticipantAvatar: '/:session_id/participants/:participant_id/avatars' // 会话单个成员头像 } ### 业务分析 **消息类型**: muc_messsges,p2p_messages,group_messages,system_messages表对应的type字段 0、系统消息 1、文本消息 2、图片消息 3、语音消息 4、文章消息 5、跳转消息 6、议题开始 7、议题结束 8、转发的消息 9、转发的图片 10、议题结束发送的消息 11、预留 12、小视频消息 13、预留 14、进入议题 系统发送的会话消息 15、续方审核消息消息 16、续方咨询血糖血压咨询消息 17、续方咨询随访问卷消息 18、个人名片 19、消息转发 20、康复计划发送 21、转诊待预约发送 22、已知悉 23、复诊说明 24、诊断结果 25、病情描述 26、专家建议 2102、上门服务-修改工单卡片信息 2103、上门服务-变更工单医生信息 2104、上门服务-变更工单服务项信息 **会话类型** sessions的type字段 a、type =1 消息保存muc_messsges、患者的三师、家庭咨询会话, sessionId:patient + "_" + ct.getTeam() + "_" + ct.getType() 居民code+"_"+服务团队code+"_"+咨询类型 b、type=2 消息保存p2p_messages、患者的名医咨询,医生间的p2p、名医咨询会话 sessionId: 参与者的code的哈希值 c、type =3 消息保存group_messages、行政团队聊天会话 sessionId:新增团队id d、type =0 消息保存system_messages 系统聊天会话 sessionId: 参与者的code的哈希值 e、type =4 临时讨论组(已经废弃 被type=5的类型代替了) sessionId: 第一条消息的消息id f、type =5 专科医生和家庭医生的固定组 sessionId: 居民code+"_"+专科医生行政团队code #type =6 患者发起名医咨询 (使用率很低) sessionId: #type =7 医生发起的名医咨询(已废弃) sessionId: g、type =8 续方咨询 sessionId: 居民code+"_"+咨询code+"_"+咨询类型 h、type =9 医院在线复诊 sessionId: 居民code+"_"+咨询code+"_"+咨询类型 i、type =10 医生发起的求助 (使用率很低) sessionId: 居民code+"_"+咨询code+"_"+咨询类型 j、type =11 思明区上门护理 sessionId: 居民code+"_"+咨询code+"_"+咨询类型 k、type =12 候诊室群聊 sessionId: 诊室code+"_"+咨询code+"_"+咨询类型 sessions的business_type字段 a、business_type =1 此会话不包含患者 b、business_type =2 此会话包含患者 sessions的status字段 **此字段只针对议题的会话有效** 标示当前会话是否有活跃的议题 **议题相关** 议题的ID就是wlyy库的wlyy_consult 的code字段可以关联wlyy_consult_team 查询对应的咨询行政团队信息,和签约医生信息,及咨询医生信息 目前有wlyy_consults的视图做关联,可根据实际业务逻辑拓展相关字段。 议题topics status及type字段讲解 status =0 reply = 0 新增的咨询 status = 0 reply =1 医生已经回复的咨询 status = 10 reply = 0 医生未回复就结束掉的咨询,PS:目前统计要求这部分数据不纳入未回复的咨询,请知悉 status = 10 reply = 1 医生已经回复,且已经结束的咨询 ### Web Socket Web Socket提供页面内长连接,并且能够通过Web Socket收发消息。 Web Socket 客户端类型包含患者、医生 Web Socket 客户端登录需带上对应的会话ID用作于区分推送对象 Web Socket 客户端在线在推送消息的时候会更新用户的活跃时间用作于消息统计 ### 微信相关模板消息 目前只有咨询回复的模板消息回复是在IM内发送微信模板 微信模板消息:结合Web Socket 当用户的sockt的连接不存在,则通过微信模板推送给予患者信息提醒,由于患者咨询只能存在一个,所以在推送的时候没有添加会话是否同一个的校验。后期出现多个需增加客户端的会话ID是否相同做校验。 ### 相关问题排查处理SQL及处理思路 -- 查询会话对象 select * from sessions s; -- 查询成员对象 select * from participants p where session_id='4d5be29f8ba0413d8658441902b958ff_c7a121954a3a4fae84852fd5668b590d_2'; -- 查询咨询消息 select * from muc_messages m; -- 查询单聊消息 select * from p2p_messages; -- 查询群组消息 select * from group_messages; -- 查看系统消息 select * from system_messages; -- 查看议题对象 select * from topics; -- 查询讨论组的医生信息 select s.id,p.participant_id,d.* from participants p,sessions s,doctors d where s.id='4d5be29f8ba0413d8658441902b958ff_c7a121954a3a4fae84852fd5668b590d_2' and s.id = p.session_id and (d.id = p.participant_id); -- 查询会话中的患者信息 select s.id,p.participant_id,d.* from participants p,sessions s,patients d where s.id = p.session_id and (d.id = p.participant_id); -- 查询咨询信息 select t.* from topics t,sessions s where t.session_id = s.id and s.business_type = 2 and s.type = 1; -- 查询未读消息数量P2P GROUP 相同只是查询的对象不一致 select * from muc_messages m,(select p.last_fetch_time,p.session_id from participants p where p.session_id ='4d5be29f8ba0413d8658441902b958ff_c7a121954a3a4fae84852fd5668b590d_2' and p.participant_id='D2016008240003') t where m.session_id ='4d5be29f8ba0413d8658441902b958ff_c7a121954a3a4fae84852fd5668b590d_2' and m.session_id = t.session_id and m.`timestamp`> t.last_fetch_time; -- 查询某个人的所有会话 select GROUP_CONCAT(p1.participant_id) as 会话的成员集合,s.* from participants p,sessions s,participants p1 where p1.session_id = s.id and p.session_id = s.id and p.participant_id='D2016008240003' GROUP BY s.id; -- 查询某个议题的消息 select w.* from muc_messages w ,( select m.`timestamp` as begin_date,m2.`timestamp` as end_date,t.session_id from topics t,muc_messages m, muc_messages m2 where m.id = t.start_message_id and m2.id = t.end_message_id and t.session_id = '4d5be29f8ba0413d8658441902b958ff_c7a121954a3a4fae84852fd5668b590d_2' ) s where w.session_id = s.session_id and w.`timestamp` >= s.begin_date and w.`timestamp`<=s.end_date; -- 系统不稳定的时候会出现wlyy和im库其中一个数据库的数据没有新增,目前是手动处理,后续可以考虑写job定时处理脏数据 SELECT * from wlyy.wlyy_patient WHERE mobile = '';-- 查找居民 SELECT * from wlyy.wlyy_consult WHERE patient = '';-- 查找居民咨询 SELECT * from wlyy.wlyy_consult_team WHERE patient = '';-- 查找居民咨询 SELECT * from im.topics WHERE id = ''; -- 查找议题 SELECT * from im.sessions WHERE id = ''; -- 查找会话 SELECT * from im.muc_messages WHERE session_id = '' ORDER BY `timestamp` desc;-- 查找聊天记录 (ps:大部分是im库有数据,wlyy库没有数据,此时只需要吧wlyy_consult和wlyy_consult_team的数据手动插入即可 如果是wlyy库有数据,im库没有数据则吧im的相关topic表插入数据) ------------------------------------------------------------------------------------------ SELECT * from dm_hospital WHERE name like '%嵩屿%'; -- 嵩屿 3502050100 -- 统计团队咨询量 SELECT a.name,b.c from wlyy_admin_team a, (SELECT admin_team_code,COUNT(*) c from wlyy_consult_team WHERE type = 2 and czrq>'2017-07-01' GROUP BY admin_team_code) b WHERE a.org_code = '3502050100' and a.id = b.admin_team_code; -- 统计团队健管师咨询量 SELECT a.name,d.name,sum(b.c) from wlyy_admin_team a, wlyy_doctor d, (SELECT admin_team_code,doctor,COUNT(*) c from wlyy_consult_team WHERE type = 2 and czrq>'2017-07-01' GROUP BY admin_team_code,doctor) b WHERE a.org_code = '3502050300' and a.id = b.admin_team_code and d.code = b.doctor GROUP BY a.name,d.name -- 统计团队长咨询量 SELECT a.name,d.name,b.c from wlyy_admin_team a, wlyy_doctor d, (SELECT admin_team_code,COUNT(*) c from wlyy_consult_team WHERE type = 2 and czrq>'2017-07-01' GROUP BY admin_team_code) b WHERE a.org_code = '3502050300' and a.id = b.admin_team_code and a.leader_code = d.code; ###更改NODE插件数据源在安装完成NODE及配置完成环境变量后执行 npm config set registry https://registry.npm.taobao.org 设置完成后执行npm install 即可安装相对于的插件。 ##专科医生的业务 1、专科医生和家庭医生聊天 /api/v2/sessions session_id : 居民code+"_"+专科医生行政团队code session_name : 某居民-共管对话 session_type : 5 participants: 此会话的成员列表,格式:["userId1:role", "userId2:role"],用户的ID及角色。 2.修改会话名称 /api/v2/sessions/:session_id/name post请求 入参:sessionId:会话id name:新的会话名称 3、临时聊天组(由医生单聊在拉其他医生进来聊天后生成的新的会话) /api/v2/sessions session_id : 为空(后台自动生成) session_name : 显示群组人名,用顿号隔开。超过10字省略显示 session_type : 4 participants: 此会话的成员列表,格式:["userId1:role", "userId2:role"],用户的ID及角色。 ###备注 topic的id=wlyy_consult的code