APICloud AVM框架 开发视频会议APP

车载AVM开发全解析:核心算法+C++实战+功能安全规范 本文系统解析了车载AVM(全景监控系统)开发的三大核心算法:图像滤波/降噪、畸变矫正和3D翘曲变换。通过"大白话+专业术语"的双重解读,详细说明了各算法的原理、应用场景及C++实现要点。重点介绍了策略模式在滤波算法切换、单例模式在标定参数管理中的实际应用,并提供了基于OpenCV的极简代码示例。全文兼顾技术深度与工程实践,特别强调车载环境下的实时性、内存优化和功能安全等关键考量。 阅读详情

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

  1. 使用 avm.js 一个技术栈可同时开发 Android & iOS 原生 App、小程序和 iOS 轻 App,且多端渲染效果统一;

  2. 全新的 App 引擎 3.0 不依赖 webView,提供百分百的原生渲染,保障 App 性能和体验与原生 App 一致;

  3. 现有 api 直接映射兼容小程序接口,延续已有开发习惯;

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

思维导图

功能介绍

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

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

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

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

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

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

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

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

应用模块

项目目录

应用展示 

开发介绍

应用导航

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

系统首页使用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
      }]
    }
  }

动态权限   

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

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

1.添加 mianfest.xml文件

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

具体的使用说明,在官方论坛中有专门的帖子,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) => {
						
					});
				}
            }

    

WebSocket

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

具体的通讯原理步骤是:

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

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

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

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

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

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

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));
			});
		}
	},

视频通话 RTC

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

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

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

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

消息事件

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

 举例说明

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

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

消息推送

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

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

//初始化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恢复事件
			})			
		},

短信验证码

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

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

 示例代码

<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>

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

清空缓存

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

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

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

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

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

AVM组件使用

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

 

 

 其中视频通话组件(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模块唤醒三方工具进行文件浏览。

//下载、浏览附件
    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('下载失败,请重试。');
          }
      });
    }

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

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();
		}
	});
}

单设备登陆

本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)
					})
				})
			}
//验证设备
		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
				});
			})
		}

接口调用

封装了 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
}

在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>

后台代码

代码示例

<?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();
    }
  }
}

插件引用

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

composer.json文件

{
	"config": {  
        "secure-http": false  
    },
	"require": {
		"jpush/jpush": "^3.6",
		"tencent/tls-sig-api-v2": "1.0",
		"alibabacloud/client": "^1.5"
	}
}
AVM环视系统开发实战:摄像头选型、主控与标定全流程解析 车载环视成像系统的核心在于将多路鱼眼摄像头画面实时拼接为鸟瞰视图,其实现涉及摄像头选型、ISP调优、畸变校正、鸟瞰变换与多路融合等关键技术。理解感光芯片的宽动态能力与主控SoC的算力匹配,是保障图像质量的基础;而标定工艺的精度则直接决定拼接效果与距离感知的准确性。该技术广泛应用于自动泊车、360°全景影像等场景,对提升驾驶安全具有重要意义。本文从硬件选型到软件算法落地,系统梳理AVM环视系统的完整工程实施路径。 阅读详情

相关推荐

辅助驾驶功能开发 - 全景影像AVM规范和控制算法

全景影像(AVM)是辅助驾驶系统中的一项重要功能,它通过多个摄像头捕捉车辆周围的实时图像,并将其合成成一个全景视图,为驾驶员提供更全面的视角和增强的安全性。通过遵循全景影像AVM规范并实现相应的控制算法,可以开发出功能强大的全景影像AVM系统,提升驾驶体验和道路安全性。障碍物检测和警告:全景影像AVM系统应具备障碍物检测功能,能够实时分析全景影像中的物体,并通过视觉或声音等方式向驾驶员发出警告,提醒其注意潜在的障碍物。全景影像显示:全景影像应以适当的方式在驾驶员的显示屏上进行展示。一、全景影像AVM规范。

CfProgram的博客 1985

一款简单易用的视频会议API介绍

5. **跨平台支持**:Daily.co可以在各种设备和平台上运行,包括Web、iOS和Android,用户可以随时随地进行视频通话。1. **简单易用**:Daily.co提供了简洁清晰的API文档和示例代码,使开发者能够快速上手,轻松集成视频通信功能。3. **灵活性**:Daily.co的API提供了丰富的定制选项,用户可以根据自己的需求定制视频通话的功能和外观。2. **高质量视频通话**:Daily.co支持高清视频通话,保证用户在会议中有流畅的视频和音频体验。

