从 0 到 1 node 项目管理系统:搭建基础平台(上)

8,207 阅读11分钟

前言

在上一个博客中,已经通过 Egg 对 Gitlab Api 进行了基础的封装,本文将会围绕 DevOps 流程介绍项目设计(偏后台),需要读者具备一定的后端知识储备。

此系列即是持续交付项目的教程亦可作为 node 开发的教程来使用,从开发-测试-构建-部署的一整套 DevOps 项目

一共包含如下 2 个系列,分为前后端两个模块

后端模块

  1. DevOps - Gitlab Api使用(已完成,点击跳转)
  2. DevOps - 搭建 DevOps 基础平台(已完成 30%)
  3. DevOps - Gitlab CI 流水线构建
  4. DevOps - Jenkins 流水线构建
  5. DevOps - Docker 使用
  6. DevOps - 发布任务流程设计
  7. DevOps - 代码审查卡点
  8. DevOps - Node 服务质量监控

前端模块

  1. DevOps - H5 基础脚手架
  2. DevOps - React 项目开发

后期可能会根据 DevOps 项目的实际开发进度对上述系列进行调整

DevOps 设计

简单分析一下此项目研发流程的架构,接下来再做后续的步骤(剧本已写好,就看怎么演了

项目需求分析(系统开发的目的跟结果)

  1. 从项目开发-测试-构建-部署一整套流程,简化交付成本
  2. 研发流程中加入能效概念(研发时间-测试时间-总体交付时间-bug 率及修复时间),作为项目提效的一个参考标准(影响因素太多,仅供参考)
  3. 合理的提测卡点,减少无效的提测,减轻测试负担,提高流程闭环质量
  4. 提供线上监控,分析每个版本使用率,报错率,提高项目研发质量
  5. 提供快速回滚指定版本功能,确保新版本崩溃情况下能够快速恢复服务

此项目是从零开发,在正式开发之前,需要先将需求理清,以免设计出现严重缺陷,造成后期开发或拓展困难(路可以走的慢,但不要走偏)。

流程设计

如上图所示,将上一篇的发布流程更进一步的细化可以分为下面 4 类:

  1. 单项目发布流程(一个需求只需要一个工程完成)
  2. 生产环境出问题,快速回滚功能
  3. 集成项目发布流程(一个需求可能会有多个工程参与开发、发布)
  4. Bug 修复发布流程(无需求,需要快速修复线上已知但不紧急 bug 的发布流程)

任务流的设计其实非常复杂,为了加快交付第一版,先将任务流固定为以上 4 类,减少开发量,后期会添加或者修改某个流程

数据库设计

sequelize 的使用

sequelize 提供了 sequelize-cli 工具来实现 Migrations,我们也可以在 egg 项目中引入 sequelize-cli(具体介绍参考 sequelize 操作)。

如果你参考上一篇博客已经将环境搭建完毕,可以使用 npm install --save-dev sequelize-cli 安装 sequelize-cli 工具,再通过下面配置生成需要的表。

use strict';
const path = require('path');

module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),
};

上述是 .sequelizerc 配置,请放在项目根目录下

npx sequelize init:config
npx sequelize init:migrations

执行完后会生成 database/config.json 文件和 database/migrations 目录,修改一下 database/config.json 中的内容,将其改成项目中使用的数据库配置:

