使用Cursor开发Strapi5插件bag-strapi-plugin

🎯 为什么开发 bag-strapi-plugin?

问题的起源

在使用 Strapi 5 开发多个项目后,我发现每个项目都需要重复实现一些通用功能:

  1. 用户认证系统 - JWT Token 管理、密码加密、登录注册
  2. API 安全保护 - 签名验证、限流防刷、加密传输
  3. 验证码功能 - 图形验证码、短信验证码
  4. 菜单管理 - 后台菜单的数据库设计和 CRUD
  5. 加密工具 - AES、RSA、Hash 等加密算法的封装

每次都要从头写这些功能,不仅耗时,而且容易出现安全漏洞。于是我决定开发一个插件,将这些通用功能封装起来,实现一次开发、多处复用

背景

我一直在使用Strapi、Egg.js 、ThinkPhp 在开发端管理系统,很少在写后端插件,今天借助Ai的势力开发一个插件

技术

选择是Strapi5,官方文档写的有插件开发,但是写的也没有具体的例子(哈哈哈哈哈哈,主要是自己英文菜哈),言归正传,我们开始

在线预览地址Github源码地址

搭建项目

yalc 必须全局安装,然后使用官方提供的命令安装插件开发文件结构

bash 复制代码
npm install -g yalc

创建插件

bash 复制代码
npx @strapi/sdk-plugin init my-strapi-plugin

🏗️ 项目架构设计

插件目录结构

bash 复制代码
bag-strapi-plugin/
├── admin/              # 前端管理界面
│   └── src/
│       ├── components/ # 自定义组件(登录页等)
│       ├── pages/      # 管理页面
│       └── index.js    # 前端入口
├── server/             # 后端逻辑
│   └── src/
│       ├── bootstrap/  # 启动钩子
│       ├── controllers/# 控制器
│       ├── services/   # 服务层
│       ├── routes/     # 路由定义
│       ├── middlewares/# 中间件
│       ├── content-types/ # 内容类型(菜单、用户表)
│       └── utils/      # 工具函数
├── docs/               # VitePress 文档
└── package.json

核心功能模块

javascript 复制代码
// 插件提供的全局对象
strapi.crypto       // 加密工具库
strapi.auth         // 认证系统
strapi.captcha      // 验证码系统
strapi.rateLimit    // 限流系统
strapi.menuService  // 菜单服务

💻 核心功能实现

1. 🔐 JWT 认证系统

使用 Cursor 快速搭建认证框架

在开发认证系统时,我使用 Cursor 的对话功能快速生成了基础代码:

我的提示词:

markdown 复制代码
帮我创建一个 Strapi 5 插件的认证控制器,需要实现:
1. 用户注册(支持用户名、邮箱、密码)
2. 用户登录(支持用户名或邮箱登录)
3. Token 刷新
4. 获取当前用户信息
5. 修改密码
使用 bcrypt 加密密码,使用 jsonwebtoken 生成 JWT

生成的核心代码:

javascript 复制代码
// server/src/controllers/auth.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

module.exports = {
  // 用户注册
  async register(ctx) {
    try {
      const { username, email, password, nickname } = ctx.request.body;

      // 验证必填字段
      if (!username || !email || !password) {
        return ctx.badRequest('用户名、邮箱和密码不能为空');
      }

      // 检查用户是否已存在
      const existingUser = await strapi.db.query('plugin::bag-strapi-plugin.bag-user').findOne({
        where: {
          $or: [{ username }, { email }]
        }
      });

      if (existingUser) {
        return ctx.badRequest('用户名或邮箱已存在');
      }

      // 加密密码
      const hashedPassword = await bcrypt.hash(password, 10);

      // 创建用户
      const user = await strapi.db.query('plugin::bag-strapi-plugin.bag-user').create({
        data: {
          username,
          email,
          password: hashedPassword,
          nickname: nickname || username,
          isActive: true,
        }
      });

      // 生成 JWT Token
      const config = strapi.plugin('bag-strapi-plugin').config('auth') || {};
      const jwtSecret = config.jwt?.secret || process.env.JWT_SECRET || 'default-secret';
      const expiresIn = config.jwt?.expiresIn || '7d';

      const token = jwt.sign(
        { id: user.id, username: user.username },
        jwtSecret,
        { expiresIn }
      );

      // 移除密码字段
      delete user.password;

      ctx.send({
        success: true,
        message: '注册成功',
        data: {
          user,
          token,
        }
      });
    } catch (error) {
      strapi.log.error('注册失败:', error);
      ctx.internalServerError('注册失败');
    }
  },

  // 用户登录
  async login(ctx) {
    try {
      const { identifier, password } = ctx.request.body;

      if (!identifier || !password) {
        return ctx.badRequest('用户名/邮箱和密码不能为空');
      }

      // 查找用户
      const user = await strapi.db.query('plugin::bag-strapi-plugin.bag-user').findOne({
        where: {
          $or: [
            { username: identifier },
            { email: identifier }
          ]
        }
      });

      if (!user) {
        return ctx.badRequest('用户不存在');
      }

      // 验证密码
      const isPasswordValid = await bcrypt.compare(password, user.password);
      if (!isPasswordValid) {
        return ctx.badRequest('密码错误');
      }

      // 检查账户状态
      if (!user.isActive) {
        return ctx.badRequest('账户已被禁用');
      }

      // 生成 Token
      const config = strapi.plugin('bag-strapi-plugin').config('auth') || {};
      const jwtSecret = config.jwt?.secret || process.env.JWT_SECRET || 'default-secret';
      const expiresIn = config.jwt?.expiresIn || '7d';

      const token = jwt.sign(
        { id: user.id, username: user.username },
        jwtSecret,
        { expiresIn }
      );

      // 移除密码字段
      delete user.password;

      ctx.send({
        success: true,
        message: '登录成功',
        data: {
          user,
          token,
        }
      });
    } catch (error) {
      strapi.log.error('登录失败:', error);
      ctx.internalServerError('登录失败');
    }
  },
};

