Skip to content

YikunHan42/Information-System-Analysis-and-Design

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

读书后台管理信息系统

1. 版本控制

1.1 迭代过程

0.1

实现登录

设置主页

搭建目录结构基本框架

0.2

实现文件上传

连通数据库

图书列表显示

基本字段的增删查改

0.3

实现文件解析

后端补齐20个页面(包括图表、图标、友链、换肤、反馈、指南等边缘功能)

0.4

实现嵌套目录解析

实现多语言解析

实现大文件解析

1.2 文件目录

前端文件目录(仅展开到三级目录并只显示文件夹):

├─plop-templates

│ ├─component

│ └─view

├─public

├─src

│ ├─api

│ ├─assets

│ │ ├─401_images

│ │ ├─404_images

│ │ └─custom-theme

│ │ └─fonts

│ ├─components

│ │ ├─BackToTop

│ │ ├─Breadcrumb

│ │ ├─Charts

│ │ ├─DndList

│ │ ├─DragSelect

│ │ ├─Dropzone

│ │ ├─EbookUpload

│ │ ├─ErrorLog

│ │ ├─GithubCorner

│ │ ├─Hamburger

│ │ ├─HeaderSearch

│ │ ├─ImageCropper

│ │ ├─JsonEditor

│ │ ├─Kanban

│ │ ├─MarkdownEditor

│ │ ├─MDinput

│ │ ├─Pagination

│ │ ├─PanThumb

│ │ ├─RightPanel

│ │ ├─Screenfull

│ │ ├─Share

│ │ ├─SizeSelect

│ │ ├─Sticky

│ │ ├─SvgIcon

│ │ ├─TextHoverEffect

│ │ ├─ThemePicker

│ │ ├─Tinymce

│ │ ├─Upload

│ │ └─UploadExcel

│ ├─directive

│ │ ├─clipboard

│ │ ├─el-drag-dialog

│ │ ├─el-table

│ │ ├─permission

│ │ └─waves

│ ├─filters

│ ├─icons

│ │ └─svg

│ ├─layout

│ │ ├─components

│ │ └─mixin

│ ├─router

│ │ └─modules

│ ├─store

│ │ └─modules

│ ├─styles

│ ├─utils

│ ├─vendor

│ └─views

└─tests

└─unit

├─components

└─utils

后端文件目录:

D:.

│ .gitignore

│ app.js

│ package-lock.json

│ package.json

├─.idea

│ admin-imooc-node.iml

│ cd

│ misc.xml

│ modules.xml

│ tree

│ workspace.xml

├─db

│ config.js

│ index.js

├─https

│ 6607692_simony.xyz.key

│ 6607692_simony.xyz.pem

├─models

│ Book.js

│ Result.js

├─node_modules

├─router

│ book.js

│ index.js

│ jwt.js

│ user.js

├─services

│ book.js

│ user.js

└─utils

constant.js

env.js

epub.js

index.js

2. 技术实现

2.1 概述

项目中的技术难点如下:登录模块的用户名密码校验、token 生成、校验和路由过滤、前端 token 校验和重定向,电子书上传模块的文件上传和静态资源服务器配置,电子书解析模块的epub 原理、zip 解压、xml 解析,电子书增删改模块的mysql 数据库应用、前后端异常处理。

2.2 用户登录

后端API处理流程如下。

响应过程封装完毕后,我们需要在数据库中查询用户信息来验证用户名和密码是否准确。这里需要基于 mysql 查询库封装一层 service,用来协调业务逻辑和数据库查询,我们不希望直接把业务逻辑写在 router 中,创建 /service/user.js

2.3 电子书上传

电子书上传过程分为新增电子书和编辑电子书。

上传组件开发基于 el-upload 封装了上传组件 EbookUpload,上传图书表单中包括以下信息:

  • 书名
  • 作者
  • 出版社
  • 语言
  • 根文件
  • 文件路径
  • 解压路径
  • 封面路径
  • 文件名称
  • 封面
  • 目录

