Skip to content

文章管理系统实践(一)

一、搭建框架

基于经典的 MVC(Model-View-Controller)设计模式。路由负责请求的导向,控制器处理业务逻辑,服务层提供业务功能,而模型负责数据的定义和数据库的交互。

1、设计模式简述

1)Controllers(控制器):

  • 作用: 控制器负责处理路由中的业务逻辑,接收请求,调用服务层处理业务,并返回响应给客户端。
  • 关联性: 控制器直接与路由相关联,每个路由对应一个控制器。控制器通过调用服务层中的方法来处理具体的业务逻辑。

2)Models(模型):

  • 作用: 模型定义了数据的结构和与数据库的交互方法。它封装了数据的处理和操作。
  • 关联性: 控制器在处理业务逻辑时可能需要调用模型层的方法,以便进行数据库操作或获取特定的数据结构。

3)Routes(路由):

  • 作用: 路由定义了路径和 HTTP 方法的映射,将请求从客户端导向相应的控制器。路由起到将请求分发到正确控制器的桥梁作用。
  • 关联性: 路由直接关联控制器,每个路由对应一个控制器。路由负责将请求参数传递给控制器,并将控制器的响应返回给客户端。

4)Services(服务层):

  • 作用: 服务层处理业务逻辑、数据操作和第三方服务集成。它提供了通用和复杂的业务功能,被控制器调用以完成特定的业务需求。
  • 关联性: 服务层通常由控制器调用。控制器将请求的数据传递给服务层,服务层处理业务逻辑并可能与模型交互以完成数据操作,最后返回结果给控制器。

关联性的具体示例:

  • 路由将请求分派到相应的控制器,控制器根据业务需求可能调用模型或服务层。
  • 控制器中可能调用服务层的方法来处理复杂的业务逻辑,如验证用户身份、生成报告等。
  • 控制器中可能调用模型的方法来查询数据库,获取数据。

流程图

An image

2、技术选型

1)主要技术栈

  • Express
  • mysql
  • sequelize
  • axios
  • typescript
  • log4js

2)框架目录结构

markdown
/my-express-app
|-- .git/ # Git 版本控制文件夹
|-- .gitignore # Git 忽略文件配置
|-- node_modules/ # 包含项目依赖的 Node.js 模块
|-- package.json # 项目的元数据和依赖配置文件
|-- package-lock.json # 记录确切安装的包版本
|-- app.ts # 项目的入口文件,包含 Express 应用程序的初始化和配置
|-- routes/ # 包含路由文件,定义不同路径的路由处理程序
| |-- index.ts # 主页路由处理程序
| |-- users.ts # 用户相关路由处理程序
|-- controllers/ # 包含控制器文件,处理路由中的业务逻辑
| |-- userController.ts # 用户相关业务逻辑控制器
|-- models/ # 包含数据模型文件,定义数据库模型和数据结构
| |-- userModel.ts # 用户数据模型
|-- services/ # 包含服务层代码
| |-- BaseService.ts # 通用服务层基类
|-- views/ # 包含视图文件(如果使用模板引擎)
| |-- index.ejs # 主页视图模板
|-- config/ # 包含配置文件,如数据库连接、环境变量等
| |-- database.ts # 数据库连接配置文件
|-- public/ # 包含公共资源文件,如样式表、客户端脚本、图像等
| |-- styles/ # 样式表文件夹
| | |-- main.css # 主样式表文件
| |-- scripts/ # 客户端脚本文件夹
| | |-- main.ts # 主客户端脚本文件
| |-- images/ # 图像文件夹
| |-- logo.png # 项目 Logo 图像
|-- middlewares/ # 包含自定义中间件文件,处理请求和响应
| |-- authentication.ts # 认证中间件
|-- tests/ # 包含测试文件,用于单元测试和集成测试
| |-- unit/ # 单元测试文件夹
| | |-- userController.test.ts # 用户控制器的单元测试
| |-- integration/ # 集成测试文件夹
| |-- app.test.ts # 应用程序的集成测试
|-- uploads/ # 包含上传的文件
|-- utils/ # 包含实用工具函数或模块
| |-- helper.ts # 实用工具函数文件
|-- logs/ # 包含应用程序生成的日志文件
|-- db/ # 包含数据库脚本,如迁移文件、种子文件等
| |-- migrations/ # 数据库迁移脚本文件夹
| |-- 20240125120000_create_users_table.ts # 创建用户表的迁移脚本
| |-- seeds/ # 数据库种子文件夹
| |-- 20240125120001_seed_users.ts # 用户数据种子文件

