記錄--uniapp開發安卓APP視頻通話模塊初實踐

来源:https://www.cnblogs.com/smileZAZ/archive/2022/11/17/16900022.html
-Advertisement-
Play Games

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 視頻通話SDK用的即構的,uniapp插件市場地址 推送用的極光的,uniapp插件市場地址 即構音視頻SDK uniapp插件市場的貌似是有些問題,導入不進項目,直接去官網下載,然後放到項目下的 nativeplugins 目錄下,在配 ...


這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

視頻通話SDK用的即構的,uniapp插件市場地址

推送用的極光的,uniapp插件市場地址

即構音視頻SDK

uniapp插件市場的貌似是有些問題,導入不進項目,直接去官網下載,然後放到項目下的 nativeplugins 目錄下,在配置文件中填入即構後臺的appID和AppSign,接下來就可以開幹了

準備兩個頁面

首頁:/pages/index/index

// 新建一個按鈕
<button @click="sendVideo">發送視頻邀請</button>

// 發送事件,主動發送直接進入下一個頁面即可
sendVideo(){
    uni.navigateTo({
        url: '/pages/call/call'
    })
}

通話頁:pages/call/call 這個頁面會複雜一點

註意這個頁面為 nvue 頁面

先把所有代碼都列出來,再一一做說明

<template>
    <view>
        <view v-if="status === 1" class="switch-bg" :style="{'height': pageH + 'px'}">
            <view class="top-info u-flex" style="flex-direction: row;">
                <image src="http://cdn.u2.huluxia.com/g3/M02/32/81/wKgBOVwN9CiARK1lAAFT4MSyQ3863.jpeg" class="avatar">
                </image>
                <view class="info u-flex u-flex-col u-col-top">
                    <text class="text">值班中心</text>
                    <text class="text" style="margin-top: 10rpx;">正在呼叫</text>
                </view>
            </view>
                <view class="switch-handle u-flex u-row-center" style="flex-direction: row; justify-content: center;">
                    <image src="/static/hang_up.png" class="img" @click="hangUp"></image>
                </view>
        </view>
        <view v-if="status === 2" class="switch-bg" :style="{'height': pageH + 'px'}">
            <view class="top-info u-flex" style="flex-direction: row;">
                <image src="http://cdn.u2.huluxia.com/g3/M02/32/81/wKgBOVwN9CiARK1lAAFT4MSyQ3863.jpeg" class="avatar">
                        </image>
                    <view class="info u-flex u-flex-col u-col-top">
                        <text class="text">值班中心</text>
                        <text class="text" style="margin-top: 10rpx;">邀請您視頻聊天</text>
                    </view>
                </view>
                <view class="switch-handle">
                    <view class="u-flex" style="justify-content: flex-end; flex-direction: row; padding-right: 10rpx; padding-bottom: 30rpx;">
                        <text style="font-size: 26rpx; color: #fff; margin: 10rpx;">切到語音接聽</text>
                        <image src="/static/notice.png" style="width: 64rpx; height: 52rpx;"></image>
                    </view>
                        <view class="u-flex u-row-center u-row-between" style="flex-direction: row; justify-content: space-between;">
                            <image src="/static/hang_up.png" class="img" @click="hangUp"></image>
                            <image src="/static/switch_on.png" class="img" @click="switchOn"></image>
                        </view>
                </view>
        </view>
        <view v-if="status === 3" style="background-color: #232323;" :style="{'height': pageH + 'px'}">
            <view style="flex-direction: row; flex-wrap: wrap;">
                <zego-preview-view class="face" style="width: 375rpx; height: 335rpx;"></zego-preview-view>
                <view v-for="(stream, index) in streamList" :key="index" style="flex-direction: row; flex-wrap: wrap;">
                    <zego-view :streamID="stream.streamID" style="width: 375rpx; height: 335rpx;"></zego-view>
                </view>
                </view>
                <view class="switch-handle">
                        <view style="flex-direction: row; justify-content: center; padding-bottom: 30rpx;">
                                <text style="font-size: 26rpx; color: #fff; margin: 10rpx;">{{minute}}:{{seconds}}</text>
                        </view>
                        <view style="flex-direction: row; justify-content: space-between;">
                                <view style="align-items: center;">
                                        <view class="icon-round">
                                                <image src="/static/notice.png" class="icon1" mode=""></image>
                                        </view>
                                        <text class="h-text">切到語音通話</text>
                                </view>
                                <view style="align-items: center;">
                                        <image src="/static/hang_up.png" class="img" @click="hangUp"></image>
                                        <text class="h-text">掛斷</text>
                                </view>
                                <view style="align-items: center;">
                                        <view class="icon-round" @click="changeCamera">
                                                <image src="/static/change_camera.png" class="icon2" mode=""></image>
                                        </view>
                                        <text class="h-text">轉換攝像頭</text>
                                </view>
                        </view>
                </view>
        </view>
    </view>
