在我做这个事情之前,前几年玩了好几年的cmp4,也不知道国内程序员有多少记得cmp晨风音乐播放器的,如果有,那就是我的同行了,在老早之前玩cmp播放器玩了几年,做音乐,做视频。 之前在社区看到过nodejs搞博客系统的很多,后来nodejs切片系统也看到了。找了一圈影视系统没找到,而我对搞播放器情有独钟。(失业原因)闲着,绝定写一套基本的nodejs影视系统。
<<< 用nodejs+mongodb开发了套影视站点cms,觉得可以的大佬可以送个star >>>
<<< 一款flutter fijkplayer播放器皮肤(fijkplayer_skin) >>>
玩了那么多年播放器,都知道爪巴呗,搜的话,能搜到很多滋援王,差不多都有开放的api供你爪巴。你可以对接比如苹果cms的接口,例如xxx.com/macapi/?ac=videolist&pg=1 ,
内容规划(后台部分)
按功能来划分,视频数据,留言模块,用户模块,配置模块,其他模块(导航,分类),模板模块,定时认为模块,脚本管理模块。
按页面来分,主页面(展示各类信息),详情页(展示单条数据的信息),播放页(展示单条数据的信息+播放),个人中心(修改密码,修改昵称),分类页(全部分类,年代,地区),搜索页(搜索数据),导航页(某个导航下的所有分类数据)
先要确定分类字段,在电影网站的分类页面,都会有分类的单独描述,关键字(这里简称seo信息)。导航下也许存在二级分类(可能需要联表查询子分类),导航可以设置显示隐藏。导航万一要调整输出顺序呢?(所以index位置少不了)
name(用于导航名称),display(显示隐藏),parent_id(存在父级导航,那么存父级的_id,不存在则存false,用于判断真假,是否是一级导航或者二级分类),seo信息用于存储
{
"name" : "动作片",
"parent_id" : ObjectId("5e819a8619f4d790bc5d0c76"),
"display" : true,
"seo" : {
"title" : "动作片",
"keywords" : "动作片",
"description" : "动作片"
},
"type" : "nav_type",
"index" : 0
}
先要确定数据字段,有哪些字段??字段设置成什么类型合适??
这里我参照资源网的来思考做
标题,导演,主演,更新时间,描述,更新状态,封面,语言,地区,发布时间。这几个字段是必须要有的,
这里需要注意的是,如果搜索中药做演员表索引搜索的话,最好吧演员表这里设置成数组.例: [“靳东”,“蒋欣”];
这样在mongodb中使用$in操作符方便查找演员,我这里因为不做演员索引,索引就直接字符串来存。
如果不是存的数组,存字符串来查找演员,那么免费版芒果,只能用正则,很浪费时间
一个电影在展示的时候是需要找对应的分类,如果字段写死了,那么后期想修改分类的名称,就要找到所有的视频数据,批量替换旧的分类名称,这就不好了。最好的办法就是《联表查询》,mongo提供了联表查询的操作,使用管道aggregate操作符来联表查询
{
$lookup: {
from: "other", // 关联的表 名称
localField: "video_type", // 当前表的字段 需要关联到目标表
foreignField: "_id", // 目标表和当前表字段对应的字段
as: "type" // 输出的字段
}
},
{
$unwind: "$type"
}
用户信息最少也需要有三个字段,username(用户名), password(密码),nickname(昵称)
其他的比如,显示隐藏display, 权限位置高低grade_id,是否默认default,可以做,这里以最简单的为例子
密码可以md5或者md5+hash,或者加点盐??
_id 是mongodb插入时自带的唯一索引的字段,对于分布式来说解决了id重复的问题,我们可以直接用。省去了自己生成id的麻烦事了,自带唯一索引
{
"_id" : ObjectId("5e7e35cf4345c47a1c8c15f6"),
"userName" : "abcdxxxx",
"passWord" : "ba0a086c8a7b0ca4232406b5efff3a95",
"nickName" : "阿打算",
"admin" : false,
"display" : true,
"default" : false,
"grade_id" : 0
}
可能新同学(这里指的是cxk打篮球的那种前端去搞留言)做留言的时候麻烦过,怎么规划?有啥字段。
这里以楼中楼形式留言为模型。
数据字段如下:
{
"_id" : ObjectId("5e7f93a60eefb36e54fe8f72"),
"vid" : ObjectId("5e7e15b04a285358100e3d6f"),
"uid" : ObjectId("5e7e35cf4345c47a1c8c15f6"),
"pid" : false,
"wid" : false,
"agree" : true,
"display" : true,
"date" : 1585419174015.0,
"sub_date" : 1585419174014.0,
"text" : "达大厦"
}
和视频数据关联分类数据一样,我们不能每条留言吧用户名和对谁回复写死,万一对方改名了呢??你全表在找到旧的数据疯狂替换吗?所以这里还得用联表查询。
查找留言的时候,规则如下:
先找到一级评价,也就是盖楼那个鳖孙
然后循环这两条数据,用这两条数据_id,去找盖楼的下面的二级回复的数据,找到吧他们加到本条数据(盖楼的那条数据)的children字段里面
至此留言和用户部分结束。
上一章说了,视频数据录入格式。播放源如何录入,长啥样?
和留言关联视频表,关联用户表一样。视频源的数据也需要单独出来,因为如果不单独出来,我们只是查询一下电影列表,不需要展示播放源列表的时候,没有必要无端带出来那么多的数据
同样使用mongodb管道联表操作就可以很容易关联出源的数据。
项目初始化默认就必须有的数据,比如博客总得有个默认的后台系统的root用户吧,默认的顶级分类,一些默认的配置,从哪来??肯定不会天上掉下来。
nodejs pakcage。json的script字段用于定义一些命令。前端的肯定知道,npm run dev,npm run build这些你肯定常用
你完全可以再script定义你的初始化数据命令。
"scripts": {
"build": "node ./build/initDataBase.js",
"restore": "mongorestore -d movie ./backup/movie",
"backup": "mongodump -d movie -o ./backup/movie"
}
build用于连接数据库,删除原有的数据库,创建全新的数据库,创建字段索引,并向数据库中插入默认的数据,比如电影cms系统的默认管理员信息。初始化的数据
const mongodb = require('mongodb'),
MongoClient = mongodb.MongoClient,
config = require('../utils/config.js'),
dbConfig = config.project;
let connectURL = 'mongodb://127.0.0.1:27017';
let client = MongoClient.connect(connectURL, { useNewUrlParser: true, useUnifiedTopology: true });
client.then(async (db) => {
let DB = db.db(dbConfig.dbName);
await new Promise(async (resolve, reject) => {
console.log('正在清除原始数据表');
await DB.dropCollection('session1').catch(err => {});
await DB.dropCollection('session2').catch(err => {});
await DB.dropCollection('config').catch(err => {});
await DB.dropCollection('logs').catch(err => {});
await DB.dropCollection('message').catch(err => {});
await DB.dropCollection('other').catch(err => {});
await DB.dropCollection('user').catch(err => {});
await DB.dropCollection('video_info').catch(err => {});
await DB.dropCollection('video_list').catch(err => {});
resolve()
})
.then(() => {
console.log('原始数据表清除完成');
})
.catch(err => {
console.log('+++', err);
})
await new Promise(async (resolve, reject) => {
console.log('开始重建数据表');
await DB.createCollection('session1');
await DB.createCollection('session2');
await DB.createCollection('config');
await DB.createCollection('logs');
await DB.createCollection('message');
await DB.createCollection('other');
await DB.createCollection('user');
await DB.createCollection('video_info');
await DB.createCollection('video_list');
resolve()
})
.then(() => {
console.log('数据表重建完成');
})
await new Promise(async (resolve, reject) => {
console.log('开始创建用户数据');
// 用户字段索引
let userColl = DB.collection('user');
// 索引字段
await userColl.createIndexes([
{key:{userName: 1}},
{key:{passWord: 1}},
{key:{display: 1}},
{key:{admin: 1}},
{key:{grade_id: 1}},
{key:{default: 1}}
]);
await userColl.insertOne({
userName: 'root',
passWord: 'e10adc3949ba59abbe56e057f20f883e', // 123456 md5
nickName: '网站所有者',
admin: true,
display: true,
default: true,
grade_id: 2, // 0用户 1管理员 2root用户
});
resolve();
})
.then(res=>{
console.log('用户数据创建成功');
})
});
pm2部署无法关闭任务是因为,用户(管理员)开启定时任务的时候,可能被pm2分发到了B线程(假设电脑6核心12线程),你:我电脑就双核哪来的六核心?
不好意思,我的12线程。
扯远了,扯回来。
当用户(管理员)想关闭的时候pm2可能把任务分发到了G线程上了,但是G线程上并没有执行这个任务,就会报错。
那是因为这个定时任务是项目部署开启服务的时候就设置好了,到了时间,pm2复制多份任务执行(一毛一样),当然就会重复跑任务。
试想一下,如果我们项目用pm2部署,复制了12份node项目。但是我们可以使用别的进程守护工具,例如forever(单份,单线程)守护定时任务。
说人话:就是分两个项目部署,定时任务使用一个单线程守护工具,守护。这样,定时任务到时间以后就是单线程跑了,不会多线程同时调用。随之而来的一个问题,怎么通讯??pm2那边的服务怎么通知forever这边守护的的进程开启关闭任务。(http服务啊)
解决1:假设pm2守护的项目监听的是8888端口,我们forever这边的js开启的http服务可以监听9999。两者之间通讯就发post请求。(因为本机的原因。所以很快,秒收到)这样,通讯的问题解决,就可以随时动态的创建/删除 定时任务了。也不会出现多线程同时执行的问题。
解决2:使用系统底层提供的定时任务接口,比如linux下的cronTab,windows下我不知道,避免pm2重复执行
进入宝塔面板软件商店,安装pm2管理器,mongodb,nginx
安装完成以后,点击文件,上传网站压缩文件,zip
// 进入网站所在目录(宝塔默认网站根目录/www/wwwroot)
cd /www/wwwroot
// 安装依赖
npm install
// 初始化数据
npm run build
进入pm2管理器,选择网站
这里其实相当于进入项目目录,执行(↑上面的操作相当于执行了这条命令)
pm2 start app.js --name app
到这里一步只是启动了项目,但是没有映射(也就是设置转发请求),因为最前面有nginx接受了所有的请求,选择映射就是让nginx吧某个网站请求转给pm2管理器开启的服务
映射完成之后就可以再网站一栏找到刚才映射的网站。