如果你和你想做的事情不在同一个频道,你就会浪费许多精力。等你有机会做你想做的事情时,你可能已经没有力气或时间了。
【Vue + Koa 前后端分离项目实战3】使用开源框架==>快速搭建后台管理系统 -- part3 权限控制+行为日志_小白Rachel的博客-CSDN博客
【Vue + Koa 前后端分离项目实战2】使用开源框架==>快速搭建后台管理系统 -- part2 后端新增期刊功能实现_小白Rachel的博客-CSDN博客_koa 开源项目
【Vue + Koa 前后端分离项目实战】使用开源框架==>快速搭建后台管理系统 -- part1 项目搭建_小白Rachel的博客-CSDN博客_vue+koa
后端分层架构的模式和框架中的文件说明 :
| 分层结构 | 说明 | 对应文件夹 | 对应文件 |
| Model层 | 实体类层 主要用于定义与数据库对象应的属性 | models | movies.js music.js sentence.js |
| Dao层 | 持久层 访问数据库,向数据库发送sql语句,完成数据的增删改查任务 | dao | movies.js music.js sentence.js |
| Service层 | 业务逻辑层,完成功能的设计 | service | contents.js |
| Controller层 | 控制层,控制请求和响应,负责前后端交互 接口控制具体的业务流程 | Validators api | content.js content.js |
本章节主要完成后端的逻辑实现,并借助接口测试postman工具测试 ,暂无涉及到前端内容。
目录
- // config/secure.js
- 'use strict';
-
- module.exports = {
- db: {
- database: 'lin-cms',
- host: 'localhost',
- dialect: 'mysql',
- port: 3306,
- username: 'root',
- password: '123456',
- logging: false,
- timezone: '+08:00',
- dialectOptions: { // 添加配置 修改日期格式
- dateStrings: true,
- typeCast: true
- }
- },
- secret:
- '\x88W\xf09\x91\x07\x98\x89\x87\x96\xa0A\xc68\xf9\xecJJU\x17\xc5V\xbe\x8b\xef\xd7\xd8\xd3\xe6\x95*4'
- };
- // models/movie.js
- import { Sequelize, Model } from 'sequelize';
- import sequelize from '../libs/db';
- import { config } from 'lin-mizar';
-
- class Movie extends Model {
-
- }
-
- Movie.init (
- {
- id: {
- type: Sequelize.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- image: {
- type: Sequelize.STRING(64)
- },
- content: {
- type: Sequelize.STRING(300),
- allowNull: true
- },
- pubdate: {
- type: Sequelize.INTEGER,
- allowNull: true
- },
- fav_nums: {
- type: Sequelize.INTEGER,
- defaultValue: 0
- },
- title: {
- type: Sequelize.STRING(50)
- },
- type: {
- type: Sequelize.INTEGER
- },
- status: {
- type: Sequelize.INTEGER
- }
- },
- {
- // 定义表名
- tableName: 'movie',
- // 定义模型名
- modelName: 'movie',
- // 删除
- paranoid: true,
- // 自动写入时间
- timestamps: true,
- // 重命名时间字段
- createdAt: 'created_at',
- updatedAt: 'updated_at',
- deletedAt: 'deleted_at',
- sequelize
- }
- )
-
- export { Movie as MovieModel }
music音乐,包含url
- // models/music.js
- import { Sequelize, Model } from 'sequelize';
- import sequelize from '../libs/db';
- import { config } from 'lin-mizar';
-
- class Music extends Model {
-
- }
-
- Music.init (
- {
- id: {
- type: Sequelize.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- image: {
- type: Sequelize.STRING(64)
- },
- content: {
- type: Sequelize.STRING(300),
- allowNull: true
- },
- url: {
- type: Sequelize.STRING(100),
- allowNull: true
- },
- pubdate: {
- type: Sequelize.INTEGER,
- allowNull: true
- },
- fav_nums: {
- type: Sequelize.INTEGER,
- defaultValue: 0
- },
- title: {
- type: Sequelize.STRING(50)
- },
- type: {
- type: Sequelize.INTEGER
- },
- status: {
- type: Sequelize.INTEGER
- }
- },
- {
- // 定义表名
- tableName: 'music',
- // 定义模型名
- modelName: 'music',
- // 删除
- paranoid: true,
- underscored: true,
- // 自动写入时间
- timestamps: true,
- // 重命名时间字段
- createdAt: 'created_at',
- updatedAt: 'updated_at',
- deletedAt: 'deleted_at',
- sequelize
- }
- )
-
- export { Music as MusicModel }
sentence句子
- // models/sentence.js
- import { Sequelize, Model } from 'sequelize';
- import sequelize from '../libs/db';
- import { config } from 'lin-mizar';
-
- class Sentence extends Model {
-
- }
-
- Sentence.init (
- {
- id: {
- type: Sequelize.INTEGER,
- primaryKey: true,
- autoIncrement: true
- },
- image: {
- type: Sequelize.STRING(64)
- },
- content: {
- type: Sequelize.STRING(300),
- allowNull: true
- },
- pubdate: {
- type: Sequelize.INTEGER,
- allowNull: true
- },
- fav_nums: {
- type: Sequelize.INTEGER,
- defaultValue: 0
- },
- title: {
- type: Sequelize.STRING(50)
- },
- type: {
- type: Sequelize.INTEGER
- },
- status: {
- type: Sequelize.INTEGER
- }
- },
- {
- // 定义表名
- tableName: 'sentence',
- // 定义模型名
- modelName: 'sentence',
- // 删除
- paranoid: true,
- underscored: true,
- // 自动写入时间
- timestamps: true,
- // 重命名时间字段
- createdAt: 'created_at',
- updatedAt: 'updated_at',
- deletedAt: 'deleted_at',
- sequelize,
- }
- )
-
- export { Sentence as SentenceModel }
项目添加权限之后,使用postman需要添加权限token
具体方法:运行(npm run serve)前端项目工程(lin-cms-vue),后打开http://localhost:8080/ 登录查看token信息,并复制到postman中

最终数据包含三张表的数据,即:从三张表中查询返回合并结果数据。
使用框架已有的findAll()方法,完成dao文件夹下的movie.js music.js sentence.js文件
- // dao/movie.js文件
- import { MovieModel } from '../models/movie';
- import { NotFound } from 'lin-mizar';
-
- // 在Dao层中调用模型层
- class Movie {
- static async getMovieList () {
- return await MovieModel.findAll()
- }
- }
-
- export { Movie as MovieDao }
- // dao/music.js文件
- import { MusicModel } from '../models/music';
- import { NotFound } from 'lin-mizar';
-
- // 在Dao层中调用模型层
- class Music {
- // 查询音乐列表
- static async getMusicList () {
- return await MusicModel.findAll()
- }
- }
-
- export { Music as MusicDao }
- // dao/sentence.js文件
- import { SentenceModel } from '../models/sentence';
- import { NotFound } from 'lin-mizar';
-
- class Sentence {
- static async getSentenceList () {
- return await SentenceModel.findAll()
- }
- }
-
- export { Sentence as SentenceDao }
新建getContentList 查询列表数据方法
- // service/content.js文件
- import { MovieDao } from '../dao/movie';
- import { MusicDao } from '../dao/music';
- import { SentenceDao } from '../dao/sentence';
- import { NotFound } from 'lin-mizar';
-
- class Content {
- static async getContentList (v) {
- const movieList = await MovieDao.getMovieList() // 电影列表数据
- const musicList = await MusicDao.getMusicList() // 音乐列表数据
- const sentenceList = await SentenceDao.getSentenceList() // 句子列表数据
- let res = [] // 整合三个资源的数据
- res.push.apply(res, movieList)
- res.push.apply(res, musicList)
- res.push.apply(res, sentenceList)
- res.sort((a, b) => b.created_at.localeCompare(a.created_at)) // 按照创建时间排序
- return res
- }
- }
-
- export { Content as ContentService };
- // api/v1/content.js
- contentApi.get("/", async (ctx) => {
- const contentList = await ContentService.getContentList();
- ctx.json(contentList);
- });
- module.exports = { contentApi };
http://localhost:5000/v1/content

第二篇文章已经详细讲过新增期刊的逻辑了,这里简单梳理。
【Vue + Koa 前后端分离项目实战】使用开源框架==>快速搭建后台管理系统 -- part2 后端新增期刊功能实现_小白Rachel的博客-CSDN博客_koa 开源项目
需要检验的内容有:所有描述信息不为空、id为数字、url合法、日期格式正确等
- class AddContentValidator extends LinValidator {
- constructor () {
- super();
- this.image = [
- new Rule('isNotEmpty', '内容封面不能为空')
- ]
- this.type = [
- new Rule('isNotEmpty', '内容类型不能为空'),
- new Rule('isInt', '内容类型id必须是数字')
- ]
- this.title = [
- new Rule('isNotEmpty', '内容标题不能为空')
- ]
- this.content = [
- new Rule('isNotEmpty', '内容介绍不能为空')
- ]
- this.url = [
- new Rule('isOptional'),
- new Rule('isURL', '内容外链必须是合法url地址')
- ]
- this.pubdate = [
- new Rule('isNotEmpty', '发布日期不能为空'),
- new Rule('isISO8601', '发布日期格式不正确')
- ]
- this.status = [
- new Rule('isNotEmpty', '内容有效状态未指定'),
- new Rule('isInt', '内容有效状态标识不正确')
- ]
- }
- }
- // dao/movie.js
- static async addMovice (v) {
- return await MovieModel.create(v)
- }
- // dao/music.js
- static async addMusic (v) {
- return await MusicModel.create(v)
- }
- // dao/sentence.js
- static async addSentence (v) {
- return await SentenceModel.create(v)
- }
- // service/content.js
- static async addContent (v) {
- // 根据不同种类判断数据
- switch (v['type']) {
- case 100:
- // 电影
- delete v['url']
- await MovieDao.addMovice(v);
- break;
- case 200:
- // 音乐
- await MusicDao.addMusic(v);
- break;
- case 300:
- // 句子
- delete v['url']
- await SentenceDao.addSentence(v);
- break;
- default:
- throw new NotFound({ msg: '内容类型不存在' });
- }
- }
- // api/v1/content.js
- contentApi.linPost(
- "addContent", // 函数唯一标识
- "/",
- {
- permission: "添加期刊内容", // 权限名称
- module: "内容管理", // 权限所属模块
- mount: true,
- },
- groupRequired, // 权限级别
- logger("{user.username}新增了期刊内容"),
- async (ctx) => {
- // 1.参数校验
- const v = await new AddContentValidator().validate(ctx);
- // 2.执行业务逻辑
- // 3.插入数据库-封装在service层
- await ContentService.addContent(v.get("body"));
- // 4.返回成功
- ctx.success({
- msg: "期刊内容新增成功",
- });
- }
- );
编辑时id不能为空,其余属性和【新增编辑器】相同,直接继承复用即可
- // validators/content.js
- class EditContentValidator extends AddContentValidator {
- // 对于id验证
- constructor () {
- super();
- this.id = [
- new Rule('isNotEmpty', '期刊id不能为空'),
- new Rule('isInt', '期刊id必须是数字且大于0', {min: 1 })
- ]
- }
- }
- export { AddContentValidator, EditContentValidator}
editMovie方法接收两个参数:id params
findBuPK(id) 根据id调用模型的查询方法。返回对象信息
update() 调用模型的更新方法用于更新数据
- // dao/movie.js文件
- static async editMovie (id, params) {
- const movie = await MovieModel.findByPk(id) // 根据id查询数据对象
- if (!movie) {
- throw new NotFound()
- }
- return await movie.update({ ...params }) // 修改相应字段数据
- }
- // dao/music.js文件
- static async editMusic (id, params) {
- const music = await MusicModel.findByPk(id)
- if (!music) {
- throw new NotFound()
- }
- return await music.update({ ...params })
- }
- // dao/sentence.js文件
- static async editSentence (id, v) {
- const sentence = await SentenceModel.findByPk(id)
- if (!sentence) {
- throw new NotFound()
- }
- return await sentence.update({ ...v })
- }
根据数据类型type(100,200,300) 三种类型,使用switch语句,实现三种类型的区分。分别调用dao层的编辑方法。对于电影、句子没有url字段,需要删除。如果类型不对应时抛出异常。
- // service/content.js
- static async editContent (id, params) {
- switch (params['type']) {
- case 100:
- delete params['url']
- await MovieDao.editMovie(id, params)
- break;
- case 200:
- await MusicDao.editMusic(id, params)
- break;
- case 300:
- delete params['url']
- await SentenceDao.editSentence(id, params)
- break;
- default:
- throw new NotFound({ msg: '内容类型不存在' })
- }
- }
使用put方法。上篇文章已讲过【权限问题】和【新增】的接口逻辑详解,不再赘述
- // api/v1/content.js
- contentApi.linPut(
- "editContent",
- "/:id",
- {
- permission: "编辑期刊内容", // 权限
- module: "内容管理",
- mount: true,
- },
- groupRequired, // 权限级别
- logger("{user.username}编辑了期刊内容"),
- async (ctx) => {
- // 1.参数校验
- const v = await new EditContentValidator().validate(ctx);
- // 2.取值
- const id = v.get("path.id");
- const params = v.get("body");
- // 3.编辑期刊逻辑
- await ContentService.editContent(id, params);
- // 4.返回成功提示
- ctx.success({
- msg: "期刊内容编辑成功",
- });
- }
- );
修改类型type=100 id=6的数据。


需要校验id和type字段,保证能够唯一定义到数据。
- class DeleteContentValidator extends LinValidator {
- constructor () {
- super();
- this.id = [
- new Rule('isNotEmpty', '期刊id不能为空'),
- new Rule('isInt', '期刊id必须是数字且大于0', { min: 1 })
- ]
- this.type = [
- new Rule('isNotEmpty', '期刊类型不能为空'),
- new Rule('isInt', '期刊类型必须是数字')
- ]
- }
- }
-
- export { AddContentValidator, EditContentValidator, DeleteContentValidator }
调用模型中的查询方法,按照id查询,并删除对应数据。
- // dao/movie.js
- static async deleteMovieById (id) {
- return MovieModel.destroy({
- where: { id }
- })
- }
- // dao/music.js
- static async deleteMusicById (id) {
- return MusicModel.destroy({
- where: { id }
- })
- }
- // dao/sentence.js
- static async deleteSentenceById (id) {
- return SentenceModel.destroy({
- where: { id }
- })
- }
使用switch语句,分类删除数据
- // service/content.js
- static async deleteContent (id, type) {
- switch (type) {
- case 100:
- await MovieDao.deleteMovieById(id)
- break;
- case 200:
- await MusicDao.deleteMusicById(id)
- break;
- case 300:
- await SentenceDao.deleteSentenceById(id)
- break;
- default:
- throw new NotFound({ msg: '内容类型不存在' })
- }
- }
定义delete方法,传递参数为id
- contentApi.linDelete(
- "deleteContent",
- "/:id",
- {
- permission: "删除期刊内容", // 权限
- module: "内容管理",
- mount: true,
- },
- groupRequired, // 权限级别
- logger("{user.username}删除了期刊内容"),
- async (ctx) => {
- const v = await new DeleteContentValidator().validate(ctx);
- const id = v.get("path.id");
- const type = v.get("query.type");
- await ContentService.deleteContent(id, type);
- ctx.success({
- msg: "期刊删除成功",
- });
- }
- );
http://localhost:5000/v1/content/1?type=100

至此,期刊模块的增删查改功能全部完成。