</template>

<script>
	// #ifdef APP-PLUS
	var jpushModule = uni.requireNativePlugin("JG-JPush")
	import ZegoExpressEngine from '../../zego-express-video-uniapp/ZegoExpressEngine';
	import {ZegoScenario} from '../../zego-express-video-uniapp/impl/ZegoExpressDefines'
	import {AppID,AppSign} from '../../zegoKey.js'
	var instance = ZegoExpressEngine.createEngine(AppID, AppSign, true, 0);
	// #endif
	export default {
		data() {
			return {
				status: 1, // 1: 主動呼叫;2: 被呼叫
				pageH: '',	// 頁面高度
				innerAudioContext: null, //	音樂對象
				streamList: [],
				msg_id: '',		// 推送消息id
				msg_cid: '',		// 推送cid
				roomID: 'dfmily110001',
				publishStreamID: uni.getStorageSync('userinfo').nickname,
				userID: uni.getStorageSync('userinfo').nickname, 
				userName: uni.getStorageSync('userinfo').nickname, 
				camera_dir: 'before', // 攝像頭 before 前置,after 後置
			};
		},
		destroyed: () => {
			console.log('destroyed');
			ZegoExpressEngine.destroyEngine();
		},
		mounted() {
			var client = uni.getSystemInfoSync()
			if (client.platform == 'android') {
				//安卓事先請求攝像頭、麥克風許可權
				var nativeEngine = uni.requireNativePlugin('zego-ZegoExpressUniAppSDK_ZegoExpressUniAppEngine');
				nativeEngine.requestCameraAndAudioPermission();
			}
		},
		onLoad(opt) {
			this.getSysInfo();
			this.playAudio();
			
			if(opt.status == 2){		// 帶參數 status=2時代表被呼叫
				this.status = parseInt(opt.status)
			}
			if(!opt.status){			// 主動呼叫、需要發推送消息
				this.getPushCid();
			}
			this.initZegoExpress();
		},
		onBackPress() {
			// return true;
			this.innerAudioContext.stop();
			this.logout();
		},
		methods: {
			getSysInfo() { // 獲取手機信息
				let sys = uni.getSystemInfoSync()
				this.pageH = sys.windowHeight
			},
			playAudio() { // 播放音樂
				this.innerAudioContext = uni.createInnerAudioContext();
				this.innerAudioContext.autoplay = true;
				this.innerAudioContext.src = '/static/message.mp3';
				this.innerAudioContext.onPlay(() => {
					console.log('開始播放');
				});
			},
			stopAudio(){		// 停止播放音樂
				if (this.innerAudioContext) {
					this.innerAudioContext.stop()
				}
			},
			hangUp() { // 掛斷
				this.stopAudio();
				this.sendCustomCommand(500)
				this.revocationPushMsg();
				this.logout();
				uni.navigateBack({
					delta:1
				})
			},
			switchOn() { // 接通
				this.stopAudio();
				this.status = 3
				this.sendCustomCommand(200)
			},
			changeCamera() { // 切換攝像頭
				var instance = ZegoExpressEngine.getInstance();
				if (this.camera_dir == 'before') {
					instance.useFrontCamera(false)
					this.camera_dir = 'after'
				} else if (this.camera_dir == 'after') {
					instance.useFrontCamera(true)
					this.camera_dir = 'before'
				}
			},
			sendCustomCommand(msg){		// 發送自定義信令
				var instance = ZegoExpressEngine.getInstance();
				instance.sendCustomCommand(this.roomID, msg, [{
					"userID": this.userID,
					"userName": this.userName
				}], res => {
					console.log(res)
				});
			},
			getPushCid(){			// 極光推送cid獲取
				uni.request({
					url: 'https://api.jpush.cn/v3/push/cid',
					header: {
						'Authorization': 'Basic ' + this.encode(
							'appKey:masterSecret')
					},
					success: (res) => {
						this.msg_cid = res.data.cidlist[0]
						this.sendPushMsg();
					}
				})
			},
			revocationPushMsg(){		// 撤銷推送
				uni.request({
					url: 'https://api.jpush.cn/v3/push/' + this.msg_id,
					method: 'DELETE',
					header: {
						'Authorization': 'Basic ' + this.encode(
							'appKey:masterSecret')
					},
					success: (res) => {
						console.log(res)
					}
				})
			},
			sendPushMsg(idArr) {
				uni.request({
					url: 'https://api.jpush.cn/v3/push',
					method: 'POST',
					header: {
						'Authorization': 'Basic ' + this.encode(
							'appKey:masterSecret')
					},
					data: {
						"cid": this.msg_cid,
						"platform": "all",
						"audience": {
							"registration_id": ['160a3797c8ae473a331']
						},
						"notification": {
							"alert": "邀請通話",
							"android": {},
							"ios": {
								"extras": {
									"newsid": 321
								}
							}
						}
					},
					success: (res) => {
						this.msg_id = res.data.msg_id
					}
				})
			},
			initZegoExpress(){		// 初始化
				// instance.startPreview();
				instance.on('roomStateUpdate', result => {
					console.log('From Native roomStateUpdate:' + JSON.stringify(result));
					if (result['state'] == 0) {
						console.log('房間斷開')
					} else if (result['state'] == 1) {
						console.log('房間連接中')
					} else if (result['state'] == 2) {
						console.log('房間連接成功')
					}
				});
				instance.on('engineStateUpdate', result => {
					if (result == 0) {
						console.log('引擎啟動')
					} else if (result['state'] == 1) {
						console.log('引擎停止')
					}
				});
				instance.on('roomStreamUpdate', result => {
					var updateType = result['updateType'];
					if (updateType === 0) {
						var addedStreamList = result['streamList'];
						this.streamList = this.streamList.concat(addedStreamList);
						for (let i = 0; i < addedStreamList.length; i++) {
							console.log('***********&&&&', addedStreamList[i].streamID)
							var streamID = addedStreamList[i].streamID;
							var instance = ZegoExpressEngine.getInstance();
							instance.startPlayingStream(streamID);
						}
					} else if (updateType === 1) {
						this.removeStreams(result['streamList']);
					}
				});
				instance.on('roomUserUpdate', result => {
					var updateType = result['updateType'];
					if (updateType === 0) {
						this.userID = result.userList[0].userID
						this.userName = result.userList[0].userName
						// this.userList = this.userList.concat(result['userList']);
					} else if (updateType === 1) {
						// this.removeUsers(result['userList']);
					}
				});
				instance.on('IMRecvCustomCommand', result => {
					var fromUser = result['fromUser'];
					var command = result['command'];
					// console.log(`收到${fromUser.userID}的消息:${JSON.stringify(result)}`)
					if(result.command == 200){
						console.log('接聽視頻通話')
						this.status = 3
						this.stopAudio();
					}else if(result.command == 500){
						console.log('拒絕通話')
						uni.navigateBack({
							delta: 1
						})
					}
				});
				this.login();
				this.publish();
			},
			login() {		// 登錄房間
				var instance = ZegoExpressEngine.getInstance();
				instance.loginRoom(this.roomID, {
					'userID': this.userID,
					'userName': this.userName
				});
			},
			logout() {		// 退出房間
				var instance = ZegoExpressEngine.getInstance();
				instance.logoutRoom(this.roomID);
				this.destroyEngine();
			},
			publish() {		// 推流
				var instance = ZegoExpressEngine.getInstance();
				instance.startPublishingStream(this.publishStreamID);
				instance.setVideoConfig({
					encodeWidth: 375,
					encodeHeight: 336
				})
			},
			destroyEngine() {
				ZegoExpressEngine.destroyEngine(boolResult => {
					this.streamList = [];
				});
			},
			removeStreams(removedStreams) {		// 刪除流
				let leg = this.streamList.length
				for (let i = leg - 1; i >= 0; i--) {
					for (let j = 0; j < removedStreams.length; j++) {
						if (this.streamList[i]) {
							if (this.streamList[i].streamID === removedStreams[j].streamID) {
								this.streamList.splice(i, 1)
								continue; //結束當前本輪迴圈,開始新的一輪迴圈
							}
						}
					}
				}
			},
			
			
			
			encode: function(str) {
				// 對字元串進行編碼
				var encode = encodeURI(str);
				// 對編碼的字元串轉化base64
				var base64 = btoa(encode);
				return base64;
			},
		}
	}
