Skip to content

后台服务

  1. 使用 TypeScript 和 Koa2 框架来构建一个 Web 应用程序
  2. 用户登录、登出、注册、重置密码、检查重置密码令牌、更新用户密码
  3. 获取文章列表、头部文章、热门文章、轮播文章、所有频道、所有目录、作者详情、作者文章、文章评论、用户评论、搜索功能、添加文章浏览量。
  4. 发送更改邮箱验证码、更新邮箱、昵称、性别、描述、地址、头像、添加文章评论、回复用户评论、添加投票。
  5. 获取和审核作者公司信息、获取和审核普通作者信息、获取和设置文章评论状态。
  6. 添加、更新、删除、获取所有作者信息、公司作者认证和审核。
  7. 添加、更新、删除频道、获取所有频道信息。
  8. 添加、更新、删除封面、批量添加和删除封面、添加、删除分类、查询分类。
  9. 添加、更新、删除目录、子目录、表单、获取所有目录、子目录、表单信息。
  10. 添加、更新、删除调查问卷。
  11. 添加、删除用户、获取所有用户信息、设置用户状态、查看用户密码。
  12. 添加、更新、删除视频、批量添加和删除视频、添加、删除分类、查询分类。
  13. 添加、更新、删除角色、获取所有角色和权限信息。
  14. 添加、更新、删除用户、获取所有用户和头像信息、重新发送 URL、解锁用户、更新用户状态、邮箱、手机号。
  15. 获取概览信息、文章趋势、频道、表单、用户、作者信息。
  16. 处理 GitHub OAuth 登录。
  17. 处理 Google OAuth 认证、成功和失败的回调
  18. 添加事件跟踪功能

中间件

tokenError 中间件

import jwt from "jsonwebtoken";
import CONFIG from "../config";
export default function () {
return async (ctx, next) => {
const { url, headers } = ctx.request;
const token = headers.authorization;
const clientType = headers["client-type"];
// 不需要 token 校验的 URL 列表
const noVerifyTokenUrls = [
/^\/management\/blog\/auth*/,
/^\/oauth*/,
/^\/common*/,
/^\/track*/,
/^\/h5\/blog\/post*/,
/^\/h5\/blog\/auth*/,
];
// 检查 URL 是否在无需验证列表中
const requiresToken = !noVerifyTokenUrls.some((reg) => reg.test(url));
if (!requiresToken) {
await next();
return;
}
if (!token) {
ctx.error(ctx, 401);
return;
}
try {
const decoded = jwt.verify(token.split(" ")[1], CONFIG.tokenSecret);
const tokenKey = getTokenKey(decoded, clientType, headers);
const redisToken = await ctx.redisDB.get(tokenKey);
if (!redisToken || isTokenExpired(decoded)) {
await ctx.redisDB.destroy(tokenKey);
throw new Error("Token expired");
}
await next();
} catch (err) {
console.log(err);
ctx.error(ctx, 401);
}
};
}
function getTokenKey(decoded, clientType, headers) {
const clientId = headers["client-id"];
if (clientType === "h5") {
return `${decoded.username}-${decoded.id}-${clientId}-${clientType}`;
} else if (clientType === "management") {
return `${decoded.email}-${decoded.name}-${decoded.id}-${clientId}-${clientType}`;
}
}
function isTokenExpired(decoded) {
const now = Math.floor(Date.now() / 1000);
return decoded.exp < now;
}
  1. 处理 JWT 令牌验证的中间件。
  2. 检查请求 URL 是否需要验证令牌。
  3. 验证令牌的有效性和过期时间。
  4. 如果令牌无效或过期,返回 401 错误。
  5. 客户端持久存储 client id,来实现多点登录。
  6. 有白名单功能,不需要验证 token 的 URL。

ctx response 封装

"use strict";
import { Context } from "koa";
import { errorCode } from "./code";
/**
* response
* @param ctx
* @param data 数据
* @param code 错误码
* @param message 错误描述
*/
const response = (ctx: Context, data, code, message) => {
ctx.body = {
data,
code,
success: code === 200 ? true : false,
message,
};
};
/**
* response 成功
* @param ctx
* @param data 数据
*/
export const success = (ctx: Context, header, data) => {
response(ctx, data, 200, "SUCCESS");
};
/**
* response 异常
* @param ctx
* @param code 错误码
*/
export const error = (ctx: Context, heaadr, code: string | number) => {
let message = "";
if (typeof code === "string" && code.includes("#")) {
let extra = code.split("#")[1];
code = Number(code.split("#")[0]);
message =
ctx.request.headers["language"] == "zh_CN"
? extra + " " + errorCode[code][0]
: extra + " " + errorCode[code][1];
} else {
message =
ctx.request.headers["language"] == "zh_CN"
? errorCode[code][0]
: errorCode[code][1];
}
response(ctx, null, code, message);
};
  1. 定义了一些用于在 Koa 应用中处理 HTTP 响应的工具函数。这些函数提供了一种标准化的方式来向客户端发送成功和错误响应。
  2. 国际化处理,根据请求头的 Accept-Language 来返回对应的语言。

