zl程序教程

您现在的位置是:首页 >  Java

当前栏目

基于kotlin开发的验证码发送注册的app

2023-02-26 09:48:52 时间

一、前言

最近其实有一点“不务正业”,快两个月了都在学网络相关的后端开发,安卓方面很久没去研究了,这次带来的demo是大项目中的一个小小的一块,由于编程语言渐渐转向kotlin,所以原本的项目需要进行重构,不过还不是非常熟练,所以这次写了这个注册功能的demo,百分百kotlin就是它了,验证码是自己搭建的服务器那边处理的,所以还是一个非常值得自己做的一个功能,起初是想采用短信实现的,后面看到腾讯短信业务要企业级用户才能使用,就放弃了,自己造轮子显然不是一个明智的选择,不过鉴于这个功能在思路上非常的简单,所以简单实现了一下,不过不要小看这个demo,”麻雀虽小五脏俱全”就是它了,设计后端即springboot的开发,app处理网络请求的开发,appUI界面的设计(虽然只有一步,但也还是吧),数据库查询相关,app搭建相关架构的实现等等,值得学习一波。下面是制作过程的思维导图

最终的成品也展示一下

二、spring boot端相关接口开发

在设计之前还需要设计两个表,建议在本地开发完成之后再上线服务器,所以最好本地也建个表,访问更快,表的设计比较简单,这里就展示一下结构图

注册表

验证表

还有这里面用到的比较多的就是MyStatus这个数据类,因为注册最终的展现形式都差不多,所以采用统一的状态方式进行返回内容,下面展示一下类的结构

data class MyStatus(var account:String, var status: String, var message:String, var time:String)

用到的json转化工具是hutool,参考一下之前写的博客hutool的使用

1.开发发送验证码接口

首先确定一下,接口的形式

http://域名:端口号/verify/{邮箱}

只需要一个参数就可以了,确认完参数,我们开始进行下一步,设计一下发送验证码的流程

//1.首先进行查询最近的验证码的发送时间,与目前的做比较
//可能会有三种情况:查询为空,间隔时间大于5分钟,间隔时间小于5分钟
//小于5分钟直接返回提示,验证频繁
//查询为空和大于5分钟继续
//下面展示核心代码,具体实现源码文末领取

//查找是否存在上一次验证码记录,并进行比较,5分钟以内就不重复发送
        val isExist = "select  * from verify WHERE mail = ? ORDER BY sendTime desc LIMIT 1"
        val psExist = con.prepareStatement(isExist)
        psExist.setString(1, mail)
        val resultSet:ResultSet = psExist.executeQuery()
		
		//这里的if语句就相当于在做判空操作
        if (resultSet.next()){
            //计算时间差
            val t = System.currentTimeMillis() - MyDate.timeToLong(resultSet.getString("sendTime"))
            //判断超过时间就不发送
            if(t < 5*60*1000){
                val status = MyStatus(mail,"404","请勿重复发送验证码",timeVerify)
                return JSONUtil.toJsonStr(status)
            }
        }
//2.生成验证码
//这里采用随机数的方法随机生成了100000~999999的数字
    val random = Random()
    val code = random.nextInt(999999 - 100000 + 1) + 100000
    val message = "【Dream】您的验证码$code,该验证码5分钟内有效,请勿泄漏于他人,时间${timeVerify}"
//3.发送邮件
//首先对邮箱进行一个检查
//无效返回提示,有效则继续

    try{
        SendMail(mail).sendTextEmail(message, "【Dream】注册验证码")
    }catch (e:Exception){
        val status = MyStatus(mail,"404","邮箱无效",timeVerify)
        return JSONUtil.toJsonStr(status)
    }
//4.插入验证码发送的记录
//这里是为后面的校验做准备
    val insertCode = "insert into verify(mail,myCode,sendTime) values(?,?,?)"
    val psCode = con.prepareStatement(insertCode)
    return try {
        psCode.setString(1, mail)
        psCode.setString(2, code.toString())
        psCode.setString(3, timeVerify)
        psCode.executeUpdate()
        val status = MyStatus(mail,"200","发送验证码成功",timeVerify)
        JSONUtil.toJsonStr(status)
    }catch (e:Exception){
        //插入时异常
        val status = MyStatus(mail,"404","系统故障",timeVerify)
        JSONUtil.toJsonStr(status)
    }

2.开发注册接口

接口格式确定一下,这里本来应该可以采用post进行开发的,由于参数也不是太多,所以采用仍然采用get进行开发

http://域名:端口号/register/{邮箱}/{密码}/{验证码}

三个参数,不算太多,可能就是浏览器手动请求有点累