baoqqq123的博客 406

Qt框架下的视频会议室系统构建指南

Qt 是一个跨平台的C++应用程序框架,广泛用于开发具有图形用户界面(GUI)的软件。它的核心库不仅包括了各种组件,如窗口、按钮、文本框等,还包括了跨平台能力,使开发者能够编写一次代码,就能在多个操作系统上运行,包括Windows、Linux、Mac OS X、iOS、Android等。自定义窗口和对话框可以提供更为个性化和专业的用户体验。Qt提供了丰富的API来创建自定义窗口和对话框,并允许开发者控制窗口的样式和行为。

weixin_32389853的博客 594

avm.js跨端框架全介绍

avm.js是APICloud新推出的跨端JavaScript开发框架,可同时开发 Android & iOS 原生 App、小程序和 iOS 轻 App,且多端渲染效果统一。 渲染效率提升巨大 全新的 App 引擎 3.0 不依赖 webView,提供百分百的原生渲染,保障 App 性能和体验与原生 App 一致。 下面第一张图是用avm.js 的grid-view组件实现的列表,第二张图是使用webview 渲染html页面实现的列表,可通过GPU呈现模式分析曲线看到蓝色,绿色平均...

ff_888888的博客 1712

从零复刻平衡小车(基于STM32F1)

平衡小车(基于STM32F1)

Legendyyy的博客 5040

AI提示词实战:用EARS语法5步搞定产品需求拆解(附完整案例)

本文详细介绍了如何运用EARS语法将模糊的产品需求进行原子化拆解,转化为清晰、可执行的结构化指令。通过一个完整的自由职业者项目管理工具案例,演示了从原始需求分析到构建增强AI提示词的五步实战流程,帮助产品经理和技术人员实现高效、精准的需求传达与协作。

nokia的博客 1031

APICloud实战案例:如何封装AVM组件?(以声网组件为例)

AVM.js(Application-View-Model)是一个移动优先的高性能跨端JavaScript框架,支持一次编写多端渲染。它提供更趋近于原生的编程体验,通过简洁的模型来分离应用的用户界面、业务逻辑和数据模型,适合高度定制的应用开发。 AVM前端组件化开发模式基于标准Web Components组件化思想,提供包含虚拟DOM和Runtime的编程框架AVM.js以及多端统一编译工具,完全兼容Web Components标准,同时兼容Vue和React语法糖编写代码,编译工具将Vue和React相

cuicuiniu521的博客 300

NE5532A与JRC4580在低噪声音频电路设计中的关键应用

本文深入探讨了经典运算放大器NE5532A与JRC4580在低噪声音频电路设计中的核心应用。文章从噪声模型分析入手,阐述了针对低阻抗信号源时,如何利用这两款BJT输入运放的低电流噪声特性进行优化设计。通过对比两者参数与实战守则,为设计高保真音频前级、耳机放大等电路提供了从原理图到PCB布局的完整低噪声解决方案。

weixin_29295541的博客 328

APICloud AVM框架 开发CRM客户管理系统

通过信息技术以及互联网技术协调企业与顾客间在销售、营销和服务上的交互,从而提升其管理方式,向客户提供创新式的个性化的客户交互和服务的过程。其最终目标是吸引新客户、保留老客户以及将已有客户转为忠实客户,增加市场。

白鱼赤乌的博客 1184

Android HTTPS抓包证书配置全解:Proxyman实战避坑指南

HTTPS抓包本质是中间人(MITM)通信的可信建立过程,其核心在于客户端对代理根证书的信任链配置。在Android平台,自7.0起系统默认不信任用户安装的CA证书,必须通过Network Security Config显式声明trust-anchors,将Proxyman导出的PEM格式根证书纳入App级信任锚点。这一机制区别于iOS和桌面浏览器,导致大量开发者卡在‘证书已安装却无法解密’的误区。技术价值在于打通开发调试闭环:精准定位SSLHandshakeException、绕过证书固定、支持React

weixin_32175665的博客 649

华为OD机试 - 不含101的数(Python/JS/C/C++ 双机位C卷 100分)

本专栏收录于《华为OD机试真题(Python/JS/C/C++)》。刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。小明在学习二进制时,发现了一类不含101的数,也就是:现在给定一个正整数区间,请问这个区间内包含了多少个不含101的数?输入的唯一一行包含两个正整数。输出的唯一一行包含一个整数,表示在区间内一共有几个不含101的数。8 1668 → “1000”

