思路: ①、通过websocket长连接的形式实时接收用户付款信息。
②、因为后端给的金额为数字形式的例如 315.25。我们需要将金额转换为读的方式 三百一拾伍点二五。
③、因为这个金额不是固定的所以需要将 ( 0 ~ 9 )以及拾、百、千、万、亿、点、到账、元,等音频文件拼接起来 组成 到账三百一拾五点二五元。
④、因为播放音频相当于是一个异步操作。如果短暂的时间收到多条收款消息就会出现多个音频同时播放的情况。写一个任务队列解决
⑤、因为每个页面都需要可以播报、所以封装一下mixin,在每个页面都注入websocket,切换页面之前,关闭socket,然后重新连接一下socket。
⑥、因为用户按两次返回键,会导致app退出应用,所以我们需要做一个简单的保活,按两次返回键让应用在后台运行。如果需要长期保活请在uniapp插件市场上搜索原生插件
⑦、遇到的问题:在使用过程中如果频繁播放会出现播放中断的问题、后来经过查阅是因为音频实例没有被销毁导致的问题。每播放完一条消息及时销毁音频实例即可解决。
准备资源:
上代码:
一、封装websocket
import {
SOCKET_URL
} from '@/env.js';
let isSocketClose = false; // 是否关闭socket
let heartbeatInterval = ""; // 心跳定时器
let socketTask = null; // websocket对象
let againTimer = null; //断线重连定时器
let onReFn = null;
let onSucFn = null;
let onErrFn = null;
let id = uni.getStorageSync('shopDetail').id
let url = SOCKET_URL + `?id=${id}`
/**
* sockeUrl:websocet的地址
* onReceive:消息监听的回调
* onErrorEvent:抛出错误的回调,且弹窗连接失败的提示框
* onErrorSucceed:抛出成功回调,主要用于隐藏连接失败的提示框
* */
const sokcet = (onReceive, onErrorEvent, onErrorSucceed) => {
onReFn = onReceive;
onErrFn = onErrorEvent;
onSucFn = onErrorSucceed;
isSocketClose = false;
//判断是否有websocet对象,有的话清空
if (socketTask) {
socketTask.close();
socketTask = null;
clearInterval(heartbeatInterval);
}
//WebSocket的地址
// 【非常重要】必须确保你的服务器是成功的,如果是手机测试千万别使用ws://127.0.0.1:9099【特别容易犯的错误】
// 连接
socketTask = uni.connectSocket({
url: url,
success(data) {
console.log("websocket创建成功");
clearInterval(againTimer) //断线重连定时器
},
fail: (err) => {
console.log("报错", err);
console.log(url)
}
});
// 连接打开
socketTask.onOpen((res) => {
console.log('WebSocket打开', res);
clearInterval(againTimer) //断线重连定时器
onErrorSucceed({
isShow: false
}) // 用于提示框的隐藏
heartbeatInterval && clearInterval(heartbeatInterval);
// 10秒发送一次心跳
heartbeatInterval = setInterval(() => {
sendMsg('心跳ing')
}, 1000 * 10)
})
// 监听连接失败
socketTask.onError((err) => {
console.log('WebSocket连接打开失败,请检查', err);
//停止发送心跳
clearInterval(heartbeatInterval)
//如果不是人为关闭的话,进行重连
if (!isSocketClose) {
reconnect(url, onErrorEvent)
}
})
// // 监听连接关闭 -
socketTask.onClose((e) => {
console.log('WebSocket连接关闭!');
clearInterval(heartbeatInterval)
if (!isSocketClose) {
reconnect(url, onErrorEvent)
}
})
// 监听收到信息
socketTask.onMessage((res) => {
uni.hideLoading()
console.log(res, 'res监听收到信息')
let serverData = JSON.parse(res.data)
//与后端规定好返回值分别代表什么,写业务逻辑
serverData && onReceive(serverData);
});
}
const reconnect = (url, onErrorEvent) => {
console.log('进入断线重连', isSocketClose);
clearInterval(againTimer) //断线重连定时器
clearInterval(heartbeatInterval);
socketTask && socketTask.close(); // 确保已经关闭后再重新打开
socketTask = null;
onErrorEvent({
isShow: true,
messge: '扫描头服务正在连接...'
})
// 连接 重新调用创建websocet方法
againTimer = setInterval(() => {
sokcet(onReFn, onErrFn, onSucFn)
console.log('在重新连接中...');
}, 1000 * 5)
}
const sendMsg = (msg) => { //向后端发送命令
msg = JSON.stringify(msg)
try {
//通过 WebSocket 连接发送数据
socketTask.send({
data: msg
});
} catch (e) {
if (isSocketClose) {
return
} else {
reconnect(url, onErrFn)
}
}
}
// 关闭websocket【必须在实例销毁之前关闭,否则会是underfined错误】beforeDestroy() {websocetObj.stop();}
const stop = () => {
isSocketClose = true
clearInterval(heartbeatInterval);
clearInterval(againTimer) //断线重连定时器
// socketTask.close(); // 确保已经关闭后再重新打开
socketTask && socketTask.close()
socketTask = null;
}
export const mySocket = {
sokcet,
stop,
sendMsg
};
二、将数字转为文字金额读法
capitalAmount(amount) {
// 汉字的数字
const cnNums = ["_0,", "_1,", "_2,", "_3,", "_4,", "_5,", "_6,", "_7,", "_8,", "_9,"];
// 基本单位
const cnIntRadice = ["", "_shi,", "_bai,", "_qian,"];
// 对应整数部分扩展单位
const cnIntUnits = ["", "_wan,", "_yi,"];
// 整数金额时后面跟的字符
const cnInteger = "_yuan";
// 整型完以后的单位
const cnIntLast = "_dian,";
// 最大处理的数字
const maxNum = 9999999999999999.99;
// 金额整数部分
let integerNum;
// 金额小数部分
let decimalNum;
// 输出的中文金额字符串
let chineseStr = "";
// 分离金额后用的数组,预定义
let parts;
if (amount === "") {
return "";
}
amount = parseFloat(amount);
if (amount >= maxNum) {
// 超出最大处理数字
return "";
}
if (amount === 0) {
chineseStr = cnNums[0];
return chineseStr;
}
// 转换为字符串
amount = amount.toString();
if (amount.indexOf(".") === -1) {
integerNum = amount;
decimalNum = "";
} else {
parts = amount.split(".");
integerNum = parts[0];
decimalNum = parts[1].substr(0, 2);
}
// 获取整型部分转换
if (parseInt(integerNum, 10) > 0) {
let zeroCount = 0;
const IntLen = integerNum.length;
for (let i = 0; i < IntLen; i++) {
const n = integerNum.substr(i, 1);
const p = IntLen - i - 1;
const q = p / 4;
const m = p % 4;
if (n === "0") {
zeroCount++;
} else {
if (zeroCount > 0) {
chineseStr += cnNums[0];
}
// 归零
zeroCount = 0;
chineseStr += cnNums[parseInt(n, 10)] + cnIntRadice[m];
}
if (m === 0 && zeroCount < 4) {
chineseStr += cnIntUnits[q];
}
}
} else {
chineseStr = cnNums[0];
}
// 小数部分
if (decimalNum !== "" && decimalNum !== "00") {
const decLen = decimalNum.length;
chineseStr += cnIntLast
for (let i = 0; i < decLen; i++) {
const n = decimalNum.substr(i, 1);
if (n !== "0") {
chineseStr += cnNums[Number(n)]
} else {
chineseStr += cnNums[0]
}
}
}
if (chineseStr === "") {
chineseStr += cnNums[0] + cnIntLast;
} else if (decimalNum === "") {
chineseStr;
}
return chineseStr + cnInteger;
},
三、音频拼接播放
let musicObj = null
// arr保存对应的音频文件
let arr = []
// 定义一个方法来拼接音频文件
function splicingAudioFiles(res) {
// 注:res为例如 123.12 的数字或者字符串
return new Promise((resolve) => {
// 创建播放器对象
musicObj = uni.createInnerAudioContext();
// 调用数字转中文读法的方法 得到['_1,'_bai','_2','_shi','_san','_dian','_1','_2','_yuan']
const moneyArr = this.$tool.capitalAmount(res).split(',');
// 重组组成资源文件本地地址路径
arr = moneyArr.map((item) => `/static/music/${item}.mp3`);
// src为播放器的播放路径 先默认播放开头话语
musicObj.src = '/static/music/_daozhang.mp3'
//play()为播放的方法
musicObj.play()
//onEnded()为播放结束的时候继续操作
musicObj.onEnded((res) => {
//这里调用playVoice()方法 arr为保存音频文件的数组 musicObj为播放器对象
playVoice(arr, musicObj).then(() => {
resolve()
return
})
})
})
}
// 定义递归方法播放每个音频文件
function playVoice(arr, music) {
return new Promise((resolve) => {
//playFile 保存arr头一个音频文件
let playFile = arr.shift()
//playFile 为空时结束语音播放
if (!playFile) {
music.destroy(); // 销毁音频实例
resolve() // resolve出去说明此段音频播放完毕,为队列做准备
return
}
music.src = playFile
music.play()
// 解决实例化太多出现错误不播放问题
music.onError((res) => {
music.destroy(); //发生错误后,销毁实例
musicObj = uni.createInnerAudioContext();
resolve()
});
music.onStop(function(res) {
if (arr.length == 0) {
music.destroy(); // 销毁音频实例
resolve() // resolve出去说明此段音频播放完毕,为队列做准备
return
} else {
setTimeout(() => {
playVoice(arr, music)
}, 2000)
}
})
})
}
export default {
splicingAudioFiles
}
四、任务队列
class Scheduler {
constructor() {
this._max = 1; // 支持同时播放的数量
this.unwork = []; // 未执行的任务列表
this.working = []; // 正在执行的任务列表
}
add(asyncTask) {
return new Promise((resolve) => {
asyncTask.resolve = resolve;
if (this.working.length < this._max) {
this.runTask(asyncTask);
} else {
this.unwork.push(asyncTask);
}
});
}
runTask(asyncTask) {
this.working.push(asyncTask);
asyncTask().then((res) => {
asyncTask.resolve(); //asyncTask异步任务完成以后,再调用外层Promise的resolve以便add().then()的执行
var index = this.working.indexOf(asyncTask);
this.working.splice(index, 1); //从正在进行的任务队列中删除
if (this.unwork.length > 0) {
this.runTask(this.unwork.shift());
}
});
}
}
module.exports = Scheduler
五、封装mixin
import Scheduler from '@/utils/queue.js'; // 队列js脚本
const scheduler = new Scheduler()
export const mixin = {
data() {
return {
scheduler: scheduler
}
},
onShow() {
// 定时器为了防止上个webSocket还未完全关闭就新创建了
setTimeout(() => {
// this.$mySocket 是我将 webSocket 在main.js里注入到 vue.prototype 里了
this.$mySocket.sokcet(this.getWebsocetData, this.getWebsocetError, this.onErrorSucceed)
}, 500)
},
// 关闭websocket【必须在实例销毁之前关闭,否则会是underfined错误】
beforeDestroy() {
// 关闭webSocket
this.$mySocket.stop();
},
methods: {
//websocet函数回调:返回监听的数据
getWebsocetData(val) {
// 注: val为例如 123.12 的数字或字符串
if (val) {
// 向任务队列里添加播放任务
// $reporter: 我在main.js中 将 封装的拼接语音文件(splicingAudioFiles)的方法挂载到全局变量上了
this.scheduler.add(() => this.$reporter(val))
}
},
//websocet函数抛错: 返回错误信息 用于用户提示
getWebsocetError(err) {
console.log('websocet函数抛错', err);
},
//websocet函数成功进入: 监听连接状态,在失败的时候弹窗提示,具体需求看自身情况
onErrorSucceed(val) {
console.log('websocet函数成功进入', val);
},
}
}
六、后台保活
// main.js
//阻止快速返回导致程序退出
// #ifdef APP-PLUS
let main = plus.android.runtimeMainActivity();
//阻止快速返回导致程序退出重写quit方法改为后台保活
plus.runtime.quit = function() {
main.moveTaskToBack(false);
};
//重写toast方法如果内容为"再次返回退出应用" 后台保活
plus.nativeUI.toast = (function(title) {
if (title === "再次返回退出应用") {
plus.runtime.quit();
} else {
// 转到后台 需要执行的方法
}
});
// #endif