//1.判断用户是否已经存在
//这里采用了主键约束,所以插入的时候根据数据库的返回结果即可判断是否已经存在
//存在,返回已经存在的提示,反之则继续
//2.判断验证码是否过期
//查询最近一次的验证码发送时间
//若查询为空,则说明用户还没发送验证码,返回提示,不为空继续
//若时间与当前的时间间隔大于5分钟就返回验证码已经过期的提示,反之继续

    //判断是否发送过验证码
    if(!resultSet.next()){
        val status = MyStatus(mail,"404","暂未发送验证码",timeRegister)
        return JSONUtil.toJsonStr(status)
    }

    //判断是否验证码是否过期
    val t = System.currentTimeMillis() - MyDate.timeToLong(resultSet.getString("sendTime"))
    if (t > 5*60*1000){
        val status = MyStatus(mail,"404","验证码过期",timeRegister)
        return JSONUtil.toJsonStr(status)
    }
//3.判断验证码是否正确
//正确则进行下一步操作,错误返回提示

    //判断验证码是否正确
    if (code != resultSet.getString("myCode")){
        val status = MyStatus(mail,"404","验证码错误",timeRegister)
        return JSONUtil.toJsonStr(status)
    }
//4.检查sql语句是否出错,即判断用户是否已经存在
//错误,返回用户已存在的提示,否则继续
//插入数据库成功,发送邮箱给用户提示已经发送成功

    //检查数据库查询是否有错误发生
    return try{
        val register = "insert into register(mail,myPassword)values(?,?)"
        val psRegister = con.prepareStatement(register)
        psRegister.setString(1,mail)
        psRegister.setString(2,password)
        psRegister.executeUpdate()
        val status = MyStatus(mail,"200","注册成功",timeRegister)

        //发送注册成功通知
        val message = "【Dream】尊敬的用户:恭喜你已成功注册Dream,后续软件使用问题关注公众号:android 踩坑小天才 进行咨询,感谢您的支持"
        SendMail(mail).sendTextEmail(message, "【Dream】注册成功通知")
        JSONUtil.toJsonStr(status)
    }catch (e:Exception){
        val status = MyStatus(mail,"404","用户已存在",timeRegister)
        JSONUtil.toJsonStr(status)
    }

三、app客户端界面UI相关开发

这方面说实话,审美不是很好,甚至这个颜色还是从某某平台扣的,过程比较简单,如果真的要开发一款比较好的软件,UI设计非常重要

//1.确定布局
//这个最简单的就是使用线性布局
//2.确定主要控件
//这里邮箱,密码,验证码就是主要的控件
//都是EditText,需要设置一下输入的数据类型
//这里以邮箱为例
//主要用到了hint即还未输入前界面显示的提示性文字
//inputType,控制输入的数据格式

<LinearLayout
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="horizontal">

    <TextView
              android:layout_width="0dp"
              android:layout_height="match_parent"
              android:layout_weight="2"
              android:text="注册邮箱:"
              android:gravity="center"
              android:textColor="#000000" />

    <EditText
              android:id="@+id/re_mail"
              android:layout_width="0dp"
              android:layout_height="match_parent"
              android:layout_weight="6"
              android:hint="请输入合法的邮箱"
              android:inputType="textEmailAddress" />
</LinearLayout>
//3.界面优化
//这部分主要是利用了Google提供的cardView进行圆角化处理
//还有就是控件直接的间隔调控即layout_margin
//这里利用线性布局中的权重配置,以适配不同分辨率的手机
//这里简单展示一下

<com.google.android.material.card.MaterialCardView
     android:layout_width="match_parent"
     android:layout_height="0dp"
     android:layout_weight="1"
     android:layout_margin="10dp"
     app:cardCornerRadius="5dp"
     app:elevation="15dp">
    <LinearLayout
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:layout_weight="1"
                  android:orientation="horizontal">

        <TextView
                  android:layout_width="0dp"
                  android:layout_height="match_parent"
                  android:layout_weight="2"
                  android:gravity="center"
                  android:text="hello"
                  android:textColor="#000000" />

    </LinearLayout>
</com.google.android.material.card.MaterialCardView>
//4.图标
//众所周知,图标是相当重要的,能不能给一个良好的第一印象就靠它了
//这里是上阿里云矢量图库里面找的
//是不是相当的帅气

四、app网络请求处理相关开发

基于retrofit开发的,下面展示一下基本流程

//1.创建接收json数据的数据模型

data class RegisterData(val account:String,val password:String,val verifyCode:String)
data class StatusResponse(val account:String, val status: String, val message:String, val time:String )
//2.创建服务接口

interface LoginService {
    //发送验证码接口
    @GET("verify/{mail}")
    fun getVerifyStatus(@Path("mail")mail:String): Call<StatusResponse>

    //注册接口
    @GET("register/{mail}/{password}/{code}")
    fun getRegisterStatus(@Path("mail")mail:String,@Path("password")password:String,@Path("code")code:String)                                                                                               :Call<StatusResponse>
}
//3.服务创建的类
//这里面进一步完成retrofit的封装

