基于node,快速生成对应所有数据库表的基础API,API名等于表名,实现直接API增删改查表

210 阅读10分钟
/*
    node 的 mySql  使用 (数据库)

    速查表
    https://www.runoob.com/nodejs/nodejs-express-framework.html   //Express
    https://www.runoob.com/nodejs/nodejs-mysql.html   //连接 MySQL
    https://www.runoob.com/sql/sql-tutorial.html    //写SQL

    功能需求:
    1、快速生成对应所有数据库表的基础API,API名等于表名,实现直接API增删改查表
    2、特殊API按功能开发,如登陆、上传、富文本编辑器
       
*/
// 基准路径 http://localhost/api/v1/

//建单表基准sql
// CREATE TABLE `demo` (
//   `id` TINYINT(4) NOT NULL AUTO_INCREMENT,
//   `title` VARCHAR(200) DEFAULT NULL,
//   `date` VARCHAR(50) DEFAULT NULL,
//   `value` TEXT,
//   `img` VARCHAR(200) DEFAULT NULL,
//   `images` VARCHAR(600) DEFAULT NULL,
//   `video` VARCHAR(200) DEFAULT NULL,
//   `child` VARCHAR(600) DEFAULT NULL,
//   `sort` INT(5) DEFAULT NULL,
//   `issue` INT(5) DEFAULT NULL,
//   `top` INT(5) DEFAULT NULL,
//   PRIMARY KEY (`id`)
// ) ENGINE=MYISAM AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

//Web应用框架
const express = require('express')
const app = express() //实例化express应用
app.use('/public/serves/images', express.static('public')) // 设置图片、视频、附件等静态资源存放路径
const bodyParser = require('body-parser') // post中间件,用于post解析参数
// 处理post请求数据解析
app.use(bodyParser.json()) // JSON请求
app.use(bodyParser.urlencoded({extended: false})) // 表单请求

//文件系统
const fs = require('fs') 

// 在处理模块中引入第三方解析模块
const multipart = require('connect-multiparty') 
const multipartMiddleware = multipart() // post数据解析

// cors跨域支持
const cors = require('cors') 
app.use(cors())

//mysql查询封装
const {query,sqlConfig} = require('./API/mysql')

//自定义工具函数
const {objectToSqlString,System,thisDate,verifyUserData} = require('./API/utils')

// API版本号
const versions = '/v1/'

//存储数据库所有表名,用于生成批量同名API增删改查,实现API同名操作数据库表
let apiArray = [] //['table_name','table_name']

//读取数据库所有表名(查询数据库)
async function initTable(){
   const data = await query("SELECT table_name FROM information_schema.tables WHERE table_schema='" + sqlConfig.database + "'");    
   apiArray = JSON.parse(JSON.stringify(data)).map(function(value){
      return value.table_name
   })   
}
  
/* 生成所有数据表常规增删改查API */
initTable().then(function(){ //初始化表回调
  // 批量生成增删改查API
  apiArray.forEach((item) => {
    getColumnNameAPI(item) //初始化列名查询
    getAPI(item) //查
    deleteAPI(item) // 删    
    if (item !== 'login' || item !== 'table_login'){ // 登录用户数据单独处理
      putAPI(item) // 改
      addAPI(item) // 增
    }     
  })  
})