{
  "development": {  // 本地数据库,其他环境数据库,照着例子自己改
    "username": "root",
    "password": "123456",
    "database": "devops_dev",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
}

再通过 npx sequelize migration:generate --name=init-users 来创建数据库表

module.exports = { // 为了减少工作量,权限我们直接使用 gitlab 的,所以我们只需要落库以下字段
  up: async (queryInterface, Sequelize) => {
    const { INTEGERDATESTRING } = Sequelize;
    await queryInterface.createTable('users', {
      id: { type: INTEGER, primaryKey: true, },
      name: STRING(30),
      username: STRING(30),
      email: STRING(100),
      avatar_url: STRING(200),
      web_url: STRING(200),
      created_at: DATE,
      updated_at: DATE,
    });
  },
  down: async queryInterface => {
    await queryInterface.dropTable('users');
  },
};

最后执行 migrate 进行数据库变更

# 升级数据库
npx sequelize db:migrate
# 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
# npx sequelize db:migrate:undo
# 可以通过 `db:migrate:undo:all` 回退到初始状态
# npx sequelize db:migrate:undo:all

设计基础表

将 gitlab project 与 branch 常用的数据落库到本地,再根据项目需求新增字段,大概的表结构如上图所示

结合上述项目流程设计,说明一下表结构关系

  1. 工程表 project 会管理多个分支 branch,可以查询当前工程下所有分支的状态(是否被提测,是否存在流程中)
  2. 创建一个流程(等同于需求)关联多个 branch 开发
  3. 流程创建完之后必走完所有步骤直至完结(开发-测试-预发-生产
  4. 当 branch 被一个流程关联之后,既被所锁定,不会再次被加入到其他流程(需求锁定隔离,保证开发过程不会有干扰
  5. 在流程的提测步骤中,可以针对不同 branch 进行多次提测(复杂需求通过分批提测,完成预期目标)
  6. 当流程中所有 branch 的状态都已测试通过之后,该流程状态才进入下一个阶段,否则一直停留在测试阶段

测试记录表没放上去,暂且先把上述功能开发完毕,再结合后面的分支管理进行修改

DevOps 开发

添加接口全局返回参数

import { Controller } from "egg";

export default class BaseController extends Controller {
  get user() {
    return this.ctx.user;
  }

  success(data) {
    this.ctx.body = {
      code0,
      data,
    };
  }

  error({ code, data, message }) {
    // 根据业务返回不同的错误 code,提供给前端做业务判断处理
    this.ctx.body = {
      code,
      data,
      message,
    };
  }
}

定义全局返回参数基础类,业务 Controller 继承基础类,前端可以根据返回的 code 值进行业务判断

jwt 权限验证

上一篇介绍了从 Gitlab 获取 access_token 来操作 open api 的方法,但我们还是需要将用户信息从在本地落库,方便我们后期使用

项目的权限验证,采取简单的 jwt 来使用,将用户数据及 access_token 保存起来,后期完成第一阶段的目标之后再进行改进

具体的 egg-jwt 的使用可以参考(egg-jwt 使用),这里直接附上业务侧的代码供参考:

const excludeUrl = ["/user/getUserToken"]; // 请求白名单,过滤不需要校验的请求路径

export default () => {
  const jwtAuth = async (ctx, next) => {
    if (excludeUrl.includes(ctx.request.url)) {
      return await next();
    }
    const token = ctx.request.header.authorization;
    if (token) {
      try {
        // 解码token
        const deCode = ctx.app.jwt.verify(
          token.replace("Bearer ", ""), // jwt 中间件验证的时候,需要去掉 Bearer
          ctx.app.config.jwt.secret
        );
        ctx.user = deCode;
        await next();
      } catch (error) {
        ctx.status = 401;
        ctx.body = {
          code: 401,
          message: error.message,
        };
      }
      return;
    }
    ctx.status = 401;
    ctx.body = {
      code: 401,
      message: "验证失败",
    };
    return;
  };
  return jwtAuth;
};

以上是全局拦截 jwt 权限中间件,验证权限之后,将用户数据存入 ctx 供后续业务侧调用。中间件的具体使用可以参考 egg 中间件

// Controller
import { PostPrefix } from "egg-shell-decorators";
import BaseController from "./base";

@Prefix("user")
export default class UserController extends BaseController {
  @Post("/getUserToken")
  public async getUserToken({
    request: {
      body: { params },
    },
  }) {
    const { ctx, app } = this;
    const { username, password } = params;

    // gitlab 获取 access_token
    const userToken = await ctx.service.user.getUserToken({
      username,
      password,
    });

    // gitlab 获取用户信息
    const userInfo = await ctx.service.user.getUserInfo({
      accessToken: userToken.access_token,
    });

    // 用户数据本地落库
    ctx.service.user.saveUser({
      userInfo,
    });

    // 将用户信息及 token 使用 jwt 注册
    const token = app.jwt.sign(
      {
        userToken,
        userInfo,
      },
      app.config.jwt.secret
    );
    
    ctx.set({ authorization: token }); // 设置 headers
    this.success(userInfo);
  }
}

// Service
import { Service } from "egg";

export default class User extends Service {
  // 使用 gitlab api 获取 access_token
  public async getUserToken({ username, password }) {
    const { data: token } = await this.ctx.helper.utils.http.post(
      "/oauth/token",
      {
        grant_type"password",
        username,
        password,
      }
    );
    if (token && token.access_token) {
      return token;
    }
    return false;
  }

  // 使用 gitlab api 获取 gitlab 用户信息
  public async getUserInfo({ accessToken }) {
    const userInfo = await this.ctx.helper.api.gitlab.user.getUserInfo({
      accessToken,
    });
    return userInfo;
  }

  // 用户信息落库
  public async saveUser({ userInfo }) {
    const { ctx } = this;
    const {
      id,
      name,
      username,
      email,
      avatar_url: avatarUrl,
      web_url: webUrl,
    } = userInfo;

    // 查询用户是否已经落库
    const exist = await ctx.model.User.findOne({
      where: {
        id,
      },
      rawtrue,
    });
    if (exist) return;

    // 创建用户信息
    ctx.model.User.create({
      id,
      name,
      username,
      email,
      avatarUrl,
      webUrl,
    });
  }
}

上述是服务端 jwt 的使用实例,在全局中间件拦截的时候可以解析出想要的信息来后续使用,客户端的实例,我们在 react 项目中单独说明。

以上是数据库建表以及用户、权限操作的实例与简介,此系列下一篇等基本的任务流开发完毕后再推出,预计 2 周左右

尾声

此项目是从零开发,后续此系列博客会根据实际开发进度推出,项目完成之后,会开放部分源码供各位同学参考。

如对文章内容有任何疑问、见解可添加微信 Cookieboty 沟通。

另外关注公众号 Cookieboty1024,欢迎加入前端小兵成长营

手动狗头镇楼