Cursor 的优势体现

  1. 智能补全 - 在输入 bcrypt. 时,Cursor 自动提示 hash()compare() 方法
  2. 错误处理 - Cursor 自动添加了完善的 try-catch 和错误提示
  3. 安全最佳实践 - 自动使用 bcrypt 加密,移除返回数据中的密码字段
  4. 代码风格 - 生成的代码符合 Strapi 的最佳实践

2. 🖼️ 验证码系统

验证码系统支持四种类型:图形验证码、数学运算验证码、邮件验证码、短信验证码。

javascript 复制代码
// server/src/controllers/captcha.js
const svgCaptcha = require('svg-captcha');

module.exports = {
  // 生成图形验证码
  async generateImageCaptcha(ctx) {
    try {
      const config = strapi.plugin('bag-strapi-plugin').config('captcha') || {};
      const captchaLength = config.length || 4;

      // 生成验证码
      const captcha = svgCaptcha.create({
        size: captchaLength,
        noise: 2,
        color: true,
        background: '#f0f0f0',
      });

      // 生成唯一 ID
      const captchaId = strapi.crypto.random.uuid();

      // 存储验证码(5分钟过期)
      const expireTime = config.expireTime || 5 * 60 * 1000;
      await strapi.cache.set(
        `captcha:${captchaId}`,
        captcha.text.toLowerCase(),
        expireTime
      );

      ctx.send({
        success: true,
        data: {
          captchaId,
          captchaImage: captcha.data, // SVG 图片
        }
      });
    } catch (error) {
      strapi.log.error('生成验证码失败:', error);
      ctx.internalServerError('生成验证码失败');
    }
  },

  // 验证验证码
  async verifyCaptcha(ctx) {
    try {
      const { captchaId, captchaCode } = ctx.request.body;

      if (!captchaId || !captchaCode) {
        return ctx.badRequest('验证码ID和验证码不能为空');
      }

      // 获取存储的验证码
      const storedCode = await strapi.cache.get(`captcha:${captchaId}`);

      if (!storedCode) {
        return ctx.badRequest('验证码已过期或不存在');
      }

      // 验证验证码
      const isValid = storedCode === captchaCode.toLowerCase();

      // 验证后删除
      await strapi.cache.del(`captcha:${captchaId}`);

      ctx.send({
        success: true,
        data: {
          isValid,
        }
      });
    } catch (error) {
      strapi.log.error('验证失败:', error);
      ctx.internalServerError('验证失败');
    }
  },
};

3. ⚡ API 限流系统

使用 rate-limiter-flexible 实现强大的限流功能:

javascript 复制代码
// server/src/middlewares/rate-limit.js
const { RateLimiterMemory, RateLimiterRedis } = require('rate-limiter-flexible');

