│ app.js
│ package.json
│ REAAME.md
│ yarn.lock
├─app
│ ├─controller
│ │ users.js
│ │ ...
│ └─modal
│ users.js
| ...
├─bin
│ www
├─config
│ constant.js
│ database.js
│ dbPool.js
├─dbhelper
│ users.js
├─middlewares
│ │ index.js
│ └─middleware
│ message.js
├─public
│ favicon.ico
│ index.html
├─routes
│ │ index.js
│ └─route
│ users.js
| ...
└─utils
autoLoadFile.js
datahandle.JS
index.js
# 初始化 yarn init # 安装koa yarn add koa -s # 安装 koa-onerror yarn add koa-onerror -s # 安装 koa-bodyparser yarn add koa-bodyparser -s # 安装 koa-static yarn add koa-static -s # 安装 koa-logger yarn add koa-logger -s # 安装 koa-cors yarn add koa-cors -s # 安装 koa-jwt yarn add koa-jwt -s # 安装 nodemon yarn add nodemon -s # 安装 mysql yarn add mysql -s
const Koa = require("koa"); const app = new Koa(); const onerror = require("koa-onerror"); const bodyparser = require("koa-bodyparser"); const logger = require("koa-logger"); const router = require("./routes/index"); const cors = require("koa-cors"); // 注册error onerror(app); // 注册bodyparser app.use(bodyparser()); // 注册日志 app.use(logger()); // 注册静态资源 app.use(require("koa-static")(__dirname + "/public")); // 注册自定义中间件 require("./middlewares/index")(app); // logger-handling app.use(async (ctx, next) => { const start = new Date(); await next(); const ms = new Date() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); // 注册跨域 app.use(cors()); // 注册路由 app.use(router.routes(), router.allowedMethods()); // error-handling app.on("error", (err, ctx) => { console.error("server error", err, ctx); }); module.exports = app;
koa-onerror、koa-bodyparser、koa-logger、koa-cors 等中间件为 koa 常见中间件
koa-router 和 middlewares 做了一些自定义操作,下面会介绍到
#!/usr/bin/env node /** * Module dependencies. */ var app = require("../app"); var debug = require("debug")("demo:server"); var http = require("http"); /** * Get port from environment and store in Express. */ var port = normalizePort("3000"); // app.set('port', port); /** * Create HTTP server. */ var server = http.createServer(app.callback()); /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on("error", onError); server.on("listening", onListening); /** * Normalize a port into a number, string, or false. */ function normalizePort(val) { var port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== "listen") { throw error; } var bind = typeof port === "string" ? "Pipe " + port : "Port " + port; // handle specific listen errors with friendly messages switch (error.code) { case "EACCES": console.error(bind + " requires elevated privileges"); // process.exit(1); break; case "EADDRINUSE": console.error(bind + " is already in use"); // process.exit(1); break; default: throw error; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { var addr = server.address(); var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; debug("Listening on " + bind); }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>koa2</title> </head> <body> <h2>hello, koa2!!!</h2> </body> </html>
在 app.js 中注册静态资源时我们知道将静态资源放在了 public 文件夹下
添加 favicon.ico 图标放在 public 文件夹下
"scripts": { "start": "node bin/www", "serve": "nodemon bin/www" // 热更新 },
{ "name": "koa2", "version": "1.0.0", "description": "koa2+mysql", "main": "index.js", "license": "MIT", "scripts": { "start": "node bin/www", "serve": "nodemon bin/www" }, "dependencies": { "debug": "^4.3.3", "jsonwebtoken": "^8.5.1", "koa": "^2.13.4", "koa-bodyparser": "^4.3.0", "koa-cors": "^0.0.16", "koa-jwt": "^4.0.3", "koa-logger": "^3.2.1", "koa-onerror": "^4.1.0", "koa-router": "^10.1.1", "koa-static": "^5.0.0", "mysql": "^2.18.1", "nodemon": "^2.0.15" } }
启动
yarn serve
游览器打开 http://localhost:3000/
通过 koa-static 静态资源处理,根目录默认打开/public/index.html。
访问时不需要加上 public,即 http://localhost:3000/文件名 这种形式即可访问。
使用过 webpack 的人应该知道, 通过 require.context 可以拿到符合条件的上下文,我们也模拟 context 实现一个自动加载文件的方法。
#!/usr/bin/env node const path = require("path"); const fs = require("fs"); const getPathInfo = (p) => path.parse(p); /** * @description // 递归读取文件,类似于webpack的require.context() * @param {String} directory 文件目录 * @param {Boolean} useSubdirectories 是否查询子目录,默认false * @param {array} extList 查询文件后缀,默认 ['.js'] */ const autoLoadFile = ( directory, useSubdirectories = false, extList = [".js"] ) => { const filesList = []; // 递归读取文件 function readFileList(directory, useSubdirectories, extList) { const files = fs.readdirSync(directory); files.forEach((item) => { const fullPath = path.join(directory, item); const stat = fs.statSync(fullPath); if (stat.isDirectory() && useSubdirectories) { readFileList(path.join(directory, item), useSubdirectories, extList); } else { const info = getPathInfo(fullPath); extList.includes(info.ext) && filesList.push(fullPath); } }); } readFileList(directory, useSubdirectories, extList); // 生成需要的对象 const res = filesList.map((item) => ({ path: item, data: require(item), ...getPathInfo(item), })); return res; }; module.exports = autoLoadFile;
const context = require("../utils/autoLoadFile"); const fileList = context(path.join(__dirname, "./route"), true);
const router = require("koa-router")(); const path = require("path"); const context = require("../utils/autoLoadFile"); /** * @param {Array} arr 需要注册路由的文件列表 */ function importAll(arr) { arr.forEach((key) => { // 第一个参数为统一路由前缀, 一般后端设置为 /api router.use("", key.data.routes(), key.data.allowedMethods()); }); } importAll(context(path.join(__dirname, "./route"), false)); module.exports = router;
// 引入路由自执行文件 const router = require("./routes/index"); // 注册路由 app.use(router.routes(), router.allowedMethods());
const router = require("koa-router")(); // 模块路由前缀 router.prefix("/users"); router.post("/", function (ctx, next) { ctx.body = "this a users response!"; }); /** * 用户登录接口 * @param {username} 用户名 * @param {password} 用户密码 */ router.post("/login", async (ctx) => { const request = ctx.request.body; const { username, password } = request; if (username && password) { ctx.body = { code: 200, msg: "success", data: "登录成功", }; } }); module.exports = router;
/api 为此服务路由前缀, 本项目未设置
/users 为模块路由前缀
/login 具体接口
const path = require("path"); const context = require("../utils/autoLoadFile"); /** * @param {Array} arr 需要注册中间件的文件列表 */ const install = (app) => { context(path.join(__dirname, "./middleware"), false).forEach((key) => { app.use(key.data); }); }; module.exports = install;
// 注册自定义中间件 require("./middlewares/index")(app);
// middlewares/middleware/message.js module.exports = async (ctx, next) => { ctx.res.$success = (data, code = 200) => { const _data = { code, }; if (typeof data === "object") { _data.msg = "success"; _data.data = data; } else { _data.msg = data; } ctx.body = _data; }; ctx.res.$error = (err, code = 500) => { const _data = { code, }; if (typeof err === "object") { _data.msg = "error"; _data.data = JSON.stringify(err); } else { _data.msg = err; } ctx.body = _data; }; await next(); };
router.post("/login", async (ctx) => { const request = ctx.request.body; const { username, password } = request; if (username && password) { ctx.res.$success("登录成功"); } else { ctx.res.$error("请求失败", 403); } });
// 成功 { "code":200, "msg":"success", "data":"登录成功" } // 失败 { "code":403, "msg":"error", "data":"请求失败" }
从文章一开始已经将 koa-jwt 安装
原理介绍
使用 JWT,服务器认证用户之后,会生成包含一个 JSON 对象信息的 token 返回给用户
然后客户端请求服务的时候,都要带上该 token 以供服务器做验证。服务器还会为这个 JSON 添加签名以防止用户篡改数据。通过使用 JWT,服务端不再保存 session 数据,更加容易实现扩展。
// /routes/route/users // token 标识 const { SECRET } = require("../../config/constant"); // 引入koa-jwt const { sign } = require("jsonwebtoken"); const router = require("koa-router")(); // 模块路由前缀 router.prefix("/users"); router.post("/", function (ctx, next) { ctx.body = "this a users response!"; }); /** * 用户登录接口 * @param {username} 用户名 * @param {password} 用户密码 */ router.post("/login", async (ctx) => { const request = ctx.request.body; const { username, password } = request; if (username && password) { // 获取用户信息 let res = await findUserServe({ phone: username }, [username]); if (res.result && res.data.length == 0) { return ctx.res.$error("用户不存在", 404); } else if (res.result && res.data[0].password != password) { return ctx.res.$error("密码不正确", 404); } else { let { id, phone, password, name } = res.data[0]; const token = sign( { id: id, phone: phone, password, name: name }, SECRET, { expiresIn: "24h", } ); return ctx.res.$success({ token }); } } }); module.exports = router;
SECRET 是一个常量,
也是一个标识,
也需要通过这个标识去解析请求的
token 信息
const { SECRET } = require("../../config/constant"); const jwt = require("koa-jwt")({ secret: SECRET }); const router = require("koa-router")(); router.prefix("/users"); const userInfo = async (ctx) => { const request = ctx.request.body; const { user_id } = request; const { phone } = ctx.state.user; let params = { id: ctx.state.user.id }; if (user_id) { params.id = user_id; } let userRes = await findUserWeb(params, [params.id]); if (res.result && res.data.length == 0) { return ctx.res.$error("用户不存在", 404); } else if (res.result) { let target = { ...res.data[0], }; return ctx.res.$success(target); } else { return ctx.res.$error("请求失败"); } }; router.get("/info", jwt, userInfo); module.exports = router;
后端针对 token 登录鉴权, 只需要做这部分就足够了, 在项目部门会接入用户权限、角色权限、菜单权限、数据权限等。但针对 jwt,做完这两点已经可以应付大部分的业务场景
/** * 处理字符串 role: ['id', { name: 'role_name' }] 转换为 role.name as 'role_name' */ function handleAs(params, table) { let _str = ""; if (!params instanceof Array) return new Error(`${table}表里字段 as 别名需要数组[{name: "别名"}]形式`); let _params = [...params].splice(1); _params.forEach((ele) => { if (ele instanceof Object) { for (let key in ele) { let asField = `${table}.${key} as '${ele[key]}'`; _str += _str ? ", " + asField : asField; } } else { return new Error('as 别名需要数组[{name: "别名"}]形式'); } }); return _str; } /** * 关联表: 将对象的key转换为用逗号拼接的字符串 */ function connectTables(options) { let keys = Object.keys(options); return keys.join(); } /** * 关联字段: 处理两表查询时关联字段 */ function contentFields(options) { if (Object.keys(options).length === 1) return ""; let str = "", count = 0, fieldList = [], frist = ""; for (let key in options) { let field = [...options[key]].splice(0, 1)[0]; if (count === 0 && field.split(",").length > 1) { frist = key; fieldList = field.split(",") || []; } if (count > 0 && fieldList.length > 1) { str += str ? ` and ${frist}.${fieldList[count - 1]}=${key}.${field}` : `${frist}.${fieldList[count - 1]}=${key}.${field}`; } if (Object.keys(options).length > 0 && fieldList.length === 0) { str += str ? `=${key}.${field}` : `${key}.${field}`; } count++; } count = 0; return Object.keys(options).length > 1 ? "where " + str : ""; } /** * 查询字段: 统一处理查询字段 */ function composeFields(options) { let str = ""; for (let key in options) { let status = options[key].length <= 1; if (status) { let fleld = `${key}.*`; str += str ? ", " + fleld : fleld; } else { let asField = `${key}.*, ${handleAs(options[key], key)}`; str += str ? ", " + asField : asField; } } return str; } class SQL { constructor(options) { this.$options = options; } queryAll(queryData, type) { return `select * from ${connectTables(this.$options)} ${contentFields( this.$options )}${ Object.keys(this.$options).length > 1 ? " and " + handleQueryAllKeys(queryData, Object.keys(this.$options)[0]) : Object.keys(handleQueryKeyValue(queryData)).length > 0 ? "where " + handleQueryAllKeys(queryData, Object.keys(this.$options)[0]) : "" } ${limit(queryData.currentPage, queryData.pageSize, type)};`; } query(queryData) { return `select ${composeFields(this.$options)} from ${connectTables( this.$options )} ${contentFields(this.$options)}${handleQueryKeys(queryData)}`; } add(table = this.$option.table) { return `insert into ${table} set ?` + ";"; } edit(params, where, table = this.$option.table) { return `update ${table} set ${handleEmptyStringKeys(params, table, ",")}${ where ? " where " + handleEmptyStringKeys(where, table) : "" }`; } delete(where, table = this.$option.table) { return ( `delete from ${table} where ${handleEmptyStringKeys(where, table, "?")}` + ";" ); } }
queryAll 查看全部字段
query
可查全部或可选字段
add 插入edit 编辑delete 删除其中有部分函数是放在了公共函数里, 如处理 key, 或处理 value 的函数, 此部分会在下面公共函数里提及.
const { SQL } = require("../../utils/sqlhandle"); class User extends SQL { constructor(options = {}) { super(options); //继承父类中的name属性 this.options = options; //又有自己的属性 } } let user = new User({ user_d_p_r_view: [], }); user.queryAll(); user.query(); user.add("user"); user.edit("user"); user.delete("user");
/** * 使用 [] 仅取key作为表名 * 使用 ['id'] 第一个字段作为关联表的关联字段 * 使用 ['id','name','age'] 使用id作为关联字段以获取key表的name,age 列数据 */ let params = { appro_flow: ["flow_no, user_id, flow_no", { status: "audit_status" }], appro_flow_detail: ["flow_no", { status: "appro_status" }], user_d_p_r_view: ["id"], induction: ["flow_no"], }; let sql = new SQL(params); /* 单表直接用字段名, 多表需要指定表名 */ sql.queryAll({ `user_d_p_r_view.name`: "白敬亭", pageSize: 10, currentPage: 1 }); sql.query({name: "白敬亭", pageSize: 10, currentPage: 1}) sql.add("user") user.edit("user"); user.delete("user"); module.exports = { users: sql }
以上只是会动态生成 SQL 语句, 并不会直接请求数据库返回数据, 需要自己在派生类或控制类中去实现处理请求和返回响应体的函数.
config/database.js
/** * 数据库配置文件 */ const MYSQL_CONFIG = { host: "localhost", // 可以是本地地址,也可以设置成远程地址 user: "root", // 数据库用户名 password: "root", // 数据库密码 port: "3306", // 数据库端口 database: "admin_db", // 数据库名 dateStrings: true, // 强制日期类型(TIMESTAMP, DATETIME, DATE)以字符串返回 }; module.exports = MYSQL_CONFIG;
const mysql = require("mysql"); const MYSQL_CONFIG = require("./database"); const pools = {}; /** * 连接池是否已存在,不存在创建连接池 */ if (!pools.hasOwnProperty("data")) { pools["data"] = mysql.createPool(MYSQL_CONFIG); } const query = (sql, values) => { return new Promise((resolve, reject) => { pools["data"].getConnection((err, connection) => { if (err) { console.log(err, "数据库连接失败"); } else { console.log("数据库连接成功"); connection.query(sql, values, (err, results) => { if (err) { reject(err); } else { connection.release(); resolve({ result: true, data: results, }); } }); } }); }); }; module.exports = { query, };
const { query } = require("../../config/dbPool"); const { users } = require("../modal/users"); /** * 用户登录接口 * @param {phone} 用户名 * @param {password} 用户密码 */ const login = async (ctx) => { const request = ctx.request.body; const { phone, password } = request; if (phone) { let res = query(users.query({ phone: phone }), [phone]); if (res.result && res.data.length == 0) { return ctx.res.$error("用户不存在", 404); } else if (res.result && res.data[0].password != password) { return ctx.res.$error("密码不正确", 404); } else if (res.result) { let { id, name } = res.data[0]; const token = sign( { id: id, phone: phone, password, name: name }, SECRET, { expiresIn: "24h", } ); // const { phone } = ctx.state.user; // 使用token中信息 return ctx.res.$success({ token, }); } else { return ctx.res.$error("请求失败"); } } else { return ctx.res.$error("请输入用户名和密码", 400); } };
新建 utils/datahandle.js
原理
是将含有 pid 一维数组, 通过递归将 id 与 pid 相等的数据处理成级联关系。
pid 为 0 或没有 pid 的数据为第一层级, 再找到 pid 与自身 id 相等的数据形成上下级关系. 递归直至找不到。
应用场景
用户所拥有的权限菜单
级联关系的菜单列表
/** * 通过特定字段将一维数组转化为带级联关系的多维数组 * @param {*} list 一维数据源 * @param {*} options label(string) 实际展示label字段 * @param {*} options value(string) 实际绑定的value字段 * @param {*} options pid(string) 实际父级id字段 * @param {*} options more(boolean) true: 展示全部 false: 只展示四个字段 * @param {*} options children(string) 实际 * @returns 带级联关系的多维数组 */ function transformTree(list, options) { if (!list instanceof Array) return new Error(`transformTree 函数数据源需要数组形式`); options = options || { id: "id", pid: "pid", children: "children", }; let tree = []; for (let i = 0, len = list.length; i < len; i++) { if (list[i][options.pid] == 0 || !list[i][options.pid]) { let item = composeTree(list[i], list, options); tree.push({ ...item }); } } return tree; } function composeTree(parent, list, options) { if (!list instanceof Array) return new Error(`composeTree 函数数据源需要数组形式`); let children = []; for (let i = 0, len = list.length; i < len; i++) { if (list[i][options.pid] == parent[options.id]) { let item = composeTree(list[i], list, options); children.push({ ...item }); } } if (children.length) { parent[options.children] = children; } return parent; }
原理
两个数据源,一个是数据库的数据源,一个是请求的数据源(可以是逗号分隔的 ids)
通过 key 字段, 分析出两份数据源中共同含有的不需要动, 数据库的数据源里除了共同含有的其余需要删除(delIds), 请求的数据源里除了共同含有的其余需要新增(addIds)
/** * 通过特定字段匹配出列表中应该新增及删除的数据项的id, 以更新传入的字符串 * @param {*} list 目标数据源 * @param {*} target 传入的逗号分隔的字段 * @param {*} key 特定字段 * @returns delIds 需要删除的ids字符串 * @returns addIds 需要新增的ids字符串 */ function updateStr(list, target, key) { const ids = target.split(","); const targetMap = {}; const delIds = []; list.forEach((item) => { if (ids.includes(String(item[key]))) { targetMap[item[key]] = true; } else { delIds.push(item[key]); } }); const addIds = ids.filter((id) => !targetMap[id]); return { delIds: delIds.toString(), addIds: addIds.toString(), }; }
原理
通过 ids 动态生成对应数量的二维数组
应用场景
批量插入
/** * 通过ids生成对应数量的二维数组, 用于批量插入 * @param {*} ids 逗号分隔的id字符串 * @param {*} callback 用于处理参数 * @returns 二维数组 */ function transformTwoArray(ids, callback) { return ids.split(",").reduce((pre, cur, index) => { let target = [...callback(cur, index)]; pre.push(target); return pre; }, []); } /* 使用 */ let params = { creator: "前端小溪" , create_time: new Date() } transformTwoArray('1,2,3', (item, index) => { return Object.values({ ...params, name: `前端小溪${index+1}`, id: item }); }),
通过上面的调用就会生成 3 条数据
| id | name | creator | create_time |
|---|---|---|---|
| 1 | 前端小溪 1 | 前端小溪 | 2023-03-09 15:41:41 |
| 2 | 前端小溪 2 | 前端小溪 | 2023-03-09 15:41:41 |
| 3 | 前端小溪 3 | 前端小溪 | 2023-03-09 15:41:41 |
其实该函数更适合用于用户绑定多角色, 角色绑定多菜单, 菜单绑定多权限, 适用于 1 : n 的关系中的批量插入。
原理
通过某一属性去辨识数组的重复数据,返回经去重处理过的数组
/** * 通过某个 prop 使对象数组去重 * @param {*} arr 数据源 * @param {*} prop 特定字段 * @returns 去重后的新数组 */ function dedup(arr, prop) { let obj = {}; return arr.reduce(function (preValue, item) { obj[item[prop]] ? "" : (obj[item[prop]] = true && preValue.push(item)); return preValue; }, []); }
主要用户审批表、审批详情表、审批抄送表、业务表的关联的唯一编号
/** * 生成随机编号 len为位数,date:true表示添加日期 */ const randomLenNum = (len, date) => { let random = ""; random = Math.ceil(Math.random() * 100000000000000) .toString() .substr(0, len ? len : 4); if (date) random = random + Date.now(); return random; };
新建 utils/index.js
const whiteList = ["currentPage", "pageSize"]; /** * 字符串去空 * @param {*} value 字符串值 * @returns 收尾去空的字符串 * 用法: let string = trim(value) */ const trim = (value) => { if (typeof value === "string") { return value.replace(/^\s+|\s+$/g, ""); } return value; }; /** * 获取当前年-月-日 时-分-秒 */ const getTime = (date = new Date()) => { var y = date.getFullYear(); var m = date.getMonth() + 1; m = m < 10 ? "0" + m : m; var d = date.getDate(); d = d < 10 ? "0" + d : d; var h = date.getHours(); h = h < 10 ? "0" + h : h; var minute = date.getMinutes(); minute = minute < 10 ? "0" + minute : minute; var second = date.getSeconds(); second = second < 10 ? "0" + second : second; return y + "-" + m + "-" + d + " " + h + ":" + minute + ":" + second; }; /** * 获取当前年-月-日 */ const getDate = (date = new Date()) => { var y = date.getFullYear(); var m = date.getMonth() + 1; m = m < 10 ? "0" + m : m; var d = date.getDate(); d = d < 10 ? "0" + d : d; return y + "-" + m + "-" + d; }; /** * 通过两个日期, 获取中间的工作日 */ const getDates = (start_time, end_time, day) => { const one_day = 86400000; const isWeekday = (date) => date.getDay() % 6 !== 0; let start = new Date(start_time).getTime(); let end = new Date(end_time).getTime(); if (start > end) return; let length = (end - start) / one_day; let result = []; for (let i = 0; i <= length; i++) { if (isWeekday(new Date(start + i * one_day))) { result.push(getDate(new Date(start + i * one_day))); } } return { result, resultStr: result.toString(), status: (day && result.length == day) || false, }; }; /** * 通过生日获取年龄 */ const getAge = (date) => { var birthday = new Date(date.replace(/-/g, "/")); var d = new Date(); var age = d.getFullYear() - birthday.getFullYear() - (d.getMonth() < birthday.getMonth() || (d.getMonth() == birthday.getMonth() && d.getDate() < birthday.getDate()) ? 1 : 0); return age; }; /** * 解析url ? 后的参数 * @param {*} url * @returns */ function transformUrl(url) { let obj = {}; let arr = url.split("&"); arr.forEach((item) => { let param = item.split("="); obj[param[0]] = param[1]; }); return obj; } /** * 生成随机编号 len为位数,date:true表示添加日期 */ const randomLenNum = (len, date) => { let random = ""; random = Math.ceil(Math.random() * 100000000000000) .toString() .substr(0, len ? len : 4); if (date) random = random + Date.now(); return random; }; /** * 全量查询处理字符串: 将{ is_delete: "0",name: '事业', };转换为 department.is_delete=? and department.name=? */ const handleQueryAllKeys = (params, table, sign = "and") => { let str = ""; for (let key in params) { if ( !whiteList.includes(key) && typeof params[key] !== "undefined" && params[key] !== "" ) { str += str ? ` ${sign} ${table}.${key}=?` : `${table}.${key}=?`; } } return str; }; /** * 查询处理字符串: 将{ is_delete: "0",name: '事业', };转换为 department.is_delete=? and department.name=? */ const handleQueryKeys = (params) => { let str = ""; for (let key in params) { if ( !whiteList.includes(key) && typeof params[key] !== "undefined" && params[key] !== "" ) { str += ` and ${key}=?`; } } return str; }; /** * 查询处理字符串: 将{ is_delete: "0",name: '事业', };转换为 ['0','事业'] */ const handleQueryValues = (params) => { let target = {}; for (let key in params) { if ( !whiteList.includes(key) && typeof params[key] !== "undefined" && params[key] !== "" ) { target[key] = params[key]; } } return Object.values(target); }; /** * 查询处理字符串: 剔除为undefined, 白名单, 空字符的数据项 */ function handleQueryKeyValue(params) { let target = {}; for (let key in params) { if ( !whiteList.includes(key) && typeof params[key] !== "undefined" && params[key] !== "" ) { target[key] = params[key]; } } return target; } /** * 查询处理字符串: 剔除为undefined, 空字符的数据项 */ function handleWhiteListKeyValue(params) { let target = {}; for (let key in params) { if (typeof params[key] !== "undefined" && params[key] !== "") { target[key] = params[key]; } } return target; } /** * 不处理带有空字符串的数据项: 将{ is_delete: "0",name: '事业', };转换为 department.is_delete=? and department.name=? */ const handleEmptyStringKeys = (params, table, sign = "and") => { let str = ""; for (let key in params) { if (!whiteList.includes(key) && typeof params[key] !== "undefined") { str += str ? ` ${sign} ${table}.${key}=?` : `${table}.${key}=?`; } } return str; }; /** * 不处理带有空字符串的数据项: 将{ is_delete: "0",name: '事业', };转换为 ['0','事业'] */ const handleEmptyStringValues = (params) => { let target = {}; for (let key in params) { if (!whiteList.includes(key) && typeof params[key] !== "undefined") { target[key] = params[key]; } } return Object.values(target); }; /** * 处理带有空字符串的数据项: 剔除为undefined, 白名单的数据项 */ function handleEmptyStringKeyValue(params) { let target = {}; for (let key in params) { if ( !whiteList.includes(key) && typeof params[key] !== "undefined" && params[key] !== "" ) { target[key] = params[key]; } } return target; } /** * 实现分页 * @param {*} obj * @returns */ function limit(currentPage, pageSize, type = "desc", field = "id") { if (!currentPage && !pageSize) return ""; return `order by ${field} ${type} limit ${currentPage},${pageSize}`; } module.exports = { trim, getTime, getDate, getDates, getAge, transformUrl, limit, randomLenNum, handleQueryAllKeys, handleQueryKeys, handleQueryValues, handleQueryKeyValue, handleWhiteListKeyValue, handleEmptyStringKeys, handleEmptyStringValues, handleEmptyStringKeyValue, };
部分项目常用的公共函数, 如去空、日期格式、时间格式、计算工作日、通过生日计算年龄、解析 url、生成随机编号
主要是将对象(键值对)形式的数据处理成 SQL 中需要的各种格式的数据
config/constant.jsmodule.exports = { SECRET: "KOA", // token USER: "user", // 用户表 ROLE: "role", // 角色表 MENU: "menu", // 菜单表 PERMISSION: "permission", // 权限表 DEPARTMENT: "department", // 部门表 POSITION: "position", // 岗位表 DICTIONARY: "dictionary", // 字典类型表 DICTIONARY_DETAIL: "dictionary_detail", // 字典详情表 AUDIT: "appro_flow", // 审核主表 AUDIT_DETAIL: "appro_flow_detail", // 审核详情表 INDUCTION: "induction", // 入职表 REGULARIZATION: "regularization", // 转正表 CHANGESALARY: "changesalary", // 调薪表 POSTTRANSFER: "posttransfer", // 转岗表 QUIT: "quit", // 离职表 LEAVE: "leave_apply", // 请假表 OVERTIME: "overtime", // 加班表 ATTENDANCE: "attendance", // 考勤表 REPLACEMENT: "replacement", // 补卡表 ARTICLE: "article", // 文章表 };
够以公司现有具体工作流程搭建一套工作流系统, 实现日常考勤、申请审批、 公文处理、工作流转和文档管理的自动化,并建立一个内部信息发布平台。同时也希望行政、人事,客户,项目等在工作量系统中一起管理。
通过对用户需求的分析,要求本系统具有以下功能:
具体来看,要求本系统具有以下功能版本:登录鉴权、基础配置、系统管理、申请审批、考勤管理。系统整体的简单关系图如下所示:
各模块具体的需求描述如下:
(1)用户登录鉴权模块
实现用户登录、注册、修改密码、退出登录功能;
(2)基础配置
实现文章管理模块,可查看公司政策、公司公告等信息详情、修改、删除;
实现数据字典模块,可增加、修改、删除、查询、启禁用;
(3)系统管理
实现用户管理模块,可增加、修改、删除、查询、启禁用、绑定角色;
实现角色管理模块,可增加、修改、删除、查询、启禁用、绑定菜单;
实现菜单管理模块,可增加、修改、删除、查询、启禁用、绑定权限;
实现权限管理模块,可增加、修改、删除、查询;
实现部门管理模块,可增加、修改、删除、查询;
实现职位管理模块,可增加、修改、删除、查询;
(4)申请审批
实现我的申请、我的审批、抄送我的管理;
实现入职管理模块,可申请、撤销、审核(同意、拒绝)、分配权限;
实现转正管理模块,可申请、撤销、审核(同意、拒绝)、查看;
实现调薪管理模块,可申请、撤销、审核(同意、拒绝)、查看;
实现转岗管理模块,可申请、撤销、审核(同意、拒绝)、查看;
实现离职管理模块,可申请、撤销、审核(同意、拒绝)、查看;
(5)考勤管理
实现请假管理模块,可申请、撤销、审核(同意、拒绝)、查看;
实现考勤管理模块,可签到、签退、审核、修改、删除、查看;
实现加班管理模块,可申请、撤销、审核(同意、拒绝)、查看;
实现补卡管理模块,可申请、撤销、审核(同意、拒绝)、查看;
(1)登录鉴权流程分析
(2)申请审批流程分析
(3)考勤管理流程分析
(1)数据流图(顶层图)
(2)数据流图符号含义
(3)详细数据流图
(1)角色关系
(2)系统总体用例图
(1)登录鉴权 E-R 图
(2)申请审批 E-R 图
(3)考勤管理 E-R 图
根据转换规则,将 E-R 模型转换成关系模型
(1)用户表(ID、员工编号、姓名、手机号、年龄、性别、部门 ID、职位 ID、账号状态、角色、密码、头像)主键:ID,外键:部门 ID、职位 ID、角色 ID;
(2)部门表(部门 ID、部门编号、部门名称、描述)主键:部门 ID;
(3)职位表(职位 ID、职位编号、职位名称、描述)主键:职位 ID;
(4)角色表(角色 ID、角色编号、角色名称、描述、角色状态、菜单 ID)主键:角色 ID,外键:菜单 ID;
(5)用户角色表(用户 ID、角色 ID)主键:用户 ID 和角色 ID(联合主键);
(6)菜单表(菜单 ID、菜单编号、菜单名称、路由 path、路由 name、描述、菜单 icon、菜单状态、权限 ID)主键:菜单 ID,外键:权限 ID;
(7)角色菜单表(角色 ID、菜单 ID)主键:角色 ID 和菜单 ID(联合主键);
(8)权限表(权限 ID、权限编号、权限名称)主键权限 ID;
(9)菜单权限表(菜单 ID、权限 ID)主键:菜单 ID 和权限 ID(联合主键);
(10)文章表(文章 ID、文章标题、文章类型、文章发布人、发布时间、发布内容)主键:文章 ID;
(11)字典类型表(类型 ID、类型名称、类型 code、类型状态)主键:类型 ID;
(12)字典详情表(字典值 ID、字典值名称、字典值 value、字典值状态、字典值类型)主键:字典值 ID,外键:字典值类型;
(13)考勤表(考勤 ID、用户 ID、日期、签到时间、签退时间、考勤类型)主键:考勤 ID,外键:用户 ID;
(14)审批主表(审批编号、标题、审批类型、申请人、添加时间、审核状态)主键:审批编号,外键:用户 ID(申请人);
(15)审批抄送表(审批编号、抄送人、抄送时间)主键:审批编号和用户 ID(抄送人);
(16)审批详情表(审批编号、审核人、审核时间、审核顺序、审核备注、审核状态)主键:审批编号和用户 ID(审核人);
(17)入职表(审批编号、用户 ID、身份证号、身份证地址、生日、最高学历、邮箱、备注、紧急联系人、紧急联系电话、紧急联系地址、开户行名、银行卡号、账户名、基本工资、入职日期)主键:审批编号和用户 ID(联合主键);
(18)调薪表(审批编号、用户 ID、调薪日期、调薪类型、原基本工资、调薪薪资、调薪原因)主键:审批编号和用户 ID(联合主键);
(19)转正表(审批编号、用户 ID、转正日期、转正类型、个人总结)主键:审批编号和用户 ID(联合主键);
(20)转岗表(审批编号、用户 ID、调动日期、转岗类型、原部门 ID、原职位 ID、现部门 ID、现职位 ID、调动原因)主键:审批编号和用户 ID(联合主键);
(21)离职表(审批编号、用户 ID、离职日期、离职类型、是否黑名单、离职原因)主键:审批编号和用户 ID(联合主键);
(22)请假表(审批编号、用户 ID、开始时间、结束时间、请假类型、缺勤天数、原因)主键:审批编号和用户 ID(联合主键);
(23)加班表(审批编号、用户 ID、开始时间、结束时间、加班类型、加班时长、加班原因)主键:审批编号和用户 ID(联合主键);
(24)补卡表(审批编号、用户 ID、开始时间、结束时间、补卡时长、补卡类型、补卡原因)主键:审批编号和用户 ID(联合主键);
user_role_viewSELECT `user`.`id` AS `id`, `user`.`no` AS `no`, `user`.`name` AS `name`, `user`.`sex` AS `sex`, `user`.`age` AS `age`, `user`.`phone` AS `phone`, `user`.`department_id` AS `department_id`, `user`.`position_id` AS `position_id`, `user`.`status` AS `status`, `user`.`state` AS `state`, `user`.`is_delete` AS `is_delete`, `user`.`role_ids` AS `role_ids`, `user`.`avatar` AS `avatar`, group_concat( `role`.`name` SEPARATOR ',' ) AS `role_names` FROM ( `user` LEFT JOIN `role` ON ( ( find_in_set( `role`.`id`, `user`.`role_ids` ) > 0 ) ) ) GROUP BY `user`.`id`
为了更方便展示部分字段的数据及通过role_ids获取到逗号分隔的角色名称
user_d_p_r_viewSELECT `user_role_view`.`id` AS `id`, `user_role_view`.`no` AS `no`, `user_role_view`.`name` AS `name`, `user_role_view`.`sex` AS `sex`, `user_role_view`.`age` AS `age`, `user_role_view`.`phone` AS `phone`, `user_role_view`.`department_id` AS `department_id`, `user_role_view`.`position_id` AS `position_id`, `user_role_view`.`status` AS `status`, `user_role_view`.`state` AS `state`, `user_role_view`.`is_delete` AS `is_delete`, `user_role_view`.`role_ids` AS `role_ids`, `user_role_view`.`role_names` AS `role_names`, `user_role_view`.`avatar` AS `avatar`, `department`.`name` AS `department_name`, `position`.`name` AS `position_name` FROM ( ( `user_role_view` JOIN `department` ) JOIN `position` ) WHERE ( ( `user_role_view`.`department_id` = `department`.`id` ) AND ( `user_role_view`.`position_id` = `position`.`id` ) )
为了能直接拿到带部门、职位、角色的数据
modal/users.jsconst { SQL } = require("../../utils/sqlhandle"); const { handleQueryValues, handleEmptyStringKeys, } = require("../../utils/index"); const { query } = require("../../config/dbPool"); const users = new SQL({ user_d_p_r_view: [], // 视图(用户相对完整的数据) }); const findUserServe = async (target, params) => { let res = JSON.parse( JSON.stringify( await query( `select * from user where ${handleEmptyStringKeys(target, "user")}`, handleQueryValues(params) ) ) ); return res; }; const findUserWeb = async (target, params) => { let res = JSON.parse( JSON.stringify( await query(users.queryAll(target), handleQueryValues(params)) ) ); return res; }; const findUserRole = async (target, params) => { let res = JSON.parse( JSON.stringify( await query(userRole.queryAll(target), handleQueryValues(params)) ) ); return res; }; const addUserRoleSql = (params, role_id) => { return `insert into user_role(${params}, ${role_id}) VALUES ?`; }; const delUserRoleSql = (user_id, role_id, ids) => { return `delete from user_role where user_id=${user_id} and ${role_id} in (${ids})`; }; module.exports = { findUserServe, findUserWeb, findUserRole, delUserRoleSql, addUserRoleSql, users, };
findUserServe
查到用户表的所有信息,
为了方便后端接口使用,对比密码、薪资等私密信息
findUserWeb
用于前台页面展示,
用户信息详情等
findUserRole
查询用户所拥有的角色接口使用
addUserRoleSql
绑定角色时批量插入接口使用
delUserRoleSql
绑定角色时批量删除接口使用
controller/users.jsconst { sign } = require("jsonwebtoken"); const { SECRET, USER } = require("../../config/constant"); const { findUserServe, findUserWeb, findUserRole, delUserRoleSql, addUserRoleSql, users, } = require("../modal/users"); const { query } = require("../../config/dbPool"); const { getTime } = require("../../utils/index"); const { handleQueryKeyValue, handleQueryValues, handleWhiteListKeyValue, handleEmptyStringValues, handleEmptyStringKeyValue, } = require("../../utils/index"); const { updateStr, transformTwoArray } = require("../../utils/datahandle"); /** * 用户登录接口 * @param {phone} 用户名 * @param {password} 用户密码 */ const login = async (ctx) => { const request = ctx.request.body; const { phone, password } = request; if (phone) { let res = await findUserServe({ phone: phone }, [phone]); if (res.result && res.data.length == 0) { return ctx.res.$error("用户不存在", 404); } else if (res.result && res.data[0].password != password) { return ctx.res.$error("密码不正确", 404); } else if (res.result) { let { id, name } = res.data[0]; const token = sign( { id: id, phone: phone, password, name: name }, SECRET, { expiresIn: "24h", } ); // const { phone } = ctx.state.user; // 使用token中信息 return ctx.res.$success({ token }); } else { return ctx.res.$error("请求失败"); } } else { return ctx.res.$error("请输入用户名和密码", 400); } }; /** * 修改密码 */ const editPassword = async (ctx) => { const request = ctx.request.body; const { phone, password } = request; if (phone) { let res = await findUserServe({ phone: phone }, [phone]); if (res.result && res.data.length == 0) { return ctx.res.$error("用户不存在", 404); } const params = { password: password, }; const userRes = await query(users.edit(params, { phone }, USER), [ ...handleEmptyStringValues(params), phone, ]); if (userRes.result) { return ctx.res.$success(userRes.data); } else { return ctx.res.$error("请求失败"); } } }; /** * 获取用户信息 | 验证用户是否登录 * @param {phone?} 用户名可选, * 传则返回对应用户信息,不传默认返回token中用户信息 */ const userInfo = async (ctx) => { const request = ctx.request.body; const { user_id } = request; const { phone } = ctx.state.user; let params = { id: ctx.state.user.id }; if (user_id) { params.id = user_id; } let res = await findUserServe(params, [params.id]); let userRes = await findUserWeb(params, [params.id]); if (res.result && res.data.length == 0) { return ctx.res.$error("用户不存在", 404); } else if (res.result) { let target = { ...res.data[0], }; userRes.data.length > 0 && (target = { ...res.data[0], ...userRes.data[0] }); return ctx.res.$success(target); } else { return ctx.res.$error("请求失败"); } }; /** * 获取用户列表 * @param {*} ctx */ const userList = async (ctx) => { const request = ctx.request.body; const params = { currentPage: 0, pageSize: 10, is_delete: "0", ...request, }; let total = await findUserWeb( handleQueryKeyValue(params), handleQueryValues(params) ); request.pageSize && (params.pageSize = Number(request.pageSize)); request.currentPage && (params.currentPage = (request.currentPage - 1) * request.pageSize); let res = await findUserWeb( handleWhiteListKeyValue(params), handleQueryValues(params) ); if (res.result) { return ctx.res.$success({ ...params, list: res.data, total: total.data.length, }); } else { return ctx.res.$error("请求失败"); } }; /** * 注册(新增)用户 */ const addUser = async (ctx) => { const request = ctx.request.body; const { phone, password, remember } = request; let data = await findUserServe({ is_delete: "0", phone }, ["0", phone]); if (data.result && data.data.length > 0) { return ctx.res.$error("用户已存在"); } let params = { phone, password, }; const res = await query(users.add(USER), handleEmptyStringKeyValue(params)); if (res.result) { const token = sign({ phone, password, id: res.data.insertId }, SECRET, { expiresIn: remember ? "720h" : "24h", }); return ctx.res.$success({ token }); } else { return ctx.res.$error("请求失败"); } }; /** * 编辑用户 */ const editUser = async (ctx) => { const request = ctx.request.body; const id = ctx.params.id; const user = ctx.state.user; if (!id) return ctx.res.$error("缺少id", 400); const params = { name: request.name, sex: request.sex, age: request.age, department_id: request.department_id, position_id: request.position_id, role_ids: request.role_ids, modifier: user.name, modified_time: getTime(), }; // 更新用户所属的角色 const roleRes = await findUserRole( handleWhiteListKeyValue({ user_id: id }), handleQueryValues({ user_id: id }) ); const { addIds, delIds } = updateStr( roleRes.data, request.role_ids, "role_id" ); addIds && addUserRole( { user_id: id, creator: user.name, create_time: getTime() }, addIds ); delIds && delUserRole({ user_id: id }, delIds); // 编辑 const res = await query(users.edit(params, { id }, USER), [ ...handleEmptyStringValues(params), id, ]); if (res.result) { return ctx.res.$success(res.data); } else { return ctx.res.$error("请求失败"); } }; /** * 更新状态 - 启用禁用 */ const editStatus = async (ctx) => { const request = ctx.request.body; const id = ctx.params.id; if (!id) return ctx.res.$error("缺少id", 400); let params = { status: request.status, }; // 编辑 const res = await query(users.edit(params, { id }, USER), [ ...handleEmptyStringValues(params), id, ]); if (res.result) { return ctx.res.$success(res.data); } else { return ctx.res.$error("请求失败"); } }; /** * 删除用户 */ const deleteUser = async (ctx) => { const id = ctx.params.id; if (!id) return ctx.res.$error("缺少id", 400); // const res = await query(users.delete({ id }, USER)); const res = await query(users.edit({ is_delete: "1" }, { id }, DEPARTMENT), [ "1", id, ]); if (res.result) { return ctx.res.$success(res.data); } else { return ctx.res.$error("请求失败"); } }; /** * 用户角色关联表 - 绑定角色 */ const addUserRole = async (reqData, ids) => { if (!ids) return; let params = { ...reqData, }; const res = await query( addUserRoleSql(Object.keys(params).toString(), "role_id"), [ transformTwoArray(ids, (item) => { return Object.values({ ...params, role_id: item }); }), ] ); }; /** * 用户角色关联表 - 解绑角色 */ const delUserRole = async (reqData, ids) => { if (!ids) return; const res = await query(delUserRoleSql(reqData.user_id, "role_id", ids)); };
此部分主要是业务逻辑的处理, 包括登录、注册、修改密码、用户信息(验证登录)、用户列表、绑定角色等
绑定角色addUserRole与解绑角色delUserRole展示了批量插入和批量删除的实际操作
存在的问题就是MYSQL应该设置提交回退功能,
通过设置回退处理
+Promise.all
可以实现并发处理,
有一处失败进行全部回退。
routes/route/users.jsconst router = require("koa-router")(); router.prefix("/user"); const { login, userInfo, userList, addUser, editUser, editStatus, deleteUser, editPassword, } = require("../../app/controller/users"); const { SECRET } = require("../../config/constant"); const jwt = require("koa-jwt")({ secret: SECRET }); router.get("/", function (ctx, next) { ctx.body = "this a users response!"; }); router.post("/login", login); router.post("/edit-password", editPassword); router.post("/register", addUser); router.get("/info", jwt, userInfo); router.post("/list", jwt, userList); router.put("/:id", jwt, editUser); router.put("/status/:id", jwt, editStatus); router.delete("/:id", jwt, deleteUser); module.exports = router;
至此, 项目的登录注册功能完成, 也包括绑定角色等功能. 前端可以通过正常请求 api 方式进行操作。
此部分包含完善信息、公司政策、公司公告、字典数据增删改查、文章管理增删改查、用户管理增删改查、绑定角色、角色管理增删改查、绑定菜单、菜单管理增删改查、绑定权限、权限管理增删改查、部门管理增删改查、职位管理增删改查。
形式比较相似,都是业务代码,都比较简单,不再重复。
包括首页的我的申请、我的审批、抄送我的及入职申请、转正申请、调薪申请、转岗申请、离职申请、请假申请、加班申请、补卡申请、考勤管理、入职审批、转正审批、调薪审批、转岗审批、离职审批、请假申请、加班申请、补卡申请。
此部分代码较多,
分为了两个模块
audit、appro
员工填写表单,选择审批人、抄送人,进行提交
常见的审批人有两种,一种是用户自行选择审批人, 在可见审批人做处理。一种是选择流程设计上设定固定的人或固定的角色去审批。
此处的审批使用的是用户自行选择审批人, 审批人可以添加处理,如用户仅能选择自身部门或只能选择总监角色的的人去审批
只需要三张核心表就可以啦,审批主表(appro_flow)、审批详情表(appro_flow_detail)、抄送表(appro_flow_copy)。通过审批编号flow_no
审批主表
| 名 | 类型 | 备注 |
|---|---|---|
| flow_no | varchar(50) not null | 审批编号返回 n 位随机数+当前时间戳 |
| title | varchar(50) not null | 标题(例如:某某人的加班申请) |
| bus_type | varchar(50) not null | 审批类型(根据业务表定义 Code,区分表单) |
| user_id | int(20) not null | 申请人 |
| create_time | datetime not null | 添加时间 |
| status | varchar(2) not null | 审核状态(1.待审,2.通过.3.驳回,4.撤销) |
审批详情表
| 名 | 类型 | 备注 |
|---|---|---|
| flow_no | varchar(50) not null | 审批编号返回 n 位随机数+当前时间戳(关联主表) |
| user_id | int(20) not null | 审核人 |
| sort | int(20) not null | 审核顺序 |
| desc | text not null | 审核备注 |
| audit_time | datetime not null | 审核时间 |
| status | varchar(2) not null | 审核状态(1.审核中,2.待我审批.3.通过,4.驳回) |
审批主表 1 :n 审批详情表, 明细表的数量取决于表单提交中审核人数量,审批顺序则按照前台选择审批人的先后顺序进行审核。
审批抄送表
| 名 | 类型 | 备注 |
|---|---|---|
| flow_no | varchar(50) not null | 审批编号返回 n 位随机数+当前时间戳(关联主表) |
| user_id | int(20) not null | 抄送人 |
| copy_time | datetime not null | 抄送时间 |
审批主表 1 :n 审批抄送表, 抄送表的数量取决于表单提交中抄送人数量。
以转正申请审批为例
regularization,审核状态默认
1(待审核)
首页有我的申请列表, 需要查出所有我申请的数据
将appro_flow审批主表和user_d_p_r_view
用户详情,
通过appro_flow的user_id字段关联查询
通过两个表的关联查询, 可以查到自己申请过的申请列表, 返回用户基本详情, 审批状态, 审批时间
过滤appro_flow审核状态为
1
并且appro_flow_detail表审核顺序第一位的审核状态为
2 的数据,
用于添加待审批状态标识,
前台此时展示撤销按钮.
将appro_flow审批主表和appro_flow_detail审批详情表,
通过flow_no字段关联查询
通过详情表关联查询, 可以获取到审批流程的实际情况, 前端通过这个数据可以渲染进度条或步骤条
首页有我的审批列表, 需要查出所有待我审批的数据
将appro_flow审批主表和user_d_p_r_view
通过 user_id 关联查询,
和appro_flow_detail审批详情表通过flow_no关联查询
过滤appro_flow审核状态为
1
并且appro_flow_detail表审核状态为
2 的数据
也可以根据appro_flow表的bus_type字段进行审批表单的分类展示
根据bus_type字段前台跳转不同类型的审批列表页面
首页有抄送我的列表, 需要查出所抄送有我的数据
将appro_flow审批主表和user_d_p_r_view
用户详情,
通过appro_flow的user_id字段关联查询,
和appro_flow_copy
抄送表通过flow_no字段进行关联查询
通过三个表的关联查询, 可以查到抄送我的列表, 返回用户基本详情, 审批状态, 审批时间
appro_flow 与
appro_flow_detail 通过
flow_no字段关联
appro_flow 与
user_d_p_r_view 通过user_id
字段关联
appro_flow 与
induction(此处是入职表,
各自的业务对应各自的业务表)
通过flow_no 关联
四张表通过各自字段进行关联查询, 下面以入职表为例
过滤appro_flow_detail status
审核状态为 2、bus_type
审批类型为
induction 并且 appro_flow status
审核状态为 1 并且appro_flow_detail
user_id 字段为 token
携带的用户 id 的数据
审核操作,基本上分为审核通过和不通过, 当然也可以根据业务自行扩展其他的审核操作。
通过业务审批列表已经拿到我的审批列表, 此时可以同意或拒绝(驳回), 而一般拒绝需要输入原因
同意
因为审批存在多人审批, 所以同意操作, 需要审批人数量与此节点审批顺序对比, 判断是否为最后审批节点
不是最后审批节点,
需要将审批详情表appro_flow_detail下一节点的审批人的审批状态改为待我审批
2,
是最后节点,
则需要将审批详情表appro_flow_detail
的审批状态改为同意
3,
且将审批主表appro_flow的审批状态改为同意,
2
拒绝
拒绝操作需要将审批详情表appro_flow_detail
的审批状态改为驳回
4,
且将审批主表appro_flow的审批状态改为驳回,
3
也就是多审批节点全部为审核通过,则将appro_flow表的审核状态设为通过.
有一条审核不通过,则将appro_flow表的审核状态设为不通过
撤销
撤销操作需要将appro_flow表的审核状态设为撤销
4
modal/audit.jsconst { SQL } = require("../../utils/sqlhandle"); const { handleQueryValues, limit } = require("../../utils/index"); const { query } = require("../../config/dbPool"); // 审批表 const audit = new SQL({ appro_flow: ["user_id", { status: "audit_status" }], user_d_p_r_view: ["id"], }); // 审批详情表 const auditDetail = new SQL({ appro_flow: ["flow_no,user_id", { status: "audit_status" }], appro_flow_detail: ["flow_no", { status: "detail_status" }], user_d_p_r_view: ["id"], }); // 审批抄送表 const auditCopy = new SQL({ appro_flow: ["flow_no,user_id", { status: "audit_status" }], appro_flow_copy: ["flow_no"], user_d_p_r_view: ["id"], }); /** * 申请sql */ // 入职 const induction = new SQL({ user_d_p_r_view: ["id"], induction: ["user_id"], }); // 转正 const regularization = new SQL({ regularization: [], }); ....
controller/audit.js... /** * 获取审批列表 - 首页 */ const auditList = async (ctx) => { const request = ctx.request.body; const user = ctx.state.user; const { type } = transformUrl(ctx.request.querystring); let total, res; const params = { ...request, }; request.pageSize && (params.pageSize = Number(request.pageSize)); request.currentPage && (params.currentPage = (request.currentPage - 1) * request.pageSize); if (type == 1) { params["appro_flow.user_id"] = user.id; params["appro_flow_detail.sort"] = "1"; total = await findAuditDetail(handleQueryKeyValue(params), params); res = await findAuditDetail(handleWhiteListKeyValue(params), params); } if (type == 2) { params["appro_flow_detail.user_id"] = user.id; params["appro_flow_detail.status"] = "2"; total = await findAuditDetail(handleQueryKeyValue(params), params); res = await findAuditDetail(handleWhiteListKeyValue(params), params); } if (type == 3) { params["appro_flow_copy.user_id"] = user.id; total = await findAuditCopy(handleQueryKeyValue(params), params); res = await findAuditCopy(handleWhiteListKeyValue(params), params); } if (res.result) { return ctx.res.$success({ list: res.data, total: total.data.length }); } else { return ctx.res.$error("请求失败"); } }; /** * 统计审批数量 - 首页 */ const countAuditNumber = async (ctx) => { const user = ctx.state.user; const applyParams = { "appro_flow.user_id": user.id, "appro_flow_detail.sort": "1", }; const approParams = { "appro_flow_detail.user_id": user.id, "appro_flow_detail.status": "2", }; const copyParams = { "appro_flow_copy.user_id": user.id, }; let applyTotal = await findAuditDetail( handleQueryKeyValue(applyParams), applyParams ); let approTotal = await findAuditDetail( handleQueryKeyValue(approParams), approParams ); let copyTotal = await findAuditCopy( handleQueryKeyValue(copyParams), copyParams ); if (applyTotal.result && approTotal.result && copyTotal.result) { return ctx.res.$success({ applyNumber: applyTotal.data.length, approNumber: approTotal.data.length, copyNumber: copyTotal.data.length, }); } else { return ctx.res.$error("请求失败"); } }; /** * 添加审批主表 */ const addAuditFlow = async (reqData) => { let params = { ...reqData, create_time: getTime(), }; let res = await query(audit.add(AUDIT), handleEmptyStringKeyValue(params)); return res; }; /** * 添加审批详情表 */ const addAuditFlowDetail = async (reqData, ids) => { if (!ids) return; let params = { ...reqData, sort: 0, user_id: ids, status: "1", }; // 第一条的审核状态为待我审批, 其他为审核中 const res = await query( addAuditFlowDetailSql(Object.keys(params).toString()), [ transformTwoArray(ids, (item, index) => { return Object.values({ ...params, sort: index + 1, user_id: item, status: index === 0 ? "2" : "1", }); }), ] ); return res; }; /** * 添加抄送表 */ const addCopy = async (reqData, ids) => { if (!ids) return; let params = { ...reqData, user_id: ids, }; const res = await query(addCopySql(Object.keys(params).toString()), [ transformTwoArray(ids, (item, index) => { return Object.values({ ...params, user_id: item, }); }), ]); return res; }; /** * 入职申请 */ const addInduction = async (ctx) => { const request = ctx.request.body; const user = ctx.state.user; const { id, name } = user; if (!request.phone) return ctx.res.$error("缺少phone"); let { data: userInfo } = await findUserWeb( { is_delete: "0", phone: request.phone }, ["0", request.phone] ); // if(userInfo.state !=='0') return ctx.res.$error("重复入职"); // 生成审批编号 let flow_no = randomLenNum(4, true); // 添加审批主表 let auditParams = { flow_no, title: `${request.name}的入职申请`, bus_type: request.bus_type || "induction", user_id: id, }; const auditRes = await addAuditFlow(auditParams); // 添加入职表 let inductionParams = { flow_no: flow_no, user_id: id, id_number: request.id_number, id_address: request.id_address, birthday: request.birthday, degree_value: request.degree_value, email: request.email, reason: request.reason, contacts_name: request.contacts_name, contacts_phone: request.contacts_phone, contacts_address: request.contacts_address, bank_name: request.bank_name, bank_number: request.bank_number, account_name: request.account_name, entry_date: request.entry_date, creator: name || request.name, create_time: getTime(), }; const inductionRes = await query( induction.add(INDUCTION), handleEmptyStringKeyValue(inductionParams) ); // 更新账号表 let userParams = { name: request.name, sex: request.sex, avatar: request.avatar, department_id: request.department_id, position_id: request.position_id, modifier: name || request.name, modified_time: getTime(), }; request.birthday && (userParams.age = getAge(request.birthday)); const userRes = await query(users.edit(userParams, { id }, USER), [ ...handleEmptyStringValues(userParams), id, ]); // 添加审批详情表 let auditDetailParams = { flow_no, }; const detailRes = await addAuditFlowDetail( auditDetailParams, request.user_ids ); // 添加抄送表 if (request.copy_ids) { let copyParams = { flow_no, copy_time: getTime(), }; const copyRes = await addCopy(copyParams, request.copy_ids); } if ( inductionRes.result && userRes.result && auditRes.result && detailRes.result ) { return ctx.res.$success(inductionRes.data); } else { return ctx.res.$error("请求失败"); } };
routes/route/audit.jsconst router = require("koa-router")(); router.prefix("/audit"); const { SECRET } = require("../../config/constant"); const jwt = require("koa-jwt")({ secret: SECRET }); router.get("/", function (ctx, next) { ctx.body = "this a audit response!"; }); router.post("/list", jwt, auditList); router.get("/count", jwt, countAuditNumber); router.post("/induction/add", jwt, addInduction); module.exports = router;
audit
包含首页我的申请、我的审批、抄送我的、审批数量统计、业务申请等接口逻辑处理
将添加审批主表、审批详情表批量插入、审批抄送表批量插入做了公共函数处理,也大大减少了代码冗余。
modal/appro.jsconst { SQL } = require("../../utils/sqlhandle"); const { handleQueryValues, limit } = require("../../utils/index"); const { query } = require("../../config/dbPool"); /** * 审批sql */ // 入职审批列表 const approInductionSql = new SQL({ appro_flow: ["flow_no,user_id, flow_no", { status: "audit_status" }], appro_flow_detail: ["flow_no", { status: "appro_status" }], user_d_p_r_view: ["id"], induction: ["flow_no"], }); // 转正审批列表 const approRegularizationSql = new SQL({ appro_flow: ["flow_no,user_id, flow_no", { status: "audit_status" }], appro_flow_detail: ["flow_no", { status: "appro_status" }], user_d_p_r_view: ["id"], regularization: ["flow_no"], }); // 调薪审批列表 const approChangesalarySql = new SQL({ appro_flow: ["flow_no,user_id, flow_no", { status: "audit_status" }], appro_flow_detail: ["flow_no", { status: "appro_status" }], user_d_p_r_view: ["id"], changesalary: ["flow_no"], }); // 入职审批列表 const findApproInduction = async (target, params) => { let res = JSON.parse( JSON.stringify( await query( `${approInductionSql.query(target)} ${limit( target.currentPage, target.pageSize, "desc", "appro_flow.flow_no" )};`, handleQueryValues(params) ) ) ); return res; }; // 转正审批列表 const findApproRegularization = async (target, params) => { let res = JSON.parse( JSON.stringify( await query( `${approRegularizationSql.query(target)} ${limit( target.currentPage, target.pageSize, "desc", "appro_flow.flow_no" )};`, handleQueryValues(params) ) ) ); return res; }; // 调薪审批列表 const findApproChangesalary = async (target, params) => { let res = JSON.parse( JSON.stringify( await query( `${approChangesalarySql.query(target)} ${limit( target.currentPage, target.pageSize, "desc", "appro_flow.flow_no" )};`, handleQueryValues(params) ) ) ); return res; }; ...
controller/appro.jsconst { query } = require("../../config/dbPool"); ... /** * 更新审批详情表并更新审批主表 * 主表status -> 审核状态(1.待审,2.通过.3.驳回,4.撤销) * 详情表status -> 审核状态(1.审核中,2.待我审批.3.通过,4.驳回) */ const updateFlowStatus = async (flow_no, id, status, reason = "") => { // 1. 更新该条数据状态 3 -> 通过 4 -> 拒绝(reason必填) let params = { desc: reason, status, audit_time: getTime(), }; // 更新审批详情状态 const detailRes = await query( appro.edit(params, { user_id: id, flow_no }, AUDIT_DETAIL), [...handleEmptyStringValues(params), id, flow_no] ); // 2. 判断该审批详情是不是最后一条详情, 是 -> 更新审批主表, 不是 -> 更新下一条审批详情 if (status === "3") { // 审批编号 - 当前详情 let { data: curItem } = JSON.parse( JSON.stringify( await query( appro.queryAll({ user_id: id, flow_no }), handleQueryValues({ user_id: id, flow_no }) ) ) ); // 审批编号 - 总详情 let { data: totalItem } = JSON.parse( JSON.stringify( await query(appro.queryAll({ flow_no }), handleQueryValues({ flow_no })) ) ); let { sort } = curItem[0]; // 更新审批主表 if (sort == totalItem.length) { return updateAudit(flow_no, "2"); } else { // 更新下一条审批详情 let nextRes = await query( appro.edit({ status: "2" }, { sort: sort + 1, flow_no }, AUDIT_DETAIL), [...handleEmptyStringValues({ status: "2" }), sort + 1, flow_no] ); nextRes.audit = true; return nextRes; } } else if (status === "4") { // 拒绝 -> 更新审批主表 return updateAudit(flow_no, "3"); } detailRes.audit = true; return detailRes; }; // 更新审批主表 const updateAudit = async (flow_no, status) => { const auditRes = await query(appro.edit({ status }, { flow_no }, AUDIT), [ ...handleEmptyStringValues({ status }), flow_no, ]); return auditRes; }; // 批量添加考勤 const addAttendances = async (reqData, ids) => { if (!ids) return; let params = { ...reqData }; const res = await query( addAttendancesSql(Object.keys(params).toString(), "date"), [ transformTwoArray(ids, (item) => { return Object.values({ ...params, date: item }); }), ] ); return res; }; /** * 撤销功能 */ const approRevoke = async (ctx) => { const request = ctx.request.body; const { user_id } = request; const flow_no = ctx.params.flow_no; const res = await updateAudit(flow_no, "4"); if (res.result) { return ctx.res.$success(res.data); } else { return ctx.res.$error("请求失败"); } }; /** * 用户角色关联表 - 绑定角色 */ const addUserRole = async (reqData, ids) => { if (!ids) return; let params = { ...reqData, }; const res = await query( addUserRoleSql(Object.keys(params).toString(), "role_id"), [ transformTwoArray(ids, (item) => { return Object.values({ ...params, role_id: item }); }), ] ); return res; }; /** * 审批列表 */ // 入职审批 - 列表 const approInductionList = async (ctx) => { const request = ctx.request.body; const { user_id } = request; const user = ctx.state.user; let params = { "appro_flow_detail.user_id": user.id, "appro_flow_detail.status": "2", "appro_flow.status": "1", "appro_flow.bus_type": "induction", }; if (user_id) { params["appro_flow_detail.user_id"] = user_id; } request.pageSize && (params.pageSize = Number(request.pageSize)); request.currentPage && (params.currentPage = (request.currentPage - 1) * request.pageSize); let total = await findApproInduction(handleQueryKeyValue(params), params); let res = await findApproInduction(handleWhiteListKeyValue(params), params); if (res.result) { return ctx.res.$success({ list: res.data, total: total.data.length }); } else { return ctx.res.$error("请求失败"); } }; // 入职审批 - 分配权限 const approInductionDispatch = async (ctx) => { const request = ctx.request.body; const user = ctx.state.user; let inductionParams = { base_wages: request.base_wages, }; let userParams = { no: request.no, role_ids: request.role_ids, }; let inductionRes = await query( appro.edit(inductionParams, { user_id: request.id }, INDUCTION), [...handleEmptyStringValues(inductionParams), request.id] ); let roleRes = await addUserRole( { user_id: request.id, creator: request.name, create_time: getTime() }, request.role_ids ); let userRes = await query(appro.edit(userParams, { id: request.id }, USER), [ ...handleEmptyStringValues(userParams), request.id, ]); if (inductionRes.result && userRes.result && roleRes.result) { return ctx.res.$success(userRes.data); } else { return ctx.res.$error("请求失败"); } }; // 入职审批 - 同意 const approInductionAgree = async (ctx) => { const request = ctx.request.body; const user = ctx.state.user; let flowStatusRef = await updateFlowStatus(request.flow_no, user.id, "3"); if (!flowStatusRef.audit) { // 审批同意 -> 将用户表用户状态改为试用期 let userRes = await query( appro.edit({ state: "1" }, { id: request.id }, USER), [...handleEmptyStringValues({ state: "1" }), request.id] ); } if (flowStatusRef.result) { return ctx.res.$success(flowStatusRef.data); } else { return ctx.res.$error("请求失败"); } }; // 入职审批 - 拒绝 const approInductionRefuse = async (ctx) => { const request = ctx.request.body; const user = ctx.state.user; let flowStatusRef = await updateFlowStatus( request.flow_no, user.id, "4", request.reason ); let userRes = await query( appro.edit({ state: "4" }, { id: request.id }, USER), [...handleEmptyStringValues({ state: "4" }), request.id] ); if (flowStatusRef.result && userRes.result) { return ctx.res.$success(flowStatusRef.data); } else { return ctx.res.$error("请求失败"); } }; ...
routes/route/appro.jsconst router = require("koa-router")(); router.prefix("/appro"); const { SECRET } = require("../../config/constant"); const jwt = require("koa-jwt")({ secret: SECRET }); router.get("/", function (ctx, next) { ctx.body = "this a appro response!"; }) router.put('/revoke/:flow_no', jwt, approRevoke); router.get("/induction/list", jwt, approInductionList); router.post("/induction/dispatch", jwt, approInductionDispatch); router.post("/induction/agree", jwt, approInductionAgree); router.post("/induction/refuse", jwt, approInductionRefuse); ...
此部分包括更新审批主表审批状态、更新审批详情表审批状态、批量添加考勤数据、业务列表、撤销功能、同意、拒绝(驳回)、绑定角色等
锻炼了后端思维,了解了后端接口处理逻辑。
提升了编写复杂SQL的能力。
但仍有部分没有功能没有实现,例如事务提交,对于批量插入,批量操作等场景下,一旦出现失败情况,则数据库出现脏数据,甚至造成程序崩溃。