/* 数据表列名(数据接口) */
function getColumnNameAPI(item){
  app.get(versions + item + 'Head',async function (req, res) { // 建立数据接口
    const rows = await query('SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME="' + item + '"') //列名查询
    res.json(rows)
  })
}
// 添加(数据接口)
function addAPI(item) {
  app.post(versions + item,async function (req, res) { // 建立数据接口
    let da = req.body // 请求体
    let te = '' //查询字段
    let valTe = '' //对应值
    da = objectToSqlString(da); //格式化请求参数value值
    for (let key in da) { // sql字段拼接
      if(key === 'id') continue; //ID不参与拼接(ID在数据库设置自增长)
      te += ',' + key //拼接key
      valTe += ',' + da[key] //拼接value
    }   
    te = te.slice(1)
    valTe = valTe.slice(1) //去掉第一个','
    // 'INSERT INTO oplog(username,text,date)  VALUES('admin','新增内容:主力店(成功)','2021-11-05 11:03:50')'
    const result = await query('INSERT INTO ' + item + '(' + te + ')  VALUES(' + valTe + ')')
    if(item === 'roles'){ //roles表关联login表添加用户数据
      da.rid = JSON.parse(JSON.stringify(result)).insertId //记录自增ID
      await query('INSERT INTO login(username,password,date,rid) VALUES(' + da.username + ',' + da.password + ',' + da.date + ',' + da.rid + ')')
    }
    res.status(200).json()
  })
}
// 修改内容(数据接口,id为必需)
function putAPI(item) {
  app.put(versions + item,async function (req, res) { // 建立数据接口
    let da = req.body // 请求体 数据案例 { top: false, id: [ 18, 16 ] }  || { top: false, id: 16 }
    const idUl = Array.isArray(da.id) ? da.id.join(",") : da.id //多个ID则用','合并ID
    let te = '';
    da = objectToSqlString(da); //格式化请求参数value值
    for(let key in da){    
      if(key === 'id') continue; //ID不参与拼接   
      te += "," + key + "=" + da[key] //拼接sql查询字段
    }
    te = te.slice(1) //去掉第一个','
    //UPDATE websites SET issue=1  WHERE id IN (19,15,16)  批量更新sql
    //UPDATE floor SET title="L21",sort=10,issue=1,top=0,date="2021-06-23 17:36:52" WHERE id IN (18) 多字段更新
    await query('UPDATE ' + item + ' SET ' + te + ' WHERE id IN (' + idUl + ")")
    if(item === 'roles'){ //roles表关联login表修改用户数据
      await query('UPDATE login SET username=' + da.username + ',password=' + da.password + ',date=' + da.date + ' WHERE rid IN (' + da.id + ')')
    }
    res.status(200).json()
  })
}
// 删除(数据接口)
function deleteAPI(item) {
  app.delete(versions + item,async function (req, res) { // 建立数据接口
    const da = req.query // 请求数据
    // DELETE FROM websites where id=6
    const result = await query('DELETE FROM ' + item + ' WHERE id= ' + da.id)
    if(item === 'roles'){ //roles表关联login表删除用户数据
      da.rid = JSON.parse(JSON.stringify(result)).insertId //记录自增ID
      await query('DELETE FROM login WHERE rid= ' + da.id)
    }
    res.status(200).json()
  })
}

//自定义get数据处理函数
const {stringToArray,filterIssue,keywordSearch,filterType,pagaData,idData,idLinkData,classData,dataSize,sortData} = require('./API/gets')

// get数据(数据接口)
function getAPI(item){
  // 建立数据接口
  app.get(versions + item,async function (req, res) { 
    //请求API指定表数据
    let apiTableData = await query('SELECT * FROM ' + item)

    //请求API返回的数据,这里用了深拷贝,以隔绝对数据库数据的影响
    let data = JSON.parse(JSON.stringify(apiTableData));
    /* 数组字符串转数组 */
    data = stringToArray(data)     
    //筛选发布数据(all=1)
    data = filterIssue(data,req)    
    //搜索关键字(keyword=?)
    data = keywordSearch(data,req)
    //筛选分类(type=?)
    data = filterType(data,req)
    //数据分页 (pagenum=?,pagesize=?) (返回:{list:[],sum:?})
    data = pagaData(data,req)
    //指定ID访问单条数据(id=?)
    data = idData(data,req)
    //返回多条ID相连数据(id=?,linksize=?)(返回:{list:[]})
    data = idLinkData(data,apiTableData,req)
    //指定分类(type=?)
    data = classData(data,req)
    //限制数据条数
    data = dataSize(data,req)
    //数据排序
    data = sortData(data)
    res.json(data) // 响应头返回相应查询数据
  })
}