3、代码片段(截取)

1)app.ts 入口

ts
// === (1)app.ts
import express from 'express'
import session from 'express-session'
import requestIp from 'request-ip'
import cookieParser from 'cookie-parser'
import { configureResponseMiddleware } from './middleware/responseMiddleware'

import bodyParser from 'body-parser'
import routes from './routes'
// 添加日志管理
import logger, { requestLoggerMiddleware } from './logger'
import dotenv from 'dotenv'

// 加载并配置dotenv
dotenv.config()

const app = express()

// 设置EJS作为视图引擎
app.set('view engine', 'ejs')

// 设置视图文件夹
app.set('views', __dirname + '/views')

// 配置 express-session 中间件
app.use(
  session({
    secret: 'yzt-cms-secret-key',
    resave: false,
    saveUninitialized: true,
    cookie: {
      secure: false, // 设置为 true 时,需要启用 HTTPS
      maxAge: 3600000 // 设置会话过期时间,单位为毫秒
    }
  })
)

// 使用 cookie-parser 中间件
app.use(cookieParser())

// 使用 request-ip 中间件
app.use(requestIp.mw())

app.use(bodyParser.json())

app.use(express.static(__dirname + '/public'))

configureResponseMiddleware(app)

// content-type:application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: true }))
app.use(requestLoggerMiddleware)

app.use('/', routes)

// 设置apidoc接口文档访问
app.use('/apidoc', express.static('apidoc'))
app.use('/assets', express.static('assets'))

// === 添加全局报错捕获程序,避免因为单个报错导致整个项目无法运行
app.use((err: Error, req: any, res: any, next: any) => {
  // 错误处理逻辑
  console.error(err) // 打印错误信息

  logger.error(`系统服务出现异常: ${err}, ${err.stack}`)

  // 返回合适的错误响应
  res.status(500).json({ code: 500, message: '系统服务出现异常', data: null })
})

// 设置监听端口
const PORT = process.env.PORT || 8082
app.listen(PORT, () => {
  logger.info(`服务器运行端口: ${PORT}.`)
  console.log(`服务器运行端口: ${PORT}.`)
})

2)route 路由配置入口

ts
// === (2)routes/index.ts
import express from 'express'
import tagindexRoute from './tagindex.route'
import arctypeRoute from './arctype.route'
import archivesRoute from './archives.route'
import logsRoute from './logs.route'

const router = express.Router()

const defaultRoutes = [
  {
    path: '/tagindex',
    route: tagindexRoute
  },
  // 栏目管理
  {
    path: '/arctype',
    route: arctypeRoute
  },
  // 文章管理
  {
    path: '/archives',
    route: archivesRoute
  },
  // ...略
  // 日志管理
  {
    path: '/logs',
    route: logsRoute
  }
]

defaultRoutes.forEach((route) => {
  router.use(route.path, route.route)
})

export default router

3)route 具体路由配置

ts
// === (3) routes/arctype.route.ts
import express from 'express'
import {
  cGetArctypeList,
  cGetArctypeListAll
  // ... 略
} from '../controllers/ArctypeController'

const router = express.Router()