module.exports = (config, { strapi }) => {
  return async (ctx, next) => {
    try {
      const rateLimitConfig = strapi.plugin('bag-strapi-plugin').config('rateLimit') || {};

      // 如果未启用限流,直接放行
      if (!rateLimitConfig.enabled) {
        return await next();
      }

      // 创建限流器
      const limiterOptions = {
        points: rateLimitConfig.points || 100, // 请求数
        duration: rateLimitConfig.duration || 60, // 时间窗口(秒)
        blockDuration: rateLimitConfig.blockDuration || 60, // 阻止时长
      };

      let rateLimiter;
      if (rateLimitConfig.storage === 'redis') {
        // 使用 Redis 存储
        const Redis = require('ioredis');
        const redisClient = new Redis(rateLimitConfig.redis);
        rateLimiter = new RateLimiterRedis({
          storeClient: redisClient,
          ...limiterOptions,
        });
      } else {
        // 使用内存存储
        rateLimiter = new RateLimiterMemory(limiterOptions);
      }

      // 获取请求标识(IP 或用户 ID)
      const key = ctx.state.user?.id || ctx.request.ip;

      // 消费一个点数
      await rateLimiter.consume(key);

      // 放行请求
      await next();
    } catch (error) {
      if (error.remainingPoints !== undefined) {
        // 触发限流
        ctx.status = 429;
        ctx.set('Retry-After', String(Math.round(error.msBeforeNext / 1000)));
        ctx.body = {
          success: false,
          message: '请求过于频繁,请稍后再试',
          retryAfter: Math.round(error.msBeforeNext / 1000),
        };
      } else {
        throw error;
      }
    }
  };
};

4. 🔒 加密工具库

这是插件的核心功能之一,提供了全局可用的加密工具:

javascript 复制代码
// server/src/utils/crypto-utils.js
import crypto from 'crypto';

/**
 * AES-256-GCM 加密
 */
function aesEncrypt(plaintext, secretKey) {
  try {
    const key = crypto.scryptSync(secretKey, 'salt', 32);
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
    
    let encrypted = cipher.update(plaintext, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    
    const authTag = cipher.getAuthTag();
    
    return {
      encrypted: encrypted,
      iv: iv.toString('hex'),
      authTag: authTag.toString('hex')
    };
  } catch (error) {
    throw new Error(`AES 加密失败: ${error.message}`);
  }
}

/**
 * AES-256-GCM 解密
 */
function aesDecrypt(encrypted, secretKey, iv, authTag) {
  try {
    const key = crypto.scryptSync(secretKey, 'salt', 32);
    const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));
    decipher.setAuthTag(Buffer.from(authTag, 'hex'));
    
    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    
    return decrypted;
  } catch (error) {
    throw new Error(`AES 解密失败: ${error.message}`);
  }
}

/**
 * RSA 密钥对生成
 */
function generateRSAKeyPair(modulusLength = 2048) {
  const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
    modulusLength: modulusLength,
    publicKeyEncoding: {
      type: 'spki',
      format: 'pem'
    },
    privateKeyEncoding: {
      type: 'pkcs8',
      format: 'pem'
    }
  });
  
  return { publicKey, privateKey };
}

/**
 * RSA 加密
 */
function rsaEncrypt(plaintext, publicKey) {
  const buffer = Buffer.from(plaintext, 'utf8');
  const encrypted = crypto.publicEncrypt(
    {
      key: publicKey,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
      oaepHash: 'sha256'
    },
    buffer
  );
  
  return encrypted.toString('base64');
}

/**
 * RSA 解密
 */
function rsaDecrypt(encrypted, privateKey) {
  const buffer = Buffer.from(encrypted, 'base64');
  const decrypted = crypto.privateDecrypt(
    {
      key: privateKey,
      padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
      oaepHash: 'sha256'
    },
    buffer
  );
  
  return decrypted.toString('utf8');
}

// 导出工具库
export default {
  aes: {
    encrypt: aesEncrypt,
    decrypt: aesDecrypt,
  },
  rsa: {
    generateKeyPair: generateRSAKeyPair,
    encrypt: rsaEncrypt,
    decrypt: rsaDecrypt,
  },
  hash: {
    sha256: (data) => crypto.createHash('sha256').update(data).digest('hex'),
    sha512: (data) => crypto.createHash('sha512').update(data).digest('hex'),
    md5: (data) => crypto.createHash('md5').update(data).digest('hex'),
    hmac: (data, secret) => crypto.createHmac('sha256', secret).update(data).digest('hex'),
  },
  random: {
    uuid: () => crypto.randomUUID(),
    bytes: (length) => crypto.randomBytes(length).toString('hex'),
    number: (min, max) => crypto.randomInt(min, max + 1),
  }
};

在 Strapi 中全局注册:

javascript 复制代码
// server/src/bootstrap.js
import cryptoUtils from './utils/crypto-utils';