学Java,找哪吒 1250

骁龙座舱平台核心解析(6): 车载摄像头驱动开发与360°全景环视(AVM)系统实战

本文介绍了基于高通第四代骁龙座舱平台(SA8295P)的车载摄像头驱动开发与360°全景环视(AVM)系统实现。首先分析了SA8295P的摄像头子系统架构,包括硬件数据流传输路径和SerDes远距离传输方案。重点展示了IMX390传感器驱动的开发过程,包含寄存器配置、初始化流程及曝光增益控制等核心代码实现。同时提供了CSI PHY接口的配置示例,为车载摄像头的硬件接入和驱动开发提供了完整的技术方案。文章通过实际工程案例,系统性地阐述了从摄像头驱动到AVM系统集成的全流程开发实践。

weixin_38498942的博客 803

[Dify实战] [安全合规] 用 Dify 优化安全巡检:从经验驱动到标准闭环-带绑定资源

摘要:企业信息系统安全管理中,安全合规巡检面临标准不统一、整改难追踪等痛点。本文提出基于Dify工具的智能化解决方案,通过自动化工作流实现标准检查清单生成、风险等级标注、整改建议输出及审计记录归档。方案采用结构化Prompt模板和Markdown输出格式,涵盖账号权限、数据安全等核心维度,并提供证据收集建议和闭环管理机制,帮助组织从人工经验驱动转向数据流程驱动的合规管理。配套可导入Dify的工作流文件将随后发布。

技术小甜甜:传播可落地技术,帮助个人和企业降低解决真实场景问题的成本 272

avm.js 怎么样?

AVM是APICloud推出的跨端JavaScript 框架。 AVM的几点特性: 1. 一套代码可编译为对应Android 、iOS 、微信小程序、iOS 轻App、H5端的安装包或代码包。 2. 兼容APICloud2.0技术栈,这意味着平台上上千款Android iOS原生模块可供使用。或者在老项目里部分引入3.0的技术,对APP局部进行优化。 3. 原生引擎渲染。如果使用 avm.js 进行开发,App 将使用无 webView 的原生引擎 3.0 进行渲染,所有组件及视图与 Andro..

ff_888888的博客 871

DCMTK编译

dcmtk-3.6.4 使用Unicode?dcmtk的编译选项里没有关于Unicode的选项,OFString的接口里明确使用了OFString(char*)作为构造函数参数,所以,我们可以假设不能够自己擅自在CMakeList.txt里增加一行 add_definitions(-DUNICODE -D_UNICODE) 来强制使用Unicode编码。 亲测:加了之后编译不过 void...

C++ 爱好者 make_it_simple888 1733

2021年全网最细 VirtualBox 虚拟机安装 Ubuntu 20.04.2.0 LTS及Ubuntu的相关配置

目录一、VirtualBox 虚拟机的安装1. VirtualBox 的下载和安装2. 设置 VirtualBox 存储文件夹3. 在 VirtualBox 创建虚拟机二、Linux 操作系统(Ubuntu)的安装1. Linux 操作系统(Ubuntu)的下载2. 安装 Ubuntu3. 启动 Ubuntu三、配置 Ubuntu1. 设置终端2. 设置共享剪贴板3. 设置共享文件夹 一、VirtualBox 虚拟机的安装 VirtualBox 是由德国 Innotek 公司开发,由 Sun Microsy

XAMO Lab|代码、调试与复盘笔记 2万+

深度测评 | 五大主流多端开发框架全面对比

2021 跨平台开发框架到底哪家强? 目前市场上有多个专业做跨平台开发的框架,那么对开发者来说究竟哪一个框架更符合自己的需求呢?笔者特地总结对比了一下不同框架的特性。 国内外笔者选择了一共 5 个主流的测评对象,分别是 RN,Flutter,Ionic,NativeScript,以及用友 APICloud 团队开发的 AVM。 目前来看比较火的应该是 Flutter,次之 RN,具体还要看企业的应用场景和领域,AVM,Ionic,NativeScript 在不少企业和个人开发者中也使用率较高。 一,安装环境

浪里行舟的博客 2万+

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

自动泊车之 AVM 环视系统算法框架