</script>

<style lang="scss">
	.switch-bg {
		position: relative;
		background-color: #6B6B6B;
	}

	.top-info {
		padding: 150rpx 35rpx;
		flex-direction: row;
		align-items: center;

		.avatar {
			width: 150rpx;
			height: 150rpx;
			border-radius: 10rpx;
		}

		.info {
			padding-left: 18rpx;

			.text {
				color: #fff;
				font-size: 26rpx;
			}
		}
	}

	.switch-handle {
		position: absolute;
		bottom: 100rpx;
		left: 0;
		right: 0;
		padding: 0 85rpx;

		.img {
			width: 136rpx;
			height: 136rpx;
		}

		.icon-round {
			align-items: center;
			justify-content: center;
			width: 136rpx;
			height: 136rpx;
			border: 1rpx solid #fff;
			border-radius: 50%;

			.icon1 {
				width: 64rpx;
				height: 52rpx;
			}

			.icon2 {
				width: 60rpx;
				height: 60rpx;
			}
		}

		.h-text {
			margin-top: 10rpx;
			font-size: 26rpx;
			color: #fff;
		}
	}
</style>

說明:

代碼中的masterSecret需要修改為極光後臺的masterSecretappKey需要修改為極光後臺的appKey

view 部分:

status=1 中的為主動呼叫方進入頁面是初始顯示內容,最重要的是 hangUp 方法,用來掛斷當前邀請