module.exports = ({ strapi }) => {
  // 注册全局加密工具
  strapi.crypto = cryptoUtils;
  
  // 添加配置辅助方法
  strapi.crypto.config = {
    getAesKey: () => {
      const config = strapi.plugin('bag-strapi-plugin').config('crypto') || {};
      return config.aesKey || process.env.CRYPTO_AES_KEY;
    },
    getHmacSecret: () => {
      const config = strapi.plugin('bag-strapi-plugin').config('crypto') || {};
      return config.hmacSecret || process.env.CRYPTO_HMAC_SECRET;
    },
  };

  strapi.log.info('✅ bag-strapi-plugin 加密工具已初始化');
};

使用示例:

javascript 复制代码
// 在任何控制器或服务中使用
module.exports = {
  async encryptData(ctx) {
    const { data } = ctx.request.body;
    
    // AES 加密
    const aesKey = strapi.crypto.config.getAesKey();
    const encrypted = strapi.crypto.aes.encrypt(data, aesKey);
    
    // RSA 加密
    const { publicKey, privateKey } = strapi.crypto.rsa.generateKeyPair();
    const rsaEncrypted = strapi.crypto.rsa.encrypt(data, publicKey);
    
    // Hash
    const hash = strapi.crypto.hash.sha256(data);
    
    ctx.send({
      aes: encrypted,
      rsa: rsaEncrypted,
      hash: hash,
    });
  }
};

5. 📋 菜单数据库表

插件会自动创建菜单数据库表,包含 16 个完整字段:

javascript 复制代码
// server/src/content-types/bag-menu-schema.json
{
  "kind": "collectionType",
  "collectionName": "bag_plugin_menus",
  "info": {
    "singularName": "bag-menu",
    "pluralName": "bag-menus",
    "displayName": "Bag Menu",
    "description": "菜单管理表"
  },
  "options": {
    "draftAndPublish": false
  },
  "pluginOptions": {
    "content-manager": {
      "visible": true
    },
    "content-type-builder": {
      "visible": true
    }
  },
  "attributes": {
    "name": {
      "type": "string",
      "required": true,
      "unique": false
    },
    "path": {
      "type": "string",
      "required": true
    },
    "component": {
      "type": "string"
    },
    "icon": {
      "type": "string"
    },
    "parentId": {
      "type": "integer",
      "default": 0
    },
    "sort": {
      "type": "integer",
      "default": 0
    },
    "isHidden": {
      "type": "boolean",
      "default": false
    },
    "permissions": {
      "type": "json"
    },
    "meta": {
      "type": "json"
    },
    "locale": {
      "type": "string",
      "default": "zh"
    }
  }
}

菜单 CRUD 服务:

javascript 复制代码
// server/src/services/menu.js
module.exports = ({ strapi }) => ({
  // 获取所有菜单
  async findAll(params = {}) {
    return await strapi.db.query('plugin::bag-strapi-plugin.bag-menu').findMany({
      where: params.where || {},
      orderBy: { sort: 'asc' },
    });
  },

  // 获取树形菜单
  async getMenuTree(parentId = 0) {
    const menus = await this.findAll({
      where: { parentId },
    });

    // 递归获取子菜单
    for (let menu of menus) {
      menu.children = await this.getMenuTree(menu.id);
    }

    return menus;
  },

  // 创建菜单
  async create(data) {
    return await strapi.db.query('plugin::bag-strapi-plugin.bag-menu').create({
      data,
    });
  },

  // 更新菜单
  async update(id, data) {
    return await strapi.db.query('plugin::bag-strapi-plugin.bag-menu').update({
      where: { id },
      data,
    });
  },

  // 删除菜单
  async delete(id) {
    return await strapi.db.query('plugin::bag-strapi-plugin.bag-menu').delete({
      where: { id },
    });
  },
});

6. ✍️ 签名验证中间件

支持三种验证模式:简单签名、加密签名、一次性签名。

javascript 复制代码
// server/src/middlewares/sign-verify.js
module.exports = (config, { strapi }) => {
  return async (ctx, next) => {
    try {
      const signConfig = strapi.plugin('bag-strapi-plugin').config('signVerify') || {};

      // 未启用签名验证
      if (!signConfig.enabled) {
        return await next();
      }

      // 检查白名单
      const whitelist = signConfig.whitelist || [];
      if (whitelist.some(pattern => ctx.request.url.match(pattern))) {
        return await next();
      }

      // 获取签名
      const sign = ctx.request.headers['sign'] || ctx.request.query.sign;

      if (!sign) {
        return ctx.unauthorized('缺少签名');
      }

      // 验证模式
      const mode = signConfig.mode || 'simple';
      let isValid = false;

      if (mode === 'simple') {
        // 简单签名验证
        const validSigns = signConfig.validSigns || [];
        isValid = validSigns.includes(sign);
      } else if (mode === 'encrypted') {
        // 加密签名验证
        try {
          const aesKey = strapi.crypto.config.getAesKey();
          const decrypted = strapi.crypto.aes.decrypt(
            sign.encrypted,
            aesKey,
            sign.iv,
            sign.authTag
          );
          isValid = decrypted.includes('bag');
        } catch (error) {
          isValid = false;
        }
      } else if (mode === 'once') {
        // 一次性签名验证
        const storedSign = await strapi.cache.get(`sign:${sign}`);
        if (storedSign) {
          await strapi.cache.del(`sign:${sign}`);
          isValid = true;
        }
      }

      if (!isValid) {
        return ctx.unauthorized('签名验证失败');
      }

      await next();
    } catch (error) {
      strapi.log.error('签名验证失败:', error);
      ctx.internalServerError('签名验证失败');
    }
  };
};

