千家信息网

如何进行Serverless + Egg.js 后台管理系统实战

发表于:2024-09-21 作者:千家信息网编辑
千家信息网最后更新 2024年09月21日,今天就跟大家聊聊有关如何进行Serverless + Egg.js 后台管理系统实战,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。背景我在文章
千家信息网最后更新 2024年09月21日如何进行Serverless + Egg.js 后台管理系统实战

今天就跟大家聊聊有关如何进行Serverless + Egg.js 后台管理系统实战,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。

背景

我在文章 基于 Serverless Component 的全栈解决方案 中讲述了,如何将一个基于 Vue.js 的前端应用和基于 Express 的后端服务,快速部署到腾讯云上。虽然受到不少开发者的喜爱,但是很多开发者私信问我,这还是一个 Demo 性质的项目而已,有没有更加实用性的解决方案。而且他们实际开发中,很多使用的正是 Egg.js 框架,能不能提供一个 Egg.js 的解决方案?如何将一个 Egg.js 的服务迁移到 Serverless 架构上呢?

Egg.js 入门

初始化 Egg.js 项目:

$ mkdir egg-example && cd egg-example$ npm init egg --type=simple$ npm i

启动项目:

$ npm run dev

然后浏览器访问 http://localhost:7001,就可以看到亲切的 hi, egg 了。

关于 Egg.js 的框架更多知识,建议阅读 官方文档

准备

对 Egg.js 有了简单了解,接下来我们来初始化我们的后台管理系统,新建一个项目目录 admin-system:

$ mkdir admin-system

将上面创建的 Egg.js 项目复制到 admin-system 目录下,重命名为 backend。然后将前端模板项目复制到 frontend 文件夹中:

$ git clone https://github.com/PanJiaChen/vue-admin-template.git frontend

说明: vue-admin-template 是基于 Vue2.0 的管理系统模板,是一个非常优秀的项目,建议对 Vue.js 感兴趣的开发者可以去学习下,当然如果你对 Vue.js 还不是太了解,这里有个基础入门学习教程 Vuejs 从入门到精通系列文章

之后你的项目目录结构如下:

.├── README.md├── backend     // 创建的 Egg.js 项目└── frontend    // 克隆的 Vue.js 前端项目模板

启动前端项目熟悉下界面:

$ cd frontend$ npm install$ npm run dev

然后访问 http://localhost:9528 就可以看到登录界面了。

开发后端服务

对于一个后台管理系统服务,我们这里只实现登录鉴权和文章管理功能,剩下的其他功能大同小异,读者可以之后自由补充扩展。

1. 添加 Sequelize 插件

在正式开发之前,我们需要引入数据库插件,这里本人偏向于使用 Sequelize ORM 工具进行数据库操作,正好 Egg.js 提供了 egg-sequelize 插件,于是直接拿来用,需要先安装:

$ cd frontend# 因为需要通过 sequelize 链接 mysql 所以这也同时安装 mysql2 模块$ npm install egg-sequelize mysql2 --save

然后在 backend/config/plugin.js 中引入该插件:

module.exports = {  // ....  sequelize: {    enable: true,    package: "egg-sequelize"  }  // ....};

backend/config/config.default.js 中配置数据库连接参数:

// ...const userConfig = {  // ...  sequelize: {    dialect: "mysql",    // 这里也可以通过 .env 文件注入环境变量,然后通过 process.env 获取    host: "xxx",    port: "xxx",    database: "xxx",    username: "xxx",    password: "xxx"  }  // ...};// ...

2. 添加 JWT 插件

系统将使用 JWT token 方式进行登录鉴权,安装配置参考官方文档,egg-jwt

3. 添加 Redis 插件

系统将使用 redis 来存储和管理用户 token,安装配置参考官方文档,egg-redis

4. 角色 API

定义用户模型,创建 backend/app/model/role.js 文件如下:

module.exports = app => {  const { STRING, INTEGER, DATE } = app.Sequelize;  const Role = app.model.define("role", {    id: { type: INTEGER, primaryKey: true, autoIncrement: true },    name: STRING(30),    created_at: DATE,    updated_at: DATE  });  // 这里定义与 users 表的关系,一个角色可以含有多个用户,外键相关  Role.associate = () => {    app.model.Role.hasMany(app.model.User, { as: "users" });  };  return Role;};

实现 Role 相关服务,创建 backend/app/service/role.js 文件如下:

const { Service } = require("egg");class RoleService extends Service {  // 获取角色列表  async list(options) {    const {      ctx: { model }    } = this;    return model.Role.findAndCountAll({      ...options,      order: [        ["created_at", "desc"],        ["id", "desc"]      ]    });  }  // 通过 id 获取角色  async find(id) {    const {      ctx: { model }    } = this;    const role = await model.Role.findByPk(id);    if (!role) {      this.ctx.throw(404, "role not found");    }    return role;  }  // 创建角色  async create(role) {    const {      ctx: { model }    } = this;    return model.Role.create(role);  }  // 更新角色  async update({ id, updates }) {    const role = await this.ctx.model.Role.findByPk(id);    if (!role) {      this.ctx.throw(404, "role not found");    }    return role.update(updates);  }  // 删除角色  async destroy(id) {    const role = await this.ctx.model.Role.findByPk(id);    if (!role) {      this.ctx.throw(404, "role not found");    }    return role.destroy();  }}module.exports = RoleService;

一个完整的 RESTful API 就该包括以上五个方法,然后实现 RoleController, 创建 backend/app/controller/role.js:

const { Controller } = require("egg");class RoleController extends Controller {  async index() {    const { ctx } = this;    const { query, service, helper } = ctx;    const options = {      limit: helper.parseInt(query.limit),      offset: helper.parseInt(query.offset)    };    const data = await service.role.list(options);    ctx.body = {      code: 0,      data: {        count: data.count,        items: data.rows      }    };  }  async show() {    const { ctx } = this;    const { params, service, helper } = ctx;    const id = helper.parseInt(params.id);    ctx.body = await service.role.find(id);  }  async create() {    const { ctx } = this;    const { service } = ctx;    const body = ctx.request.body;    const role = await service.role.create(body);    ctx.status = 201;    ctx.body = role;  }  async update() {    const { ctx } = this;    const { params, service, helper } = ctx;    const body = ctx.request.body;    const id = helper.parseInt(params.id);    ctx.body = await service.role.update({      id,      updates: body    });  }  async destroy() {    const { ctx } = this;    const { params, service, helper } = ctx;    const id = helper.parseInt(params.id);    await service.role.destroy(id);    ctx.status = 200;  }}module.exports = RoleController;

之后在 backend/app/route.js 路由配置文件中定义 role 的 RESTful API:

router.resources("roles", "/roles", controller.role);

通过 router.resources 方法,我们将 roles 这个资源的增删改查接口映射到了 app/controller/roles.js 文件。详细说明参考 官方文档

5. 用户 API

同 Role 一样定义我们的用户 API,这里就不复制粘贴了,可以参考项目实例源码 admin-system。

6. 同步数据库表格

上面只是定义好了 RoleUser 两个 Schema,那么如何同步到数据库呢?这里先借助 Egg.js 启动的 hooks 来实现,Egg.js 框架提供了统一的入口文件(app.js)进行启动过程自定义,这个文件返回一个 Boot 类,我们可以通过定义 Boot 类中的生命周期方法来执行启动应用过程中的初始化工作。

我们在 backend 目录中创建 app.js 文件,如下:

"use strict";class AppBootHook {  constructor(app) {    this.app = app;  }  async willReady() {    // 这里只能在开发模式下同步数据库表格    const isDev = process.env.NODE_ENV === "development";    if (isDev) {      try {        console.log("Start syncing database models...");        await this.app.model.sync({ logging: console.log, force: isDev });        console.log("Start init database data...");        await this.app.model.query(          "INSERT INTO roles (id, name, created_at, updated_at) VALUES (1, 'admin', '2020-02-04 09:54:25', '2020-02-04 09:54:25'),(2, 'editor', '2020-02-04 09:54:30', '2020-02-04 09:54:30');"        );        await this.app.model.query(          "INSERT INTO users (id, name, password, age, avatar, introduction, created_at, updated_at, role_id) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 20, 'https://yugasun.com/static/avatar.jpg', 'Fullstack Engineer', '2020-02-04 09:55:23', '2020-02-04 09:55:23', 1);"        );        await this.app.model.query(          "INSERT INTO posts (id, title, content, created_at, updated_at, user_id) VALUES (2, 'Awesome Egg.js', 'Egg.js is a awesome framework', '2020-02-04 09:57:24', '2020-02-04 09:57:24', 1),(3, 'Awesome Serverless', 'Build web, mobile and IoT applications using Tencent Cloud and API Gateway, Tencent Cloud Functions, and more.', '2020-02-04 10:00:23', '2020-02-04 10:00:23', 1);"        );        console.log("Successfully init database data.");        console.log("Successfully sync database models.");      } catch (e) {        console.log(e);        throw new Error("Database migration failed.");      }    }  }}module.exports = AppBootHook;

通过 willReady 生命周期函数,我们可以执行 this.app.model.sync() 函数来同步数据表,当然这里同时初始化了角色和用户数据记录,用来做为演示用。

注意:这的数据库同步只是本地调试用,如果想要腾讯云的 Mysql 数据库,建议开启远程连接,通过 sequelize db:migrate 实现,而不是每次启动 Egg 应用时同步,示例代码已经完成此功能,参考 Egg Sequelize 文档。 这里本人为了省事,直接开启腾讯云 Mysql 公网连接,然后修改 config.default.js 中的 sequelize 配置,运行 npm run dev 进行开发模式同步。

到这里,我们的用户和角色的 API 都已经定义好了,启动服务 npm run dev,访问 https://127.0.0.1:7001/users 可以获取所有用户列表了。

7. 用户登录/注销 API

这里登录逻辑比较简单,客户端发送 用户名密码/login 路由,后端通过 login 函数接受,然后从数据库中查询该用户名,同时比对密码是否正确。如果正确则调用 app.jwt.sign() 函数生成 token,并将 token 存入到 redis 中,同时返回该 token,之后客户端需要鉴权的请求都会携带 token,进行鉴权验证。思路很简单,我们就开始实现了。

流程图如下:

Login Process

首先,在 backend/app/controller/home.js 中新增登录处理 login 方法:

class HomeController extends Controller {  // ...  async login() {    const { ctx, app, config } = this;    const { service, helper } = ctx;    const { username, password } = ctx.request.body;    const user = await service.user.findByName(username);    if (!user) {      ctx.status = 403;      ctx.body = {        code: 403,        message: "Username or password wrong"      };    } else {      if (user.password === helper.encryptPwd(password)) {        ctx.status = 200;        const token = app.jwt.sign(          {            id: user.id,            name: user.name,            role: user.role.name,            avatar: user.avatar          },          config.jwt.secret,          {            expiresIn: "1h"          }        );        try {          await app.redis.set(`token_${user.id}`, token);          ctx.body = {            code: 0,            message: "Get token success",            token          };        } catch (e) {          console.error(e);          ctx.body = {            code: 500,            message: "Server busy, please try again"          };        }      } else {        ctx.status = 403;        ctx.body = {          code: 403,          message: "Username or password wrong"        };      }    }  }}

注释:这里有个密码存储逻辑,用户在注册时,密码都是通过 helper 函数 encryptPwd() 进行加密的(这里用到最简单的 md5 加密方式,实际开发中建议使用更加高级加密方式),所以在校验密码正确性时,也需要先加密一次。至于如何在 Egg.js 框架中新增 helper 函数,只需要在 backend/app/extend 文件夹中新增 helper.js 文件,然后 modole.exports 一个包含该函数的对象就行,参考 Egg 框架扩展文档

然后,在 backend/app/controller/home.js 中新增 userInfo 方法,获取用户信息:

async userInfo() {  const { ctx } = this;  const { user } = ctx.state;  ctx.status = 200;  ctx.body = {    code: 0,    data: user,  };}

egg-jwt 插件,在鉴权通过的路由对应 controller 函数中,会将 app.jwt.sign(user, secrete) 加密的用户信息,添加到 ctx.state.user 中,所以 userInfo 函数只需要将它返回就行。

之后,在 backend/app/controller/home.js 中新增 logout 方法:

async logout() {  const { ctx } = this;  ctx.status = 200;  ctx.body = {    code: 0,    message: 'Logout success',  };}

userInfologout 函数非常简单,重点是路由中间件如何处理。

接下来,我们来定义登录相关路由,修改 backend/app/router.js 文件,新增 /login, /user-info, /logout 三个路由:

const koajwt = require("koa-jwt2");module.exports = app => {  const { router, controller, jwt } = app;  router.get("/", controller.home.index);  router.post("/login", controller.home.login);  router.get("/user-info", jwt, controller.home.userInfo);  const isRevokedAsync = function(req, payload) {    return new Promise(resolve => {      try {        const userId = payload.id;        const tokenKey = `token_${userId}`;        const token = app.redis.get(tokenKey);        if (token) {          app.redis.del(tokenKey);        }        resolve(false);      } catch (e) {        resolve(true);      }    });  };  router.post(    "/logout",    koajwt({      secret: app.config.jwt.secret,      credentialsRequired: false,      isRevoked: isRevokedAsync    }),    controller.home.logout  );  router.resources("roles", "/roles", controller.role);  router.resources("users", "/users", controller.user);  router.resources("posts", "/posts", controller.post);};

Egg.js 框架定义路由时,router.post() 函数可以接受中间件函数,用来处理一些路由相关的特殊逻辑。

比如 /user-info,路由添加了 app.jwt 作为 JWT 鉴权中间件函数,至于为什么这么用,egg-jwt 插件有明确说明。

这里稍微复杂的是 /logout 路由,因为我们在注销登录时,需要将用户的 tokenredis 中移除,所以这里借助了 koa-jwt2 的 isRevokded 参数,来进行 token 删除。

后端服务部署

到这里,后端服务的登录和注销逻辑基本完成了。那么如何部署到云函数呢?可以直接使用 tencent-egg 组件,它是专门为 Egg.js 框架打造的 Serverless Component,使用它可以快速将我们的 Egg.js 项目部署到腾讯云云函数上。

1. 准备

我们先创建一个 backend/sls.js 入口文件:

const { Application } = require("egg");const app = new Application();module.exports = app;

然后修改 backend/config/config.default.js 文件:

const config = (exports = {  env: "prod", // 推荐云函数的 egg 运行环境变量修改为 prod  rundir: "/tmp",  logger: {    dir: "/tmp"  }});

注释:这里之所有需要修改运行和日志目录,是因为云函数运行时,只有 /tmp 才有写权限。

全局安装 serverless 命令:

$ npm install serverless -g

2. 配置 Serverless

在项目根目录下创建 serverless.yml 文件,同时新增 backend 配置:

backend:  component: "@serverless/tencent-egg"  inputs:    code: ./backend    functionName: admin-system    # 这里必须指定一个具有操作 mysql 和 redis 的角色,具体角色创建,可访问 https://console.cloud.tencent.com/cam/role    role: QCS_SCFFull    functionConf:      timeout: 120      # 这里的私有网络必须和 mysql、redis 实例一致      vpcConfig:        vpcId: vpc-xxx        subnetId: subnet-xxx    apigatewayConf:      protocols:        - https

此时你的项目目录结构如下:

.├── README.md         // 项目说明文件├── serverless.yml    // serverless yml 配合文件├── backend           // 创建的 Egg.js 项目└── frontend          // 克隆的 Vue.js 前端项目模板

3. 执行部署

执行部署命令:

$ serverless --debug

之后控制台需要进行扫码登录验证腾讯云账号,扫码登录就好。等部署成功会发挥如下信息:

  backend:    region:              ap-guangzhou    functionName:        admin-system    apiGatewayServiceId: service-f1bhmhk4    url:                 https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/

这里输出的 url 就是部署成功的 API 网关接口,可以直接访问测试。

注释:云函数部署时,会自动在腾讯云的 API 网关创建一个服务,同时创建一个 API,通过该 API 就可以触发云函数执行了。

4. 账号配置(可选)

当前默认支持 Serverless cli 扫描二维码登录,如果希望配置持久的环境变量/秘钥信息,也可以在项目根目录创建 .env 文件

.env 文件中配置腾讯云的 SecretId 和 SecretKey 信息并保存,密钥可以在 API 密钥管理 中获取或者创建.

# .envTENCENT_SECRET_ID=123TENCENT_SECRET_KEY=123

5. 文章 API

跟用户 API 类似,只需要复制粘贴上面用户相关模块,修改名称为 posts, 并修改数据模型就行,这里就不粘贴代码了。

前端开发

本实例直接使用的 vue-admin-template 的前端模板。

我们需要做如下几部分修改:

  1. 删除接口模拟:更换为真实的后端服务接口

  2. 修改接口函数:包括用户相关的 frontend/src/api/user.js 和文章相关接口 frontend/src/api/post.js

  3. 修改接口工具函数:主要是修改 frontend/src/utils/request.js 文件,包括 axios请求的 baseURL 和请求的 header。

  4. UI 界面修改:主要是新增文章管理页面,包括列表页和新增页。

1. 删除接口模拟

首先删除 frontend/mock 文件夹。然后修改前端入口文件 frontend/src/main.js

// 1. 引入接口变量文件,这个会依赖 @serverless/tencent-website 组件自动生成import "./env.js";import Vue from "vue";import "normalize.css/normalize.css";import ElementUI from "element-ui";import "element-ui/lib/theme-chalk/index.css";import locale from "element-ui/lib/locale/lang/en";import "@/styles/index.scss";import App from "./App";import store from "./store";import router from "./router";import "@/icons";import "@/permission";// 2. 下面这段就是 mock server 引入,删除就好// if (process.env.NODE_ENV === 'production') {//   const { mockXHR } = require('../mock')//   mockXHR()// }Vue.use(ElementUI, { locale });Vue.config.productionTip = false;new Vue({  el: "#app",  router,  store,  render: h => h(App)});

2. 修改接口函数

修改 frontend/src/api/user.js 文件,包括登录、注销、获取用户信息和获取用户列表函数如下:

import request from "@/utils/request";// 登录export function login(data) {  return request({    url: "/login",    method: "post",    data  });}// 获取用户信息export function getInfo(token) {  return request({    url: "/user-info",    method: "get"  });}// 注销登录export function logout() {  return request({    url: "/logout",    method: "post"  });}// 获取用户列表export function getList() {  return request({    url: "/users",    method: "get"  });}

新增 frontend/src/api/post.js 文件如下:

import request from "@/utils/request";// 获取文章列表export function getList(params) {  return request({    url: "/posts",    method: "get",    params  });}// 创建文章export function create(data) {  return request({    url: "/posts",    method: "post",    data  });}// 删除文章export function destroy(id) {  return request({    url: `/posts/${id}`,    method: "delete"  });}

3. 修改接口工具函数

因为 @serverless/tencent-website 组件可以定义 env 参数,执行成功后它会在指定 root 目录自动生成 env.js,然后在 frontend/src/main.js 中引入使用。 它会挂载 env 中定义的接口变量到 window 对象上。比如这生成的 env.js 文件如下:

window.env = {};window.env.apiUrl =  "https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/";

根据此文件我们来修改 frontend/src/utils/request.js 文件:

import axios from "axios";import { MessageBox, Message } from "element-ui";import store from "@/store";import { getToken } from "@/utils/auth";// 创建 axios 实例const service = axios.create({  // 1. 这里设置为 `env.js` 中的变量 `window.env.apiUrl`  baseURL: window.env.apiUrl || "/", // url = base url + request url  timeout: 5000 // request timeout});// request 注入service.interceptors.request.use(  config => {    // 2. 添加鉴权token    if (store.getters.token) {      config.headers["Authorization"] = `Bearer ${getToken()}`;    }    return config;  },  error => {    console.log(error); // for debug    return Promise.reject(error);  });// 请求 response 注入service.interceptors.response.use(  response => {    const res = response.data;    // 只有请求code为0,才是正常返回,否则需要提示接口错误    if (res.code !== 0) {      Message({        message: res.message || "Error",        type: "error",        duration: 5 * 1000      });      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {        // to re-login        MessageBox.confirm(          "You have been logged out, you can cancel to stay on this page, or log in again",          "Confirm logout",          {            confirmButtonText: "Re-Login",            cancelButtonText: "Cancel",            type: "warning"          }        ).then(() => {          store.dispatch("user/resetToken").then(() => {            location.reload();          });        });      }      return Promise.reject(new Error(res.message || "Error"));    } else {      return res;    }  },  error => {    console.log("err" + error);    Message({      message: error.message,      type: "error",      duration: 5 * 1000    });    return Promise.reject(error);  });export default service;

4. UI 界面修改

关于 UI 界面修改,这里就不做说明了,因为涉及到 Vue.js 的基础使用,如果还不会使用 Vue.js,建议先复制示例代码就好。如果对 Vue.js 感兴趣,可以到 Vue.js 官网 学习。也可以阅读本人的 Vuejs 从入门到精通系列文章,喜欢的话,可以送上您宝贵的 Star (*^▽^*)

这里只需要复制 Demo 源码 的 frontend/routerfrontend/views 两个文件夹就好。

前端部署

因为前端编译后都是静态文件,我们需要将静态文件上传到腾讯云的 COS(对象存储) 服务,然后开启 COS 的静态网站功能就可以了,这些都不需要你手动操作,使用 @serverless/tencent-website 组件就可以轻松搞定。

1. 修改 Serverless 配置文件

修改项目根目录下 serverless.yml 文件,新增前端相关配置:

name: admin-system# 前端配置frontend:  component: "@serverless/tencent-website"  inputs:    code:      src: dist      root: frontend      envPath: src # 相对于 root 指定目录,这里实际就是 frontend/src      hook: npm run build    env:      # 依赖后端部署成功后生成的 url      apiUrl: ${backend.url}    protocol: https    # TODO: CDN 配置,请修改!!!    hosts:      - host: sls-admin.yugasun.com # CDN 加速域名        https:          certId: abcdedg # 为加速域名在腾讯云平台申请的免费证书 ID          http2: off          httpsType: 4          forceSwitch: -2# 后端配置backend:  component: "@serverless/tencent-egg"  inputs:    code: ./backend    functionName: admin-system    role: QCS_SCFFull    functionConf:      timeout: 120      vpcConfig:        vpcId: vpc-6n5x55kb        subnetId: subnet-4cvr91js    apigatewayConf:      protocols:        - https

2. 执行部署

执行部署命令:

$ serverless --debug

输出如下成功结果:

  frontend:    url:  https://dtnu69vl-470dpfh-1251556596.cos-website.ap-guangzhou.myqcloud.com    env:      apiUrl: https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/    host:      - https://sls-admin.yugasun.com (CNAME: sls-admin.yugasun.com.cdn.dnsv1.com)  backend:    region:              ap-guangzhou    functionName:        admin-system    apiGatewayServiceId: service-f1bhmhk4    url:                 https://service-f1bhmhk4-1251556596.gz.apigw.tencentcs.com/release/

注释:这里 frontend 中多输出了 host,是我们的 CDN 加速域名,可以通过配置 @serverless/tencent-website 组件的 inputs.hosts 来实现。有关 CDN 相关配置说明可以阅读 基于 Serverless Component 的全栈解决方案 - 续集。当然,如果你不想配置 CDN,直接删除,然后访问 COS 生成的静态网站 url。

部署成功后,我们就可以访问 https://sls-admin.yugasun.com 登录体验了。

看完上述内容,你们对如何进行Serverless + Egg.js 后台管理系统实战有进一步的了解吗?如果还想了解更多知识或者相关内容,请关注行业资讯频道,感谢大家的支持。

文件 函数 用户 项目 配置 登录 接口 前端 数据 角色 服务 文章 路由 开发 管理 数据库 腾讯 插件 目录 系统 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 对数据库表创建索引的缺点 win10自带数据库 服务器日常维修判断故障常用命令 sql数据库二次注册 数据库查询时时间分隔符 马赛克数据库提示连接数过多 公司服务器内部搭建 虹口区管理软件开发厂家批发价 为什么苹果显示查找不到服务器 什么是分布式数据库ppt 成都双路机架服务器 服务器常见的安全设施 高青进销存erp软件开发 守护5g网络安全 网络安全再提升 软件开发者需要单独学习数据库吗 上海雷霆互联网科技有限公司 一文看懂网络安全 数据库中怎么查找字段的值 内蒙古鑫泷网络技术有限公司 网络安全 dmz 查看某个数据库中有什么表 服务器延时查询 梦幻西游安徽区共开了多少服务器 软件工程互联网软件开发 服务器面罩安装说明pdf下载 新华互联网科技学校是全日制吗 数据库拆分函数 海口国产信创服务器 贵州省网络安全线上知识竞答题目
0