status=2 中的為被邀請者進入頁面初始顯示的內容,有兩個按鈕,一個hangUp掛斷,一個switchOn 接聽

status=3中為接聽後顯示的內容(顯示自己與對方視頻畫面)

script 部分:

最開始五行是引入相關SDK的。極光推送、即構音視頻

onLoad 中有一個判斷語句,這個就是用於判斷進入頁面時是主動呼叫方還是被動答應方的,顯示不同內容

if(opt.status == 2){		// 帶參數 status=2時代表被呼叫
    this.status = parseInt(opt.status)
}
if(!opt.status){			// 主動呼叫、需要發推送消息
    this.getPushCid();
}

sendCustomCommand 是用來在房間內發送自定義信令的,用於通知另一個人是接聽了還是掛斷了通話

getPushCid 是獲取極光推送的cid,避免重覆發送推送消息(極光推送)

changeCamera 切換攝像頭

revocationPushMsg 撤銷推送(主動呼叫方掛斷通話)

sendPushMsg 發推送消息

initZegoExpress 初始化即構音視頻SDK相關,與官網demo,此處我做了小改動

login 登錄即構房間

logout 退出即構房間

publish 推流

destroyEngine 銷毀音視頻實例

removeStreams 刪除流

encode base64轉碼

在App.vue中進行極光推送的初始化

onLaunch: function() {
    console.log('App Launch')
    // #ifdef APP-PLUS
    if (uni.getSystemInfoSync().platform == "ios") {
        // 請求定位許可權
        let locationServicesEnabled = jpushModule.locationServicesEnabled()
        let locationAuthorizationStatus = jpushModule.getLocationAuthorizationStatus()
        console.log('locationAuthorizationStatus', locationAuthorizationStatus)
        if (locationServicesEnabled == true && locationAuthorizationStatus < 3) {
            jpushModule.requestLocationAuthorization((result) => {
                console.log('定位許可權', result.status)
            })
        }


        jpushModule.requestNotificationAuthorization((result) => {
            let status = result.status
            if (status < 2) {
                uni.showToast({
                    icon: 'none',
                    title: '您還沒有打開通知許可權',
                    duration: 3000
                })
            }
        })

        jpushModule.addGeofenceListener(result => {
            let code = result.code
            let type = result.type
            let geofenceId = result.geofenceId
            let userInfo = result.userInfo
            uni.showToast({
                icon: 'none',
                title: '觸發地理圍欄',
                duration: 3000
            })
        })

        jpushModule.setIsAllowedInMessagePop(true)
        jpushModule.pullInMessage(result => {
            let code = result.code
            console.log(code)
        })

        jpushModule.addInMessageListener(result => {
            let eventType = result.eventType
            let messageType = result.messageType
            let content = result.content
            console.log('inMessageListener', eventType, messageType, content)

            uni.showToast({
                icon: 'none',
                title: JSON.stringify(result),
                duration: 3000
            })
        })

    }

    jpushModule.initJPushService();
    jpushModule.setLoggerEnable(true);
    jpushModule.addConnectEventListener(result => {
        let connectEnable = result.connectEnable
        uni.$emit('connectStatusChange', connectEnable)
    });

    jpushModule.addNotificationListener(result => {
        let notificationEventType = result.notificationEventType
        let messageID = result.messageID
        let title = result.title
        let content = result.content
        let extras = result.extras
        console.log(result)
        this.$util.router(`/pages/public/answer?status=2`)
    });

    jpushModule.addCustomMessageListener(result => {
        let type = result.type
        let messageType = result.messageType
        let content = result.content
        console.log(result)
        uni.showToast({
            icon: 'none',
            title: JSON.stringify(result),
            duration: 3000
        })
    })

    jpushModule.addLocalNotificationListener(result => {
        let messageID = result.messageID
        let title = result.title
        let content = result.content
        let extras = result.extras
        console.log(result)
        uni.showToast({
            icon: 'none',
            title: JSON.stringify(result),
            duration: 3000
        })
    })
    // #endif
},