/**
 *
 * @api {post} /arctype/create 栏目新增
 * @apiName addArctype
 * @apiGroup 栏目管理
 *
 * @apiParam  {Number} reid 上级栏目ID,顶级栏目时传0
 * @apiParam  {Number} topid 顶级栏目ID,顶级栏目时传0
 * @apiParam  {String} typename 栏目名称
 * @apiParam  {String} [channeltype] 栏目类型(可选)
 * @apiParam  {Number} sortrank 排序
 * @apiParam  {Number} ishidden 是否隐藏,0:显示,1:隐藏
 * @apiParam  {Number} [ispart]   栏目属性(可选)
 * @apiParam  {String} [seotitle] SEO标题
 * @apiParam  {String} [keywords] 关键词
 * @apiParam  {String} [description] 栏目描述
 *
 * @apiSuccess {Number} code 状态码
 * @apiSuccess {Object} data null
 * @apiSuccess {String} message 信息内容
 *
 */
router.post('/create', cCreateArctype) // 栏目管理-新增

// ... 略
export default router

4)控制器

ts
// === (4) controllers/ArctypeController.ts
import url from 'url'
import arctypeServicesInstance from '../services/ArctypeService'

// 栏目新增
export const cCreateArctype = (req: any, res: any, next: any) => {
  // ...
}

// 栏目删除
export const cDeleteArctype = (req: any, res: any, next: any) => {
  // ...
}

// 栏目更新
export const cUpdateArctype = (req: any, res: any, next: any) => {
  // 判断必填参数是否漏传
  let { id, typename } = req.body
  if (!id) {
    return res.error(400, 'Invalid parameters. id参数必传.')
  }
  if (!typename) {
    return res.error(400, 'typename参数必传')
  }
  arctypeServicesInstance
    .updateArctype(req.body)
    .then((result: any) => {
      res.status(result.code).json(result || [])
    })
    .catch((error: any) => {
      console.log('Error:', error)
      // 处理其他可能的错误情况
      next(error)
    })
}

// 栏目列表-分页
export const cGetArctypeList = (req: any, res: any, next: any) => {
  // ...
}

// 栏目列表-所有
export const cGetArctypeListAll = (req: any, res: any, next: any) => {
  // ...
}

// 栏目详情
export const cDetailArctype = (req: any, res: any, next: any) => {
  // ...
}
// 栏目字典
export const cDictArctype = (req: any, res: any, next: any) => {
  arctypeServicesInstance
    .dictArctype()
    .then((result: any) => {
      res.status(result.code).json(result || [])
    })
    .catch((error: any) => {
      console.log('Error:', error)
      // 处理其他可能的错误情况
      next(error)
    })
}

5)服务层:栏目 service

ts
// === (5) services/ArctypeService.ts
import { Op } from 'sequelize'
import BaseService from './BaseService'
import { sequelizeModel } from '../models/initModel'
import { messages } from '../config/messages'

class ArctypeService extends BaseService {
  constructor() {
    super('yb_cms_arctype')
  }
  // 数据格式化成树状结构关系
  formatArcTypeList(list: any) {
    const map: any = {}
    const roots: any = []

    // 将所有栏目对象添加到map对象中
    for (const item of list) {
      item.children = []
      map[item.id] = item
    }

    // 将子栏目对象添加到父栏目对象的children数组中
    for (const item of list) {
      const parent = map[item.reid]
      if (parent) {
        parent.children.push(item)
      } else {
        roots.push(item)
      }
    }

    return roots
  }
  // 递归遍历栏目嵌套关系
  filterRelatedItems(a: [], b: []) {
    const result: any = []
    function findRelatedItems(item: any) {
      result.push(item)
      const children = a.filter((child: any) => child.reid === item.id)
      children.forEach((child) => findRelatedItems(child))
    }
    b.forEach((item) => findRelatedItems(item))
    return result
  }

