起因
2024年3月31日,我被公司裁员了。
2024年4月1日,果断踏上了回家的路,决定先休息一个星期。晚上回到了郑州,先跟一起被裁的同事在郑州小聚一下,聊聊后面的打算。第二天下午回家。
2024年4月8日,知道现在的大环境不好,不敢错过“金三银四”,赶忙回上海开始找工作。结果环境比预想的还要差啊,以前简历放开就有人找,现在每天投个几十封都是石沉大海。。。
2024年4月15日,有个好朋友找我,想让我给他们公司开发一个“拨号APP”(主要原因其实是这个好哥们想让我多个赚钱门路😌),主要的功能就是在他们的系统上点击一个“拨号”按钮,然后员工的工作手机上就自动拨打这个号码。
可行性分析
涉及到的修改:
拿到这个需求之后,我并没有直接拒绝或者同意,而是先让他把他公司那边的的源代码发我了一份,大致看了一下使用的框架,然后找一些后端的朋友看有没人有人一起接这个单子;而我自己则是要先看下能否实现APP的功能(因为我以前从来没有做过APP!!!)。
我们各自看过自己的东西,然后又沟通了一番简单的实现过程后达成了一致,搞!
因为我这边之前的技术栈一直是VUE,所以决定使用uni-app实现,主要还是因为它的上手难度会低很多。
第一版
需求分析
虽说主体的功能是拨号,但其实是隐含很多辅助性需求的,比如拨号日志、通时通次统计、通话录音、录音上传、后台运行,另外除了这些外还有额外的例如权限校验、权限引导、获取手机号、获取拨号状态等功能需要实现。
但是第一次预算给的并不高,要把这些全部实现显然不可能。因此只能简化实现功能实现。
- 拨号APP
- 权限校验
- ❌权限引导
- 查询当前手机号
- 查询当前手机号的拨号任务
- 因为后端没有socket,使用setTimeout模拟轮询实现。
- 拨号、录音、监测拨号状态
- 更新任务状态
- ❌通话录音上传
- ❌通话日志上传
- ❌本地通时通次统计
- 程序运行日志
- 其他
- 增加开始工作、开启录音的状态切换
- 兼容性,只兼容安卓手机即可
基础设计
一个input框来输入用户手机号,一个开始工作的switch,一个开启录音的切换。用户输入手机号,点击开始工作后开启轮询,轮询到拨号任务后就拨号同时录音,同时监听拨号状态,当挂断后结束录音、更新任务状态,并开启新一轮的轮询。
开干
虽然本人从未开发过APP,但本着撸起袖子就是干的原则,直接打开了uni-app的官网就准备开怼。
1、下载 HbuilderX。
2、新建项目,直接选择了默认模板。
3、清空 Hello页面,修改文件名,配置路由。
4、在vue文件里写主要的功能实现,并增加 Http.js
、Record.js
、PhoneCall.js
、Power.js
来实现对应的模块功能。
⚠️关于测试和打包
运行测试
在 HbuilderX 中点击“运行-运行到手机或模拟器-运行到Android APP基座”会打开一个界面,让你选择运行到那个设备。这是你有两种选择:
- 把你手机通过USB与电脑连接,然后刷新列表就可以直接运行了。
- 很遗憾,可能是苹果电脑与安卓手机的原因,插上后检测不出设备😭。。。
- 安装Android Studio,然后通过运行内置的模拟器来供代码运行测试。
- 这种很麻烦,要下载很久,且感觉测试效果并不好,最好还是用windows电脑连接手机的方法测试。
关于自定义基座和标准基座的差别,如果你没有买插件的话,直接使用基准插座就好。如果你要使用自定义基座,就首先要点击上图中的制作自定义基座,然后再切换到自定义基座执行。
但是不知道为什么,我这里一直显示安装自定义基座失败。。。
打包测试
除了以上运行测试的方法外,你还有一种更粗暴的测试方法,那就是打包成APP直接在手机上安装测试。
点击“发行-原生APP 云打包”,会生成一个APK文件,然后就可以发送到手机上安装测试。不过每天打包的次数有限,超过次数需要购买额外的打包服务或者等第二天打包。
我最终就是这样搞得,真的我哭死,我可能就是盲调的命,好多项目都是盲调的。
另外,在打包之前我们首先要配置manifest.json
,里面包含了APP的很多信息。比较重要的一个是AppId,一个是App权限配置。参考uni-app 权限配置、Android官方权限常量文档。以下是拨号所需的一些权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.CALL_PRIVILEGED" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
⚠️权限配置这个搞了很长时间,即便现在有些权限还是不太清楚,也不知道是不是有哪些权限没有配置上。。。
⚠️权限校验
1、安卓 1
好像除了这样的写法还可以写"scope.record"
或者permission.CALL_PHONE
。
permision.requestAndroidPermission("android.permission.CALL_PHONE").then(res => {
});
2、安卓 2
plus.android.requestPermissions(["android.permission.CALL_PHONE"], e => {
});
3、uni-app
这个我没测试,AI给的,我没有用这种方法。有一说一,百度AI做的不咋地。
uni.hasPermission({
permission: 'makePhoneCall',
success() {
console.log('已经获得拨号权限');
},
fail() {
uni.authorize({
scope: 'scope.makePhoneCall',
success() {
console.log('已经获得授权');
},
fail() {
console.log('用户拒绝授权');
uni.showModal({
title: '提示',
content: '请在系统设置中打开拨号权限',
success: function(res) {
if (res.confirm) {
uni.openSetting();
}
}
});
}
});
}
});
✅拨号
三种方法都可以实现拨号功能,只要有权限,之所以找了三种是为了实现APP在后台的情况下拨号的目的,做了N多测试,甚至到后面搞了一份原生插件的代码不过插件加载当时没搞懂就放弃了,不过到后面才发现原来后台拨号出现问题的原因不在这里,,具体原因看后面。
另外获取当前设备平台可以使用let platform = uni.getSystemInfoSync().platform;
,我这里只需要兼容固定机型。
1、uni-app API
uni.makePhoneCall({
phoneNumber: phone,
success: () => {
log(`成功拨打电话${phone}`);
},
fail: (err) => {
log(`拨打电话失败! ${err}`);
}
});
2、Android
plus.device.dial(phone, false);
3、Android 原生
写这个的时候有个小插曲,当时已经凌晨了,再加上我没有复制,是一个个单词敲的,结果竟然敲错了一个单词,测了好几遍都没有成功。。。还在想到底哪里错了,后来核对一遍才发现😭,control cv才是王道啊。
function PhoneCallAndroid(phone) {
if (!plus || !plus.android) return;
var Intent = plus.android.importClass("android.content.Intent");
var Uri = plus.android.importClass("android.net.Uri");
var main = plus.android.runtimeMainActivity();
var uri = Uri.parse("tel:" + phone);
var call = new Intent("android.intent.action.CALL", uri);
main.startActivity(call);
}
✅拨号状态查询
第一版用的就是这个获取状态的代码,有三种状态。第二版的时候又换了一种,因为要增加呼入、呼出、挂断、未接等状态的判断。
export function getCallStatus(callback) {
if (!plus || !plus.android) return;
let maintest = plus.android.runtimeMainActivity();
let Contexttest = plus.android.importClass("android.content.Context");
let telephonyManager = plus.android.importClass("android.telephony.TelephonyManager");
let telManager = plus.android.runtimeMainActivity().getSystemService(Contexttest.TELEPHONY_SERVICE);
let receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
onReceive: (Contexttest, intent) => {
plus.android.importClass(intent);
let phoneStatus = telManager.getCallState();
callback && callback(phoneStatus);
}
});
let IntentFilter = plus.android.importClass("android.content.IntentFilter");
let filter = new IntentFilter();
filter.addAction(telephonyManager.ACTION_PHONE_STATE_CHANGED);
maintest.registerReceiver(receiver, filter);
}
⚠️录音
录音功能这个其实没啥,都是官网的API,无非是简单的处理一些东西。但是这里有一个大坑!
一坑
就是像通话录音这种涉及到的隐私权限很高,正常的这种录音是在通话过程中是不被允许的。
二坑
后来一次偶然的想法,在接通之后再开启录音,发现就可以录音了。
但随之而来的是第二个坑,那就是虽然录音了,但是当播放的时候发现没有任何的声音,还是因为保护隐私的原因,我当时还脱离代码专门试了试手机自带的录音器来在通话时录音,发现也不行。由此也发现uni的录音本身也是用的手机录音器的功能。
三坑
虽然没有声音,但是我还是试了下保存,然后就发现了第三个坑,那就是虽然获取了文件权限,但是现在手机给的读写权限都是在限定内的,录音所在的文件夹是无权访问的。。。
另辟蹊径
其实除了自己手动录音外还可以通过手机自带的通话录音来实现,然后只要手机去读取录音文件并找到对应的那个就可以了。思路是没啥问题,不过因为设置通话录音指引、获取录音文件都有问题,这一版本就没实现。
var log = console.log,
recorder = null,
isRecording = false;
export function startRecording(logFun = console.log) {
if (!uni.getRecorderManager || !uni.getRecorderManager()) return logFun('不支持录音!');
log = logFun;
recorder = uni.getRecorderManager();
recorder.onStart(() => {
isRecording = true;
log(`录音已开始 ${new Date()}`);
});
recorder.onError((err) => {
log(`录音出错:${err}`);
console.log("录音出错:", err);
});
recorder.onInterruptionBegin(() => {
log(`检测到录音被来电中断...`);
});
recorder.onPause(() => {
log(`检测到录音被来电中断后尝试启动录音..`);
recorder.start({
duration: 10 * 60 * 1000,
});
});
recorder.start({
duration: 10 * 60 * 1000,
});
}
export function stopRecording() {
if (!recorder) return
recorder.onStop((res) => {
isRecording = false;
log(`录音已停止! ${new Date()}`);
saveRecording(res.tempFilePath);
});
recorder.stop();
}
export function saveRecording(filePath) {
log('开始保存录音文件');
uni.saveFile({
tempFilePath: filePath,
success(res) {
log(`录音保存成功:${res.savedFilePath}`);
},
fail(err) {
log(`录音保存失败! ${err}`);
console.error("录音保存失败:", err);
},
});
}
运行日志
为了更好的测试,也为了能实时的看到执行的过程,需要一个日志,我这里就直接渲染了一个倒序的数组,数组中的每一项就是各个函数push的字符串输出。简单处理。。。。嘛。
联调、测试、交工
搞到最后,大概就交了个这么玩意,不过也没有办法,一是自己确实不熟悉APP开发,二是满共就给了两天的工时,中间做了大量的测试代码的工作,时间确实有点紧了。所幸最起码的功能没啥问题,也算是交付了。
第二版
2024年05月7日,老哥又找上我了,想让我们把他们的这套东西再给友商部署一套,顺便把这个APP再改一改,增加上通时通次的统计功能。同时也是谈合作,如果后面有其他的友商想用这套系统,他来谈,我们来实施,达成一个长期合作关系。
我仔细想了想,觉得这是个机会,这块东西的市场需求也一直有,且自己现在失业在家也有时间,就想着把这个简单的功能打磨成一个像样的产品。也算是做一次尝试。
需求分析
- ✅拨号APP
- 登录
- 权限校验
- 权限引导
- 文件权限引导
- 通话录音配置引导
- 获取手机号权限配置引导
- 后台运行权限配置引导
- 当前兼容机型说明
- 拨号
- 获取手机号
- 是否双卡校验
- 直接读取手机卡槽中的手机号码
- 如果用户不会设置权限兼容直接input框输入
- 拨号
- 全局拨号状态监控注册、取消
- 支持呼入、呼出、通话中、来电未接或挂断、去电未接或挂断
- 录音
- 通时通次统计
- 云端数据根据上面状态监控获取并上传
- 本地数据读取本机的通话日志并整理统计
- 支持按时间查询
- 支持呼入、呼出、总计的通话次数、通话时间、接通率、有效率等
- 其他
- 优化日志显示形式
- 封装了一个类似聊天框的组件,支持字符串、Html、插槽三种显示模式
- 在上个组件的基础上实现权限校验和权限引导
- 在上两个组件的基础上实现主页面逻辑功能
- 增加了拨号测试、远端连接测试
- 修改了APP名称和图标
- 打包时增加了自有证书
中间遇到并解决的一些问题
关于框架模板
这次重构我使用了uni中uni-starter + uni-admin 项目模板。整体倒还没啥,这俩配合还挺好的,就只是刚开始不知道还要配置东西一直没有启动起来。
建立完项目之后还要进uniCloud/cloudfunctions/common/uni-config-center/uni-id
配置一个JSON文件来约定用户系统的一些配置。
打包的时候也要在manifest.json
将部分APP模块配置进去。
还搞了挺久的,半天才查出来。。
类聊天组件实现
- 设计
- 每个对话为一个无状态组件
- 一个图标、一个名称、一个白底的展示区域、一个白色三角
- 内容区域通过类型判断如何渲染
- 根据前后两条数据时间差判断是否显示灰色时间
- 参数
- 样式
- 根据左边右边区分发送接收方,给与不同的类名
- flex布局实现
样式实现这里,我才知道原来APP和H5的展示效果是完全不同的,个别地方需要写两套样式。
关于后台运行
这个是除了录音最让我头疼的问题了,我想了很多实现方案,也查询了很多相关的知识,但依旧没效果。总体来说有以下几种思路。
- 通过寻找某个权限和引导(试图寻找到底是哪个权限控制的)
- 通过不停的访问位置信息
- 通过查找相应的插件、询问GPT、百度查询
- 通过程序切入后台之后,在屏幕上留个悬浮框(参考游戏脚本的做法)
- 通过切入后台后,发送消息实现(没测试)
测试了不知道多少遍,最终在一次无意中,终于发现了如何实现后台拨号,并且在之后看到后台情况下拨号状态异常,然后又查询了应用权限申请记录,也终于知道,归根到底能否后台运行还是权限的问题。
关于通话状态、通话记录中的类型
这个倒还好,就是测试的问题,知道了上面为啥异常的情况下,多做几次测试,就能知道对应的都是什么状态了。
通话状态:呼入振铃、通话中(呼入呼出)、通话挂断(呼入呼出)、来电未接或拒绝、去电未接或拒接。
通话日志:呼入、呼出、未接、语音邮件、拒接
交付
总体上来说还过得去,相比于上次简陋的东西,最起码有了一点APP的样子,基本上该有的功能也基本都已经实现了,美中不足的一点是下面的图标没有找到合适的替换,然后录音上传的功能暂未实现,不过这个也好实现了。
后面的计划
- 把图标改好
- 把录音文件是否已上传、录音上传功能做好
- 把APP的关于页面加上,对接方法、使用方法和视频、问题咨询等等
- 原本通话任务、通时通次这些是放在一个PHP后端的,对接较麻烦。要用云函数再实现一遍,然后对外暴露几个接口,这样任何一个系统都可以对接这个APP,而我也可以通过控制云空间的跨域配置来开放权限
- 把数据留在这边之后,就可以再把uni-admin上加几个页面,并且绑定到阿里云的云函数前端网页托管上去
- 如果有可能的话,上架应用商店,增加上一些广告或者换量联盟之类的东西
- 后台运行时,屏幕上加个悬浮图标,来电时能显示个振铃啥的
大致的想法就这些了,如果这个产品能继续卖下去,我就会不断的完善它。
最后
现在的行情真的是不好啊,不知道有没有大哥给个内推的机会,本人大专计算专业、6.5年Vue经验(专精后台管理、监控大屏方向,其他新方向愿意尝试),多个0-1-2项目经验,跨多个领域如人员管理、项目管理、产品设计、软件测试、数据爬虫、NodeJS、流程规范等等方面均有了解,工作稳定不经常跳,求路过的大哥给个内推机会把!