🎨 使用 Cursor 的开发技巧

1. 智能代码生成

场景: 需要创建用户表的 Schema

我的操作:

  1. 打开 Cursor,按 Cmd+K(或 Ctrl+K)打开聊天
  2. 输入提示词:
diff 复制代码
创建一个 Strapi 5 插件的用户内容类型,包含以下字段:
- username: 用户名(唯一)
- email: 邮箱(唯一)
- password: 密码
- nickname: 昵称
- avatar: 头像 URL
- phone: 手机号
- isActive: 是否激活
- lastLoginAt: 最后登录时间
- createdAt: 创建时间
- updatedAt: 更新时间

Cursor 生成的代码:

json 复制代码
{
  "kind": "collectionType",
  "collectionName": "bag_users",
  "info": {
    "singularName": "bag-user",
    "pluralName": "bag-users",
    "displayName": "Bag User"
  },
  "options": {
    "draftAndPublish": false,
    "timestamps": true
  },
  "attributes": {
    "username": {
      "type": "string",
      "required": true,
      "unique": true,
      "minLength": 3,
      "maxLength": 30
    },
    "email": {
      "type": "email",
      "required": true,
      "unique": true
    },
    "password": {
      "type": "string",
      "required": true,
      "private": true
    },
    "nickname": {
      "type": "string"
    },
    "avatar": {
      "type": "string"
    },
    "phone": {
      "type": "string",
      "unique": true
    },
    "isActive": {
      "type": "boolean",
      "default": true
    },
    "lastLoginAt": {
      "type": "datetime"
    }
  }
}

2. 代码重构

场景: 优化重复的错误处理代码

操作:

  1. 选中重复的代码块
  2. 右键 → "Ask Cursor"
  3. 输入:将这些重复的 try-catch 封装成一个通用的错误处理函数

Cursor 的建议:

javascript 复制代码
// utils/error-handler.js
const handleAsyncError = (fn) => {
  return async (ctx, next) => {
    try {
      await fn(ctx, next);
    } catch (error) {
      strapi.log.error('请求失败:', error);
      
      if (error.name === 'ValidationError') {
        return ctx.badRequest(error.message);
      }
      
      if (error.name === 'UnauthorizedError') {
        return ctx.unauthorized(error.message);
      }
      
      ctx.internalServerError('服务器内部错误');
    }
  };
};

// 使用
module.exports = {
  register: handleAsyncError(async (ctx) => {
    const { username, email, password } = ctx.request.body;
    // ... 业务逻辑
  }),
};

3. 文档生成

场景: 为加密工具生成文档

操作:

  1. 选中 crypto-utils.js 文件
  2. Cmd+L 打开侧边栏聊天
  3. 输入:为这个文件生成完整的 README 文档,包括所有函数的使用示例

Cursor 会自动生成包含所有 API 说明、参数说明、返回值说明和使用示例的完整文档。

4. Bug 修复

场景: 限流中间件偶尔会报错

操作:

  1. 在出错的文件中按 Cmd+K
  2. 输入:这个限流中间件在高并发时会出错,帮我找出问题并修复

Cursor 的分析:

markdown 复制代码
问题分析:
1. RateLimiter 实例在每次请求时都重新创建,效率低下
2. Redis 连接没有复用,可能导致连接池耗尽
3. 缺少对 Redis 连接失败的容错处理

建议修复:
1. 将 RateLimiter 实例缓存起来
2. 复用 Redis 连接
3. 添加降级策略,Redis 失败时使用内存存储

5. 测试用例生成

操作:

  1. 选中 auth.js 控制器
  2. 输入:为这个认证控制器生成完整的单元测试

生成的测试代码:

javascript 复制代码
// server/tests/auth.test.js
const request = require('supertest');

