前后端架构分离
先做服务接口,再做客户端应用
后端技术选型:
web框架:eggjs
数据库:MongoDB
ORM框架:mongoose
身份认证:JWT
客户端选型:vue3系列技术栈
https://www.yuque.com/books/share/6eb0a508-d745-4e75-8631-8eb127b7b7ca?#
npm i create-egg -g
create-egg youtube-clone-eggjs (smaple)
cd youtube-clone-eggjs
npm install
npm run dev
npm i egg-mongoose --save
/config/config.default.js
/* eslint valid-jsdoc: "off" */
'use strict';
/**
* @param {Egg.EggAppInfo} appInfo app info
*/
module.exports = appInfo => {
/**
* built-in config
* @type {Egg.EggAppConfig}
**/
const config = exports = {};
// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1649334278876_9943';
// add your middleware config here
config.middleware = [];
// add your user config here
const userConfig = {
// myAppName: 'egg',
};
// 加入mongoose
+ config.mongoose = {
client: {
url: 'mongodb://127.0.0.1/youtube-clone',
options: {},
plugins: [],
},
};
return {
...config,
...userConfig,
};
};
/config/plugin.js
'use strict';
/** @type Egg.EggPlugin */
// module.exports = {
// // had enabled by egg
// // static: {
// // enable: true,
// // }
// };
+ exports.mongoose = {
enable: true,
package: 'egg-mongoose'
}
// app/model/user.js
module.exports = app => {
const mongoose = app.mongoose
const Schema = mongoose.Schema
const userSchema = new Schema({
username: { // 用户名
type: String,
required: true
},
email: { // 邮箱
type: String,
required: true
},
password: { // 密码
type: String,
select: false, // 查询中不包含该字段
required: true
},
avatar: { // 头像
type: String,
default: null
},
cover: {
type: String, // 封面
default: null
},
channelDescription: { // 频道介绍
type: String,
default: null
},
subscribersCount: {
type: Number,
default: 0
},
createdAt: { // 创建时间
type: Date,
default: Date.now
},
updatedAt: { // 更新时间
type: Date,
default: Date.now
}
})
return mongoose.model('User', userSchema)
}
视频:
// app/model/video.js
module.exports = app => {
const mongoose = app.mongoose
const Schema = mongoose.Schema
const videoSchema = new Schema({
title: { // 视频标题
type: String,
required: true
},
description: { // 视频介绍
type: String,
required: true
},
vodVideoId: { // VOD 视频 ID
type: String,
required: true
},
cover: { // 视频封面
type: String,
required: true
},
user: {
type: mongoose.ObjectId, // 视频作者
required: true,
ref: 'User'
},
commentsCount: { // 评论数量
type: Number,
default: 0
},
dislikesCount: { // 不喜欢数量
type: Number,
default: 0
},
likesCount: { // 喜欢数量
type: Number,
default: 0
},
viewsCount: { // 观看次数
type: Number,
default: 0
},
createdAt: { // 创建时间
type: Date,
default: Date.now
},
updatedAt: { // 更新时间
type: Date,
default: Date.now
}
})
return mongoose.model('Video', videoSchema)
}
视频点赞:
// app/model/video_like.js
module.exports = app => {
const mongoose = app.mongoose
const Schema = mongoose.Schema
const likeSchema = new Schema({
like: { // 点赞状态
type: Number,
enum: [1, -1], // 喜欢 1,不喜欢 -1
required: true
},
user: { // 点赞用户
type: mongoose.ObjectId,
ref: 'User', // 关联到User表
required: true
},
video: { // 点赞视频
type: mongoose.ObjectId,
ref: 'Video',
required: true
},
createdAt: { // 创建时间
type: Date,
default: Date.now
},
updatedAt: { // 更新时间
type: Date,
default: Date.now
}
})
return mongoose.model('VideoLike', likeSchema)
}
视频评论:
// app/model/video_comment.js
module.exports = app => {
const mongoose = app.mongoose
const Schema = mongoose.Schema
const commentSchema = new Schema({
content: { // 评论内容
type: String,
required: true
},
user: { // 评论用户
type: mongoose.ObjectId,
ref: 'User',
required: true
},
video: { // 评论视频
type: mongoose.ObjectId,
ref: 'Video',
required: true
},
createdAt: { // 创建时间
type: Date,
default: Date.now
},
updatedAt: { // 更新时间
type: Date,
default: Date.now
}
})
return mongoose.model('Comment', commentSchema)
}
频道(用户)订阅
// app/model/subscription.js
module.exports = app => {
const mongoose = app.mongoose
const Schema = mongoose.Schema
const subscriptionSchema = new Schema({
user: { // 订阅用户
type: mongoose.ObjectId,
ref: 'User',
required: true
},
channel: { // 订阅频道
type: mongoose.ObjectId,
ref: 'User',
required: true
},
createdAt: { // 创建时间
type: Date,
default: Date.now
},
updatedAt: { // 更新时间
type: Date,
default: Date.now
}
})
return mongoose.model('Subscription', subscriptionSchema)
}
观看历史:
// app/model/video_view.js
module.exports = app => {
const mongoose = app.mongoose
const Schema = mongoose.Schema
const viewSchema = new Schema({
user: { // 用户
type: mongoose.ObjectId,
ref: 'User',
required: true
},
video: { // 视频
type: mongoose.ObjectId,
ref: 'Video',
required: true
},
createdAt: { // 创建时间
type: Date,
default: Date.now
},
updatedAt: { // 更新时间
type: Date,
default: Date.now
}
})
return mongoose.model('View', viewSchema)
}
app/router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
router.prefix('/api/v1') // 设置基础路由
router.get('/users', controller.user.create)
};
app/controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async create() {
const { ctx } = this;
ctx.body = 'UserController'
}
}
module.exports = UserController;
config/config.default.js
/* eslint valid-jsdoc: "off" */
'use strict'
/**
* @param {Egg.EggAppInfo} appInfo app info
*/
module.exports = appInfo => {
/**
* built-in config
* @type {Egg.EggAppConfig}
**/
const config = {}
// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1611716016238_6422'
// add your middleware config here
config.middleware = ['errorHandler']
// add your user config here
const userConfig = {
// myAppName: 'egg',
}
config.mongoose = {
client: {
url: 'mongodb://127.0.0.1/youtube-clone',
options: {
useUnifiedTopology: true
},
// mongoose global plugins, expected a function or an array of function and options
plugins: []
}
}
// 解决403问题,没有权限问题。关闭权限的验证
+ config.security = {
csrf: {
enable: false
}
}
return {
...config,
...userConfig
}
}
参数校验 :https://www.eggjs.org/zh-CN/basics/controller#%E5%8F%82%E6%95%B0%E6%A0%A1%E9%AA%8C
https://github.com/eggjs/egg-validate
npm i egg-validate --save
// config/plugin.js
exports.validate = {
enable: true,
package: 'egg-validate',
};
// controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async create() {
const { ctx } = this;
// 1.数据验证
+ this.ctx.validate({
username: { type: string },
email: { type: email },
})
// 2.保存用户
// 3.生成token
// 4.发送响应
}
}
module.exports = UserController;
2种方式
1.try catch
2.框架层统一处理异常:https://www.eggjs.org/zh-CN/core/error-handling
或 https://www.eggjs.org/zh-CN/tutorials/restful#%E7%BB%9F%E4%B8%80%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86
通一错误处理
// app/middleware/error_handler.js
// 外层函数负责接收参数
module.exports = () => {
// 返回一个中间件处理函数
return async function errorHandler(ctx, next) {
try {
await next();
} catch (err) {
// 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
ctx.app.emit('error', err, ctx);
const status = err.status || 500;
// 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
const error =
status === 500 && ctx.app.config.env === 'prod'
? 'Internal Server Error'
: err.message;
// 从 error 对象上读出各个属性,设置到响应中
ctx.body = { error };
if (status === 422) {
ctx.body.detail = err.errors;
}
ctx.status = status;
}
};
};
config/config.default.js
/* eslint valid-jsdoc: "off" */
'use strict';
/**
* @param {Egg.EggAppInfo} appInfo app info
*/
module.exports = appInfo => {
+ // 配置errorHandler中间件
+ config.middleware = ['errorHandler'];
return {
...config,
...userConfig,
};
};
// controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async create() {
const { ctx } = this;
// 1.数据验证
this.ctx.validate({
username: { type: string },
email: { type: email },
password: { type: string },
})
// 2.保存用户
// 3.生成token
// 4.发送响应
}
}
module.exports = UserController;
// controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
async index() {
const { ctx } = this;
// 1.数据验证
const body = this.ctx.request.body // 获取请求体
this.ctx.validate({
username: { type: string },
email: { type: email },
password: { type: string },
})
if(await this.service.user.findByUsername(body.username)) {
this.ctx.throw(422, '用户已经存在')
}
if(await this.service.user.findByEmail(body.username)) {
this.ctx.throw(422, '邮箱已经存在')
}
// 2.保存用户
const user = await this.service.user.createUser(body)
// 3.生成token
// 4.发送响应
this.ctx.body = {
user: {
// token
email: user.email,
username: user.username,
avator: user.username,
}
}
ctx.body = 'UserController'
}
}
module.exports = UserController;
service/user.js 专门处理数据库的操作
const Service = require('err').Service
class UserService extends Service {
get User() {
return this.app.model.User // 获取User的数据模型
}
// 查找username
findByUsername(username) {
return this.User.findOne({ username })
}
// 查找邮箱
findByEmail(email) {
return this.User.findOne({ email })
}
// 创建用户
async createUser(data) {
data.password = this.ctx.helper.md5(data.password)
const user = await this.User(data) // 获取User的实例
await user.save() // 保存到数据库中
return user
}
}
app/entend/htlper.js
const crypto = require('crypto')
const _ = require('lodash')
exports.md5 = str => {
return crypto.createHash('md5').update(str).digest('hex')
}
exports._ = _
npm i jsonwebtoken
config/config.default.js
/* eslint valid-jsdoc: "off" */
'use strict'
/**
* @param {Egg.EggAppInfo} appInfo app info
*/
module.exports = appInfo => {
// 添加jwt的secret
+ config.jwt = {
secret: 'a6e8561e-58df-4715-aa21-b5d1a091e71a',
expiresIn: '1d'
}
return {
...config,
...userConfig
}
}
service/user.js
const Service = require('err').Service
+ const jwt = require('jsonwebtoken')
class UserService extends Service {
get User() {
return this.app.model.User // 获取User的数据模型
}
// 查找username
findByUsername(username) {
return this.User.findOne({ username })
}
// 查找邮箱
findByEmail(email) {
return this.User.findOne({ email })
}
// 创建用户
async createUser(data) {
data.password = this.ctx.helper.md5(data.password)
const user = await this.User(data) // 获取User的实例
await user.save() // 保存到数据库中
return user
}
// 生成token
+ createToken (data) {
const token = jwt.sign(data, this.app.config.jwt.secret, {
expiresIn: this.app.config.jwt.expiresIn
})
return token
}
}
controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
// 创建用户
async create() {
const { ctx } = this;
// 1.数据验证
const body = this.ctx.request.body // 获取请求体
this.ctx.validate({
username: { type: string },
email: { type: email },
password: { type: string },
})
const userService = this.service.user
if(await userService.findByUsername(body.username)) {
this.ctx.throw(422, '用户已经存在')
}
if(await userService.findByEmail(body.username)) {
this.ctx.throw(422, '邮箱已经存在')
}
// 2.保存用户
const user = await userService.createUser(body)
// 3.生成token
+ const token = await userService.createToken({ userId: user._id })
// 4.发送响应
this.ctx.body = {
user: {
+ token,
email: user.email,
username: user.username,
avator: user.username,
}
}
}
}
module.exports = UserController;
router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
+ const auth = app.middleware.auth()
router.prefix('/api/v1') // 设置基础路由
// 创建用户
router.post('/users', controller.user.index)
// 登录
router.get('/users/login', controller.user.login)
// 获取当前用户
+ router.get('/user', auth, controller.user.getCurrentUser)
};
middleware/auth.js
module.exports = () => {
return async (ctx, next) => {
// 1. 获取请求头中的token数据
const token = ctx.headers['authorzation']
token = token ? ctx.headers['authorzation'].split('Bearer ')[1] : null // Bearer空格token数据
// 2。验证token,无效401
if (!token) {
ctx.throw(401)
}
// 3.token有效,根据userId,获取用户数据挂载到ctx对象中,后续中间件都可以使用
try {
const data = ctx.service.user.veriftToken(token)
ctx.user = ctx.module.User.findById(data.user)
} catch (error) {
ctx.throw(error)
}
// 4.next执行后续中间件
await next()
}
}
controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
// 获取当前用户
+ async getCurrentUser() {
// 1.验证token
// 2.获取用户
// 3.发送响应
const user = this.ctx.user
this.ctx.body = {
user: {
email: user.email,
token: this.ctx.headers['authorzation'],
username: user.username,
avator: user.avator,
}
}
}
}
module.exports = UserController;
service/user.js
const Service = require('err').Service
const jwt = require('jsonwebtoken')
class UserService extends Service {
get User() {
return this.app.model.User // 获取User的数据模型
}
// 查找username
findByUsername(username) {
return this.User.findOne({ username })
}
// 查找邮箱
findByEmail(email) {
return this.User.findOne({ email })
}
// 创建用户
async createUser(data) {
data.password = this.ctx.helper.md5(data.password)
const user = await this.User(data) // 获取User的实例
await user.save() // 保存到数据库中
return user
}
// 生成token
createToken (data) {
const token = jwt.sign(data, this.app.config.jwt.secret, {
expiresIn: this.app.config.jwt.expiresIn
})
return token
}
// 验证token
+ veriftToken(token) {
return jwt.verify(token, this.app.config.jwt.secret)
}
}
router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
const auth = app.middleware.auth()
router.prefix('/api/v1') // 设置基础路由
// 创建用户
router.post('/users', controller.user.index)
// 登录
router.get('/users/login', controller.user.login)
// 获取当前用户
router.get('/user', auth, controller.user.getCurrentUser)
// 更新用户
+ router.patch('/user', auth, controller.user.update)
};
service/user.js
const Service = require('err').Service
const jwt = require('jsonwebtoken')
class UserService extends Service {
// 更新用户信息
+ updaetUser(data) {
// findByIdAndUpdate: 默认返回更新之前的信息,需要配置 { new: true },返回更新之后的数据
return this.User.findByIdAndUpdate(this.ctx.user._id, data, { new: true })
}
}
controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
// 更新用户信息
+ async update() {
// 1.基本数据验证
// 1.数据验证
const body = this.ctx.request.body // 获取请求体
this.ctx.validate({
username: { type: 'string', required: false }, // required:flase:传了就验证,不传就不验证。
email: { type: 'email', required: false },
password: { type: 'string', required: false},
})
// 2.校验用户是否已经存在
this.userService = this.service.user
if (body.username) {
if (body.username !== this.ctx.user.username && await userService.findByUsername(body.username)) {
this.ctx.throw(422, '用户已经存在')
}
}
if (body.password) {
body.password = this.ctx.header.md5(password)
}
// 3.校验邮箱是否已经存在
if (body.email) {
if (body.email !== this.ctx.user.email && await userService.findByUsername(body.email)) {
this.ctx.throw(422, '邮箱已经存在')
}
}
// 4.更新用户信息
const user = await userService.updaetUser(body)
// 5.返回更新之后的用户信息
this.ctx.body = {
user: {
email: user.email,
username: user.username,
avator: user.avator,
}
}
}
}
module.exports = UserController;
router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
const auth = app.middleware.auth()
router.prefix('/api/v1') // 设置基础路由
// 创建用户
router.post('/users', controller.user.index)
// 登录
router.get('/users/login', controller.user.login)
// 获取当前用户
router.get('/user', auth, controller.user.getCurrentUser)
// 更新用户
router.patch('/user', auth, controller.user.update)
// 用户订阅
+ router.post('/users/:userId/subscribe', auth, controller.user.subscribe)
};
service/user.js
const Service = require('err').Service
const jwt = require('jsonwebtoken')
class UserService extends Service {
+ async subscribe(channelId, userId) {
const { Subscription } = this.app.model
// 1.检查是否已经订阅
const record = Subscription.findOne({ user: userId, channelId: channelId })
// 2。没有订阅添加订阅
const user = User.findById(channelId)
if (!record) {
await new Subscription({ user: userId, channelId: channelId }).save()
}
user.subscribesCount++
await user.save() // 更新到数据库中。
// 3.返回用户信息
return user
}
}
controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
// 用户订阅
+ async subscribe() {
const userId = this.ctx.user._id
const channelId = this.ctx.params.userId // 获取路径参数的userId
// 1.用户不能订阅自己
if(userId.equals(channelId)) {
this.ctx.throw(422, '用户不能订阅自己')
}
// 2.添加订阅
const user = await this.service.user.subscribe(userId, channelId)
// 3.发送响应
this.ctx.body = {
user: {
...user.toJson()
isSubscribe: true,
}
}
}
}
module.exports = UserController;
npm install lodash
extend/helper.js
const crypto = require('crypto')
+ const _ = require('lodash')
exports.md5 = str => {
return crypto.createHash('md5').update(str).digest('hex')
}
//直接挂载到helper上,可以直接在ctx上使用
+ exports._ = _
controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
// 用户订阅
async subscribe() {
const userId = this.ctx.user._id
const channelId = this.ctx.params.userId // 获取路径参数的userId
// 1.用户不能订阅自己
if(userId.equals(channelId)) {
this.ctx.throw(422, '用户不能订阅自己')
}
// 2.添加订阅
const user = await this.service.user.subscribe(userId, channelId)
// 3.发送响应
this.ctx.body = {
user: {
+ ...this.ctx.helper._.pick(user, ['username', 'email', 'avator', 'cover', 'channelDescription', 'subscribersCount']),
isSubscribe: true,
}
}
}
}
module.exports = UserController;
router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
const auth = app.middleware.auth()
router.prefix('/api/v1') // 设置基础路由
// 创建用户
router.post('/users', controller.user.index)
// 登录
router.get('/users/login', controller.user.login)
// 获取当前用户
router.get('/user', auth, controller.user.getCurrentUser)
// 更新用户
router.patch('/user', auth, controller.user.update)
// 用户订阅
router.post('/users/:userId/subscribe', auth, controller.user.subscribe)
// 取消订阅
+ router.delete('/users/:userId/subscribe', auth, controller.user.unsubscribe)
};
service/user.js
const Service = require('err').Service
const jwt = require('jsonwebtoken')
class UserService extends Service {
// 取消订阅
+ async unsubscribe(channelId, userId) {
const { Subscription } = this.app.model
// 1.检查是否已经订阅
const record = Subscription.findOne({ user: userId, channelId: channelId })
// 2。没有订阅添加订阅
const user = User.findById(channelId)
if (record) {
await record.remove() // 删除用户订阅
}
user.subscribesCount--
await user.save() // 更新到数据库中。
// 3.返回用户信息
return user
}
}
controller/user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
// 取消用户订阅
+ async unsubscribe() {
const userId = this.ctx.user._id
const channelId = this.ctx.params.userId // 获取路径参数的userId
// 1.用户不能取消订阅自己
if(userId.equals(channelId)) {
this.ctx.throw(422, '用户不能订阅自己')
}
// 2.取消订阅
const user = await this.service.user.unsubscribe(userId, channelId)
// 3.发送响应
this.ctx.body = {
user: {
...this.ctx.helper._.pick(user, ['username', 'email', 'avator', 'cover', 'channelDescription', 'subscribersCount']),
isSubscribe: false,
}
}
}
}
module.exports = UserController;
router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
const auth = app.middleware.auth()
router.prefix('/api/v1') // 设置基础路由
// 创建用户
router.post('/users', controller.user.index)
// 登录
router.get('/users/login', controller.user.login)
// 获取当前用户
router.get('/user', auth, controller.user.getCurrentUser)
// 更新用户
router.patch('/user', auth, controller.user.update)
// 用户订阅
router.post('/users/:userId/subscribe', auth, controller.user.subscribe)
// 取消订阅
router.delete('/users/:userId/subscribe', auth, controller.user.unsubscribe)
// 获取用户资料
+ router.get('/user/:userid', app.middleware.auth({ require: true }), controller.user.getUser)
};
middleware/auth.js
+ module.exports = (options = { require: true }) => {
return async (ctx, next) => {
// 1. 获取请求头中的token数据
const token = ctx.headers['authorzation']
token = token ? ctx.headers['authorzation'].split('Bearer ')[1] : null// Bearer空格token数据
// 2。验证token,无效401
if (!token) {
ctx.throw(401)
}
+ if (token) {
// 3.token有效,根据userId,获取用户数据挂载到ctx对象中,后续中间件都可以使用
try {
const data = ctx.service.user.veriftToken(token)
ctx.user = ctx.module.User.findById(data.user)
} catch (error) {
ctx.throw(error)
}
+ } else if (options.required) {
ctx.throw(401)
}
// 4.next执行后续中间件
await next()
}
}
controller/user.js
// 获取用户资料
async getUser() {
// 1.获取订阅状态
let isSubscribe = false
if (this.ctx.user) {
// 获取订阅记录
const record = await this.app.model.Subscription({
user: this.ctx.user._id,
cancel: this.ctx.params.userId
})
if (record) {
isSubscribe = true
}
}
// 2.获取用户信息
const user = await this.app.model.User.findById(this.ctx.params.userId)
// 3.发送响应
this.ctx.body = {
...this.ctx.helper._.pick(user, ['username', 'email', 'avator', 'cover', 'channelDescription', 'subscribersCount']),
}
}
router.js
// 获取用户订阅的频道列表
router.get('/users/:userId/subscriptions', auth, controller.user.getSubscriptions)
controller/user.js
// 获取用户订阅的频道列表
async getSubscriptions() {
const Subscription = this.app.model.Subscription
const subscription = await Subscription.find({ user: this.ctx.params.userId }).populate('channel')
subscription = subscription.map (item => {
return this.ctx.helper._.pick(item.channel, ['_id', 'username', 'avtor'])
})
this.ctx.body = {
subscription
}
}
npm i @alicloud/pop-core --save
router.js
// 阿里云 VOD
router.get('/vod/CreateUploadVideo', auth, controller.vod.createUploadvideo)
'use strict';
const Controller = require('egg').Controller;
const RPCClient = require('@alicloud/pop-core').RPCClient
function initVodClient (accessKeyId, accessKeySecret) {
const regionId = 'cn-shanghai' // 点播服务接入区域
const client = new RPCClient({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
endpoint: 'http://vod.' + regionId + '.aliyuncs.com',
apiVersion: '2017-03-21'
})
class VodController extends Controller {
// 获取上传地址和凭证
async createUploadvideo() {
const query = this.ctx.query
this.ctx.validate({ title: { type: 'string' }, FilName: { type: 'string' } })
const vodClient = initVodClient('LTAI4FzgRRRN2MjwBzc3xQtp', 'xAllGuORtBDVcrTQpTOWu4HfjYgN1p')
// 发送响应
this.ctx.body = await vodClient.request('CreateUploadVideo',query, {})
}
}
module.exports = VodController;
先解决跨域的问题。
npm i egg-cors --save
plugin.js
+ exports.cors = {
enable: true,
package: 'egg-cors'
}
config.default.js
+ config.cors = {
origin: '*'
// {string|Function} origin: '*',
// {string|Array} allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH'
}
extend/application.js
const RPCClient = require('@alicloud/pop-core').RPCClient
function initVodClient (accessKeyId, accessKeySecret) {
const regionId = 'cn-shanghai' // 点播服务接入区域
const client = new RPCClient({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
endpoint: 'http://vod.' + regionId + '.aliyuncs.com',
apiVersion: '2017-03-21'
})
return client
}
let vodClient = null
module.exports = {
// get访问器的方法。
get vodClient () {
if (!vodClient) {
vodClient = initVodClient('LTAI4FzgRRRN2MjwBzc3xQtp', 'xAllGuORtBDVcrTQpTOWu4HfjYgN1p')
}
return vodClient
}
}
conroller/vod.js
'use strict';
const Controller = require('egg').Controller;
class VodController extends Controller {
// 获取上传地址和凭证
async createUploadVideo() {
const query = this.ctx.query
this.ctx.validate({ title: { type: 'string' }, FilName: { type: 'string' } }, query)
// 发送响应
+ this.ctx.body = await this.app.vodClient.request('CreateUploadVideo', query, {})
}
// 视频上传-刷新视频上传凭证
+ async refreshUploadvideo() {
const query = this.ctx.query
this.ctx.validate({ VideoId: { type: 'string' } }, query)
// 发送响应
this.ctx.body = await this.app.vodClient.request('RefreshUploadvideo', query, {})
}
}
module.exports = VodController;
config/config.local.js 只针对本地开发的文件
/**
* 本地开发的配置文件
*/
// 本地如果还是根据环境变量读取有点麻烦,直接硬编码写死。
const secret = require('./secret')
exports.vod = {
...secret.vod
}
ignore.js
exports.vod = {
accessKeyId: 'LTAI4FzgRRRN2MjwBzc3xQtp',
accessKeySecret: 'xAllGuORtBDVcrTQpTOWu4HfjYgN1p'
}
config/config.prod.js 只针对生产环境的文件
/**
* 生产环境的配置文件
*/
exports.vod = {
// 通过环境变量设置
accessKeyId: process.env.accessKeyId,
accessKeySecret: process.env.accessKeySecret
}
extend/application.js
const RPCClient = require('@alicloud/pop-core').RPCClient
function initVodClient (accessKeyId, accessKeySecret) {
const regionId = 'cn-shanghai' // 点播服务接入区域
const client = new RPCClient({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
endpoint: 'http://vod.' + regionId + '.aliyuncs.com',
apiVersion: '2017-03-21'
})
return client
}
let vodClient = null
module.exports = {
// get访问器的方法。
get vodClient () {
if (!vodClient) {
+ const { accessKeyId, accessKeySecret } = this.config.vod
+ vodClient = initVodClient(accessKeyId, accessKeySecret)
}
return vodClient
}
}
// 创建视频
router.post('/videos', auth, controller.vod.createVideo)
controller/video.js
const Controller = require('egg').Controller
class VideoController extends Controller {
async createVideo () {
const body = this.ctx.request.body
const { Video } = this.app.model
this.ctx.validate({
title: { type: 'string' },
description: { type: 'string' },
vodVideoId: { type: 'string' }
// cover: { type: 'string' }
}, body)
// 默认视频封面
body.cover = 'http://vod.lipengzhou.com/image/default/A806D6D6B0FD4D118F1C824748826104-6-2.png'
body.user = this.ctx.user._id
const video = await new Video(body).save()
this.ctx.status = 201
this.ctx.body = {
video
}
const setVideoCover = async (video) => {
// 获取视频信息
const vodVideoInfo = await this.app.vodClient.request('GetVideoInfo', {
VideoId: video.vodVideoId
})
if (vodVideoInfo.Video.CoverURL) {
// 使用自动生成的封面
video.cover = vodVideoInfo.Video.CoverURL
// 将修改保存到数据库中
await video.save()
} else {
await new Promise(resolve => {
setTimeout(() => {
resolve()
}, 3000)
})
await setVideoCover(video)
}
}
setVideoCover(video)
}
}
module.exports = VideoController
// 获取视频详情
router.get('/videos/:videoId', auth, controller.vod.getVideo)
controller/Video.js
// 获取视频详情
async getVideo() {
// 获取数据模型
const { Video, VideoLike, Subscription } = this.app.model
const { videoId } = this.ctx.params // 获取路径中的videoId
// 在数据库的Video表中根据videoId查询记录
let video = await Video.findById(videoId).populate('user', '_id username avatar subscribersCount')
// 没有查到抛出异常
if (!video) {
this.ctx.throw(404, 'Video Not Found')
}
// 转换成普通的js对象
video = video.toJSON()
// 在video中添加自定义数据
video.isLiked = false // 是否喜欢
video.isDisliked = false // 是否不喜欢
video.user.isSubscribed = false // 是否已订阅视频作者
// 如果用户登录
if(this.ctx.user) {
// 喜欢: 1, 不喜欢: -1
// 查询记录
const userId = this.ctx.user._id
if (await VideoLike.findOne({ user: userId, video: videoId, like: 1 })) {
video.isLiked = true
}
if (await VideoLike.findOne({ user: userId, video: videoId, like: -1 })) {
video.isDisliked = true
}
if (await Subscription.findOne({ user: userId, channel: video.user._id })) {
video.user.isSubscribed = true
}
}
// 查询的结果video直接发送到客户端
this.ctx.body = {
video
}
}
router.js
// 获取视频列表
router.get('/videos', controller.vod.getVideos)
controller/video.js
async getVideos() {
const { Video } this.app.model
const { pageNum = 1, pageSize = 10 } = this.ctx.query
pageNum = Number.parseInt(pageNum)
pageSize = Number.parseInt(pageSize)
// 按条件在数据库中查询
const getVideos = Video.find().popupate('user')
.sort({ createdAt: -1 })
.skip((pageNum -1)) * pageSize)
.limit(pageSize)
const getVideosCount = Video.countDocuments() // 获取视频总数量
const [videos, videosCount] = await Promise.all([
getVideos,
getVideosCount
])
// 查询到的数据返回给客户端。
this.ctx.body = {
videos,
videosCount
}
}
router.js
// 获取用户发布的视频列表
router.get('/users/:userId/videos', controller.video.getUserVideos)
controller/video.js
async getUserVideos () {
const { Video } = this.app.model
let { pageNum = 1, pageSize = 10 } = this.ctx.query
const userId = this.ctx.params.userId
pageNum = Number.parseInt(pageNum)
pageSize = Number.parseInt(pageSize)
const getVideos = Video
.find({
user: userId
})
.populate('user')
.sort({
createdAt: -1
})
.skip((pageNum - 1) * pageSize)
.limit(pageSize)
const getVideosCount = Video.countDocuments({
user: userId
})
const [videos, videosCount] = await Promise.all([
getVideos,
getVideosCount
])
this.ctx.body = {
videos,
videosCount
}
}
router.js
// 获取用户关注的频道视频列表-接口实现
router.get('/user/videos/feed', auth, controller.video.getUserFeedVideos)
controller/video.js
async getUserFeedVideos () {
const { Video, Subscription } = this.app.model
let { pageNum = 1, pageSize = 10 } = this.ctx.query
const userId = this.ctx.user._id
pageNum = Number.parseInt(pageNum)
pageSize = Number.parseInt(pageSize)
const channels = await Subscription.find({ user: userId }).populate('channel')
const getVideos = Video
.find({
user: {
$in: channels.map(item => item.channel._id) // 用户关注的列表
}
})
.populate('user')
.sort({
createdAt: -1
})
.skip((pageNum - 1) * pageSize)
.limit(pageSize)
const getVideosCount = Video.countDocuments({
user: {
$in: channels.map(item => item.channel._id)
}
})
const [videos, videosCount] = await Promise.all([
getVideos,
getVideosCount
])
this.ctx.body = {
videos,
videosCount
}
}
router.js
// 更新视频
router.patch('/videos/:videoId', auth, controller.video.updateVideo)
controller/video.js
async updateVideo () {
const { body } = this.ctx.request
const { Video } = this.app.model
const { videoId } = this.ctx.params
const userId = this.ctx.user._id
// 数据验证
this.ctx.validate({
title: { type: 'string', required: false },
description: { type: 'string', required: false },
vodVideoId: { type: 'string', required: false },
cover: { type: 'string', required: false }
})
// 查询视频
const video = await Video.findById(videoId)
if (!video) {
this.ctx.throw(404, 'Video Not Found')
}
// 视频作者必须是当前登录用户
if (!video.user.equals(userId)) {
this.ctx.throw(403)
}
Object.assign(video, this.ctx.helper._.pick(body, ['title', 'description', 'vodVideoId', 'cover']))
// 把修改保存到数据库中
await video.save()
// 发送响应
this.ctx.body = {
video
}
}
router.js
// 删除视频
router.delete('/videos/:videoId', auth, controller.video.deleteVideo)
controller/video.js
async deleteVideo () {
const { Video } = this.app.model
const { videoId } = this.ctx.params
const video = await Video.findById(videoId)
// 视频不存在
if (!video) {
this.ctx.throw(404)
}
// 视频作者不是当前登录用户
if (!video.user.equals(this.ctx.user._id)) {
this.ctx.throw(403)
}
await video.remove()
this.ctx.status = 204
}
router.js
// 添加视频评论
router.post('/videos/:videoId/comments', auth, controller.video.createComment)
controller/video.js
async createComment () {
const body = this.ctx.request.body
const { Video, VideoComment } = this.app.model
const { videoId } = this.ctx.params
// 数据验证
this.ctx.validate({
content: 'string'
}, body)
// 获取评论所属的视频
const video = await Video.findById(videoId)
if (!video) {
this.ctx.throw(404)
}
// 创建评论
const comment = await new VideoComment({
content: body.content,
user: this.ctx.user._id,
video: videoId
}).save()
// 更新视频的评论数量
video.commentsCount = await VideoComment.countDocuments({
video: videoId
})
await video.save()
// 映射评论所属用户和视频字段数据
await comment.populate('user').populate('video').execPopulate()
this.ctx.body = {
comment
}
}
router.js
// 获取视频评论列表
router.get('/videos/:videoId/comments', controller.video.getVideoComments)
controller/video.js
async getVideoComments () {
const { videoId } = this.ctx.params
const { VideoComment } = this.app.model
let { pageNum = 1, pageSize = 10 } = this.ctx.query
pageNum = Number.parseInt(pageNum)
pageSize = Number.parseInt(pageSize)
const getComments = VideoComment
.find({
video: videoId
})
.skip((pageNum - 1) * pageSize)
.limit(pageSize)
.populate('user')
.populate('video')
const getCommentsCount = VideoComment.countDocuments({
video: videoId
})
const [comments, commentsCount] = await Promise.all([
getComments,
getCommentsCount
])
this.ctx.body = {
comments,
commentsCount
}
}
router.js
// 删除视频评论
router.delete('/videos/:videoId/comments/:commentId', auth, controller.video.deleteVideoComment)
controller/video.js
async deleteVideoComment () {
const { Video, VideoComment } = this.app.model
const { videoId, commentId } = this.ctx.params
// 校验视频是否存在
const video = await Video.findById(videoId)
if (!video) {
this.ctx.throw(404, 'Video Not Found')
}
const comment = await VideoComment.findById(commentId)
// 校验评论是否存在
if (!comment) {
this.ctx.throw(404, 'Comment Not Found')
}
// 校验评论作者是否是当前登录用户
if (!comment.user.equals(this.ctx.user._id)) {
this.ctx.throw(403)
}
// 删除视频评论
await comment.remove()
// 更新视频评论数量
video.commentsCount = await VideoComment.countDocuments({
video: videoId
})
await video.save()
this.ctx.status = 204
}
router.js
// 喜欢视频
router.post('/videos/:videoId/like', auth, controller.video.likeVideo)
// 不喜欢视频
router.post('/videos/:videoId/dislike', auth, controller.video.dislikeVideo)
controller/video.js
// 喜欢视频
async likeVideo () {
const { Video, VideoLike } = this.app.model
const { videoId } = this.ctx.params
const userId = this.ctx.user._id
const video = await Video.findById(videoId)
if (!video) {
this.ctx.throw(404, 'Video Not Found')
}
const doc = await VideoLike.findOne({
user: userId,
video: videoId
})
let isLiked = true
if (doc && doc.like === 1) {
await doc.remove() // 取消点赞
isLiked = false
} else if (doc && doc.like === -1) {
doc.like = 1
await doc.save()
} else {
await new VideoLike({
user: userId,
video: videoId,
like: 1
}).save()
}
// 更新喜欢视频的数量
video.likesCount = await VideoLike.countDocuments({
video: videoId,
like: 1
})
// 更新不喜欢视频的数量
video.dislikesCount = await VideoLike.countDocuments({
video: videoId,
like: -1
})
// 将修改保存到数据库中
await video.save()
this.ctx.body = {
video: {
...video.toJSON(),
isLiked
}
}
}
// 不喜欢视频
async dislikeVideo () {
const { Video, VideoLike } = this.app.model
const { videoId } = this.ctx.params
const userId = this.ctx.user._id
const video = await Video.findById(videoId)
if (!video) {
this.ctx.throw(404, `No video found for ID - ${videoId}`)
}
const doc = await VideoLike.findOne({
user: userId,
video: videoId
})
let isDisliked = true
if (doc && doc.like === -1) {
await doc.remove()
isDisliked = false
} else if (doc && doc.like === 1) {
doc.like = -1
await doc.save()
} else {
await new VideoLike({
user: userId,
video: videoId,
like: -1
}).save()
}
// 更新视频喜欢和不喜欢的数量
video.likesCount = await VideoLike.countDocuments({
video: videoId,
like: 1
})
video.dislikesCount = await VideoLike.countDocuments({
video: videoId,
like: -1
})
this.ctx.body = {
video: {
...video.toJSON(),
isDisliked
}
}
}