// 系统信息API
app.get(versions + 'system', function (req, res) { // 返回系统信息
  res.json(new(System)() ) //系统信息为自定义的Class
})

// 用户登录数据接口(req.params.id(get请求参数 /:id)  req.query.id(get请求 ?id=0)   req.body.id(post请求体))
app.post(versions + 'login',async function (req, res) { // 建立数据接口
  const loginData = await query('SELECT * FROM login') //登陆数据
  res.json(verifyUserData(req.body,loginData)) // 以json形式发送响应数据
})

// 修改密码数据接口
app.put(versions + 'login',async function (req, res) { // 建立数据接口
  let reqBody = req.body // post请求数据
  const loginData = await query('SELECT * FROM login') //登陆数据
  const resData = verifyUserData(reqBody,loginData); //返回响应数据
  if(resData.meta.status === 200){//为成功响应执行修改密码操作
    resData.meta.message = "密码修改成功"
    reqBody = objectToSqlString(reqBody); //格式化请求数据
    const {setPassword,username} = reqBody
    await query('UPDATE login SET password=' + setPassword + ' WHERE username IN (' + username + ')') //修改登陆密码
    await query('UPDATE roles SET password=' + setPassword + ' WHERE username IN (' + username + ')') //关联修改角色表用户密码
  }
  res.json(resData) // 以json形式发送响应数据
})

/* neditor编辑器 */
let buf = new Buffer.alloc(3000)
fs.open(__dirname + '/ueConfig.json', 'r+', function (error, fd) { // 打开ueditor配置文件
  if (error) throw error;
  // console.log("配置文件打开成功");
  fs.read(fd, buf, 0, buf.length, 0, function (error, bytes) { // 读取配置
    if (error) throw error;
    // console.log(bytes + "  字节被读取");
    app.get(versions + 'ueditor', function (req, res) { // 文件上传
      const daOb = JSON.parse(buf.slice(0, bytes).toString())
      res.jsonp(daOb) // 传送JSONP响应
    })
  })
})

/* element文件上传 */
let response
app.post(versions + 'file_upload', multipartMiddleware, function (req, res) { // 文件上传
  //console.log(req.files.file) // 上传文件信息
  const des_file = '/images/' + thisDate() + req.files.file.originalFilename // 文件存放相对路径
  fs.readFile(req.files.file.path, function (error, data) {
    fs.writeFile(__dirname + des_file, data, function (error) { // _dirname (写入需绝对路径,把相对路径转换成绝对路径)
      if (error) throw error;
      response = {
        message: '文件上传成功',
        data: '/serves'+des_file
      }      
      //console.log(response)
      res.json(response)
    })
  })
})

// 编辑器文件上传
app.post(versions + 'ueditor', multipartMiddleware, function (req, res) { 
  // const action = req.query.action;
  // if (action == 'uploadimage' || action == 'uploadvideo' || action == 'uploadfile') { //图片/视频/附件上传判断
  const des_file = '/images/' + thisDate() + req.files.upfile.originalFilename // 文件存放相对路径
  fs.readFile(req.files.upfile.path, function (error, data) {
    fs.writeFile(__dirname + des_file, data, function (error) { // _dirname (写入需绝对路径,把相对路径转换成绝对路径)
      if (error) throw error;
      let responseImg = { //要按neditor后端请求规范返回响应数据
        code: '200',
        message: 'SUCCESS',
        url: '/serves'+des_file,
        thumbnail: '/serves'+des_file,
        title: req.files.upfile.originalFilename,
        original: req.files.upfile.originalFilename,
        error: error
      }
      res.jsonp(responseImg)
    })
  })
})

//统一的异常处理,防止程序崩溃
process.on('uncaughtException', function (err) { 
  //打印出错误
  console.log(err) 
});

