页面展示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

服务端部分

项目目录结构

│  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
  1. middelware 中间件
  2. controller、控制层 业务代码
  3. config 数据库等设置
  4. dbhelper sql 语句
  5. routes 路由
  6. modal 对象层

项目初始化

基本配置

1. 创建项目

# 初始化
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

2. 新建 app.js

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 做了一些自定义操作,下面会介绍到

3. 创建项目入口

  1. 创建入口文件 wwww
#!/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);
}
  1. 创建 public/index.html
<!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 文件夹下

4. 改写 package.json

  1. 添加启动脚本
"scripts": {
    "start": "node bin/www",
    "serve": "nodemon bin/www" // 热更新
},
  1. 完整 package.json
{
  "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"
  }
}

5. 启动项目

  1. 启动

    yarn serve

  2. 游览器打开 http://localhost:3000/

  3. 通过 koa-static 静态资源处理,根目录默认打开/public/index.html。

  4. 访问时不需要加上 public,即 http://localhost:3000/文件名 这种形式即可访问。

在这里插入图片描述

配置路由

  1. 将路由按模块拆分,方便路由解耦.
  2. 将注册路由与业务层解耦,方便业务逻辑复用.
  3. 在路由模块添加路由前缀,使路由更易懂易读.

使用过 webpack 的人应该知道, 通过 require.context 可以拿到符合条件的上下文,我们也模拟 context 实现一个自动加载文件的方法。

1. 实现自动加载文件函数

  1. 创建文件 utils/autoLoadFile.js
#!/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;
  1. 使用示例
const context = require("../utils/autoLoadFile");
const fileList = context(path.join(__dirname, "./route"), true);

2. 解析路由文件

  1. 创建路由自执行文件 routes/index.js
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;

3. 注册路由

  1. app.js
// 引入路由自执行文件
const router = require("./routes/index");
// 注册路由
app.use(router.routes(), router.allowedMethods());

4. 创建路由模块

  1. 基础搭建已经好了,创建一个模块试试 users
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;
  1. 最终路由 path: localhost:3000/users/login
/api 为此服务路由前缀, 本项目未设置
/users 为模块路由前缀
/login 具体接口

在这里插入图片描述

中间件配置

1. 中间件文件解析

  1. 创建自执行文件 middlewares/index.js
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;

2. 中间件自注册

  1. app.js
// 注册自定义中间件
require("./middlewares/index")(app);

3. 响应中间件

  1. 从登录接口返回示例可以看到返回的响应体需要设置 code, msg, data 但比较麻烦, 我们可以通过中间件设置一个简单的响应体
  2. 创建封装响应体的中间件 message.js
// 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();
};

4. 响应体中间件使用示例

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);
  }
});

5. 响应体中间件结果

// 成功
{
    "code":200,
    "msg":"success",
    "data":"登录成功"
}

// 失败
{
    "code":403,
    "msg":"error",
    "data":"请求失败"
}

登录 token 验证

1. 使用 koa-jwt

从文章一开始已经将 koa-jwt 安装

原理介绍

使用 JWT,服务器认证用户之后,会生成包含一个 JSON 对象信息的 token 返回给用户

然后客户端请求服务的时候,都要带上该 token 以供服务器做验证。服务器还会为这个 JSON 添加签名以防止用户篡改数据。通过使用 JWT,服务端不再保存 session 数据,更加容易实现扩展。

  1. 直接使用
// /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 信息

2. 解析 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,做完这两点已经可以应付大部分的业务场景

参考资料

  1. Koa 官网 https://koajs.com/
  2. Koa 中间件使用之 koa-router https://www.jianshu.com/p/f169c342b4d5
  3. koa-router 官方文档 https://wohugb.gitbooks.io/koajs/content/route/koa-router.html
  4. Koa-bodyparser 源码 https://github.com/koajs/bodyparser
  5. Koa-error 源码 https://github.com/koajs/error#readme
  6. koa-logger 源码 https://github.com/koajs/logger
  7. 自编写 Koa2 文档: http://wang_xi_long.gitee.io/koa2-template-blog/handbook/init.html

ORM

创建基类

  1. 创建 utils/sqlhandle.js