describe('Auth Controller', () => {
  let app;
  let testUser;

  beforeAll(async () => {
    app = await setupStrapi();
  });

  afterAll(async () => {
    await cleanupStrapi(app);
  });

  describe('POST /auth/register', () => {
    it('应该成功注册新用户', async () => {
      const response = await request(app)
        .post('/bag-strapi-plugin/auth/register')
        .send({
          username: 'testuser',
          email: 'test@example.com',
          password: 'password123',
        });

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data.user.username).toBe('testuser');
      expect(response.body.data.token).toBeDefined();

      testUser = response.body.data.user;
    });

    it('应该拒绝重复的用户名', async () => {
      const response = await request(app)
        .post('/bag-strapi-plugin/auth/register')
        .send({
          username: 'testuser',
          email: 'another@example.com',
          password: 'password123',
        });

      expect(response.status).toBe(400);
      expect(response.body.success).toBe(false);
    });
  });

  describe('POST /auth/login', () => {
    it('应该成功登录', async () => {
      const response = await request(app)
        .post('/bag-strapi-plugin/auth/login')
        .send({
          identifier: 'testuser',
          password: 'password123',
        });

      expect(response.status).toBe(200);
      expect(response.body.success).toBe(true);
      expect(response.body.data.token).toBeDefined();
    });

    it('应该拒绝错误的密码', async () => {
      const response = await request(app)
        .post('/bag-strapi-plugin/auth/login')
        .send({
          identifier: 'testuser',
          password: 'wrongpassword',
        });

      expect(response.status).toBe(400);
      expect(response.body.success).toBe(false);
    });
  });
});

🐛 开发过程中的踩坑经验

1. Strapi 5 插件路由注册

问题: 路由无法访问,总是返回 404

原因: Strapi 5 的路由注册方式与 v4 不同

解决方案:

javascript 复制代码
// ❌ 错误的方式(Strapi v4)
module.exports = {
  routes: [
    {
      method: 'GET',
      path: '/test',
      handler: 'controller.test',
    }
  ]
};

// ✅ 正确的方式(Strapi 5)
module.exports = {
  type: 'content-api', // 或 'admin'
  routes: [
    {
      method: 'GET',
      path: '/test',
      handler: 'controller.test',
      config: {
        policies: [],
        middlewares: [],
      },
    }
  ]
};

2. 中间件执行顺序

问题: 签名验证中间件总是在 JWT 验证之后执行

原因: 没有正确配置中间件的加载顺序

解决方案:

javascript 复制代码
// config/middlewares.js
module.exports = [
  'strapi::logger',
  'strapi::errors',
  'strapi::security',
  'strapi::cors',
  // 先加载签名验证
  'plugin::bag-strapi-plugin.sign-verify',
  // 再加载 JWT
  'plugin::bag-strapi-plugin.jwt-auth',
  'strapi::poweredBy',
  'strapi::query',
  'strapi::body',
  'strapi::session',
  'strapi::favicon',
  'strapi::public',
];

3. 加密工具全局注册

问题: 在控制器中调用 strapi.crypto 时提示 undefined

原因: 全局对象需要在 bootstrap 中注册,且要等待 Strapi 完全启动

解决方案:

javascript 复制代码
// server/src/bootstrap.js
module.exports = async ({ strapi }) => {
  // ✅ 使用 async/await 确保初始化完成
  await Promise.resolve();
  
  // 注册全局对象
  strapi.crypto = cryptoUtils;
  
  strapi.log.info('✅ 加密工具已注册');
};

4. 数据库表创建时机

问题: 菜单表有时候不会自动创建

原因: Content-Type 需要在正确的生命周期钩子中注册

解决方案:

javascript 复制代码
// server/src/register.js
module.exports = ({ strapi }) => {
  // 在 register 阶段注册 Content-Type
  strapi.contentTypes = {
    ...strapi.contentTypes,
    'plugin::bag-strapi-plugin.bag-menu': require('./content-types/bag-menu'),
    'plugin::bag-strapi-plugin.bag-user': require('./content-types/bag-user'),
  };
};

5. Redis 连接池问题

问题: 高并发时限流中间件报 "Too many connections" 错误

原因: 每次请求都创建新的 Redis 连接

解决方案:

javascript 复制代码
// 创建单例 Redis 连接
let redisClient = null;

const getRedisClient = () => {
  if (!redisClient) {
    const Redis = require('ioredis');
    const config = strapi.plugin('bag-strapi-plugin').config('rateLimit.redis');
    redisClient = new Redis({
      ...config,
      maxRetriesPerRequest: 3,
      enableOfflineQueue: false,
      lazyConnect: true,
    });
  }
  return redisClient;
};