draft-socket 中间件

import { Server as SocketServer } from "socket.io";
import CONFIG from "../config";
import RedisDB from "./redis-db";
import * as redisMysql from "./redis-mysql";
const draftPostRedisKey = CONFIG.draftPostRedisKey;
export function initSocket(server: any): void {
console.log("init websocket");
const socketHandle = new SocketServer(server);
let redisDB = new RedisDB(CONFIG.db.redis);
socketHandle.on("connection", function (socket: any) {
console.log("socket connected");
// 离开编辑文章页面
socket.on("disconnect", function () {
console.info("[%s] DISCONNECTED", socket.sid);
});
// 进入新增文章页面,获取已保存的草稿(可以为空)
socket.on("getDraftPost", async function () {
let data = await redisDB.get(draftPostRedisKey);
if (!data) {
data = await redisMysql.getDraftPostFromMysql();
socket.emit("getDraftPost", data);
await redisDB.set(draftPostRedisKey, data);
} else {
socket.emit("getDraftPost", data);
}
});
// 实时保存文章内容
socket.on("saveDraftPost", async function (data: any) {
let res = await redisDB.set(draftPostRedisKey, data);
socket.emit("saveDraftPost", res);
});
// 保存后清空已保存的文章草稿
socket.on("clearDraftPost", async function () {
await redisDB.destroy(draftPostRedisKey);
await redisMysql.clearDraftPostOfMysql();
socket.emit("clearDraftPost", true);
});
});
}
  1. 文件用于初始化和处理 WebSocket 连接,主要功能是处理文章草稿的实时保存和获取
  2. connection:当客户端连接时触发,打印连接日志。
  3. disconnect:当客户端断开连接时触发,打印断开连接日志。
  4. getDraftPost:当客户端请求获取草稿文章时触发,先从 Redis 获取数据,如果没有则从 MySQL 获取并缓存到 Redis,然后将数据发送给客户端。
  5. saveDraftPost:当客户端请求保存草稿文章时触发,将数据保存到 Redis,并将保存结果发送给客户端。
  6. clearDraftPost:当客户端请求清空草稿文章时触发,删除 Redis 中的草稿数据,并清空 MySQL 中的草稿数据,然后将清空结果发送给客户端。

minio s3 中间件

  1. 雨云第三方对象存储服务,用于存储用户上传的文件
  2. 分别把不同类型的文件存储到 bucket 中,比如 file 类型,url,base64 等
  3. 先临时把文件存储到本地,然后再上传到 minio s3 中,最后删除本地文件

session 和 redis 中间件

import Redis from 'ioredis';
import CONFIG from '../config';
import { isEqual } from 'lodash';
const salt = CONFIG.db.db_salt;
const saltBuffer = Buffer.from(salt, 'base64');
/**
* Encrypt input password
*
* @param {string} password
* @return {string}
* @api public
*/
class RedisDB {
public redis: any;
private redis2: any;
constructor(redisConfig: any) {
this.redis = new Redis(redisConfig);
}
async get<T>(key: string): Promise<T | null> {
let data = await this.redis.get(key);
return JSON.parse(data);
}
async set<T>(
key: string,
data: T,
maxAge = 7 * 24 * 60 * 60 * 1000,
): Promise<string> {
try {
// Use redis set EX to automatically drop expired sessions
await this.redis.set(key, JSON.stringify(data), 'EX', maxAge / 1000);
return Promise.resolve(key);
} catch (e) {
}
return 'success';
}
async destroy(key: string): Promise<number> {
// const md5_key = crypto
// .pbkdf2Sync(key, saltBuffer, 10000, 64, "sha1")
// .toString("base64");
return await this.redis.del(key);
}
async update<T>(key: string, data: T): Promise<string> {
try {
// 获取当前 key 的剩余过期时间(以秒为单位)
const ttl = await this.redis.ttl(key);
if (ttl > 0) {
// 如果 key 有有效的剩余过期时间,使用它来设置新值
await this.redis.set(key, JSON.stringify(data), 'EX', ttl);
} else {
// 如果 key 没有设置过期时间或已过期,只设置新值
await this.redis.set(key, JSON.stringify(data));
}
return Promise.resolve(key);
} catch (e) {
// 错误处理
console.error(e);
throw e; // 或返回错误信息
}
}
// 获取所有的value
async getAllValue(): Promise<any> {
const keys = await this.redis.keys('*');
const values = await this.redis.mget(keys);
return values.map((value: string) => {
return JSON.parse(value);
});
}
// 找出value对应的key
async getKeyByValue(value: any): Promise<string | null> {
const keys = await this.redis.keys('*');
const values = await this.redis.mget(keys);
const index = values.findIndex((val: string) => {
return isEqual(JSON.parse(val), value);
});
return keys[index];
}
}
export default RedisDB;
  1. 文件定义了一个 RedisDB 类,该类提供了与 Redis 数据库交互的方法。该类包括设置、获取、更新和删除键的方法,以及处理 Redis 数据的其他实用方法
  2. salt 和 saltBuffer 用于加密目的
    1. get: 从 Redis 获取并解析一个值。
    2. set: 将一个值存储到 Redis,并可选设置过期时间。
    3. destroy: 从 Redis 删除一个键。
    4. update: 更新 Redis 中的一个值,同时保留其 TTL。
    5. getAllValue: 获取 Redis 中的所有值。
    6. getKeyByValue: 使用深度比较查找值对应的键。

