悦读FM——项目总结
一、小前言
悦读FM这个项目最主要的目的还是练习自己的技术,因为突然看到了别人的毕业论文是一个本地播放音频的题目,于是想了想,似乎在数据库存储音频视频这类东西时,自己没有关注过。同时也为了弥补在上个易班项目中没有练习过前端代码,于是自己动手也写了写网页,和易班项目不同的时这个总结时按照项目产生到完成的顺序来写,至于原因,还是那句话一切按照实际开发来。
二、立项——项目背景
不论那个项目从设计到落地肯定有一定的现实意义,而悦读FM的主要功能就是为用户提供可以听的图书资源,通过作者上传书籍,设置书籍章节再通过网页录制和章节绑定的声音。服务器对这些数据进行储存,所有用户都能通过网页来听书。次要功能就是登录,注册。对于没有注册的账号密码跳转到注册页面,对于已经登录的非作者用户只展示书籍章节音频列表,对已登录的作者用户展示,上传书籍,章节,音频功能。一个项目的出发点对于一个企业来说大多数考虑的是市场需求,对我来说更多的还是学习和练习。(其实也有现实意义,因为有人指导哈哈)悦读FM和易班对比来说,其实二者并没有很多差别,唯一在于易班可能是在已经搭建好的前后端框架下去实现后端的具体代码,而悦读FM项目中的音频采集和前端js的开发需要从头学起。
三、 项目的边界——可行性评估
所谓 项目的边界就是我要做什么?什么是核心功能,什么是辅助功能?
对于悦读FM这个项目来说本项目的核心是:书/章节的管理和对应章节音频数据的管理,登录功能也是要实现的一个功能但是不算是核心功能。
于是怎么确定项目边界呢?通常是我们站在用户的视角去思考用户的需求,根据这些需求去启发我们如何设计业务。
比如说:
1.匿名用户,可以查看所有的书的列表,为了选择出想听的书
2.作为已登陆的用户,可以上传一本书,为了让书被其他用户看到
3.作为一本书的上传者,可以录入新的章节,为了让被其他用户看到
4.作为一本书的上传者,可以为某个章节录入音频,为了让书背其他用户者
5.作为匿名用户,可以选择一本想听书,为了看到该书下有哪些章节
6.作为匿名用户,可以选择某个章节点击,听书
7.用户管理
这些实际应用到的场景都是可能会发生的,于是乎我们程序猿和产品经理就协商,那些功能可以实现?那些需要一些特定技术才能实现?那些功能可能目前实现不了等等?在悦读FM这个项目中除了书籍管理、章节管理、用户管理还有就是音频管理。前四个可能都很熟悉,但是音频管理是我需要预研的部分。
四、 音频管理功能预研
音频管理功能的核心就是:
怎么样在浏览器中录制和播放音频?
html5标准中支持 audio 标签,提供音频的URL链接即可
如何在浏览器进行音频的存储?
JavaScript中的mediarecorder进行声音的录制+保存(ES6)
1 .浏览器中录制和播放音频
到这就转头学学js:(也是用到学多少,所以我就直接在浏览器的开发者工具上搞了)JavaScript(简称“JS”) 是一种具有函数优先的轻量级,解释型或即时编译型的高级编程语言,多用于web网页开发。
1.变量的定义:js 没有变量类型,let 关键字可以指向任何类型的引用类型。
举例:2.方法的定义:关键字 function
举例:
3.js执行特点——事件驱动逻辑
简单来说所谓的事件驱动逻辑是把一件事件分成若干个小事件,通过执行这些小事件来完成。这些小事件有有序的。如果一个事件开始执行那么一定是执行完该事件再去执行别的事件。
但是js不是三言两语能介绍清楚的,在这过程中我也主要看了看官网上的教程,而且用的都是比较基本。(😓)
附上教程链接:https://www.runoob.com/js/js-tutorial.html.
前端网页设计部分:
HTML代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>音频录制并上传</title>
<link rel="stylesheet" href="test.css">
</head>
<body>
<div>
<div id="startButton" class="button">开始采集</div>
<div id="recordButton" class="button">开始录制</div>
<div id="stopButton" class="button">停止录制</div>
<div id="submitButton" class="button">上传</div>
<h2>试听</h2>
<audio id="preview" controls></audio>
</div>
<div>
<pre id="log"></pre>
</div>
<script charset="utf-8" src="test.js"></script>
</body>
</html>
css代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>音频录制并上传</title>
<link rel="stylesheet" href="test.css">
</head>
<body>
<div>
<div id="startButton" class="button">开始采集</div>
<div id="recordButton" class="button">开始录制</div>
<div id="stopButton" class="button">停止录制</div>
<div id="submitButton" class="button">上传</div>
<h2>试听</h2>
<audio id="preview" controls></audio>
</div>
<div>
<pre id="log"></pre>
</div>
<script charset="utf-8" src="test.js"></script>
</body>
</html>
js代码:
// 日志显示功能
let logElement = document.getElementById("log");
function log(message) {
logElement.innerHTML += (message + "\n");
}
// 定义全局变量(java 中就没有全局变量,js 中有全局变量),功能类似于 java 中的静态变量、属性
let captureStream; // 用来 保存采集的 stream
let mediaRecorder; // 用来 保存录制器
let data = []; // 用来 保存录好的数据
let audioBlob; // 用来 保存转成 Blob 类型的录好的数据
// 从 html 中获取标签
let startButton = document.getElementById("startButton");
let recordButton = document.getElementById("recordButton");
let stopButton = document.getElementById("stopButton");
let submitButton = document.getElementById("submitButton");
let preview = document.getElementById("preview");
// 为开始采集按钮绑定 点击(click)事件的动作
startButton.addEventListener("click", function () {
log("点击了开始采集按钮 -> 会弹出授权请求");
let promise = navigator.mediaDevices.getUserMedia({
audio: true // 只有 audio,没有 video
});
promise.then(function (stream) {
log("用户同意了授权 -> 记录了采集数据");
captureStream = stream; // 保存 stream 到全局变量 captureStream 中
});
});
// 为开始录制按钮绑定 点击(click)事件的动作
recordButton.addEventListener("click", function () {
log("点击了开始录制按钮 -> 开始录制,每 3 秒收集一次数据");
if (!captureStream) {
log("错误:必须先点击开始采集按钮");
return;
}
// mediaRecorder 也是全局变量
mediaRecorder = new MediaRecorder(captureStream);
mediaRecorder.ondataavailable = function (event) {
log("录制数据可用事件 -> 保存数据");
data.push(event.data);
};
mediaRecorder.start(3000);
});
// 为停止录制按钮绑定 点击(click)事件的动作
stopButton.addEventListener("click", function () {
log("点击了停止录制按钮 -> 停止录制");
if (!mediaRecorder) {
log("错误:必须先点击开始录制按钮");
return;
}
mediaRecorder.onstop = function () {
log("录制停止事件 -> 准备预览功能的数据流");
audioBlob = new Blob(data, {
type: "audio/webm" // 类型是 audio/webm
});
preview.src = URL.createObjectURL(audioBlob);
};
mediaRecorder.stop();
});
// 为上传按钮绑定 点击(click)事件的动作
submitButton.addEventListener("click", function () {
log("点击了上传按钮 -> 通过 form 表单,向服务器提交录制下来的数据");
if (!audioBlob) {
log("错误:必须先点击停止录制按钮");
return;
}
let formData = new FormData();
formData.set("audio", audioBlob);
let xhr = new XMLHttpRequest();
xhr.open("post", "/upload/audio");
xhr.onload = function () {
log("服务器应答事件 -> 打印应答信息");
log(xhr.status);
log(xhr.responseText);
};
xhr.send(formData);
});
实际效果:
1.整体网页
2.授权允许:
3.录制
录制部分如果没有按照顺序点击按钮,会报错。需要注意的是上传时报错时因为时本地打开的html文件并不算是http请求。所以会报错,也就是上传功能还没有真正使用到。
2.服务器接受音频
实际上悦读FM项目需要具备的是:1.在网页上录制音频2.录制的音频上传到服务器。第一个问题通过研究教程已经解决了,现在关心的就是如何把录制的音频上传到服务器。我是通过java中自带原生类的ajax请求完成的,实际上大部分时候都采用jQuery实现ajax。第一因为jQuery是第三方库,第二其实用到也不复杂所以原生类方便。
1.Ajax介绍
Ajax的全称是AsynchronousJavascript+XML。Ajax是多种技术的组合,包括我们的JavaScript 异步数据获取技术,就是XMLHttpRequest以及xml以及Dom还有表现技术XHTML 和CSS,Ajax的核心是XMLHttpRequest 是支持异步请求的技术,可以发送请求给服务器,并且不阻塞用户在浏览器中首次引用,使我们的网络应用更加强大。其实XMLHttpRequest是JavaScript的一种语法子集,是它的一套API,支持发送GET和POST请求直白说没有ajax之前,浏览器发送http请求需要整页请求,但是有了ajax之后可以进行局部的请求,其实也可以认为是动态的、随时的进行各种http请求。
翻译翻译就是:异步+js+xml(一种数据格式)
1.为什么是异步?
异步就是请求和结果分开,我们在请求的时候不必等待结果。可以去做别的事情,等结果出来再去看。
2.什么是js?
JavaScript 是 Web 的编程语言。基本上所有现代的 HTML 页面都使用 JavaScript。(有个一个老生常谈的问题js和java有什么关系?答:一个是甲鱼一个是乌龟,长的像然而并没有半毛钱关系)
3.什么是xml?
xml只是一种数据格式,在这件事里并不重要,我们在更新一行字的时候理论上说不需要这个格式,但如果我们更新很多内容,那么格式化的数据可以使我们有条理地去实现更新。不过现在更多的人选择json格式,因为json目前解析更快而且更加简洁明了。
2.同源策略
为什么不允许访问?那么就提到了一个同源策略。ajax默认是不能访问不同域名的,也就是说baidu.com不能发起tengxun.com的ajax请求,因为在ajax看来他们是不同公司的网站,涉及安全问题,保证了不同域名的数据不能够相互抓取。而file://就是本地访问专用协议,不是http协议,也没有域,所以无法跨域。解决办法是让浏览器通过http协议访问网页即可,对我们来说就是放在tomcat上。
通过tomcat访问:
对应的代码说明:
到这基本上所有前端的工作都已经完成了。除了服务器如何保存上传后的音频,这都是我们后端要做的事情。
五、 数据建模——建库建表
1.建库建表
确定了这些后就开始寻找数据之间的关系(er图)和建库建表。
悦读FM数据对应关系
建库建表:
create table users (
uid int primary key auto_increment comment '用户id',
username varchar(64) not null unique comment '用户名',
password char(64) not null comment '经过sha-256计算后的用户密码'
);
create table books (
bid int primary key auto_increment comment '小说id',
uid int not null comment '上传用户的id',
title varchar(100) not null comment '小说名称'
);
create table sections (
sid int primary key auto_increment comment '章节id',
bid int not null comment '属于哪本小说的id',
name varchar(100) comment '章节名称'
);
create table audios (
aid int primary key auto_increment comment '音频id',
sid int not null unique comment '属于哪个章节的id',
uuid char(36) not null comment 'uuid',
content_type varchar(20) not null comment '音频类型 audio/wmv audio/mp3',
content longblob default null comment '音频内容'
);
2.注意事项
1.auto_increment是什么
auto_increment是用于主键自动增长的,从1开始增长,当你把第一条记录删除时,再插入第二跳数据时,主键值是2,不是1。
在使用AUTO_INCREMENT时,应注意以下几点:
1、AUTO_INCREMENT是数据列的一种属性,只适用于整数类型数据列。
2、设置AUTO_INCREMENT属性的数据列应该是一个正数序列,所以应该把该数据列声明为UNSIGNED,这样序列的编号个可增加一倍。
3、AUTO_INCREMENT数据列必须有唯一索引,以避免序号重复(即是主键或者主键的一部分)。AUTO_INCREMENT数据列必须具备NOT NULL属性。
4、AUTO_INCREMENT数据列序号的最大值受该列的数据类型约束,如TINYINT数据列的最大编号是127,如加上UNSIGNED,则最大为255。一旦达到上限,AUTO_INCREMENT就会失效。
5、当进行全表删除时,MySQL AUTO_INCREMENT会从1重新开始编号。
这是因为进行全表操作时,MySQL(和PHP搭配之最佳组合)实际是做了这样的优化操作:先把数据表里的所有数据和索引删除,然后重建数据表。
如果想删除所有的数据行又想保留序列编号信息,可这样用一个带where的delete命令以抑制MySQL(和PHP搭配之最佳组合)的优化:delete from table_name where 1;
ps:可用last_insert_id()获取刚刚自增过的值。
2.主键约束和唯一性约束
1.主键约束(PRIMARY KEY)
- 主键用于唯一地标识表中的每一条记录,可以定义一列或多列为主键。
- 是不可能(或很难)更新.
- 主键列上没有任何两行具有相同值(即重复值),不允许空(NULL).
- 主健可作外健,唯一索引不可;
2.唯一性约束(UNIQUE)
- 唯一性约束用来限制不受主键约束的列上的数据的唯一性,用于作为访问某行的可选手段,一个表上可以放置多个唯一性约束.
- 只要唯一就可以更新.
- 即表中任意两行在 指定列上都不允许有相同的值,允许空(NULL).
- 一个表上可以放置多个唯一性约束
3.为什么要在音频再加一个uuid
使用aid虽然存储数据的量上来说会比少但是会造成《二战德国坦克的故事》(感兴趣可以了解哈 挺有意思的),所以使用uuid这种机制虽然不适合存储,但是基本上大多数语言都支持。
36可以说是进制,常用的有10(0-9),36(0-9+a-z),62(0-9+a-z+A-Z)
4.Mysql中bolb类型数据
Mysql中bolb是一个二进制对象,可以储存大量数据的容器,例如:图片,音乐等等,并且根据类型的不同可以存储数据容量也不同。
①TinyBlob类型 最大能容纳255B的数据
②Blob类型 最大能容纳65KB的
③MediumBlob类型 最大能容纳16MB的数据
④LongBlob类型 最大能容纳4GB的数据
读取和插入:
1、插入Blob类型的数据
插入Blob类型的数据时,需要注意必须要用PreparedStatement,因为Blob类型的数据是不能够用字符串来拼的,在传入了SQL语句后,就需要去调用PreparedStatement对象中的setBlob(int index , InputStream in)方法来设置传入的的参数,其中index表示Blob类型的数据所对应的占位符(?)的位置,而InputStream类型的in表示被插入文件的节点流。
2、读取Blob类型的数据
读取Blob类型相对来说比较容易,当获取了查询的结果集之后,使用getBlob()方法读取到Blob对象,然后调用Blob的getBinaryStream()方法得到输入流,再使用IO操作进行文件的写入操作即可。
六、 正式开发——后端接口实现
1.准备阶段
因为本项目核心来说还是数据库的增删查改。所以sql语句是非常非常非常重要的。最核心的我们应该设想用户有什么需求?把对应的需求转化成sql语句。这就是讲用户故事就是开发者站在用户的角度上去思考问题。先想好可能有那些需求,找到对应的sql,自然而然的就可以接着进行后端资源接口的设计。
场景:
想到了一些场景之后,把这些场景进行分类,罗列出需求,根据这些需求就可以大概想到需要设计那些资源接口。悦读FM项目的资源接口都是最基础的。我没有加太多高端的东西,旨在掌握,求精不求多。
资源接口:
有了上述这些东西后,还应该考虑的一个就是页面之间的跳转,比如说如果用户不存在则跳转到注册页面,如果用户存在跳转到首页…这些实际上可能不是我们程序员考虑的事情,既然是自己写项目肯定是开发是自己,产品经理也是自己。哈哈,所以这个也是我们要考虑的事情。
页面跳转示意:
实际开发中我们不把所有的类文件都放在一个包下,那样太傻了,根据代码完成的功能不同对class文件进行分类,这个思想不仅仅用在易班项目中,实际上不管开发什么项目,这种分门别类的思想一定要掌握。
分层逻辑:
本来流程应该是tomcat把请求转给servlet然后再进行sql的查询等等。添加的DAO层其实是专门处理数据库查询的一些逻辑,servlet处理的是接入逻辑。service层的功能是数据加工。model层就是在这个传输过程中的一些数据有时候也叫object层。还有一个就是util层,就是工具箱因为数据库连接常用所以把它封装成一个类。
实际分层:
2.功能实现
1.用户管理
实现用户管理实际上只需要完成这四个接口
1.准备model
public class User {
public int uid;
public String username;
public User() {
}
public User(int uid, String username) {
this.uid = uid;
this.username = username;
}
@Override
public String toString() {
return "User{" +
"uid=" + uid +
", username='" + username + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 两个引用是不是指向同一个对象
if (!(o instanceof User)) return false; // 两个对象的类型是否一致
User user = (User) o; // 判断重要属性是否一样
return uid == user.uid &&
Objects.equals(username, user.username);
}
@Override
public int hashCode() {
return Objects.hash(uid, username);
}
}
<