6. 环境变量加载问题

问题: 在插件中无法读取 .env 文件中的环境变量

原因: Strapi 5 需要显式配置环境变量的加载

解决方案:

javascript 复制代码
// 在 config/plugins.js 中使用 env() 辅助函数
module.exports = ({ env }) => ({
  'bag-strapi-plugin': {
    enabled: true,
    config: {
      auth: {
        jwt: {
          // ✅ 使用 env() 函数,支持默认值
          secret: env('JWT_SECRET', 'default-secret'),
          expiresIn: env('JWT_EXPIRES_IN', '7d'),
        },
      },
    },
  },
});

📦 插件打包与发布

1. 配置 package.json

json 复制代码
{
  "name": "bag-strapi-plugin",
  "version": "0.0.4",
  "description": "bag-strapi-plugin provide a commonly used plugin for management",
  "strapi": {
    "kind": "plugin",
    "name": "bag-strapi-plugin",
    "displayName": "Bag Plugin",
    "description": "通用功能插件"
  },
  "keywords": [
    "strapi",
    "strapi-plugin",
    "authentication",
    "rate-limit",
    "encryption",
    "menu"
  ],
  "scripts": {
    "build": "strapi-plugin build",
    "watch": "strapi-plugin watch",
    "verify": "strapi-plugin verify"
  },
  "files": [
    "dist",
    "README.md",
    "docs/*.md"
  ],
  "dependencies": {
    "@strapi/design-system": "^2.0.0-rc.30",
    "@strapi/icons": "2.0.0-rc.30",
    "bcrypt": "^5.1.1",
    "jsonwebtoken": "^9.0.2",
    "rate-limiter-flexible": "^5.0.3",
    "svg-captcha": "^1.4.0"
  },
  "peerDependencies": {
    "@strapi/strapi": "^5.28.0"
  }
}

2. 使用 yalc 本地测试

bash 复制代码
# 在插件项目中
npm run build
yalc publish

# 在 Strapi 项目中
yalc add bag-strapi-plugin
npm install

# 启动测试
npm run develop

3. 发布到 npm

bash 复制代码
# 登录 npm
npm login

# 发布
npm publish

# 如果是 scoped package
npm publish --access public

4. 版本管理

bash 复制代码
# 补丁版本(bug 修复)
npm version patch

# 次版本(新功能)
npm version minor

# 主版本(破坏性更新)
npm version major

# 发布新版本
git push --tags
npm publish

📚 文档编写

使用 VitePress 构建文档站点

bash 复制代码
# 安装 VitePress
pnpm add -D vitepress

# 初始化文档目录
mkdir docs

配置文件 .vitepress/config.mjs

javascript 复制代码
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: "bag-strapi-plugin",
  description: "Strapi 通用功能插件",
  themeConfig: {
    nav: [
      { text: '指南', link: '/guide/introduction' },
      { text: 'API', link: '/api/overview' },
      { text: 'GitHub', link: 'https://github.com/hangjob/bag-strapi-plugin' }
    ],
    sidebar: {
      '/guide/': [
        {
          text: '开始',
          items: [
            { text: '简介', link: '/guide/introduction' },
            { text: '快速开始', link: '/guide/quick-start' },
            { text: '安装', link: '/guide/installation' },
            { text: '配置', link: '/guide/configuration' },
          ]
        },
        {
          text: '功能',
          items: [
            { text: 'JWT 认证', link: '/features/auth' },
            { text: '验证码', link: '/features/captcha' },
            { text: 'API 限流', link: '/features/rate-limit' },
            { text: '加密工具', link: '/features/crypto' },
            { text: '菜单管理', link: '/features/menu' },
          ]
        }
      ],
      '/api/': [
        {
          text: 'API 文档',
          items: [
            { text: '概述', link: '/api/overview' },
            { text: '认证 API', link: '/api/auth' },
            { text: '验证码 API', link: '/api/captcha' },
            { text: '加密 API', link: '/api/crypto' },
          ]
        }
      ]
    },
    socialLinks: [
      { icon: 'github', link: 'https://github.com/hangjob/bag-strapi-plugin' }
    ]
  }
})

部署文档:

bash 复制代码
# 构建文档
npm run docs:build

# 预览
npm run docs:preview

# 部署到 GitHub Pages
# 在 .github/workflows/deploy.yml
name: Deploy Docs

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm install
      - run: npm run docs:build
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: docs_web

🎯 使用 Cursor 的心得总结