上传API开发通过指定目的 nginx 上传路径实现。一旦电子书拷贝到指定目录下后,就可以通过 nginx 生成下载链接。

上传通过node的multer组件实现,包括基本的异常处理。

前端逻辑为上传成功时,会将解析的电子书内容填入表单;删除电子书时,会将电子书表单内容复原;而因为需要将表单复原,所以需要表单的默认值。电子书编辑提交表单时需要提供创建书籍和更新书籍两个接口。

2.4 电子书解析

电子书核心解析的核心在于对Book对象的创建和后续处理。

构建Book 对象分为两种场景,第一种是直接从电子书文件中解析出 Book 对象,第二种是从 data 对象中生成 Book 对象。

逻辑分别为从文件读取电子书后,初始化 Book 对象。

createBookFromFile(file) { const { destination, filename, mimetype = MIME_TYPE_EPUB, path, originalname } = file // 电子书的文件后缀名 const suffix = mimetype === MIME_TYPE_EPUB ? '.epub' : '' // 电子书的原有路径 const oldBookPath = path // 电子书的新路径 const bookPath = ${destination}/${filename}${suffix} // 电子书的下载URL const url = ${UPLOAD\_URL}/book/${filename}${suffix} // 电子书解压后的文件夹路径 const unzipPath = ${UPLOAD\_PATH}/unzip/${filename} // 电子书解压后的文件夹URL const unzipUrl = ${UPLOAD\_URL}/unzip/${filename} if (!fs.existsSync(unzipPath)) { fs.mkdirSync(unzipPath, { recursive: true }) } if (fs.existsSync(oldBookPath) && !fs.existsSync(bookPath)) { fs.renameSync(oldBookPath, bookPath) } this.fileName = filename // 文件名 this.path = /book/${filename}${suffix}// epub文件相对路径 this.filePath = this.path this.unzipPath = /unzip/${filename}// epub解压后相对路径 this.url = url // epub文件下载链接 this.title = ''// 书名 this.author = ''// 作者 this.publisher = ''// 出版社 this.contents = [] // 目录 this.contentsTree = [] // 树状目录结构 this.cover = ''// 封面图片URL this.coverPath = ''// 封面图片路径 this.category = -1// 分类ID this.categoryText = ''// 分类名称 this.language = ''// 语种 this.unzipUrl = unzipUrl // 解压后文件夹链接 this.originalName = originalname // 电子书文件的原名 }

和从表单对象中创建 Book 对象。

| createBookFromData(data) { this.fileName = data.fileName this.cover = data.coverPath this.title = data.title this.author = data.author this.publisher = data.publisher this.bookId = data.fileName this.language = data.language this.rootFile = data.rootFile this.originalName = data.originalName this.path = data.path || data.filePath this.filePath = data.path || data.filePath this.unzipPath = data.unzipPath this.coverPath = data.coverPath this.createUser = data.username this.createDt = new Date().getTime() this.updateDt = new Date().getTime() this.updateType = data.updateType === 0 ? data.updateType : 1 this.category = data.category || 99 this.categoryText = data.categoryText || '自定义' this.contents = data.contents || [] } | | --- |

创建Book对象后,初始化后,可以调用 Book 实例的 parse 方法解析电子书,这里使用了 github上的epub 库,直接将 epub 库源码集成到项目中。epub 库源码为https://github.com/julien-c/epub,并使用epub库解压电子书。解压过程中包括基础的异常处理。

| parse() { returnnew Promise((resolve, reject) => { const bookPath = ${UPLOAD\_PATH}${this.filePath} if (!fs.existsSync(bookPath)) { reject(new Error('电子书不存在')) } const epub = new Epub(bookPath) epub.on('error', err => { reject(err) }) epub.on('end', err => { if (err) { reject(err) } else { const { language, creator, creatorFileAs, title, cover, publisher } = epub.metadata if (!title) { reject(new Error('图书标题为空')) } else { this.title = title this.language = language || 'en' this.author = creator || creatorFileAs || 'unknown' this.publisher = publisher || 'unknown' this.rootFile = epub.rootFile const handleGetImage = (err, file, mimeType) => { if (err) { reject(err) } else { const suffix = mimeType.split('/')[1] const coverPath = ${UPLOAD\_PATH}/img/${this.fileName}.${suffix} const coverUrl = ${UPLOAD\_URL}/img/${this.fileName}.${suffix} fs.writeFileSync(coverPath, file, 'binary') this.coverPath = /img/${this.fileName}.${suffix} this.cover = coverUrl resolve(this) } } try { this.unzip() this.parseContents(epub).then(({ chapters, chapterTree }) => { this.contents = chapters this.contentsTree = chapterTree epub.getImage(cover, handleGetImage) }) } catch (e) { reject(e) } } } }) epub.parse() }) } | | --- |

电子书解析过程中我们需要自定义电子书目录解析,第一步需要对电子书进行解压,转变为解压后的zip文件。

通过递归式访问来实现嵌套目录解析算法。

3. 运行截图

3.1 核心页面

图书上传:

大文件:

嵌套目录解析:

多语言:

异常处理:

图书列表:

3.2 非核心页面

读书前端系统

1 版本迭代过程

0.1

书城、书架页面的基本实现,包括布局、基本结构(使用静态模拟数据进行测试)。实现的页面有:书城首页、搜索页面、搜索结果页面、分学科展示页面、书架页面、听书页、书籍详情页面、精选推荐页面、随机推荐页面、编辑书架内容界面。

0.2

页面UI优化;搜索功能实现;书架书籍添加删除功能实现;各页面切换跳转的检查、调整与优化。

0.3

联调阅读器和书籍详情,使用户可以通过书籍详情页跳转到阅读器并进行阅读(前端内容合并);联调前端和后端,使项目基本成型。

0.4

通过科大讯飞提供的API,实现听书功能。

2 技术实现

项目前端书城、书架页使用vue-cli 3.0、HTML5、CSS、Javascript进行开发,项目的重难点在于:UI设计、与后端的交互(发送请求、处理返回数据)。在开发过程中,实现整个项目的流程是:书城首页框架页面+路由配置书城首页标题+搜索框 布局、交互热门搜索布局、交互实现书城首页推荐页面实现;书架标题组件布局、交互书架搜索框布局、交互书架数据获取实现书架图书列表实现书架图书编辑功能开发;听书页布局、交互听书API接入。

在开发过程中,页面的内容布局和交互实现的方法和路径代码较为简单,困难的是思路上的突破,UI设计上的和谐。此处以搜索栏组件为例,展示省略CSS的代码:

| <template> <div> <divclass="search-bar" :class="{'hide-title': !titleVisible, 'hide-shadow': !shadowVisible}"> <transitionname="title-move"> <divclass="search-bar-title-wrapper"v-show="titleVisible"> <divclass="title-text-wrapper"> <spanclass="title-text title">{{$t('home.title')}}</span> </div> <divclass="title-icon-shake-wrapper" @click="showFlapCard"> <spanclass="icon-shake icon"></span> </div> </div> </transition> <divclass="title-icon-back-wrapper" :class="{'hide-title': !titleVisible}" @click="back"> <spanclass="icon-back icon"></span> </div> <divclass="search-bar-input-wrapper" :class="{'hide-title': !titleVisible}"> <divclass="search-bar-blank" :class="{'hide-title': !titleVisible}"></div> <divclass="search-bar-input"> <spanclass="icon-search icon"></span> <inputclass="input" type="text" :placeholder="$t('home.hint')" v-model="searchText" @click="showHotSearch" @keyup.13.exact="search"> </div> </div> </div> <hot-search-listv-show="hotSearchVisible" ref="hotSearch"></hot-search-list> </div></template><script> import { storeHomeMixin } from '../../utils/mixin' import HotSearchList from './HotSearchList' export default { components: { HotSearchList }, mixins: [storeHomeMixin], data() { return { searchText: '', titleVisible: true, shadowVisible: false, hotSearchVisible: false } }, watch: { offsetY(offsetY) { if (offsetY > 0) { this.hideTitle() this.showShadow() } else { this.showTitle() this.hideShadow() } }, hotSearchOffsetY(offsetY) { if (offsetY > 0) { this.showShadow() } else { this.hideShadow() } } }, methods: { search() { this.$router.push({ path: '/store/list', query: { keyword: this.searchText } }) }, showFlapCard() { this.setFlapCardVisible(true) }, back() { if (this.offsetY > 0) { this.showShadow() } else { this.hideShadow() } if (this.hotSearchVisible) { this.hideHotSearch() } else { this.$router.push('/store/shelf') } }, showHotSearch() { this.hideTitle() this.hideShadow() this.hotSearchVisible = true this.$nextTick(() => { this.$refs.hotSearch.reset() }) }, hideHotSearch() { this.hotSearchVisible = false if (this.offsetY > 0) { this.hideTitle() this.showShadow() } else { this.showTitle() this.hideShadow() } }, hideTitle() { this.titleVisible = false }, showTitle() { this.titleVisible = true }, hideShadow() { this.shadowVisible = false }, showShadow() { this.shadowVisible = true } } }</script>

| | --- |

在前后端分离开发的实现中,向后端发送请求和响应后端的请求并解析后端返回数据是重中之中。在实现向后端发送请求时,使用HTTP get/post 请求;解析返回数据时,使用vue.js 或javascript函数。

发送请求代码示例:

// 向后端发送请求的js函数

exportfunction flatList() {

return axios({

method: 'get',

url: `${process.env.VUE\_APP\_BOOK\_URL}/book/flat-list`

})

}

exportfunction shelf() {

return axios({

method: 'get',

url: `${process.env.VUE\_APP\_BASE\_URL}/book/shelf`

})

}

exportfunction list() {

return axios({

method: 'get',

url: `${process.env.VUE\_APP\_BASE\_URL}/book/list`

})

}

exportfunction download(book, onSucess, onError, onProgress) {

if (onProgress == null) {

onProgress = onError

onError = null

}

return axios.create({

baseURL: process.env.VUE\_APP\_EPUB\_URL,

method: 'get',

responseType: 'blob',

timeout: 180 \* 1000,

onDownloadProgress: progressEvent =\> {

  if (onProgress) onProgress(progressEvent)

}

}).get(${book.categoryText}/${book.fileName}.epub)

.then(res =\> {

  const blob = new Blob([res.data])

  setLocalForage(book.fileName, blob,

    () =\> onSucess(book),

    err =\> onError(err))

}).catch(err =\> {

  if (onError) onError(err)

})

}

Shape1

处理返回数据的代码示例:

// 通过API找到当前电子书的详情数据

  findBookFromList(fileName) {

    flatList().then(response =\> {

      if (response.status === 200) {

        const bookList = response.data.data.filter(item =\> item.fileName === fileName)

        if (bookList && bookList.length \> 0) {

          this.bookItem = bookList[0]

          this.init()

        }

      }

    })

  },

  // 初始化参数信息

  init() {

    const fileName = this.$route.query.fileName

    if (!this.bookItem) {

      this.bookItem = findBook(fileName)

    }

//为减少篇幅,省略部分代码

  // 解析电子书

  parseBook(blob) {

    // 解析电子书

    this.book = new Epub(blob)

    // 获取电子书的metadata

    this.book.loaded.metadata.then(metadata =\> {

      this.metadata = metadata

    })

    // 获取电子书的目录信息

    this.book.loaded.navigation.then(nav =\> {

      this.navigation = nav

    })

    // 渲染电子书

    this.display()

  },

Shape2

3 页面展示

书城首页

分类页面

按学科分类展示书籍

书籍详情页

听书页

搜索页

搜索结果页

书架页

编辑书架页面

随机推荐页面

精选页面

About

SCU Information System Analysis and Design 2021 Fall

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published