  // 栏目新增
  async createArctype(jsonData: any) {
    // 顶级ID处理
    if (jsonData.topid === 0 && jsonData.reid > 0) {
      jsonData.topid = jsonData.reid
    }
    const transaction = await sequelizeModel.transaction()
    try {
      const res: number = await this.create(jsonData, { transaction })
      // 提交事务
      await transaction.commit()
      // res: 0 表示没有记录被删除,1 表示成功删除一条记录
      if (res) {
        return this.createResponse(200, messages[200], {})
      } else {
        return this.createResponse(200, messages.notFoundItem, {})
      }
    } catch (error) {
      // 回滚事务
      if (transaction) {
        await transaction.rollback()
      }
      // 处理其他可能的错误情况
      return this.createResponse(500, messages[500], null)
    }
  }

  // 栏目删除
  async deleteArctype(id: Array<any>) {
    // ...
  }

  // 栏目更新
  async updateArctype(jsonData: any) {
    // ...
  }

  // 获取栏目-默认查询状态为显示状态
  async getList(req: any) {
    // ...
  }

  // 获取栏目列表 - 条件查询
  async getListAll(req: any) {
    // ...
  }

  // 栏目详情
  async detailArctype(id: any) {
    try {
      const result: number = await this.findByPk(id, {
        // transaction,
      })
      // 成功时返回响应对象
      return this.createResponse(200, messages[200], result)
    } catch (error) {
      // 处理其他可能的错误情况
      return this.createResponse(500, messages[500], null)
    }
  }

  async dictArctype() {
    // ...
  }
}

export default new ArctypeService()

6)基础 BaseService

ts
// === (6) services/BaseService.ts
import { Model } from 'sequelize'
import { Attributes, FindOptions, Identifier } from 'sequelize/types/model'
// models管理
import initModels from '../models/initModel'

// 封装统一的返回结构体
class Response {
  constructor(public code: number, public message: string, public data: any) {}
}

// 抽象类
abstract class BaseService {
  model: any
  modelName: string

  constructor(modelName: string) {
    this.modelName = modelName
    this.model = initModels[this.modelName as keyof typeof initModels]
  }

  protected createResponse(code: number, message: string, data: any): Response {
    return new Response(code, message, data)
  }

  // 保存
  save(options?: any) {
    return this.model.save(options)
  }
  // 新增
  create(values?: any, options?: any) {
    return this.model.create(values, options)
  }
  bulkCreate(records: any, options?: any) {
    return this.model.bulkCreate(records, options)
  }
  // 查询数据库中符合条件的所有记录
  findAll<M extends Model>(options?: FindOptions<Attributes<M>>): Promise<M[]> {
    return this.model.findAll(options)
  }
  // 查询
  findAndCountAll<M extends Model>(
    options?: FindOptions<Attributes<M>>
  ): Promise<M[]> {
    return this.model.findAndCountAll(options)
  }

  // 根据主键(Primary Key)查找单个模型实例
  findByPk(identifier: Identifier, options?: any) {
    return this.model.findByPk(identifier, options)
  }

  // T
  findOne(options?: any) {
    return this.model.findOne(options)
  }

  // 更新数据库中已有记录
  update(values: any, options: any) {
    return this.model.update(values, options)
  }
  // 更新
  bulkUpdate(values: any, options: any) {
    return this.model.bulkUpdate(values, options)
  }

  // 删除数据库中记录
  destroy(options?: any) {
    return this.model.destroy(options)
  }

  max(field: any, options?: any): any {
    return this.model.max(field, options)
  }

  // ... TODO
}

export default BaseService

7)模型 models:连接数据库

ts
// === (7) models/initModel.ts
import { Dialect, Sequelize } from 'sequelize'
import DB_INFO from '../config/db'
import { initModels } from './init-models'

export const sequelizeModel = new Sequelize(
  DB_INFO.db_name,
  DB_INFO.username,
  DB_INFO.password,
  {
    host: DB_INFO.host,
    port: DB_INFO.port,
    dialect: DB_INFO.dialect as Dialect
  }
)

var models = initModels(sequelizeModel)

export default models

4、参考资料

dede cms