object ServiceCreator {

    //如果是本地测试的话,用自己电脑的ip地址即可
    //cmd ipconfig即可获取
    private const val BASE_URL = "http://IP地址:8080/"
    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    fun <T> create (serviceClass:Class<T>):T {
        return retrofit.create(serviceClass)
    }
    //泛型实化,用到reified关键字
    inline fun <reified T>create():T = create(T::class.java)
}
//4.统一处理网络请求
//这里进行网络请求的处理,用到协程使得网络请求可以异步执行

object LoginDemoNetwork {
    private val loginService = ServiceCreator.create<LoginService>()
    //suspend kotlin中协程的关键字
    suspend fun getVerifyStatus(query: String): StatusResponse = loginService.getVerifyStatus(query).await()
    suspend fun getRegisterStatus(query1: String,query2: String,query3: String) = 			             loginService.getRegisterStatus(query1,query2,query3).await()
    
    //网络回调的处理
    private suspend fun <T> Call<T>.await():T{
        return suspendCoroutine { continuation ->
            enqueue(object : Callback<T> {
                override fun onResponse(call: Call<T>, response: Response<T>) {
                    val body = response.body()
                    if (body != null)continuation.resume(body)
                    else continuation.resumeWithException(RuntimeException("response body is null"))
                }
                override fun onFailure(call: Call<T>, t: Throwable) {
                    continuation.resumeWithException(t)
                }
            })
        }
    }
}
//5.respository里面进行网络请求的最终处理并且返回回调结果

object Repository {

    fun getVerifyStatus(query: String) = fire(Dispatchers.IO){
        val verifyResponse = LoginDemoNetwork.getVerifyStatus(query)
        Result.success(verifyResponse)
    }
    fun getRegisterStatus(query1: String,query2: String,query3: String) = fire(Dispatchers.IO){
        val registerResponse = LoginDemoNetwork.getRegisterStatus(query1,query2, query3)
        Result.success(registerResponse)
    }
    /**
     * 对于结果处理进行一个高阶函数的封装
     */
    private fun <T> fire(context: CoroutineContext,block:suspend () -> Result<T>) = liveData<Result<T>>(context) {
        val result = try {
            block()
        }catch (e:Exception){
            Result.failure<T>(e)
        }
        emit(result)
    }
}

五、基于MVVM架构的模块组装

这一块主要是对fragment和viewModel进行设计,由于我们的需求比较简单,所以这一块也实现的比较简单

//1.viewModel层设计
class LoginViewModel : ViewModel() {
    //创建了两个网络请求的liveData
    private val verifyLiveData = MutableLiveData<String>()
    private val registerLiveData = MutableLiveData<RegisterData>()

    //对于liveData进行转换
    val verifyStatusLiveData = Transformations.switchMap(verifyLiveData) { query ->
        Repository.getVerifyStatus(query)
    }
    val registerStatusLiveData = Transformations.switchMap(registerLiveData){register->
        Repository.getRegisterStatus(register.account,register.password,register.verifyCode)
    }
    
    //赋值
    fun getVerifyStatus(query:String){
        verifyLiveData.value = query
    }
    fun getRegisterStatus(query1: String,query2: String,query3: String){
        registerLiveData.value = RegisterData(query1,query2,query3)
    }

}
//2.fragment设计
//这里主要就是创建了两个liveData的监听和两个listener
//还有一些简单的网络判断
class LoginFragment : Fragment() {

    private val viewModel by lazy { ViewModelProvider(this).get(LoginViewModel::class.java) }
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.login_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        sent.setOnClickListener {
            viewModel.getVerifyStatus(re_mail.text.toString())
        }
        register.setOnClickListener {
            if(re_password.text.toString() == check_password.text.toString()){
                viewModel.getRegisterStatus(re_mail.text.toString(),re_password.text.toString(),check_code.text.toString())
            }else{
                "两次密码输入不一致".showToast()
            }
        }
        viewModel.verifyStatusLiveData.observe(viewLifecycleOwner) { result ->
            val status = result.getOrNull()
            if (status != null){
                if(status.status == "200"){
                    status.message.showToast()
                }else{
                    status.message.showToast()
                }
            }else{
                "网络错误".showToast()
            }
        }
        viewModel.registerStatusLiveData.observe(viewLifecycleOwner) { result ->
            val status = result.getOrNull()
            if (status != null){
                if(status.status == "200"){
                    status.message.showToast()
                }else{
                    status.message.showToast()
                }
            }else{
                "网络错误".showToast()
            }
        }
    }

}

最后,如果有感兴趣的,可以发送关键字:验证码 到公众号:android 踩坑小天才 获取源码和签名好的app