/**
 * 处理字符串 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, "?")}` +
      ";"
    );
  }
}
  1. 创建 SQL 基类, 包含 queryAll、query、add、edit、delete 方法
  2. queryAll 查看全部字段
  3. query 可查全部或可选字段
  4. add 插入
  5. edit 编辑
  6. delete 删除

其中有部分函数是放在了公共函数里, 如处理 key, 或处理 value 的函数, 此部分会在下面公共函数里提及.

使用基类

  1. 新建 modal/user.js

方式一

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 语句, 并不会直接请求数据库返回数据, 需要自己在派生类或控制类中去实现处理请求和返回响应体的函数.

请求数据

  1. 新建 mysql 数据库配置文件 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;
  1. 新建请求连接池 config/dbPool.js
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,
};
  1. 请求 controller/user.js
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;
}
  1. 这个函数主要用于获取用户自身拥有的菜单权限和菜单列表接口
  2. 该函数可以将带 pid 标识的一维数组处理成级联关系的多维数组

多选绑定更新 ids 字符串

原理

两个数据源,一个是数据库的数据源,一个是请求的数据源(可以是逗号分隔的 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 中需要的各种格式的数据

系统常量

  1. 新建 config/constant.js
module.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. 查询、修改、删除、添加数据方便,能够对异常情况进行处理,保证数据的安全性、系统的稳定性和可靠性。

具体来看,要求本系统具有以下功能版本:登录鉴权、基础配置、系统管理、申请审批、考勤管理。系统整体的简单关系图如下所示:

在这里插入图片描述

详细分析

各模块具体的需求描述如下:
(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(联合主键);

系统实施

新建视图

  1. 新建视图 user_role_view
SELECT
	`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获取到逗号分隔的角色名称

  1. 新建视图 user_d_p_r_view
SELECT
	`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` ) )

为了能直接拿到带部门、职位、角色的数据

实现登录注册

  1. 新建 modal/users.js
const { 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 绑定角色时批量删除接口使用

  1. 新建 controller/users.js
const { 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 可以实现并发处理, 有一处失败进行全部回退。

  1. 新建routes/route/users.js
const 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 方式进行操作。

基础配置

此部分包含完善信息、公司政策、公司公告、字典数据增删改查、文章管理增删改查、用户管理增删改查、绑定角色、角色管理增删改查、绑定菜单、菜单管理增删改查、绑定权限、权限管理增删改查、部门管理增删改查、职位管理增删改查。

形式比较相似,都是业务代码,都比较简单,不再重复。

申请审批

包括首页的我的申请、我的审批、抄送我的及入职申请、转正申请、调薪申请、转岗申请、离职申请、请假申请、加班申请、补卡申请、考勤管理、入职审批、转正审批、调薪审批、转岗审批、离职审批、请假申请、加班申请、补卡申请。

此部分代码较多, 分为了两个模块 auditappro

设计流程

员工填写表单,选择审批人、抄送人,进行提交

常见的审批人有两种,一种是用户自行选择审批人, 在可见审批人做处理。一种是选择流程设计上设定固定的人或固定的角色去审批。

此处的审批使用的是用户自行选择审批人, 审批人可以添加处理,如用户仅能选择自身部门或只能选择总监角色的的人去审批

只需要三张核心表就可以啦,审批主表(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 审批抄送表, 抄送表的数量取决于表单提交中抄送人数量。

申请审批流程

以转正申请审批为例

  1. 试用期的员工首先填写表单, 选择审批人、抄送人,审批人员和抄送人员可多选,审批顺序会按照选择审批的顺序进行流转。前端进行必要的校验。
  2. 审批服务接收到该请求后,验证表单的合法性,通过后开始走事务
  1. 以上五是同时进行操作,必须要满足事务,否则数据会出现问题
  2. 四张表数据插入的 flow_no 字段必须是相同的
  3. 插入审批流主表数据的时候,bus_type 字段的值可以设置为regularization,审核状态默认 1(待审核)
  4. 插入审批流明细表数据的条数取决与用户提交表单选择的审核人数量,如这里选择了三个审批人,就需要插入三条数据,第一条的审核状态 设为 2(待我审批),其他两条的审核状态设为 1(审核中)
  5. 插入转正表需要对用户进行判定, 如必须是试用期员工等
我的申请

首页有我的申请列表, 需要查出所有我申请的数据

appro_flow审批主表和user_d_p_r_view 用户详情, 通过appro_flowuser_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_flowuser_id字段关联查询, 和appro_flow_copy 抄送表通过flow_no字段进行关联查询

通过三个表的关联查询, 可以查到抄送我的列表, 返回用户基本详情, 审批状态, 审批时间

业务审批列表

appro_flowappro_flow_detail 通过 flow_no字段关联

appro_flowuser_d_p_r_view 通过user_id 字段关联

appro_flowinduction(此处是入职表, 各自的业务对应各自的业务表) 通过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

申请代码展示

  1. 新建 modal/audit.js
const { 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: [],
});
....
  1. 新建 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("请求失败");
  }
};
  1. 新建 routes/route/audit.js
const 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 包含首页我的申请、我的审批、抄送我的、审批数量统计、业务申请等接口逻辑处理

将添加审批主表、审批详情表批量插入、审批抄送表批量插入做了公共函数处理,也大大减少了代码冗余。

审批代码展示

  1. 新建 modal/appro.js
const { 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;
};

...
  1. 新建 controller/appro.js
const { 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("请求失败");
  }
};

...
  1. 新建 routes/route/appro.js
const 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的能力。

但仍有部分没有功能没有实现,例如事务提交,对于批量插入,批量操作等场景下,一旦出现失败情况,则数据库出现脏数据,甚至造成程序崩溃。