mysql-async

import mysql from 'mysql2';
import CONFIG from '../config';
const pool = mysql.createPool({
host: CONFIG.db.mysql.host,
user: CONFIG.db.mysql.user,
port: CONFIG.db.mysql.port,
password: CONFIG.db.mysql.password,
database: CONFIG.db.mysql.database,
connectionLimit: CONFIG.db.mysql.connectionLimit,
});
const query = function (sqls: string | string[], values?: any): Promise<any> {
return new Promise((resolve, reject) => {
pool.getConnection((err, connection) => {
if (err) {
console.log(err);
return reject(err);
}
if (typeof sqls === 'string') {
connection.query(sqls, values, (err, rows) => {
connection.release();
if (err) {
return reject(err);
} else {
return resolve(rows);
}
});
} else {
// 开始事务
connection.beginTransaction((err) => {
if (err) {
connection.release();
return reject(err);
}
// 定义一个数组,用于保存多条 SQL 语句的执行结果
const results = [];
// 遍历 SQL 语句数组,依次执行每条 SQL 语句
Promise.all(
sqls.map((sql) => {
return new Promise<void>((resolve, reject) => {
connection.query(sql, values, (err, rows) => {
if (err) {
// 如果执行出错,回滚事务并释放连接
connection.rollback(() => {
connection.release();
console.log(err.message, sql);
reject(err);
});
} else {
// 将执行结果保存到数组中
results.push(rows);
resolve();
}
});
});
})
)
.then(() => {
// 如果所有 SQL 语句都执行成功,提交事务并释放连接
connection.commit((err) => {
if (err) {
connection.rollback(() => {
connection.release();
reject(err);
});
} else {
connection.release();
resolve(results);
}
});
})
.catch((err) => {
// 如果有任何一条 SQL 语句执行失败,回滚事务并释放连接
connection.rollback(() => {
connection.release();
reject(err);
});
});
});
}
});
});
};
export default query;
  1. 用于与 MySQL 数据库进行异步交互的模块。该模块使用 mysql2 库创建了一个连接池,并提供了一个 query 函数来执行 SQL 查询。该函数支持单条 SQL 语句和事务处理。
  2. 包含导入,连接,查询,如果 sqls 是数组,则开启事务,依次执行每条 SQL 语句,并在所有语句成功后提交事务,否则回滚事务。

route decorator 中间件

  1. 定义了一些装饰器,用于简化路由和文件上传的处理,装饰器可以将路由和文件上传的逻辑从业务代码中分离出来,装饰器使得路由和文件上传的定义更加直观和清晰,便于理解和维护
  2. 导入:
    reflect-metadata:用于反射元数据。 koa-multer:用于处理文件上传的中间件。
  3. 常量:
    ROUTE_PREFIX:用于存储路由元数据的键。 HTTP_METHODS:定义了常见的 HTTP 方法。
  4. 接口:
    RouteMeta:定义了路由元数据的结构,包括路径、方法和处理函数。
  5. 装饰器函数: Route:用于定义路由的装饰器,接受路径和方法作为参数。 Upload:用于处理文件上传的装饰器,接受字段名和目标路径作为参数。

发送邮件中间件

  1. 用于发送电子邮件。该函数使用 nodemailer 和 nodemailer-smtp-transport 库来配置和发送邮件