/*
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"]
}