不要忘了在最開始引入極光推送的插件

var jpushModule = uni.requireNativePlugin("JG-JPush")

官方demo的代碼,直接拿過來了。。

其中最重要的就是下麵這段,用來監聽獲取推送消息的,這裡如果收到推送消息自動跳轉至通話頁面,也就是上面status=2的狀態下

jpushModule.addNotificationListener(result => {
    let notificationEventType = result.notificationEventType
    let messageID = result.messageID
    let title = result.title
    let content = result.content
    let extras = result.extras
    console.log(result)
    this.$util.router(`/pages/call/call?status=2`)
});

https://juejin.cn/post/6954172658195906567

如果對您有所幫助,歡迎您點個關註,我會定時更新技術文檔,大家一起討論學習,一起進步。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • //源文件 void TimerPhyConfig() { RCC->APB1ENR |= (1<<1); //使能Timer3 TIM3->ARR = arr; TIM3->PSC = psc; TIM3->DIER = (1<<0); //Update interrupt enabled TIM ...
  • 本文主要從技術層面探討大數據目前的現狀以及面臨的挑戰。在此之前,如果你對大數據的概念還比較模糊,可閱讀什麼是大數據一文瞭解。 如何定義大數據 目前我們已經瞭解到,大數據是由於數據量的巨大增長而產生的。所以,“大數據”一詞主要描述的是規模巨大的混合數據集,這種數據集是結構化與非結構化數據的融合。 通常 ...
  • 摘要:一文帶你細數幾種ETCD服務異常實例狀態。 本文分享自華為雲社區《【實例狀態】GaussDB ETCD服務異常》,作者:酷哥 。 首先確認是否是虛擬機、網路故障 虛擬機故障導致ETCD服務異常告警 問題現象 管控面上報etcd服務異常告警,虛擬機發生重啟,熱遷移、冷遷移,HA等動作。 問題分析 ...
  • 最近,我們袋鼠雲的UED部⻔小伙伴們,不聲不響地⼲了⼀件⼤事——升級了全新設計語言「數棧UI5.0」。 眾所周知,用戶在使用產品時,是一個動態的過程,用戶和產品之間進行交互的可用性,能否讓用戶愉悅、快速地在產品內達成目的,直接影響用戶使用產品的體驗。 在設計中,有一個廣泛的經驗法則被稱為「尼爾森十大 ...
  • Babelfish for PostgreSQL開源已快一月,不過全網還沒有實踐者總結。今天我們就測試看看,Babelfish到底是如何部署與使用的! ...
  • 現在要是說mysql是什麼東西,就不禮貌了 雖然有的同學沒有進行系統的深入學習,但應該也有個基本概念 【不瞭解也沒關係,後續會進行mysql專欄講解】簡單來說,存儲數據的 學習mysql,就要先安裝它 上官網 : https://dev.mysql.com/downloads/mysql/ 打開網址 ...
  • 11月15日,HMS Core手語服務在2022(第二十一屆)中國互聯網大會 “互聯網助力經濟社會數字化轉型”案例評選活動中,榮獲“特別推薦案例”。 經過一年多的技術迭代和經驗積累,HMS Core手語服務已與多個行業的開發者合作,將AI手語翻譯能力應用在了教育、社交、新聞、政務辦理等場景,助力開發 ...
  • 項目同步git:https://gitee.com/lixin_ajax/vue3-vite-ts-pinia-vant-less.git 覺得有幫助的小伙伴請點下小心心哦 為避免贅述,過於基礎的點會直接省略或貼圖,比如創建文件夾/文件的路徑/路由一類 配置相應功能,也儘量只貼相關代碼,並不代表整個 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...