create-pixle-koa-template 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/bin/create-app.js +76 -0
  2. package/package.json +17 -0
  3. package/template/.env +20 -0
  4. package/template/app.js +49 -0
  5. package/template/config/db.js +29 -0
  6. package/template/config/email.js +30 -0
  7. package/template/config/index.js +36 -0
  8. package/template/config/redis.js +19 -0
  9. package/template/controllers/AuthController.js +71 -0
  10. package/template/controllers/DownloadController.js +18 -0
  11. package/template/controllers/UploadController.js +60 -0
  12. package/template/controllers/UserController.js +90 -0
  13. package/template/middleware/auth.js +91 -0
  14. package/template/middleware/errorHandler.js +17 -0
  15. package/template/middleware/logger.js +41 -0
  16. package/template/middleware/notFound.js +84 -0
  17. package/template/middleware/upload.js +165 -0
  18. package/template/models/Auth.js +11 -0
  19. package/template/models/BaseDAO.js +449 -0
  20. package/template/models/User.js +10 -0
  21. package/template/package-lock.json +3427 -0
  22. package/template/package.json +34 -0
  23. package/template/public/404.html +160 -0
  24. package/template/routes/auth.js +21 -0
  25. package/template/routes/download.js +9 -0
  26. package/template/routes/index.js +105 -0
  27. package/template/routes/upload.js +28 -0
  28. package/template/routes/user.js +22 -0
  29. package/template/services/AuthService.js +190 -0
  30. package/template/services/CodeRedisService.js +94 -0
  31. package/template/services/DownloadService.js +54 -0
  32. package/template/services/EmailService.js +245 -0
  33. package/template/services/JwtTokenService.js +50 -0
  34. package/template/services/PasswordService.js +133 -0
  35. package/template/services/TokenRedisService.js +29 -0
  36. package/template/services/UserService.js +128 -0
  37. package/template/utils/crypto.js +9 -0
  38. package/template/utils/passwordValidator.js +81 -0
  39. package/template/utils/prototype/day.js +237 -0
  40. package/template/utils/prototype/deepClone.js +32 -0
  41. package/template/utils/prototype/index.js +61 -0
  42. package/template/utils/response.js +26 -0
  43. package/template//344/275/277/347/224/250/346/225/231/347/250/213.md +881 -0
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ async function createProject() {
7
+ try {
8
+ // 获取项目名称
9
+ const projectName = process.argv[2];
10
+
11
+ if (!projectName) {
12
+ console.log('\x1b[36m%s\x1b[0m', '✨ Create Pixle Koa Template ✨');
13
+ console.log('\x1b[90m%s\x1b[0m', 'Usage: npm create pixle-koa-template <project-name>\n');
14
+ console.log('\x1b[31m%s\x1b[0m', 'Please provide a project name.');
15
+ console.log('Example: npm create pixle-koa-template my-project');
16
+ process.exit(1);
17
+ }
18
+
19
+ // 验证项目名称
20
+ if (!/^[a-z0-9-]+$/.test(projectName)) {
21
+ console.log('\x1b[31m%s\x1b[0m',
22
+ 'Project name can only contain lowercase letters, numbers, and hyphens');
23
+ process.exit(1);
24
+ }
25
+
26
+ const targetPath = path.join(process.cwd(), projectName);
27
+
28
+ // 检查目录是否存在
29
+ if (fs.existsSync(targetPath)) {
30
+ console.log('\x1b[31m%s\x1b[0m', `Directory "${projectName}" already exists!`);
31
+ process.exit(1);
32
+ }
33
+
34
+ console.log('\x1b[34m%s\x1b[0m', `📦 Creating project: ${projectName}`);
35
+
36
+ // 创建项目目录
37
+ fs.mkdirSync(targetPath, { recursive: true });
38
+
39
+ // 复制模板文件
40
+ const templatePath = path.join(__dirname, '../template');
41
+ copyDirSync(templatePath, targetPath, projectName);
42
+
43
+ console.log('\x1b[32m%s\x1b[0m', `✅ Project created successfully!`);
44
+ console.log('\x1b[36m%s\x1b[0m', '\n📋 Next steps:');
45
+ console.log(` cd ${projectName}`);
46
+ console.log(' npm install');
47
+ console.log(' npm run dev\n');
48
+
49
+ } catch (error) {
50
+ console.error('\x1b[31m%s\x1b[0m', `\n❌ Error: ${error.message}`);
51
+ process.exit(1);
52
+ }
53
+ }
54
+
55
+ // 递归复制目录
56
+ function copyDirSync(src, dest, projectName) {
57
+ fs.mkdirSync(dest, { recursive: true });
58
+
59
+ const entries = fs.readdirSync(src, { withFileTypes: true });
60
+
61
+ for (const entry of entries) {
62
+ const srcPath = path.join(src, entry.name);
63
+ const destPath = path.join(dest, entry.name);
64
+
65
+ if (entry.isDirectory()) {
66
+ copyDirSync(srcPath, destPath, projectName);
67
+ } else {
68
+ // 读取文件内容并替换变量
69
+ let content = fs.readFileSync(srcPath, 'utf8');
70
+ content = content.replace(/{{project-name}}/g, projectName);
71
+ fs.writeFileSync(destPath, content, 'utf8');
72
+ }
73
+ }
74
+ }
75
+
76
+ createProject();
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "create-pixle-koa-template",
3
+ "version": "1.0.0",
4
+ "description": "Koa template",
5
+ "bin": {
6
+ "create-pixle-koa-template": "./bin/create-app.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "template/"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "keywords": ["koa", "template", "pixle" ],
16
+ "license": "MIT"
17
+ }
package/template/.env ADDED
@@ -0,0 +1,20 @@
1
+ # 服务器配置
2
+ PORT=3001 # 端口
3
+ NODE_ENV=development
4
+
5
+ # JWT配置
6
+ JWT_SECRET=your_jwt_secret_key_here # 密钥
7
+
8
+
9
+ # 邮箱配置
10
+ EMAIL_HOST=smtp.qq.com # QQ邮箱SMTP服务器地址
11
+ EMAIL_PORT=465 # SSL加密端口(推荐)
12
+ EMAIL_USER=your_email@example.com # 发件人邮箱地址
13
+ EMAIL_PASS=your_email_authorization_code # 邮箱授权码(非登录密码)
14
+ EMAIL_FROM=your_email@example.com # 显示的发件人地址
15
+
16
+
17
+ # 验证码配置
18
+ VERIFICATION_CODE_EXPIRE=300 # 验证码过期时间 5分钟
19
+ VERIFICATION_SEND_LIMIT_EXPIRE=60 # 重复发送限制时间60秒
20
+ MAX_ATTEMPTS_PER_DAY=10 # 每日最多发送10次
@@ -0,0 +1,49 @@
1
+ require("dotenv").config(); // 在最顶部引入并配置
2
+ require("./utils/prototype").loadAndExecutePrototypes(); // 加载并执行所有原型扩展
3
+ require("./config/db.js");//加载数据库配置
4
+ require("./config/redis.js");// 加载Redis配置
5
+ const Koa = require("koa");
6
+ const app = new Koa();
7
+ const errorHandlerMiddleware = require("./middleware/errorHandler.js");
8
+ const loggerMiddleware = require("./middleware/logger.js");
9
+ const authMiddleware = require("./middleware/auth.js");
10
+ const notFoundMiddleware = require("./middleware/notFound.js");
11
+ const bodyParser = require("koa-bodyparser");
12
+ const static = require("koa-static");
13
+ const path = require("path");
14
+ const mount = require("koa-mount");
15
+ const { registerRoutes } = require("./routes");
16
+ const config = require("./config");
17
+
18
+
19
+ //错误处理
20
+ app.use(errorHandlerMiddleware);
21
+ // 日志中间件
22
+ app.use(loggerMiddleware);
23
+ app.on("error", (err, ctx) => {
24
+ console.error("server error", err);
25
+ });
26
+
27
+ //添加静态资源,前缀/static,静态资源目录为public
28
+ app.use(mount("/static", static(path.join(__dirname, "public"))));
29
+ // 使用 bodyParser 中间件
30
+ app.use(bodyParser());
31
+
32
+ // JWT 认证中间件(必须放在路由注册之前,确保所有API请求都经过认证)
33
+ app.use(authMiddleware);
34
+
35
+ // 注册路由
36
+ registerRoutes(app, config.routes.prefix);
37
+ //404中间件
38
+ app.use(notFoundMiddleware);
39
+
40
+ // 端口号
41
+ const PORT = process.env.PORT || 3000;
42
+ // 启动服务器
43
+ async function startServer() {
44
+ app.listen(PORT, () => {
45
+ console.log(`🚀 服务器已启动: http://localhost:${PORT}`);
46
+ });
47
+ }
48
+
49
+ startServer();
@@ -0,0 +1,29 @@
1
+ const mysql = require("mysql2/promise");
2
+ const pool = mysql.createPool({
3
+ host: "localhost",//数据库服务器地址,默认localhost
4
+ port: 3306,// 默认端口
5
+ user: "root",//用户名,默认root
6
+ password: "1234qwer",//密码
7
+ database: "demo",//数据库名称
8
+ // 连接池配置
9
+ waitForConnections: true,// 没有连接时等待
10
+ connectionLimit: 10, // 连接池最大连接数
11
+ queueLimit: 0, // 等待队列无限制(0表示不限制)
12
+ });
13
+
14
+ // 测试数据库连接
15
+ async function testConnection() {
16
+ try {
17
+ const connection = await pool.getConnection();
18
+ console.log("数据库连接成功");
19
+ connection.release();// 释放连接回池
20
+ } catch (e) {
21
+ console.log(e, "数据库连接失败");
22
+ }
23
+ }
24
+
25
+ //执行连接测试
26
+ testConnection()
27
+
28
+ module.exports = pool
29
+
@@ -0,0 +1,30 @@
1
+ // QQ邮箱专用配置
2
+ const QQ_EMAIL_CONFIG = {
3
+ // QQ邮箱固定配置
4
+ host: process.env.EMAIL_HOST,
5
+ port: process.env.EMAIL_PORT,
6
+ secure: true, // 使用SSL
7
+
8
+ // 认证信息(从环境变量获取)
9
+ auth: {
10
+ user: process.env.EMAIL_USER,
11
+ pass: process.env.EMAIL_PASS
12
+ },
13
+
14
+ // 连接配置
15
+ connectionTimeout: 10000, // 10秒连接超时
16
+ greetingTimeout: 10000, // 10秒问候超时
17
+ socketTimeout: 60000, // 60秒socket超时
18
+
19
+ // TLS配置
20
+ tls: {
21
+ rejectUnauthorized: false // 允许自签名证书
22
+ },
23
+
24
+ // 调试模式
25
+ debug: process.env.NODE_ENV === 'development',
26
+ logger: process.env.NODE_ENV === 'development'
27
+ };
28
+
29
+
30
+ module.exports = QQ_EMAIL_CONFIG;
@@ -0,0 +1,36 @@
1
+ const path = require("path");
2
+
3
+ let config = {
4
+ // 应用配置
5
+ app: {},
6
+ //路由配置
7
+ routes: {
8
+ prefix: "/api", //路由前缀
9
+ },
10
+ // JWT 配置
11
+ jwt: {
12
+ secret: process.env.JWT_SECRET, // 64 字节的 Base64 编码密钥,引用utils/crypto.js工具生成
13
+ expiresIn: 24 * 60 * 60 * 7, // Token 有效期7天
14
+ //不需要鉴权路由白名单
15
+ pathWhiteList: [
16
+ /^\/static\/.*/, //静态资源
17
+ '/api/auth/login', //登录接口
18
+ '/api/auth/register', //注册接口
19
+ '/api/auth/verify-code',//发送验证码
20
+ '/api/auth/reset-password',//重置密码接口
21
+ ],
22
+ },
23
+ //上传文件配置
24
+ upload: {
25
+ // 上传文件目录
26
+ uploadDir: {
27
+ image: path.join(__dirname, "../storage/uploads/image"), //图片上传目录路径
28
+ file: path.join(__dirname, "../storage/uploads/file"), //文件上传目录路径
29
+ },
30
+ // 上传文件大小限制(字节)
31
+ maxSize: 1024 * 1024 * 20, // 20MB
32
+ maxCount: 9, //多文件上传,一次最多9个文件
33
+ },
34
+ };
35
+
36
+ module.exports = config;
@@ -0,0 +1,19 @@
1
+ const Redis = require("ioredis");
2
+ const redis = new Redis({
3
+ host: "localhost",
4
+ port: 6379,
5
+ password: '',
6
+ db: 0,
7
+ });
8
+
9
+ // 错误处理
10
+ redis.on('error', (err) => {
11
+ console.error('Redis error:', err);
12
+ });
13
+
14
+ // 连接成功
15
+ redis.on('ready', () => {
16
+ console.log('Redis链接成功');
17
+ })
18
+
19
+ module.exports = redis;
@@ -0,0 +1,71 @@
1
+ const AuthService = require("../services/AuthService.js");
2
+ const PasswordService = require("../services/PasswordService.js");
3
+ const { success, error } = require("../utils/response.js");
4
+
5
+ class AuthController {
6
+ //登录
7
+ async login(ctx) {
8
+ let { account, password } = ctx.request.body;
9
+ let res = await AuthService.login(ctx, account, password);
10
+ res && success(ctx, res, "登录成功");
11
+ }
12
+
13
+ //退出登录
14
+ async logout(ctx) {
15
+ let res = await AuthService.logout(ctx);
16
+ res && success(ctx, true, "退出登录成功");
17
+ }
18
+
19
+ //注册
20
+ async register(ctx) {
21
+ let res = await AuthService.register(ctx, ctx.request.body);
22
+ res && success(ctx, res, "注册成功");
23
+ }
24
+
25
+ //发送验证码
26
+ async sendVerificationCode(ctx) {
27
+ let { email,type } = ctx.request.body;
28
+ let res = await AuthService.sendVerificationCode(ctx, email,type);
29
+ res && success(ctx, true, "验证码发送成功");
30
+ }
31
+ //修改密码
32
+ async changePassword(ctx) {
33
+ let { oldPassword, newPassword } = ctx.request.body;
34
+ // 验证旧密码是否为空
35
+ if (!oldPassword) {
36
+ return error(ctx, "旧密码不能为空", 400);
37
+ }
38
+ // 验证新密码是否为空
39
+ if (!newPassword) {
40
+ return error(ctx, "新密码不能为空", 400);
41
+ }
42
+ if (oldPassword === newPassword) {
43
+ return error(ctx, "新密码不能与旧密码相同", 400);
44
+ }
45
+ let res = await PasswordService.changePassword(
46
+ ctx,
47
+ oldPassword,
48
+ newPassword
49
+ );
50
+ res && success(ctx, true, "密码修改成功");
51
+ }
52
+
53
+ //重置密码
54
+ async resetPassword(ctx) {
55
+ let { email, code, password } = ctx.request.body;
56
+ if (!email) {
57
+ return error(ctx, "邮箱不能为空", 400);
58
+ }
59
+ if (!code) {
60
+ return error(ctx, "验证码不能为空", 400);
61
+ }
62
+ if (!password) {
63
+ return error(ctx, "新密码不能为空", 400);
64
+ }
65
+
66
+ let res = await PasswordService.resetPassword(ctx, email, code, password);
67
+ res && success(ctx, true, "密码重置成功");
68
+ }
69
+ }
70
+
71
+ module.exports = new AuthController();
@@ -0,0 +1,18 @@
1
+ const { error } = require("../utils/response.js");
2
+ const DownloadService = require("../services/DownloadService.js");
3
+
4
+ // 下载控制器
5
+ class DownloadController {
6
+ async download(ctx) {
7
+ let { url } = ctx.query;
8
+ if (!url) {
9
+ return error(ctx, "url不能为空", 400);
10
+ }
11
+ let urlArr = url.split("/");
12
+ if (urlArr.length < 2) {
13
+ return error(ctx, "url格式错误", 400);
14
+ }
15
+ await DownloadService.download(ctx, url);
16
+ }
17
+ }
18
+ module.exports = new DownloadController();
@@ -0,0 +1,60 @@
1
+ const { success } = require("../utils/response.js");
2
+ // 上传控制器
3
+ class UploadController {
4
+
5
+ /**
6
+ * 单文件上传控制器
7
+ * @param {Object} ctx - Koa上下文对象,包含请求和响应信息
8
+ * @returns {Promise<void>} - 无返回值,直接通过ctx返回响应
9
+ */
10
+ async single(ctx) {
11
+ // 从上下文对象中获取上传的单个文件
12
+ // ctx.file 是由文件上传中间件(如 koa-multer)处理后生成的文件对象
13
+ const file = ctx.file;
14
+
15
+ // 上传成功,返回处理后的文件信息
16
+ success(
17
+ ctx,
18
+ {
19
+ filename: file.filename, // 原始文件名
20
+ // 构建可访问的文件URL路径
21
+ // 1. 将文件路径按"\\storage\\"分割,获取存储目录后的部分
22
+ // 2. 将Windows风格的反斜杠(\\)替换为URL标准的正斜杠(/)
23
+ // 3. 拼接上"/storage/"前缀,形成完整的可访问URL
24
+ // 示例: /storage/uploads/image/2025/12/16/image_1765852398032_hcjukkjyamu.png
25
+ url: `/storage/${file.path.split("\\storage\\")[1].replaceAll("\\", "/")}`,
26
+ size: file.size, // 文件大小,单位为字节(B)
27
+ },
28
+ "上传成功" // 响应成功提示信息
29
+ );
30
+ }
31
+
32
+
33
+
34
+ /**
35
+ * 多文件上传控制器
36
+ * @param {Object} ctx - Koa上下文对象,包含请求和响应信息
37
+ * @returns {Promise<void>} - 无返回值,直接通过ctx返回响应
38
+ */
39
+ async multiple(ctx) {
40
+ // 从上下文对象中获取上传的文件列表
41
+ // ctx.files 是由文件上传中间件(如 koa-multer)处理后生成的文件数组
42
+ const files = ctx.files;
43
+
44
+ // 处理每个上传的文件,构建返回给客户端的文件信息列表
45
+ let list = files.map((item) => ({
46
+ filename: item.filename, // 原始文件名
47
+ // 构建可访问的文件URL路径
48
+ // 1. 将文件路径按"\\storage\\"分割,获取存储目录后的部分
49
+ // 2. 将Windows风格的反斜杠(\\)替换为URL标准的正斜杠(/)
50
+ // 3. 拼接上"/storage/"前缀,形成完整的可访问URL
51
+ url: `/storage/${item.path.split("\\storage\\")[1].replaceAll("\\", "/")}`,
52
+ size: item.size, // 文件大小,单位为字节(B)
53
+ }));
54
+
55
+ // 上传成功,返回处理后的文件信息列表和成功提示
56
+ success(ctx, list, "上传成功");
57
+ }
58
+ }
59
+
60
+ module.exports = new UploadController();
@@ -0,0 +1,90 @@
1
+ const UserService = require("../services/UserService.js");
2
+ const AuthService = require("../services/AuthService.js");
3
+ const { success, error } = require("../utils/response.js");
4
+
5
+ class UserController {
6
+ //获取当前用户详情
7
+ async getCurrentUser(ctx) {
8
+ // 从JWT令牌解密后的payload中获取用户ID,默认挂载到ctx.state.user
9
+ let { userId } = ctx.state.user;
10
+
11
+ // 检查用户ID是否存在(即用户是否已登录)
12
+ if (userId) {
13
+ // 调用用户服务层获取用户详细信息
14
+ let result = await UserService.getUserById(ctx, userId);
15
+ // 如果查询成功,返回用户信息
16
+ result && success(ctx, result);
17
+ } else {
18
+ error(ctx, "请登录", 400);
19
+ }
20
+ }
21
+
22
+ //获取单个用户
23
+ async getUserById(ctx) {
24
+ let userId = ctx.params.id;
25
+
26
+ // 验证用户ID是否为空
27
+ if (!userId) {
28
+ return error(ctx, "用户ID不能为空", 400);
29
+ }
30
+
31
+ // 调用用户服务层获取用户详细信息
32
+ let result = await UserService.getUserById(ctx, userId);
33
+ result && success(ctx, result);
34
+ }
35
+
36
+ //添加用户
37
+ async createUser(ctx) {
38
+ // 调用认证服务层注册用户,第三个参数false表示不是验证码注册类型来添加用户
39
+ let result = await AuthService.register(ctx, ctx.request.body, false);
40
+
41
+ // 如果创建成功,返回创建结果
42
+ result && success(ctx, result);
43
+ }
44
+
45
+ //更新用户
46
+ async updateUser(ctx) {
47
+ const { id } = ctx.params;
48
+ const params = ctx.request.body;
49
+
50
+ // 验证用户ID是否为空
51
+ if (!id) {
52
+ return error(ctx, "用户ID不能为空", 400);
53
+ }
54
+
55
+ // 验证更新参数是否有效:必须是对象、不能是数组、不能为空对象
56
+ if (!(params
57
+ && typeof params === 'object'
58
+ && !Array.isArray(params)
59
+ && Object.keys(params).length > 0)) {
60
+ return error(ctx, "参数不能为空", 400);
61
+ }
62
+
63
+ // 调用用户服务层更新用户信息
64
+ const result = await UserService.updateUser(ctx, id, params);
65
+ result && success(ctx, result);
66
+ }
67
+
68
+ //删除用户
69
+ async deleteUser(ctx) {
70
+ const { id } = ctx.params;
71
+ // 验证用户ID是否为空
72
+ if (!id) {
73
+ return error(ctx, "用户ID不能为空", 400);
74
+ }
75
+ // 调用用户服务层删除用户
76
+ const result = await UserService.deleteUser(ctx, id);
77
+ result && success(ctx, result);
78
+ }
79
+
80
+ //获取用户列表
81
+ async getUserList(ctx) {
82
+ // 调用用户服务层获取用户列表
83
+ let result = await UserService.getUserList(ctx);
84
+
85
+ // 如果查询成功,返回用户列表数据
86
+ result && success(ctx, result);
87
+ }
88
+ }
89
+
90
+ module.exports = new UserController();
@@ -0,0 +1,91 @@
1
+ const koajwt = require("koa-jwt");
2
+ const config = require("../config");
3
+ const { getToken } = require("../services/TokenRedisService.js");
4
+
5
+ /**
6
+ * JWT 认证中间件(token校验)
7
+ * 只对已匹配的路由进行认证,未匹配的路由返回404
8
+ */
9
+ const authMiddleware = async (ctx, next) => {
10
+ // 使用从routes/index.js自动收集的路由信息
11
+ const registeredRoutes = ctx.app.context.registeredRoutes || [];
12
+ let pathExists = false;
13
+
14
+ // 检查请求路径是否匹配任何已注册的路由
15
+ for (const route of registeredRoutes) {
16
+ try {
17
+ // 获取路由路径,用于后续分析
18
+ const routePath = route.path;
19
+
20
+ // 对于静态路由,直接比较路径
21
+ if (routePath === ctx.path) {
22
+ pathExists = true;
23
+ break;
24
+ }
25
+
26
+ // 对于动态路由(包含:param格式的参数),我们需要特殊处理
27
+ if (routePath.includes(':')) {
28
+ // 分割路径部分进行比较
29
+ const requestParts = ctx.path.split('/').filter(v=>!!v);
30
+ const routeParts = routePath.split('/').filter(v=>!!v);
31
+
32
+ // 如果路径部分数量相同,可能是一个动态路由匹配
33
+ if (requestParts.length === routeParts.length) {
34
+ let isMatch = true;
35
+
36
+ // 逐个比较路径部分,跳过参数部分
37
+ for (let i = 0; i < routeParts.length; i++) {
38
+ // 如果路由部分不是参数格式(不以:开头),则必须完全匹配
39
+ if (!routeParts[i].startsWith(':') && routeParts[i] !== requestParts[i]) {
40
+ isMatch = false;
41
+ break;
42
+ }
43
+ }
44
+
45
+ if (isMatch) {
46
+ pathExists = true;
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ } catch (e) {
52
+ // 忽略可能的错误
53
+ }
54
+ }
55
+
56
+ // 如果路径不存在,直接继续,最终返回404
57
+ if (!pathExists) {
58
+ await next();
59
+ return;
60
+ }
61
+
62
+ // 使用koajwt进行认证
63
+ const jwtAuth = koajwt({
64
+ secret: config.jwt.secret,
65
+ //是否撤销,返回true token失效
66
+ isRevoked: async (ctx, payload, token) => {
67
+ try {
68
+ if (!payload?.userId) {
69
+ return true; //格式错误,视为已撤销
70
+ }
71
+ // 根据用户id从白名单中获取token
72
+ const storedToken = await getToken(payload.userId);
73
+ //存在白名单中而且和 redis 中token一致,则视为有效
74
+ if (storedToken && storedToken === token) {
75
+ return false;
76
+ }
77
+ return true;
78
+ } catch (error) {
79
+ return true; // 出错时保守处理,视为已撤销
80
+ }
81
+ },
82
+ }).unless({
83
+ //白名单
84
+ path: config.jwt.pathWhiteList,
85
+ });
86
+
87
+ // 执行JWT认证
88
+ return jwtAuth(ctx, next);
89
+ };
90
+
91
+ module.exports = authMiddleware;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 错误处理中间件
3
+ */
4
+ const errorHandlerMiddleware = async (ctx, next) => {
5
+ try {
6
+ await next();
7
+ } catch (err) {
8
+ ctx.status = err?.status ?? 500;
9
+ ctx.body = {
10
+ code: ctx.status,
11
+ message: ctx.status == 500 ? "服务器内部错误" : err?.message ?? "error",
12
+ };
13
+ ctx.app.emit("error", err, ctx);
14
+ }
15
+ };
16
+
17
+ module.exports = errorHandlerMiddleware;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * 日志中间件
3
+ * 记录请求和响应的信息,包括URL、方法、状态码、响应时间等
4
+ */
5
+
6
+ // 颜色配置
7
+ const colors = {
8
+ red: "\x1b[31m",
9
+ green: "\x1b[32m",
10
+ yellow: "\x1b[33m",
11
+ cyan: "\x1b[36m",
12
+ white: "\x1b[37m",
13
+ };
14
+
15
+ //根据状态码获取颜色
16
+ const getStatusColor = (status) => {
17
+ if (status >= 500) return colors.red; // 服务器错误 - 红色
18
+ if (status >= 400) return colors.yellow; // 客户端错误 - 黄色
19
+ if (status >= 300) return colors.cyan; // 重定向 - 青色
20
+ if (status >= 200) return colors.green; // 成功 - 绿色
21
+ return colors.white; // 其他 - 白色
22
+ };
23
+
24
+ //日志中间件
25
+ const loggerMiddleware = async (ctx, next) => {
26
+ const start = Date.now();
27
+ const { method, url, ip } = ctx;
28
+
29
+ await next();
30
+ const responseTime = Date.now() - start;
31
+ const status = ctx.status;
32
+ const timestamp = new Date().format("yyyy-MM-dd HH:mm:ss");
33
+ // 根据状态码设置日志颜色
34
+ let logColor = getStatusColor(status);
35
+ // 打印日志
36
+ console.log(
37
+ `${logColor}[${timestamp}] ${method} ${url} - ${status} - ${responseTime}ms - ${ip}`
38
+ );
39
+ };
40
+
41
+ module.exports = loggerMiddleware;