APICloud AVM框架 开发视频会议APP

简介: 1.创建会议,确认会议时间、参会人员、会议主题、确定会议主持人(默认为发起人)可开启会议;同时会通过应用消息和短信通知参会人员。2.加入会议,可通过会议大厅找的会议列表直接加入,也可通过输入会议编号加入会议;加入会议的前提是会议已在进行中。3.快速会议,可直接确认会议人员然后发起实时视频会议,参会人员实时接收应用消息或短信,快速进入会议。3.历史会议,分为我主持的会议、我参与的会议。4.会议大厅,列表显示今天需要参加的会议。

 

APP开发采用的APICloud平台的AVM 多端应用开发框架

    1. 使用 avm.js 一个技术栈可同时开发 Android & iOS 原生 App、小程序和 iOS 轻 App,且多端渲染效果统一;
    2. 全新的 App 引擎 3.0 不依赖 webView,提供百分百的原生渲染,保障 App 性能和体验与原生 App 一致;
    3. 现有 api 直接映射兼容小程序接口,延续已有开发习惯;

    后台使用的PHP的thinkphp框架,通过composer集成各类插件。

    思维导图

    image.gif编辑

    功能介绍

    1.创建会议,确认会议时间、参会人员、会议主题、确定会议主持人(默认为发起人)可开启会议;同时会通过应用消息和短信通知参会人员。

    2.加入会议,可通过会议大厅找的会议列表直接加入,也可通过输入会议编号加入会议;加入会议的前提是会议已在进行中。

    3.快速会议,可直接确认会议人员然后发起实时视频会议,参会人员实时接收应用消息或短信,快速进入会议。

    3.历史会议,分为我主持的会议、我参与的会议。

    4.会议大厅,列表显示今天需要参加的会议。

    5.会议纪要,会议结束后,会议主持人可通过APP或后台系统,把会议纪要整理发布到相关会议中,参会人员可在会议详情中查看会议纪要。

    6.会议附件,主持人员可在会议详情中,把会议相关的附件上传至相关会议中,参与人员可在会议详情中下载附件。

    7.通讯录,展示系统内的联系人,在创建会议时,会议中邀请人的时候会用到。

    应用模块

    image.gif编辑

    项目目录

    image.gif编辑

    应用展示

    image.gif编辑

    开发介绍

    应用导航

    使用的是tabLayout布局作为应用的导航。

    image.gif编辑

    系统首页使用tabLayout,可以将相关参数配置在JSON文件中,再在config.xml中将content的 值设置成该JSON文件的路径。如果底部导航没有特殊需求这里强烈建议大家使用tabLayout为APP进行布局,官方已经将各类手机屏幕及不同的分辨率进行了适配,免去了很多关于适配方面的问题。

    {
        "name": "root",
        "hideNavigationBar": true,
        "navigationBar": {
          "background": "#ffffff",
          "color": "#333333",
          "shadow": "#ffffff",
          "hideBackButton": true
        },
        "tabBar": {
          "scrollEnabled": false,
          "background": "#fff",
          "shadow": "#dddddd",
          "color": "#aaaaaa",
          "selectedColor": "#333333",
          "index":0,
          "preload": 0,
          "frames": [{
            "name": "home",
            "url": "pages/main/home.stml",
            "title": "会议"
          }, {
            "name": "classify-index",
            "url": "pages/classify/classify-index.stml",
            "title": "消息"
          }, {
            "name": "shopping-index",
            "url": "pages/shopping/shopping-index.stml",
            "title": "文档"
          }, {
            "name": "my-index",
            "url": "pages/my/my-index.stml",
            "title": "我的"
          }],
          "list": [{
            "text": "会议",
            "iconPath": "image/tabbar/meeting.png",
            "selectedIconPath": "image/tabbar/meeting-o.png",
            "scale":3
          }, {
            "text": "消息",
            "iconPath": "image/tabbar/message.png",
            "selectedIconPath": "image/tabbar/message-o.png",
            "scale":3
          }, {
            "text": "文档",
            "iconPath": "image/tabbar/doc.png",
            "selectedIconPath": "image/tabbar/doc-o.png",
            "scale":3
          }, {
            "text": "我的",
            "iconPath": "image/tabbar/user.png",
            "selectedIconPath": "image/tabbar/user-o.png",
            "scale":3
          }]
        }
      }

    image.gif

    动态权限  

    安卓10之后,对应用的权限要求提高,不在像老版本一样配置上就会自动获取,必须进行提示。

    依据官方给出的教程进行了动态权限的设置。

    1.添加 mianfest.xml文件

    image.gif编辑

    <?xml version="1.0" encoding="UTF-8"?>
    <manifest>
        <application name="targetSdkVersion" value="30"/>
    </manifest>

    image.gif

    具体的使用说明,在官方论坛中有专门的帖子,APP动态权限及Android平台targetSdkVersion设置

    在系统主页进行动态权限获取,也可在特殊页面的中获取本页面所需的权限,这个可根据具体的业务需求进行处理。本系统涉及到了文件存储、摄像头、麦克风的获取,具体的获取方式见如下代码,因为本系统的初始化页面时home.stml,所以在本页面的apiready()中进行权限验证。

    apiready(){
                    let limits=[];
            //获取权限
            var resultList = api.hasPermission({
              list: ['storage', 'camera', 'microphone']
            });
            if (resultList[0].granted) {
              // 已授权,可以继续下一步操作
            } else {
              limits.push(resultList[0].name);
            }
            if (resultList[1].granted) {
              // 已授权,可以继续下一步操作
            } else {
              limits.push(resultList[1].name);
            }
            if (resultList[2].granted) {
              // 已授权,可以继续下一步操作
            } else {
              limits.push(resultList[2].name);
            } 
            if(limits.length>0){
              api.requestPermission({
                list: limits,
              }, (res) => {
              });
            }
                }

    image.gif

       

    WebSocket

    用于即时通话的时候,监听用户在线状态,可通知用户加入会议。

    具体的通讯原理步骤是:

    会议发起人发起会议-》通过websocket给参会人员发送消息指令-》参会人员接收发送的websocket消息,通过监听触发进入会议房间,同时给会议发起人发送进入会议房间的消息-》会议发起人收到有人进入了会议房间消息后,通过监听触发进入会议房间的操作。 这种流程是会议发起人不必先进入回房间进行等待,不用启用RTC模块,只有当有其他人员收到提醒进入会议房间后才会启用RTC模块进入房间。可以有效的避免资源浪费。

    还有一中简易模式,会议发起人发起会议,并启用RTC模块,进入会议房间进行等待(判断等待时间,比如超过3分钟没有其他人员加入房间,自动退出会议房间结束会议)-》通过websocket给参会人员发送消息指令-》参会人员接收发送的websocket消息,通过监听触发进入会议房间。这种模式如果其他参会人员不及时参加会议的时候会造成部分资源的浪费。

    进入会议后其他后续的操作,就可以通过tencnetTRTC模块中的方法进行处理。

    websocket的目的就是即时的通知参会人员有会议要参加,因为RTC模块本身没有集成这个功能。这部分操作是在进入会议房间之前的操作。

    本APP用的是websocket模块,本模块可配置全局变量,方便实用。当然也可以尝试其他的websocket模块。

    image.gif编辑image.gif编辑

    AVM框架里官方就集成了websocket。使用说明文档

    image.gif编辑

    apiready(){
        //链接websocket
        var webSocket = api.require('webSocket');
        //消息监听,可以监听连接,断开,接收消息等事件
        webSocket.addEventListener((ret, err) => {
          console.log(JSON.stringify(ret) + "  " + JSON.stringify(err));
          //断开重连
          if(ret.evenType=='Closed'){
            webSocket.open({
              url : 'ws://192.168.1.5:8888/socket'
            }, (ret, err) => {
              console.log(JSON.stringify(ret) + "  " + JSON.stringify(err));
            });
          }
          //收到消息
          if(ret.evenType=='ReturnData'){
            //解析data中的内容,获取会议房间ID进入会议
          }
        });
        //获取当前的websocket链接状态
        var webSocketStatus = webSocket.getConnectState();
        //未链接则进行链接,如果已链接则无效操作
        if(webSocketStatus.State =='CLOSED'){
          webSocket.open({
            url : 'ws://192.168.1.5:8888/socket'
          }, (ret, err) => {
            console.log(JSON.stringify(ret) + "  " + JSON.stringify(err));
          });
        }
      },

    image.gif

    视频通话 RTC

    使用的是tencnetTRTC模块,查看模块文档

    image.gif编辑image.gif编辑

    首先需要去申请腾讯云 SDKAppId,进入腾讯云实时音视频控制台 创建应用,即可看到 SDKAppId。

    为什要用tencnetTRTC呢,因为tencnetTRTC模块不会把SDKAppId与应用进行绑定,这样就可以使用一个SDKAppId来实现两个不同的APP之间的视频通话了,共用腾讯云的通话时长。

    而且tencnetTRTC的接口相比较其他RTC模块更丰富,可以更好的满足一些个性化的需求。

    消息事件

    通过sendEvent把事件广播出去,然后在其他页面通过addEventListener监听事件,通过事件名和附带的参数进行其他操作。API对象说明文档

    image.gif编辑image.gif编辑

    举例说明

    1.当创建会议成功之后,需要发送一个会议创建成功的事件;在会议列表或者其他展示会议的页面,需要监听此事件,然后在监听成功的回调中做刷新的操作。

    2.当会议开始或者结束之后,需要发送相应的事件,在会议列表或者其他展示会议的页面,需要监听此类事件,在监听成功的回调中做刷新列表或者更改会议状态的操作。

    消息推送

    ajpush模块封装了极光推送平台的SDK,使用此模块可实现接收推送通知和透传消息功能。

    image.gif编辑

    关于模块使用及注意事项,请仔细阅读模块说明文档

    image.gif编辑

    //初始化JpushSDK
        initJpush(){
          var jpush = api.require('ajpush');
          jpush.init((ret, err)=>{
            if(ret && ret.status){
              //绑定别名
              if(api.getPrefs({sync: true,key: 'userid'})){
                jpush.bindAliasAndTags({
                  alias:api.getPrefs({sync: true,key: 'userid'}),
                  tags:['APPUSER']
                }, (ret, err)=>{
                  if(ret.statusCode==0){
                    api.toast({ msg: '推送服务初始化成功'});
                  }
                  else{
                    api.toast({ msg: '绑定别名失败'});
                  }
                });
              }
              //监听消息
              jpush.setListener((ret) => {
                // var content = ret.content;
                api.toast({ msg: ret.content});
              });
            }
          else{
              api.toast({ msg: '推送服务初始化失败'});
            }
          });
          api.addEventListener({name:'pause'}, function(ret,err) {
            jpush.onResume();//监听应用进入后台,通知jpush暂停事件
          })
          api.addEventListener({name:'resume'}, function(ret,err) {
            jpush.onResume();//监听应用恢复到前台,通知jpush恢复事件
          })      
        },

    image.gif

    短信验证码

    用户注册的时候需要通过手机短信验证码进行校验,以保证手机号真实有效,能够正常接收应用推送的各类短信通知提醒。

    本应用中使用的是AVM模块库中的verification-code-input组件,可自定义验证码长度和再次获取时间间隔,自动校验验证码有效性。

    image.gif编辑

    示例代码

    <template>
      <view class="page">
        <safe-area></safe-area>
        <verification-code-input :limitSecond={seconds} :limitCode={codeLen} onsetCode="getCode"></verification-code-input>
      </view>
    </template>
    <script>
      import '../../components/verification-code-input.stml'
      export default {
        name: 'demo-verification-code-input',
        apiready(){
        },
        data() {
          return{
            code:'',
            seconds:60,
            codeLen:4
          }
        },
        methods: {    
          getCode(e){
            // console.log(JSON.stringify(e.detail));
            this.data.code = e.detail;
          }
        }
      }
    </script>

    image.gif

    关于验证码的有效时间,是通过后台进行设定的,通过session缓存每个手机号的验证码,并设置缓存有效时间,表单提交的时候通过session去获取验证码,如果session失效,则无法获取验证码,接口可直接返回验证码失效提示。

    清空缓存

    首先通过getCacheSize获取应用的缓存数量,并在标签中显示,然后给标签添加点击事件,在事件中通过clearCache清除应用缓存。

    image.gif编辑image.gif编辑

    计算当前应用的缓存大小,保留以为小数。

    apiready(){
      //获取APP缓存 异步返回结果:
      api.getCacheSize((ret) => {
        this.data.cache = parseInt(ret.size/1024/1024).toFixed(1);
      });
    },

    image.gif

    执行清除缓存,并提示信息。

    clearCache(){
      api.clearCache(() => {
        this.data.cache=0.0;
        api.toast({
          msg:'清除完成'
        })
      });   
    }

    image.gif

    AVM组件使用

    项目中使用了很多的AVM组件,其中包括视频通话组件、通讯录组件、滑动单元格组件、日期时间Picker组件、数字键盘组件等等。

    image.gif编辑image.gif编辑image.gif编辑image.gif编辑

    image.gif编辑image.gif编辑image.gif编辑

    其中视频通话组件(easy-video-call、easy-voice-communication、multi-person-video-call)用的是声网的SDK,这里借用了样式,把模块换成了TencentRTC。

    消息列表列表中使用了easy-swiper-cell滑动单元格组件,来实现滑动操作已读。

    时期和时间选择用到了time-picker、date-picker组件。

    通讯录使用的是address-book组件。

    在通过会议编号进入会议时,由于会议编号全是数字,这里使用了number-keyboard数组键盘组件。

    文档下载、图片浏览

    会议结束后会上传会议纪要,会议相关文件等各类文档,主要包括doc、excel、pdf和图片。

    对于doc、excel、pdf这类文件使用的是docReader模块。方式是先通过api.download方法下载文,然后在回调中通过docReader模块唤醒三方工具进行文件浏览。

    image.gif编辑

    image.gif编辑

    //下载、浏览附件
        loadfile(url){
          api.download({
              url: url,
              // savePath: 'fs://appDownload/',//不选自动创建路径
              report: true,
              cache: true,
              allowResume: true
          }, (ret, err)=> {
              if (ret.state == 1) {
                  //下载成功
                  api.hideProgress();
                  var path=ret.savePath;
                  // alert('下载成功,文件路径:'+ret.savePath);
                  var docReader = api.require('docReader');
                  docReader.open({
                      path: path,
                      autorotation: false
                  }, (ret, err) => {
                      if (!ret.status) {
                          if(err.code=='1'){
                            alert('打开文件错误,请自行查找文件打开,路径:'+path);
                          }
                          else if(err.code=='2'){
                            alert('文件格式错误,请自行查找文件打开,路径:'+path);
                          }
                      }
                  });
              }
              else if(ret.state == 0){
                api.showProgress({
                  title: '努力下载中...',
                  text: ret.percent+'%',
                  modal: false
                });
              }
              else if(ret.state == 2) {
                  api.hideProgress();
                  alert('下载失败,请重试。');
              }
          });
        }

    image.gif

    图片使用的是photoBrowser模块进行浏览

    image.gif编辑

    picturePreview(e){
      let images = e.currentTarget.dataset.list;
      //预览图片
      var photoBrowser = api.require('photoBrowser');
      photoBrowser.open({
        images: images,
        bgColor: '#000'
      }, function(ret, err) {
        if(ret.eventType=='click'){
          photoBrowser.close();
        }
      });
    }

    image.gif

    单设备登陆

    本APP做了单一设备登陆的限制,具体实现方式是,通过api.deviceId可以获取到收的设备ID,用户登陆成功之后进行设备绑定;APP初始化的时候进行设备验证,先通过接口获取数据库中记录的用户上次登录的设备ID,然后与本机设备ID进行比对,如果设备ID不一致则跳转登陆页面。

    //登记设备
              setDeviceID(){
            var data={
              secret:'',
              userid:api.getPrefs({sync: true,key: 'userid'}),
              deviceid:api.deviceId
            };
            api.showProgress();
            POST('updatedeviceid',data,{}).then(ret =>{
              // console.log(JSON.stringify(ret));
              if(ret.flag=='Success'){
                api.toast({
                  msg:'设备登记成功'
                })
              }       
              api.hideProgress();
            }).catch(err =>{
              api.toast({
                msg:JSON.stringify(err)
              })
            })
          }

    image.gif

    //验证设备
        checkDeviceID(){
          var data={
            secret:'',
            userid:api.getPrefs({sync: true,key: 'userid'})
          };
          api.showProgress();
          POST('querydeviceidbynew',data,{}).then(ret =>{
            // console.log(JSON.stringify(api.deviceId));
            if(ret.flag=='Success'){
              if(ret.data.deviceid != api.deviceId){
                api.toast({
                  msg:'您的设备已在其他设备上登录,请重新登录。'
                })
                $util.openWin({
                  name: 'login',
                  url: 'widget://pages/seeting/login.stml',
                  title: '',
                  hideNavigationBar:true
                });
              }
            }       
            api.hideProgress();
          }).catch(err =>{
            api.toast({
              msg:'设备登陆异常,请重新登陆。'
            })
            $util.openWin({
              name: 'login',
              url: 'widget://pages/seeting/login.stml',
              title: '',
              hideNavigationBar:true
            });
          })
        }

    image.gif

    接口调用

    封装了 req.js进行接口调用,采用了ES6语法中的Promise是异步编程的一种解决方案(比传统的回调函数更加合理、强大),用同步操作将异步流程表达出来。避免层层嵌套回调。promise 对象提供统一接口,使得控制异步操作更加容易。有兴趣的同学可以多研究一下Promise。

    const config = {
        schema: 'http',
        host: '192.168.1.5',
        path: 'index.php/Home/api/',
        secret:'1f3ef6ac********6deecd990f'
    }
    function req(options) {
        const baseUrl = `${config.schema}://${config.host}/${config.path}/`;
        options.url = baseUrl + options.url;
        return new Promise((resolve, reject) => {
            api.ajax(options,  (ret, err) => {
                console.log('[' + options.method + '] ' + options.url + ' [' + api.winName + '/' + api.frameName + ']\n' + JSON.stringify({
                    ...options, ret, err
                }))
                if (ret) {
                    resolve(ret);
                    api.hideProgress();
                } else {
                    reject(err); 
                    api.hideProgress();
                }
            });
        })
    }
    /**
     * GET请求快捷方法
     * @constructor
     * @param url {string} 地址
     * @param options {Object} 附加参数
     */
    function GET(url, options = {}) {
        return req({
            ...options, url, method: 'GET'
        });
    }
    /**
     * POST 请求快捷方法
     * @param url
     * @param data
     * @param options {Object} 附加参数
     * @returns {Promise<Object>}
     * @constructor
     */
    function POST(url, data, options = {}) {
        data.secret = config.secret;
        return req({
            ...options, url, method: 'POST', data: {
                values: data
            }
        });
    }
    export {
        req, GET, POST, config
    }

    image.gif

    在stml页面中,首先要引用封装好的req.js,目前只封装了POST、GET两种方式,如果接口中有其他的方式,可以在此基础上进行封装。

    下面以登录页为例,展示具体的使用。

    <template>
        <scroll-view class="page">
        <safe-area></safe-area>
        <view class="top">
          <text class="top-title">登录</text>
          <text class="top-sub-title">欢迎使用逍遥自在云视频会议,让您从此无忧工作!</text>
        </view>
        <view class="input-box">
          <image class="item-ico" src='../../image/user.png' mode="widthFix"></image>
          <input class="item-input" placeholder="请输入账号" v-model="username"/>
        </view>
        <view class="input-box">
          <image class="item-ico" src='../../image/psw.png' mode="widthFix"></image>
          <input class="item-input" type="password" placeholder="请输入密码" v-model="password"/>
        </view>
        <view class="btn-box">
          <button class="btn" onclick={this.login}>确定</button>
        </view>
        </scroll-view>
    </template>
    <script>
      import {POST} from '../../script/req.js'
      export default {
        name: 'login',
        apiready(){
          //监听返回  双击退出程序
          api.setPrefs({
            key: 'time_last',
            value: '0'
          });
          api.addEventListener({
            name : 'keyback'
            }, function(ret, err) {
            var time_last = api.getPrefs({sync: true,key: 'time_last'});
            var time_now = Date.parse(new Date());
            if (time_now - time_last > 2000) {
              api.setPrefs({key:'time_last',value:time_now});
              api.toast({
                msg : '再按一次退出APP',
                duration : 2000,
                location : 'bottom'
              });
            } else {
              api.closeWidget({
                silent : true
              });
            }
          });
        },
        data() {
          return{
            username:'',
            password:''
          }
        },
        methods: {
          login(){
            if (!this.data.username) {
              this.showToast("姓名不能为空");
              return;
            }
            if (!this.data.password) {
              this.showToast("密码不能为空");
              return;
            } 
            var data={
              secret:'',
              user:this.data.username,
              psw:this.data.password
            };
            api.showProgress();
            POST('loginuser',data,{}).then(ret =>{
              // console.log(JSON.stringify(ret));
              if(ret.flag=='Success'){
                api.setPrefs({key:'username',value:ret.data.username});
                api.setPrefs({key:'userid',value:ret.data.id});
                api.setPrefs({key:'deviceid',value:ret.data.deviceid});
                api.setPrefs({key:'phone',value:ret.data.phone});
                //登记设备
                this.setDeviceID();
                api.sendEvent({
                  name: 'loginsuccess',
                });
                api.closeWin();
              }
              else{
                api.toast({
                  msg:'登录失败!请稍后再试。'
                })
              }
              api.hideProgress();
            }).catch(err =>{
              api.toast({
                msg:JSON.stringify(err)
              })
            })
          },
          //登记设备
          setDeviceID(){
            var data={
              secret:'',
              userid:api.getPrefs({sync: true,key: 'userid'}),
              deviceid:api.deviceId
            };
            api.showProgress();
            POST('updatedeviceid',data,{}).then(ret =>{
              // console.log(JSON.stringify(ret));
              if(ret.flag=='Success'){
                api.setPrefs({key:'deviceid',value:api.deviceid});
                api.toast({
                  msg:'设备登记成功'
                })
              }       
              api.hideProgress();
            }).catch(err =>{
              api.toast({
                msg:JSON.stringify(err)
              })
            })
          }
        }
      }
    </script>
    <style>
        .page {
            height: 100%;
        background-color:#ffffff;
        }
      .top{
        margin-top: 50px;
        margin-left: 20px;
        margin-bottom: 100px;
      }
      .top-title{
        font-size: 25px;
        font-weight: bold;
      }
      .top-sub-title{
        font-size: 13px;
        font-weight: bold;
      }
      .input-box{
        margin: 20px;
        border-bottom: 1px solid #ccc;
        padding-bottom: 5px;
        flex-flow: row nowrap;
        align-items: center;
      }
      .item-input{
        width: auto;
        border: 0;
        font-size: 18px;
        margin-left: 10px;
      }
      .item-ico{
        width: 35px;
      }
      .btn-box{
        margin-top: 50px;
        margin-left: 10px;
        margin-right: 10px;
      }
      .btn{
        background-color: #256fff;
        color: #ffffff;
        font-size: 20px;
        border-radius: 20px;
        padding: 10px 0;
        font-weight: bold;
      }
    </style>

    image.gif

    后台代码

    代码示例

    <?php
    namespace Home\Controller;
    require 'vendor/autoload.php';    // 注意位置一定要在 引入ThinkPHP入口文件 之前
    use Think\Controller;
    use JPush\Client as JPushClient;
    use AlibabaCloud\Client\AlibabaCloud;
    use AlibabaCloud\Client\Exception\ClientException;
    use AlibabaCloud\Client\Exception\ServerException;
    class ApiController extends Controller {
        public function index(){
            $this->show('');
        }
        //用户登录
        public function loginuser(){
            checkscret('secret');//验证授权码
            checkdataPost('user');//账号
            checkdataPost('psw');//密码
            $map['username']=$_POST['user'];
            $map['password']=$_POST['psw'];
            $map['zt']='T';
            $releaseInfo=M()->table('user')->field('id,username,phone,deviceid,role')->where($map)->find();
            if($releaseInfo){
                returnApiSuccess('登录成功',$releaseInfo);
              }
              else{
                returnApiError( '登录失败,请稍后再试');
                exit();
              }
          }
           //记录登录设备ID
          public function updatedeviceid(){
            checkscret('secret');//验证授权码
            checkdataPost('userid');//用户ID
            checkdataPost('deviceid');//设备ID
            $userid=$_POST['userid'];
            $deviceid=$_POST['deviceid'];
            $map['id']=$userid;
            $data['deviceid']=$deviceid;
            $releaseInfo=M()->table('user')->where($map)->save($data);
            if($releaseInfo){
              returnApiSuccess('登记成功',$releaseInfo);
            }
            else{
              returnApiError( '登记失败,请稍后再试');
              exit();
            }
        }
        //获取最新的登录用户设备ID
        public function querydeviceidbynew(){
            checkscret('secret');//验证授权码
            checkdataPost('userid');//用户ID
            $userid=$_POST['userid'];
            $map['id']=$userid;
            $releaseInfo=M()->table('user')->field('deviceid')->where($map)->find();
            if($releaseInfo){
              returnApiSuccess('查询成功',$releaseInfo);
            }
            else{
              returnApiError( '查询失败,请稍后再试');
              exit();
            }
        }
        //APP修改密码
        public function updatepassword(){
            checkscret('secret');//验证授权码
            checkdataPost('userid');//用户ID 
            checkdataPost('password');//密码
            $userid=$_POST['userid'];
            $password=$_POST['password'];
            $map['id']=$userid;
            $data['password']=$password;
            $releaseInfo=M()->table('user')->where($map)->save($data);
            if($releaseInfo){
                returnApiSuccess('修改成功',$releaseInfo);
            }
            else{
                returnApiError( '修改失败,请稍后再试');
                exit();
            }
          }
        //新增会议
        public function addhuiyi(){
            checkscret('secret');//验证授权码
            checkdataPost('userid');//ID
            $userid=$_POST['userid'];
            $title=$_POST['title'];
            $content=$_POST['content'];
            $users=$_POST['users'];
            $hysj=$_POST['hysj'];
            $hylx=$_POST['hylx'];
            $data['title']=$title;
            $data['content']=$content;
            $data['fqr']=$userid;
            $data['cyr']=$users;
            $data['hysj']=$hysj;
            $data['flag']='01';//未开始
            $data['cjsj']=time();
            $data['type']=$hylx;
            $data['txsj']=date('Y-m-d H:i:s',strtotime("$hysj-10 minute"));
            $data['istip']='01';
            $arruser=explode(',',$users);
            $releaseInfo=M()->table('meeting')->data($data)->add();
            if($releaseInfo){     
                //发送消息
                $this->setmessage($users,'您有一个视频会议需要参加,时间:'.$hysj);
                //发送短信通知
                //$this->pushmsgbyusers($users,$hysj);
                //极光推送
                try{
                  $jpush = new JPushClient(C('JPUSH_APP_KEY'), C('JPUSH_MASTER_SECRET'));
                  $response = $jpush->push()
                      ->setPlatform('all')  //机型 IOS ANDROID
                      ->addAlias($arruser)
                      ->androidNotification($content)
                      ->iosNotification($content,'',0,true)
                      ->options(array(
                          'apns_production' => true,
                      ))
                      ->send();
                      returnApiSuccess('添加成功');
                  }
                  catch(\Exception $e){
                    returnApiSuccess('添加成功');
                    exit();
                  }         
            }
            else{
              returnApiError('添加失败,请稍后再试!');
              exit();
            }
        }
        //查询会议大厅
        public function querymeeting(){
          checkscret('secret');//验证授权码
          checkdataPost('userid');//用户ID
          checkdataPost('limit');//下一次加载多少条
          $userid=$_POST['userid'];
          $where['fqr']=$userid;
          $where['_string']='find_in_set('.$userid.',cyr)';
          $where['_logic']='or';
          $map['_complex']=$where;
          $map['flag']=array('neq','03');   
          $limit=$_POST['limit'];
          $skip=$_POST['skip'];
          if(empty($skip)){
            $skip=0;
          }
          $releaseInfo=M()->table('meeting')->field('id,title,flag,hysj,sjzd(type,\'会议类型\') hylx,cyr,fqr,type')->where($map)->limit($skip,$limit)->order('hysj desc')->select();   
          if($releaseInfo){
            returnApiSuccess('查询成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //设置会议状态
        public function setmeeting(){
          checkscret('secret');//验证授权码
          checkdataPost('id');//会议ID
          checkdataPost('flag');//会议状态
          $id=$_POST['id'];
          $flag=$_POST['flag'];
          $map['id']=$id;
          $data['flag']=$flag;
          if($flag=='02'){
            $data['start']=time();
          }
          else if($flag=='03'){
            $data['end']=time();
          }
          $releaseInfo=M()->table('meeting')->where($map)->save($data);
          if($releaseInfo){
            returnApiSuccess('更新成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //上传会议纪要
        public function addhyjy(){
          checkscret('secret');//验证授权码
          checkdataPost('id');//会议ID
          checkdataPost('hyjy');//会议纪要
          $id=$_POST['id'];
          $hyjy=$_POST['hyjy'];
          $map['id']=$id;
          $data['jiyao']=$hyjy;
          $releaseInfo=M()->table('meeting')->where($map)->save($data);
          if($releaseInfo){
            returnApiSuccess('上传成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //查询历史会议
        public function queryhistory(){
          checkscret('secret');//验证授权码
          checkdataPost('userid');//用户ID
          checkdataPost('limit');//下一次加载多少条
          $userid=$_POST['userid'];
          $where['fqr']=$userid;
          $where['_string']='find_in_set('.$userid.',cyr)';
          $where['_logic']='or';
          $map['_complex']=$where;
          $map['flag']=array('eq','03');   
          $limit=$_POST['limit'];
          $skip=$_POST['skip'];
          if(empty($skip)){
            $skip=0;
          }
          $releaseInfo=M()->table('meeting')->field('id,title,hysj')->where($map)->limit($skip,$limit)->order('hysj desc')->select();   
          if($releaseInfo){
            returnApiSuccess('查询成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //查询会议详情
        public function queryhistoryinfo(){
          checkscret('secret');//验证授权码
          checkdataPost('id');//会议ID
          $id=$_POST['id'];
          $map['id']=$id;
          $releaseInfo=M()->table('meeting')->field('id,title,hysj,content,getusers(cyr) users,sjzd(type,\'会议类型\') type,jiyao,getmeetinglong(id) sc')->where($map)->find();   
          if($releaseInfo){
            returnApiSuccess('查询成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //发送消息通知
        function setmessage($users,$content){
          $arruser=explode(',',$users);
          foreach ($arruser as $item) {
            $data['user']=$item;
            $data['content']=$content;
            $data['shijian']=time();
            $data['sfyd']='01';
            $info=M()->table('sp_message')->data($data)->add();
          }
        }
        //查询消息
        public function querymessage(){
          checkscret('secret');//验证授权码
          checkdataPost('userid');//用户ID
          checkdataPost('limit');//下一次加载多少条
          $userid=$_POST['userid'];
          $map['user']=$userid;
          $limit=$_POST['limit'];
          $skip=$_POST['skip'];
          if(empty($skip)){
            $skip=0;
          }
          $releaseInfo=M()->table('message')->field('id,content,sfyd,from_unixtime(shijian,\'%Y-%m-%d %H:%i:%s\') sj')->where($map)->limit($skip,$limit)->order('sj desc')->select();   
          if($releaseInfo){
            returnApiSuccess('查询成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //设置消息已读
        public function setxxyd(){
          checkscret('secret');//验证授权码
          checkdataPost('id');//ID
          $id=$_POST['id'];
          $map['id']=$id;
          $data['sfyd']='02';
          $releaseInfo=M()->table('message')->where($map)->save($data);
          if($releaseInfo){
            returnApiSuccess('设置成功',$data);
          }
          else{
            returnApiError( '设置失败,请稍后再试');
            exit();
          }      
        }
      //推送用户短信提醒
      function pushmsgbyusers($users,$shijian){
        $map['_string']='find_in_set(id,\''.$users.'\')';
        $data=M()->table('user')->field('group_concat(trim(phone)) phones')->where($map)->find();
        if($data){
            $phones=$data['phones'];
            //发送验证码       
            AlibabaCloud::accessKeyClient(C('accessKeyId'), C('accessSecret'))
                              ->regionId('cn-beijing')
                              ->asDefaultClient();
            try {
                $param = array("datetime"=>$shijian);
                $result = AlibabaCloud::rpc()
                          ->product('Dysmsapi')
                          // ->scheme('https') // https | http
                          ->version('2017-05-25')
                          ->action('SendSms')
                          ->method('POST')
                          ->host('dysmsapi.aliyuncs.com')
                          ->options([
                                'query' => [
                                'RegionId' => "cn-beijing",
                                'PhoneNumbers' =>$phones,
                                'SignName' => "****有限公司",
                                'TemplateCode' => "SMS_****",
                                'TemplateParam' => json_encode($param),
                              ],
                          ])
                          ->request();
            }catch (ClientException $e) {
            }
            return $result;
        }
      }
      //获取腾讯视频RTC usersig
      public function getQQrtcusersig(){
        checkscret('secret');//验证授权码
        checkdataPost('userid');//用户ID
        $sdkappid=C('sdkappid');
        $key=C('usersig_key');
        $userid=$_POST['userid'];
        require 'vendor/autoload.php';
        $api = new \Tencent\TLSSigAPIv2($sdkappid, $key);
        $sig = $api->genSig($userid);
        if($sig){
          returnApiSuccess('查询成功',$sig);
        }
        else{
          returnApiError( '查询失败,请稍后再试');
          exit();
        }
      }
    }

    image.gif

    插件引用

    用到了阿里短信插件、极光推送插件、腾讯RTC签名插件;通过composer安装。

    composer.json文件

    {
      "config": {  
            "secure-http": false  
        },
      "require": {
        "jpush/jpush": "^3.6",
        "tencent/tls-sig-api-v2": "1.0",
        "alibabacloud/client": "^1.5"
      }
    }

    image.gif

    相关文章
    |
    1月前
    |
    Java 测试技术 持续交付
    【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
    本文重点讲解如何搭建App自动化测试框架的思路,而非完整源码。主要内容包括实现目的、框架设计、环境依赖和框架的主要组成部分。适用于初学者,旨在帮助其快速掌握App自动化测试的基本技能。文中详细介绍了从需求分析到技术栈选择,再到具体模块的封装与实现,包括登录、截图、日志、测试报告和邮件服务等。同时提供了运行效果的展示,便于理解和实践。
    84 4
    【入门思路】基于Python+Unittest+Appium+Excel+BeautifulReport的App/移动端UI自动化测试框架搭建思路
    |
    3月前
    |
    开发工具
    uniapp, 短剧视频类App实现参考,支持滑动播放,仿抖音 仿陌陌 短视频 无限滑动播放 视频流
    阿里云点播服务web播放器sdk,短剧视频类App实现参考。仿抖音 仿陌陌 短视频 无限滑动播放 视频流。无uniapp video 原生组件的层级、遮挡、覆盖问题,适合与不同功能视图组合使用,实现丰富的应用功能。
    uniapp, 短剧视频类App实现参考,支持滑动播放,仿抖音 仿陌陌 短视频 无限滑动播放 视频流
    |
    3月前
    |
    开发框架 JavaScript 前端开发
    uni-app x 跨平台开发框架
    uni-app x 是一个强大的跨平台开发框架 uni-app x 是一个庞大的工程,它包括uts语言、uvue渲染引擎、uni的组件和API、以及扩展机制。
    97 1
    |
    5月前
    |
    小程序 前端开发
    仿哔哩哔哩视频app小程序模板源码
    仿哔哩哔哩视频app小程序模板源码
    220 5
    仿哔哩哔哩视频app小程序模板源码
    |
    4月前
    |
    Python Windows 内存技术
    【Azure 应用服务】Azure App Service (Windows) 使用Flask框架部署Python应用,如何在代码中访问静态文件呢?如何设置文件路径?是相对路径还是绝对路径呢?
    【Azure 应用服务】Azure App Service (Windows) 使用Flask框架部署Python应用,如何在代码中访问静态文件呢?如何设置文件路径?是相对路径还是绝对路径呢?
    |
    5月前
    |
    JavaScript Java 测试技术
    基于springboot+vue.js+uniapp的成人教育APP附带文章源码部署视频讲解等
    基于springboot+vue.js+uniapp的成人教育APP附带文章源码部署视频讲解等
    58 2
    |
    5月前
    |
    JavaScript Java 测试技术
    基于springboot+vue.js+uniapp的房屋租赁App附带文章源码部署视频讲解等
    基于springboot+vue.js+uniapp的房屋租赁App附带文章源码部署视频讲解等
    56 1
    |
    5月前
    |
    JavaScript Java 测试技术
    基于springboot+vue.js+uniapp的多功能智能手机阅读APP附带文章源码部署视频讲解等
    基于springboot+vue.js+uniapp的多功能智能手机阅读APP附带文章源码部署视频讲解等
    82 1
    |
    5月前
    |
    JavaScript Java 测试技术
    基于springboot+vue.js+uniapp的宠物饲养管理APP附带文章源码部署视频讲解等
    基于springboot+vue.js+uniapp的宠物饲养管理APP附带文章源码部署视频讲解等
    35 1
    |
    5月前
    |
    JavaScript Java 测试技术
    基于springboot+vue.js+uniapp的课程考勤及作业提交App附带文章源码部署视频讲解等
    基于springboot+vue.js+uniapp的课程考勤及作业提交App附带文章源码部署视频讲解等
    34 1