AVM(Around View Monitor),中文:全景环视系统。在自动驾驶领域,AVM属于自动泊车系统的一部分,是一种实用性极高、可大幅提升用户体验和驾驶安全性的功能。AVM已经是一种较为成熟的技术,中高端车型均有部署,但详细讲述AVM系统算法的技术博文并不多。作者在工作中搭建了一套AVM算法框架,有一些效果还不错的demo。撰写本文,主要是想将AVM算法框架中每个算子讲述清楚,与大家共同进步。本博文的风格为理论与实践结合,含有部分代码,适合有一些计算机视觉基础的同学。...

人无远虑,必有近忧 6724

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

AVM 环视拼接方法介绍

关于车辆的全景环视系统网上已经有很多的资料,然而几乎没有可供参考的代码,这一点对入门的新人来说非常不友好。全景环视系统,又称AVM。在自动驾驶领域,AVM属于自动泊车系统的一部分,是一种实用性极高、可大幅提升用户体验和驾驶安全性的功能。AVM汽车环视影像系统如图所示,由安装在前保险杠、后备箱、后视镜上的四个外置鱼眼相机构成。该系统包含的算子按照先后顺序:去畸变、四路鱼眼相机联合标定、投影变换、鸟瞰图微调、拼接融合、3D模型纹理映射等。下面我们将围绕着算子的先后顺序来对AVM进行介绍。

人无远虑,必有近忧 5903

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

基于OpenVidu的视频会议解决方案

OpenVidu 是一个开源的实时音视频通信平台,基于 Apache 2.0 协议开源的 WebRTC 视频会议平台,源码仓库地址:https://github.com/OpenVidu/openvidu。它提供了一组 API 和 SDK ,使开发人员能够轻松地在 Web 和移动应用程序中集成实时音视频通信功能。 OpenVidu 支持多种功能,包括实时音视频通话、屏幕共享、录制和回放等。它基于 WebRTC 技术,可以在不同的设备和浏览器上实现高质量的音视频通信。

Heartsuit的博客 4903

使用APICloud AVM框架开发预约应用

前段时间跟朋友一起搞了一个预约的项目,前端用的APICloud的AVM框架做的,后端用的php开发的,用的tp5框架,没几天就搞出来了。简单跟大家分享一下开发中的一些功能点的实现吧。也欢迎大家一起探讨。 一、整理项目脑图 二、项目原型图 原型是用的码前做的,大家可以通过​​www.devbefore.com/protomobile/47942983009476608​​查看 三、项目前端 本项目中前端采用APICloud AVM多端开发技术进行开发,要点包括 swiper 轮播图、网络请求封装等。使用 A

qq_31307319的博客 691

APICloud AVM框架 密码输入框组件

带网格的输入框组件,可以用于输入密码、短信验证码等场景,通常与数字键盘组件配合使用。支持自定义密码长度,支持加密和明文显示。

白鱼赤乌的博客 466

APICloud AVM框架 封装数据表格组件

用以展示基础表格数据的组件。组件的核心功能点是在数据展示的时候,用到了2个v-for循环,第一层循环是数据对象的循环,然后嵌套列名的对象,通过列名中的key值在数据对象中查询对应的数据,这样就保证了在数据对象与列名对象顺序打乱的情况下也可以把数据对应起来,并能够在列名没有对应的数据的时候进行特殊处理。.........

白鱼赤乌的博客 414

AVM框架的模块使用───以客服功能为示例

以开发客服功能为例,基于AVM框架编写,结合iflyRecognition 模块(封装了 科大讯飞 的SDK 的语音听写、语音在线合成功能),轻松简单地实现了语音转文字客服功能。

csbnlyz的博客 495

封装 avm 组件经验分享

avm 怎样定义组件、怎么实现组件传值、声网模块实现1对1语音通话

ff_888888的博客 387

视频会议十大开源项目排行

在视频会议领域,有许多可以值得参考的开源项目,这些开源项目有的是协议栈、有的是编码器或者是传输协议,由于视频会议系统是一个综合性的应用系统,里面包含功能较多,如能把这些开源项目选择性的加入我们的视频会议开发当中,我们的开发效率肯定会事半功倍,下面我们列举一下视频会议相关的十大开源项目,并对其重要性及优缺点做一个全面的评价与排名。 1、 OpenH323项目        (★★★★★)

yuyin86的专栏 4779
上一篇: APICloud AVM框架 封装SKU(商品规格选择)组件
下一篇: MySQL Cluster(MySQL集群)数据节点无法启动
白鱼赤乌
博客等级 码龄10年 7粉丝 23原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白鱼赤乌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值