// 配置服务端口
app.listen(5009, () => {
  console.log('server :localhost:5009/v1/')
})

关联模块 config.js、mysql.js、gets.js、utils.js

// config.js
const sqlConfig = { 
  host: 'localhost', // 主机地址
  user: 'root',
  password: '@kingdom10',
  database: 'www_wxking_com', // 数据库名
  port:3306
}
module.exports = {
  sqlConfig
}

//mysql.js
const mysql = require('mysql')
/* 配置数据库*/
const {sqlConfig} = require('./config')
//创建连接数据池
const pool = mysql.createPool(sqlConfig); 
//封装mysql请求
function query( sql, values ) {
  // 返回一个 Promise
  console.log(sql)
  return new Promise(( resolve, reject ) => {
    pool.getConnection(function(err, connection) {
      if (err) {
        reject( err )
      } else {
        connection.query(sql, values, ( err, rows) => {
          if ( err ) {
            reject( err )
          } else {
            resolve( rows )
          }
          // 结束会话
          connection.release()
        })
      }
    })
  })
}
module.exports = {
  query,
  sqlConfig
}
//gets.js
/*
  get数据说明文档
  1、筛选发布数据 通过(all=?)可以获取全部数据(包括待发布数据)(用于后台显示数据)
  2、数组字符串转数组
  3、筛选分类(type=?)
  4、数据分页(pagenum:当前页码,pagesize:每页条数)(pagenum=?,pagesize=?)(返回数据类型 {list:[],sum:?})
  5、指定ID访问单条数据(id=?)
  6、指定ID的多条相连数据(id=?,linksize=?)
  7、指定分类(type=?)
  8、搜索关键字(keyword=?)
  9、限制条数(size=?)
  10、数据排序
*/
/* 搜索关键字(keyword=?) */
function keywordSearch(data,req){
  if(req.query.keyword){    
    return data.filter((item)=>{
      return JSON.stringify(item).includes(req.query.keyword)
    })
  }
  return data
}

/* 指定分类(type=?) */
function classData(data,req){
  if(req.query.type && data[0] && data[0].child){
    data.filter((item)=>{
      return item.child.includes(req.query.type)
    })
  }
  return data
}

/* 限制条数,默认最多30条(size=?) */
function dataSize(data,req){
  if(req.query.size){
    return data.slice(0,req.query.size)
  }
  return data
}