优势

  1. 极大提高开发效率 - 编写代码速度提升 3-5 倍
  2. 减少低级错误 - AI 自动处理边界情况和错误处理
  3. 学习新技术更快 - 可以直接问 Cursor 关于 Strapi 5 的新特性
  4. 代码质量提升 - AI 生成的代码通常符合最佳实践
  5. 文档编写轻松 - 自动生成注释和文档

最佳实践

  1. 清晰的提示词 - 提供具体的需求和上下文
  2. 分步开发 - 将大功能拆分成小任务,逐个完成
  3. 人工审查 - AI 生成的代码需要人工审查和测试
  4. 保持迭代 - 通过对话不断优化代码
  5. 建立代码规范 - 让 Cursor 学习你的代码风格

注意事项

  1. 不要盲目信任 - AI 生成的代码可能有 bug
  2. 理解原理 - 要理解代码的工作原理,不能只复制粘贴
  3. 安全审查 - 涉及安全的代码必须仔细审查
  4. 版本兼容 - 确认生成的代码与项目版本兼容
  5. 性能优化 - AI 可能不会考虑性能问题,需要人工优化

🚀 插件使用示例

在 Strapi 项目中安装

bash 复制代码
npm install bag-strapi-plugin

配置插件

javascript 复制代码
// config/plugins.js
module.exports = ({ env }) => ({
  'bag-strapi-plugin': {
    enabled: true,
    config: {
      // JWT 认证
      auth: {
        enableCaptcha: true,
        jwt: {
          secret: env('JWT_SECRET'),
          expiresIn: '7d',
        },
      },
      
      // API 限流
      rateLimit: {
        enabled: true,
        points: 100,
        duration: 60,
      },
      
      // 加密工具
      crypto: {
        aesKey: env('CRYPTO_AES_KEY'),
        hmacSecret: env('CRYPTO_HMAC_SECRET'),
      },
    },
  },
});

前端集成

javascript 复制代码
// 用户注册
const register = async (userData) => {
  const response = await fetch('/bag-strapi-plugin/auth/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData),
  });
  return response.json();
};

// 用户登录
const login = async (identifier, password) => {
  const response = await fetch('/bag-strapi-plugin/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ identifier, password }),
  });
  const result = await response.json();
  if (result.success) {
    localStorage.setItem('token', result.data.token);
  }
  return result;
};

// 获取当前用户
const getCurrentUser = async () => {
  const token = localStorage.getItem('token');
  const response = await fetch('/bag-strapi-plugin/auth/me', {
    headers: { 'Authorization': `Bearer ${token}` },
  });
  return response.json();
};

// 获取验证码
const getCaptcha = async () => {
  const response = await fetch('/bag-strapi-plugin/captcha/image');
  return response.json();
};

📊 项目数据

  • 代码量: 约 5000+ 行
  • 开发时间: 使用 Cursor 仅用 2 周(传统开发预计需要 1-2 个月)
  • 文档数量: 25+ 个 Markdown 文档
  • 功能模块: 6 大核心模块
  • npm 下载量: 持续增长中

🎓 总结与展望

开发收获

通过这次使用 Cursor 开发 Strapi 5 插件的经历,我深刻体会到:

  1. AI 辅助编程已经成为现实 - Cursor 大幅提升了开发效率
  2. 代码质量可以更高 - AI 帮助我们避免很多低级错误
  3. 学习新技术更快 - 通过与 AI 对话快速掌握 Strapi 5
  4. 文档编写不再痛苦 - AI 可以生成高质量的文档
  5. 专注于业务逻辑 - 将重复性工作交给 AI
相关推荐
专注前端30年5 小时前
【JavaScript】reduce 方法的详解与实战
开发语言·前端·javascript
ikoala6 小时前
Node.js 25 正式发布:性能飙升、安全升级、全面向 Web 靠拢!
前端·面试·node.js
陈振wx:zchen20086 小时前
前端-ES6-11
前端·es6
菜鸟una6 小时前
【瀑布流大全】分析原理及实现方式(微信小程序和网页都适用)
前端·css·vue.js·微信小程序·小程序·typescript
专注前端30年7 小时前
2025 最新 Vue2/Vue3 高频面试题(10月最新版)
前端·javascript·vue.js·面试
文火冰糖的硅基工坊7 小时前
[嵌入式系统-146]:五次工业革命对应的机器人形态的演进、主要功能的演进以及操作系统的演进
前端·网络·人工智能·嵌入式硬件·机器人
2401_837088508 小时前
ResponseEntity - Spring框架的“标准回复模板“
java·前端·spring
yaoganjili8 小时前
用 Tinymce 打造智能写作
前端
angelQ8 小时前
Vue 3 中 ref 获取 scrollHeight 属性为 undefined 问题定位
前端·javascript