最近手有點癢琢磨着作個啥,牽腸掛肚仍是寫個KTV點歌系統,模擬了一下KTV開戶的思路,7天累死我了,不過技術點還挺多的,但願你能夠看完(〜^㉨^)〜html
用Node(Express)教你寫KTV點歌系統,包括前臺內容和後臺管理系統,整合Express框架和Mongodb數據庫服務器開發;教你用Vue.JS,ElementUI和iViewUI寫出超漂亮的頁面,隨心點歌隨心聽前端
做者原創文章, 轉載前請留言或聯繫做者!!! vue
後端: Express + Mongodb + jsonwebtoken等等node
前端: Vue.JS + ElementUI + iViewUI + Axios等等ios
本項目分前臺開發,後臺開發和服務器開發git
-- 服務器基本架構
ktv-select_music-system
├── README.md
├── index.js -- 後臺文件入口
├── test.http -- 測試文件
├── api -- 路由文件
│ ├── admin.js -- 配置管理員的操做
| ├── music.js -- 配置歌曲信息
| ├── user.js -- 配置用戶的相關操做
| └── safecode.js -- 配置安全碼
├── config -- 配置
| ├── Date.js -- 配置日期格式化插件
| ├── delNoUse.js -- 封裝閒置刪除閒置資源方法
| ├── http.js -- 配置跨域
| ├── isBadAccount.js -- 封裝帳戶是否合法
| ├── newaccount.js -- 封裝隨機開戶方法
| ├── passport.js -- 驗證token是否合法
| ├── uploadImg.js -- 封裝上傳圖片方法
| └── uploadMusic.js -- 封裝上傳歌曲方法
├── ktv-admin --後臺管理系統界面
├── ktv-client --前臺用戶點歌項目界面
├── dbModel
| └── ** -- Mongodb數據庫的一些模型
├── mongodb
| └── mongodb.js -- 配置Mongodb,連接數據庫
├── secret
| ├── mongodbURI.js -- Mongodb地址
| └── jwtkey.js -- token的私鑰
├── static -- 資源存放處
| ├── music -- 歌曲上傳目標文件夾
| ├── poster -- 歌曲海報上傳目標文件夾
└── └── view -- 配置404文件
複製代碼
-- 後臺管理系統架構
ktv-admin
├── README.md
├── public
| ├── index.html -- vue掛載頁面
| └── ** -- 你能夠在這裏連接少許靜態資源
├── src -- 開發文件夾
| ├── App.vue -- Vue掛載根頁面
| ├── main.js -- Vue程序入口文件,掛載各類組件
| ├── router.js -- Vue路由配置文件
| ├── store.js -- Vuex的狀態管理文件
| ├── assets -- 靜態資源文件夾
| ├── components --公共組件
| | └── nav.vue -- 後臺導航欄
| ├── plugins --插件
| | ├── axios.js -- 配置跨域,攔截器等等
| | ├── Date.js -- 格式化日期
| | └── Date.js -- 加載動畫Loading
| ├── stores -- 狀態管理文件夾
| | └── adminStore.js -- 管理員狀態
| ├── views -- 頁面文件夾
| | ├── 404.vue -- 404頁面
| | ├── adminlikes.vue -- 管理員處理ktv收藏歌曲
| | ├── allorders.vue -- 訂單管理
| | ├── Home.vue -- 後臺根頁面
| | ├── Index.vue -- 後臺首頁
| | ├── managemusic.vue -- 音樂管理
| | ├── user_service.vue -- 給用戶開戶
| | └── login.vue -- 後臺登陸
└── babel.config.js -- babel配置
複製代碼
-- 前臺用戶聽歌架構
ktv-client
├── README.md
├── public
| ├── index.html -- vue掛載頁面
| └── ** -- 你能夠在這裏連接少許靜態資源
├── src -- 開發文件夾
| ├── App.vue -- Vue掛載根頁面
| ├── main.js -- Vue程序入口文件,掛載各類組件
| ├── router.js -- Vue路由配置文件
| ├── store.js -- Vuex的狀態管理文件
| ├── assets -- 靜態資源文件夾
| ├── components --公共組件
| | ├── bottomNav.vue -- 底部音樂控制區域
| | └── topNav.vue -- 頂部信息區域
| ├── config --配置
| | ├── addSong.js --封裝選取歌曲方法
| | ├── isBadAccount.js --驗證帳戶合法性
| | ├── isLogin.js --是否登陸
| | ├── nextSong.js --封裝下一首歌曲方法
| | └── prevSong.js --封裝上一首歌曲方法
| ├── plugins --插件
| | ├── axios.js -- 配置跨域,攔截器等等
| | └── wsmLoading.js -- 加載動畫Loading
| ├── stores -- 狀態管理文件夾
| | └── song.js -- 存儲歌曲信息
| ├── views -- 頁面文件夾
| | ├── 404.vue -- 404頁面
| | ├── abc.vue -- 拼音點歌
| | ├── artist.vue -- 明星點歌
| | ├── Home.vue -- 後臺根頁面
| | ├── Index.vue -- 後臺首頁
| | ├── hot.vue -- 熱播歌曲
| | ├── ktvlikes.vue -- ktv推薦歌曲
| | ├── selected.vue -- 已選歌曲
| | ├── style.vue -- 風格點歌
| | └── language.vue -- 語種點歌
├── babel.config.js -- babel配置
└── vue.config.js -- vue配置
複製代碼
首先不要改變服務器端口,不然報錯.github
首先在最外層文件夾下載依賴:npm install 下載後端依賴,mongodb
接着進入ktv-client, npm install 下載用戶前端依賴.chrome
接着進入ktv-admin, npm install 下載管理員前端依賴.
以上工做完成後,使用命令npm run server 或者 node index
命令啓動Node服務器,啓動成功會顯示:
Server is running on port [8633].
Mongodb is Connected.Please have a great coding.
進入ktv-client,打開命令板,使用命令npm run client
啓動前臺用戶項目,啓動成功後用瀏覽器訪問http://localhost:xxxx
進入ktv-admin,打開命令板,使用命令npm run admin
啓動後臺管理系統項目,啓動成功後用瀏覽器訪問http://localhost:xxxx
本例中將Mongodb部署在本地電腦上,若是你仔細閱讀了這篇文檔,啓動項目應該是很容易的。若是你把Mongodb部署在其餘地方,請自行修改secret/mongodbURI.js
配置文件信息。
項目啓動成功,最好用 chrome 瀏覽器打開, 美化了滾動條
admin.js最下面有個註冊接口
// 管理員註冊
router.post("/account/register", (req, res) => {
const email = req.body.email;
Admin.findOne({email})
.then(hasOne => {
if(hasOne){
return req.status(422).json({status:"422", result:"郵箱被佔用"});
}else{
const username = req.body.username;
const password = req.body.password;
const identity = req.body.identity ? req.body.identity : null;
const date = new Date().format("yyyy/MM/dd HH:mm:ss");
const newAdmin = new Admin({
email,
username,
password,
identity,
date
});
newAdmin.save()
.then(() => {
res.json({status:"200", result:"註冊成功"})
}).catch(err => {
console.log(err);
res.status(500).json({status:"500", result:"未知錯誤,註冊失敗"})
})
}
})
})
複製代碼
而後用postman註冊或者其餘工具
因爲腦殼很差使的緣由加上js沒有元素格式化日期的方法,就瞎掰一個(值得學習)
Date.js
/**
*
* @author: Mr_Wei
* @version: 1.0.0
* @description: 格式化日期
* @Date: 2019/10/16 09:32
*
*/
Date.prototype.format = function(format) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"H+": this.getHours(), //小時
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"f+": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(format))
format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(format))
format = format.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return format;
}
export default Date.prototype.format
而後咱們使用其格式日期
require(Date);
// const now = new Date().format("yyyy/MM/dd HH:mm:ss.S");
const now = new Date().format("yyyy/MM/dd HH:mm:ss");
複製代碼
svg-captcha驗證碼的運用,防止暴力破解密碼,增強安全性. 詳細的文檔地址:svg-captcha
使用驗證碼
// 後臺生成驗證碼
router.get("/getCaptcha", (req, res) => {
var captcha = svgCaptcha.create({
// 翻轉顏色
inverse: false,
// 字體大小
fontSize: 38,
// 噪聲線條數
noise: 3,
// 寬度
width: 80,
// 高度
height: 32,
});
// 保存到session,忽略大小寫
req.session = captcha.text.toLowerCase();
console.log(req.session); //0xtg 生成的驗證碼
//保存到cookie 方便前端調用驗證
res.cookie('captcha', req.session);
res.setHeader('Content-Type', 'image/svg+xml');
res.send(String(captcha.data));
res.end();
})
// 前臺獲取驗證碼
--HTML
<img width="80" style="background:#EEE9E9;margin-left:30px;" ref="captcha" height="32" src="http://localhost:3001/api/user/getCaptcha" @click="refreshCaptcha">
--js
// 獲取驗證碼cookie
getCookie(cname){
var name = cname + "=";
var ca = document.cookie.split(';');
for(var i=0; i<ca.length; i++){
var c = ca[i].trim();
if (c.indexOf(name)==0) return c.substring(name.length,c.length);
}
return "";
},
// 刷新驗證碼
refreshCaptcha(){
this.$refs.captcha.src = "http://localhost:3001/api/user/getCaptcha?d=" + Math.random();
},
最後用 填寫的驗證碼進行對比
複製代碼
formidable來處理文件上傳信息,用起來方便,很友好,若是你沒有接觸過文件操做,趕忙收藏起來
封裝歌曲方法uploadMusic.js
/**
*
* @author: Mr_Wei
* @version: 1.0.0
* @description: 封裝上傳音樂方法
* @Date: 2019/10/16 08:35
*
*/
const fs = require('fs');
const path = require('path');
const formidable = require('formidable'); // 文件處理庫
const formatTime = require('silly-datetime'); // 格式化數據
module.exports = (req, res) => {
let form = new formidable.IncomingForm(); //建立上傳表單
form.encoding = 'utf-8'; // 設置編碼格式
form.uploadDir = path.join(__dirname, '../static/music'); // 設置上傳目錄(這個目錄必須先建立好)
form.keepExtensions = true; // 保留文件後綴名
form.maxFieldsSize = 20 * 1024 *1024; // 設置文件大小
/* 格式化form數據 */
form.parse(req, (err, fields, files) => {
let file = files.file;
/* 獲取異常 */
if(err) {
return res.status(500).json({'status': 500, result: '服務器內部錯誤'});
}
if(file.size > form.maxFieldsSize) {
fs.unlink(file.path);
return res.status(412).json({'status': 412, result: '音頻不能超過20M'});
}
/* 存儲後綴名 */
let extName = '';
switch (file.type) {
case 'audio/mp3':
extName = 'mp3';
break;
}
if(extName.length == 0) {
fs.unlink(file.path);
return res.status(412).json({'status': 412, result: '只支持mp3格式音頻'});
}
/* 拼接新的文件名 */
let time = formatTime.format(new Date(), 'YYYYMMDDHHmmss');
let num = Math.floor(Math.random() * 8999 + 10000);
let songName = `${time}_${num}.${extName}`;
let newPath = form.uploadDir + '/' + songName;
/* 更更名字和路徑 */
fs.rename(file.path, newPath, (err) => {
if(err) {
return res.status(500).json({'status': 500, result: '音頻上傳失敗'});
} else {
return res.send({'status': 200, 'msg': '音頻上傳成功', result: {src: songName}});
}
})
})
};
複製代碼
關於ElementUI分頁詳細請見:ElementUI的Pagination分頁學習
上圖
-- html
<el-pagination
v-if='paginations.total > 0'
:page-sizes="paginations.page_sizes"
:page-size="paginations.page_size"
:layout="paginations.layout"
:total="paginations.total"
:current-page.sync='paginations.page_index'
@current-change='handleCurrentChange'
@size-change='handleSizeChange'>
</el-pagination>
-- js
data(){
return{
allUsers:[], // 用來存儲最終信息, 被顯示的dom點調用
allTableData:[], // 用戶承接分頁設置的數據
paginations: { // 分頁組件信息
page_index: 1, // 當前位於哪頁
total: 0, // 總數
page_size: 5, // 1頁顯示多少條
page_sizes: [5, 10, 15, 20], //每頁顯示多少條
layout: "total, sizes, prev, pager, next, jumper" // 翻頁屬性
},
}
},
methods:{
// 獲取當前頁
handleCurrentChange(page) {
let sortnum = this.paginations.page_size * (page - 1);
let table = this.allTableData.filter((item, index) => {
return index >= sortnum;
});
// 設置默認分頁數據
this.getAllUsers = table.filter((item, index) => {
return index < this.paginations.page_size;
});
this.getAllUsers = table.filter((item, index) => {
return index < this.paginations.page_size;
});
},
// 切換size
handleSizeChange(page_size) {
this.paginations.page_index = 1;
this.paginations.page_size = page_size;
this.getAllUsers = this.allTableData.filter((item, index) => {
return index < page_size;
});
},
// 總頁數
setPaginations() {
this.paginations.total = this.allTableData.length;
this.paginations.page_index = 1;
this.paginations.page_size = 5;
// 設置默認分頁數據
this.getAllUsers = this.allTableData.filter((item, index) => {
return index < this.paginations.page_size;
});
},
}
複製代碼
沒了嗎?對,分頁就是這麼簡單!你學會了嗎?有些前端開發的同窗老是對分頁比較陌生,學會這個,讓你再也不產生煩惱!
jsonwebtoken是對用戶信息加密成不可逆向破解的token.關於passport-jwt,是用來對用戶請求時所帶的token信息進行過時驗證,若是超過簽證的合法時間,則會請前臺發出token失效的信息,提示用戶從新獲取合法的token信息,不然沒法繼續請求加密的信息;
用法
- passport-jwt
const key = require("../config/keys").KEYORSECRET;
const JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
var opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = key;
module.exports = passport => {
passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
UserInfo.findById(jwt_payload.id)
.then(user => {
if (user) {
return done(null, user);
} else {
return done(null, false);
// or you could create a new account
}
})
}));
}
// 設置token
// 規則
const rule = {
id:String(userinfo._id),
username:userinfo.username,
email:userinfo.email,
date:user.date,
signdate:userinfo.signdate,
signcount:userinfo.signcount,
avatar:userinfo.avatar,
phone:userinfo.phone
};
// 簽證加密
// jwt.sign(規則, key(私鑰), {配置:好比過時時長}, (err, token){ 響應程序 })
jwt.sign(rule,key,{expiresIn:7200},(err, token) => {
if(err) throw err;
res.json({"token" : "Bearer " + token})
})
自定義驗證方法
/**
*
* @author: Mr_Wei
* @version: 1.0.0
* @description: 判斷是否過時用戶
* @Date: 2019/10/19 12:19
*
*/
const UserOrOrders = require("../dbModel/user");
module.exports = async params => {
const flag = await new Promise((resolve) => {
if(params){
const account = params.account;
UserOrOrders.findOne({account})
.then(user => {
if(user){
if(new Date().getTime() > new Date(user.endTime).getTime()){
console.log("過時用戶");
// 處理
return resolve(false);
}else{
console.log("合法用戶");
return resolve(true);
}
}else{
return resolve(false);
}
})
}else{
console.log("不合法用戶");
return resolve(false);
}
})
return flag;
}
使用:
// 測試 isBadAccount(params)方法
router.post("/test", passport.authenticate("jwt", {session:false}), async (req, res) => {
// console.log(req.user)
if(await isBadAccount(req.user)){
// do something
res.send("OK");
}else{
res.status(401).json({status:"401", result:"賬號過時,請聯繫管理員"})
}
})
複製代碼
詳細的文檔地址:Passport-Jwt合法驗證,token加密
以上代碼均已上傳 github
若是你們有興趣,歡迎star. 歡迎你們加入個人前端交流羣:866068198 ,一塊兒交流學習前端技術。博主目前一直在自學Node中,技術有限,若是能夠,會盡力給你們提供一些幫助,或是一些學習方法.
If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.
若是對你有幫助,請賞個star~ github地址