/* 筛选发布数据 通过(all=?)可以获取全部数据(包括待发布数据)(用于后台显示数据) */
function filterIssue(data,req){
  if(data[0] && data[0].issue && req.query.all != 1){ 
    return data.filter(function(item){  //前台只显示发布数据
      return parseInt(item.issue) === 1;
    })
  }
  return data
}
/* 数组字符串转数组 */
function stringToArray(data){
  //所有数组字符串(原因:MYSQL以字符串形式存放数组)
  const arrayValue = ['child','child2','child3','images','images2','images3'];
  data.forEach(function(item){ 
    for(let key in item){
      if(arrayValue.includes(key) && item[key]){          
        item[key] = item[key].split(",")
      }
    }  
  })
  return data
}
//筛选分类(type=?)
function filterType(data,req){
  if(req.query.type){
    const type = req.query.type
    return data.filter((item)=>{
      return type.includes('全部') || item.child && item.child.includes(type)
    })
  } 
  return data
}
//数据分页(pagenum:当前页码,pagesize:每页条数)(pagenum=?,pagesize=?)(返回数据类型 {list:[],sum:?})
function pagaData(data,req){
  if(req.query.pagenum && req.query.pagesize){
    const pagenum = parseInt(req.query.pagenum)
    const pagesize = parseInt(req.query.pagesize)
    let pageData = {
      list:[],//指定页码分页数据
      sum:0 //数据总条数
    }         
    pageData.list = data.slice(pagenum*pagesize,(pagenum+1)*pagesize)
    pageData.sum = data.length    
    return pageData
  } 
  return data
}
//指定ID访问单条数据(id=?)
function idData(data,req){
  if(req.query.id){  
    return data.find((item)=>{
      return parseInt(item.id) === parseInt(req.query.id)
    })
  }
  return data
}
//指定ID的多条相连数据(id=?,linksize=?)
function idLinkData(data,apiTableData,req){  
  if(req.query && req.query.id && req.query.linksize){ //指定ID,返回多条相连数据
    const size = req.query.linksize    
    const index = apiTableData.findIndex((item)=>{
      return Number(item.id) === Number(req.query.id)
    })
    //深拷贝数据,不影响外部数据
    let newData = JSON.parse(JSON.stringify(apiTableData))
    newData.splice(index,1)
    let start = index - Math.ceil(size/2)
    if(start < 0 || newData.length < size){
      //如果起始索引小于0或数据数量小于请求数量,则起始索引为0
      start = 0
    }else if(start+size > newData.length){
      //如果起始索引加请求数量超过数据数量,则从后往前算起始索引
      start = newData.length - size    
      //起始索引依然不能小于0
      if(start < 0){
        start = 0
      }
    }
    return {...data,list:newData.slice(start,start+size)}
  }
  return data
}
// 排序数据
function sortData(data){
  if(!Array.isArray(JSON.parse(JSON.stringify(data)))){return data}
  return JSON.parse(JSON.stringify(data)).sort((a, b) => {
      a = new Date(a.date).getTime()
      b = new Date(b.date).getTime()
      return b - a
    }).sort((a, b) => {
      return b.sort - a.sort
    }).sort((a, b) => { //勾选TOP优先级最高
      return b.top - a.top
    })
}
module.exports = {
  filterIssue,
  stringToArray,
  filterType,
  pagaData,
  idData,
  idLinkData,
  dataSize,
  classData,
  keywordSearch,
  sortData
}
//utils.js
/*
  工具类与工具函数
*/
//获取系统信息
const os = require('os')
class System {
    constructor() {
        this.hostname = os.hostname() // 服务器名
        this.ip = this.getIPAdress() // 服务器IP
        this.node = 'node v12.18.1'
        this.location = __dirname.slice(0, __dirname.lastIndexOf('\\'))
        this.system = os.type() + ' ' + os.release()
        this.arch = os.arch()
        this.cpus = os.cpus().length
        this.uptime = parseInt(os.uptime() / 60 / 60)
        this.totalmem = parseInt((os.totalmem() - os.freemem()) / 1048576)
    }
    getIPAdress() { // 获取IP
        var interfaces = os.networkInterfaces()
        for (var devName in interfaces) {
            var iface = interfaces[devName]
            for (var i = 0; i < iface.length; i++) {
                var alias = iface[i]
                if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
                    return alias.address
                }
            }
        }
    }
} 
//格式化请求参数value值,使其符合sql查询字段规范
function objectToSqlString(da){
  for(let key in da){     
    if(da[key].constructor.toString().indexOf('Array') > 0){ //给sql的Array值转字符串并加''号
      da[key] = '\'' + da[key].toString() + '\''
    }else if(typeof da[key] === 'boolean'){//布尔值转存0、1
      da[key] = Number(da[key])
    }else if(typeof da[key] === 'string'){ //给sql的string值加''号
      da[key] = '\'' + da[key] + '\''
    }
  }
  return da
} 
//当前日期时间
function thisDate(){
  const d = new Date();
  return d.getFullYear()+''+ (d.getMonth() + 1)+''+ d.getDate()+''+d.getHours()+''+d.getMinutes()+''+d.getSeconds()
}
//用户响应数据
function verifyUserData({username,password},loginData){
  let resData = {} // 响应数据 
  /* 记录登录成功与否、记录用户名是否存在、记录密码是否正确(1:成功,0:失败) */
  const logAr = [[],[],[]]  
  loginData.forEach(function (item) {
    //记录是否登录成功
    if(username == item.username && password == item.password){
      logAr[0].push(1)  
      //添加登录成功返回头数据
      return resData = {
        data:{
          id:item.id,
          rid:item.rid,
          username:item.username,
          token:'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1MTI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.eGrsrvwHm-tPsO9r_pxHIQ5i5L1kX9RX444uwnRGaIM'
        },
        meta:{
          'status': 200,
          'message': '用户登录成功'
        }
      }
    }else{
      logAr[0].push(0)
    }     
    //记录用户名是否存在 
    if (username == item.username) {
      logAr[1].push(1)
      // 记录密码是否正确
      password == item.password ? logAr[2].push(1) : logAr[2].push(0)              
    } else {
      logAr[1].push(0)
    }
  })
  if (!logAr[0].includes(1) &&!logAr[1].includes(1)) {
    resData.meta = {
      'status': 400,
      'message': '用户名不存在'
    }
  } else if (!logAr[0].includes(1) &&!logAr[2].includes(1)) {
    resData.meta = {
      'status': 400,
      'message': '密码错误'
    }
  }
  return resData
}
module.exports = {
  objectToSqlString,
  System,
  thisDate,
  verifyUserData
}

    //PS:使用了ueditor,需做相应配置(ueConfig.json)
    {
      "imageActionName": "uploadimage",
      "imageFieldName": "upfile",
      "imageMaxSize": 5242880,
      "imageAllowFiles": [".png", ".jpg", ".jpeg", ".gif"],
      "imageCompressEnable": true,
      "imageCompressBorder": 1600,
      "imageInsertAlign": "none",
      "imageUrlPrefix": "",
      "imagePathFormat": "UpFiles/I/{yyyy}-{mm}/{dd}{time}{rand:6}",
      "scrawlActionName": "uploadscrawl",
      "scrawlFieldName": "upfile",
      "scrawlPathFormat": "UpFiles/I/{yyyy}-{mm}/{dd}{time}{rand:6}",
      "scrawlMaxSize": 2048000,
      "scrawlUrlPrefix": "",
      "scrawlInsertAlign": "none",
      "snapscreenActionName": "uploadimage",
      "snapscreenPathFormat": "UpFiles/I/{yyyy}-{mm}/{dd}{time}{rand:6}",
      "snapscreenUrlPrefix": "",
      "snapscreenInsertAlign": "none",
      "catcherLocalDomain": ["127.0.0.1", "localhost", "img.baidu.com"],
      "catcherActionName": "catchimage",
      "catcherFieldName": "source",
      "catcherPathFormat": "UpFiles/I/{yyyy}-{mm}/{dd}{time}{rand:6}",
      "catcherUrlPrefix": "/",
      "catcherMaxSize": 2048000,
      "catcherAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"],
      "videoActionName": "uploadvideo",
      "videoFieldName": "upfile",
      "videoPathFormat": "UpFiles/I/{yyyy}-{mm}/{dd}{time}{rand:6}",
      "videoUrlPrefix": "",
      "videoMaxSize": 2147483647,
      "videoAllowFiles": [".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid"],
      "fileActionName": "uploadfile",
      "fileFieldName": "upfile",
      "filePathFormat": "UpFiles/F/{yyyy}-{mm}/{filename}",
      "fileUrlPrefix": "",
      "fileMaxSize": 2147483647,
      "fileAllowFiles": [".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".rar", ".zip", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt"],
      "imageManagerActionName": "listimage",
      "imageManagerListPath": "UpFiles/I",
      "imageManagerListSize": 20,
      "imageManagerUrlPrefix": "",
      "imageManagerInsertAlign": "none",
      "imageManagerAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp"],
      "fileManagerActionName": "listfile",
      "fileManagerListPath": "UpFiles/F",
      "fileManagerUrlPrefix": "",
      "fileManagerListSize": 20,
      "fileManagerAllowFiles": [".png", ".jpg", ".jpeg", ".gif", ".bmp", ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"]
    }