🎯 为什么开发 bag-strapi-plugin?
问题的起源
在使用 Strapi 5 开发多个项目后,我发现每个项目都需要重复实现一些通用功能:
- 用户认证系统 - JWT Token 管理、密码加密、登录注册
- API 安全保护 - 签名验证、限流防刷、加密传输
- 验证码功能 - 图形验证码、短信验证码
- 菜单管理 - 后台菜单的数据库设计和 CRUD
- 加密工具 - AES、RSA、Hash 等加密算法的封装
每次都要从头写这些功能,不仅耗时,而且容易出现安全漏洞。于是我决定开发一个插件,将这些通用功能封装起来,实现一次开发、多处复用
背景
我一直在使用Strapi、Egg.js 、ThinkPhp 在开发端管理系统,很少在写后端插件,今天借助Ai的势力开发一个插件
技术
选择是Strapi5,官方文档写的有插件开发,但是写的也没有具体的例子(哈哈哈哈哈哈,主要是自己英文菜哈),言归正传,我们开始

搭建项目
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 的优势体现
- 智能补全 - 在输入
bcrypt.
时,Cursor 自动提示hash()
和compare()
方法 - 错误处理 - Cursor 自动添加了完善的 try-catch 和错误提示
- 安全最佳实践 - 自动使用 bcrypt 加密,移除返回数据中的密码字段
- 代码风格 - 生成的代码符合 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
我的操作:
- 打开 Cursor,按
Cmd+K
(或Ctrl+K
)打开聊天 - 输入提示词:
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. 代码重构
场景: 优化重复的错误处理代码
操作:
- 选中重复的代码块
- 右键 → "Ask Cursor"
- 输入:
将这些重复的 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. 文档生成
场景: 为加密工具生成文档
操作:
- 选中
crypto-utils.js
文件 - 按
Cmd+L
打开侧边栏聊天 - 输入:
为这个文件生成完整的 README 文档,包括所有函数的使用示例
Cursor 会自动生成包含所有 API 说明、参数说明、返回值说明和使用示例的完整文档。
4. Bug 修复
场景: 限流中间件偶尔会报错
操作:
- 在出错的文件中按
Cmd+K
- 输入:
这个限流中间件在高并发时会出错,帮我找出问题并修复
Cursor 的分析:
markdown
问题分析:
1. RateLimiter 实例在每次请求时都重新创建,效率低下
2. Redis 连接没有复用,可能导致连接池耗尽
3. 缺少对 Redis 连接失败的容错处理
建议修复:
1. 将 RateLimiter 实例缓存起来
2. 复用 Redis 连接
3. 添加降级策略,Redis 失败时使用内存存储
5. 测试用例生成
操作:
- 选中
auth.js
控制器 - 输入:
为这个认证控制器生成完整的单元测试
生成的测试代码:
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 的心得总结
优势
- 极大提高开发效率 - 编写代码速度提升 3-5 倍
- 减少低级错误 - AI 自动处理边界情况和错误处理
- 学习新技术更快 - 可以直接问 Cursor 关于 Strapi 5 的新特性
- 代码质量提升 - AI 生成的代码通常符合最佳实践
- 文档编写轻松 - 自动生成注释和文档
最佳实践
- 清晰的提示词 - 提供具体的需求和上下文
- 分步开发 - 将大功能拆分成小任务,逐个完成
- 人工审查 - AI 生成的代码需要人工审查和测试
- 保持迭代 - 通过对话不断优化代码
- 建立代码规范 - 让 Cursor 学习你的代码风格
注意事项
- 不要盲目信任 - AI 生成的代码可能有 bug
- 理解原理 - 要理解代码的工作原理,不能只复制粘贴
- 安全审查 - 涉及安全的代码必须仔细审查
- 版本兼容 - 确认生成的代码与项目版本兼容
- 性能优化 - 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 插件的经历,我深刻体会到:
- AI 辅助编程已经成为现实 - Cursor 大幅提升了开发效率
- 代码质量可以更高 - AI 帮助我们避免很多低级错误
- 学习新技术更快 - 通过与 AI 对话快速掌握 Strapi 5
- 文档编写不再痛苦 - AI 可以生成高质量的文档
- 专注于业务逻辑 - 将重复性工作交给 AI