refactor: 完成静态网站转换,移除所有 CMS 和动态功能
- 删除数据库相关代码 (src/db/) - 删除 API 路由 (src/app/api/) - 删除认证相关代码 (src/lib/auth/, src/providers/) - 删除监控和安全中间件 (src/lib/security/, src/lib/monitoring/) - 删除 hooks (use-news, use-products, use-services) - 更新组件为静态数据源 - 添加 nginx 静态配置和部署脚本 - 添加 static-link 组件
This commit is contained in:
@@ -1,111 +0,0 @@
|
||||
# 管理员账号信息
|
||||
|
||||
## 默认管理员凭据
|
||||
|
||||
### 登录信息
|
||||
- **邮箱**: `admin@novalon.cn`
|
||||
- **密码**: `admin123456`
|
||||
- **登录地址**: `http://localhost:3000/admin/login`
|
||||
|
||||
### 权限说明
|
||||
- 管理员账号拥有完整的后台管理权限
|
||||
- 可以管理内容、用户、配置和日志
|
||||
- 可以修改其他用户信息(包括密码)
|
||||
|
||||
## 修改管理员密码
|
||||
|
||||
### 方法1:通过后台管理界面
|
||||
1. 使用管理员账号登录后台
|
||||
2. 进入"用户管理"页面
|
||||
3. 找到管理员账号
|
||||
4. 点击"编辑"按钮
|
||||
5. 输入新密码并保存
|
||||
|
||||
### 方法2:通过API
|
||||
```bash
|
||||
# 使用管理员token修改密码
|
||||
curl -X PUT http://localhost:3000/api/admin/users/[user-id] \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-d '{
|
||||
"password": "new_password_here"
|
||||
}'
|
||||
```
|
||||
|
||||
### 方法3:重新运行seed脚本
|
||||
```bash
|
||||
npm run db:seed
|
||||
```
|
||||
|
||||
## 创建新管理员账号
|
||||
|
||||
### 通过API创建
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/admin/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-d '{
|
||||
"email": "new_admin@example.com",
|
||||
"password": "secure_password",
|
||||
"name": "新管理员",
|
||||
"isAdmin": true
|
||||
}'
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
### ⚠️ 重要提示
|
||||
1. **立即修改默认密码** - 生产环境必须修改默认密码
|
||||
2. **使用强密码** - 至少12位,包含大小写字母、数字和特殊字符
|
||||
3. **定期更换密码** - 建议每3个月更换一次
|
||||
4. **启用双因素认证** - 如果支持的话
|
||||
5. **限制登录尝试** - 防止暴力破解
|
||||
|
||||
### 密码安全策略
|
||||
- 最小长度:8个字符
|
||||
- 必须包含:大小写字母、数字
|
||||
- 建议包含:特殊字符
|
||||
- 禁止使用:常见密码、个人信息
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 忘记管理员密码怎么办?
|
||||
A: 重新运行seed脚本会重置为默认密码,但这会删除所有现有用户数据。
|
||||
|
||||
### Q: 如何创建多个管理员账号?
|
||||
A: 通过后台管理界面或API创建新用户,并将 `isAdmin` 设置为 `true`。
|
||||
|
||||
### Q: 登录时提示"认证配置错误"?
|
||||
A: 检查NextAuth配置,确保环境变量正确设置。
|
||||
|
||||
### Q: 如何禁用某个管理员账号?
|
||||
A: 通过API或数据库将该用户的 `isAdmin` 设置为 `false`。
|
||||
|
||||
## 相关文件
|
||||
|
||||
- **Seed脚本**: `src/db/seed.ts`
|
||||
- **用户Schema**: `src/db/schema.ts`
|
||||
- **认证配置**: `src/lib/auth.ts`
|
||||
- **登录页面**: `src/app/admin/login/page.tsx`
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 密码加密
|
||||
- 使用 `bcryptjs` 进行密码哈希
|
||||
- 盐值轮数:10
|
||||
- 算法:bcrypt
|
||||
|
||||
### 会话管理
|
||||
- 使用NextAuth.js进行会话管理
|
||||
- 会话存储:数据库
|
||||
- 会话有效期:可配置
|
||||
|
||||
### 权限检查
|
||||
- 基于 `isAdmin` 字段
|
||||
- 通过 `checkIsAdmin()` 函数验证
|
||||
- 支持细粒度权限控制
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-03-13
|
||||
**版本**: 1.0.0
|
||||
@@ -1,357 +0,0 @@
|
||||
# 🚀 CI/CD流水线快速设置指南
|
||||
|
||||
## 📋 前置条件
|
||||
|
||||
- ✅ Gitea已部署并配置 (https://git.f.novalon.cn)
|
||||
- ✅ Woodpecker CI已部署并配置 (https://ci.f.novalon.cn)
|
||||
- ✅ Docker Registry已部署并配置 (https://registry.f.novalon.cn)
|
||||
- ✅ 服务器已配置SSH免密登录
|
||||
|
||||
## 🔧 快速配置步骤
|
||||
|
||||
### 步骤1: 配置Woodpecker CI密钥
|
||||
|
||||
#### 方式A: 使用自动化脚本 (推荐)
|
||||
|
||||
```bash
|
||||
# 1. 上传脚本到服务器
|
||||
scp scripts/setup-woodpecker-secrets.sh root@139.155.109.62:/home/novalon/scripts/
|
||||
|
||||
# 2. SSH到服务器
|
||||
ssh root@139.155.109.62
|
||||
|
||||
# 3. 运行配置脚本
|
||||
chmod +x /home/novalon/scripts/setup-woodpecker-secrets.sh
|
||||
/home/novalon/scripts/setup-woodpecker-secrets.sh
|
||||
```
|
||||
|
||||
#### 方式B: 手动配置
|
||||
|
||||
```bash
|
||||
# 1. SSH到服务器
|
||||
ssh root@139.155.109.62
|
||||
|
||||
# 2. 设置SSH私钥
|
||||
woodpecker-cli secret add \
|
||||
--repository novalon/novalon-website \
|
||||
--name ssh_private_key \
|
||||
--value @- <<< "$(cat ~/.ssh/id_rsa)"
|
||||
|
||||
# 3. 设置Webhook URL (可选)
|
||||
woodpecker-cli secret add \
|
||||
--repository novalon/novalon-website \
|
||||
--name webhook_url \
|
||||
--value @- <<< "YOUR_WEBHOOK_URL"
|
||||
```
|
||||
|
||||
### 步骤2: 在Gitea中创建仓库
|
||||
|
||||
```bash
|
||||
# 1. 访问 https://git.f.novalon.cn
|
||||
# 2. 使用管理员账户登录
|
||||
# 用户名: novalon-admin
|
||||
# 密码: Novalon@Admin2026
|
||||
# 3. 创建新仓库: novalon/novalon-website
|
||||
# 4. 添加远程仓库
|
||||
git remote add origin https://git.f.novalon.cn/novalon/novalon-website.git
|
||||
```
|
||||
|
||||
### 步骤3: 在Woodpecker CI中激活仓库
|
||||
|
||||
```bash
|
||||
# 1. 访问 https://ci.f.novalon.cn
|
||||
# 2. 使用Gitea账户登录 (自动SSO)
|
||||
# 3. 点击"Add Repository"
|
||||
# 4. 选择 novalon/novalon-website 仓库
|
||||
# 5. 点击"Activate"
|
||||
```
|
||||
|
||||
### 步骤4: 配置服务器部署目录
|
||||
|
||||
```bash
|
||||
# SSH到服务器
|
||||
ssh root@139.155.109.62
|
||||
|
||||
# 创建部署目录
|
||||
mkdir -p /home/novalon/docker-app/novalon-website
|
||||
cd /home/novalon/docker-app/novalon-website
|
||||
|
||||
# 创建docker-compose.yml
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
novalon-website:
|
||||
image: registry.f.novalon.cn/novalon-website:latest
|
||||
container_name: novalon-website
|
||||
restart: always
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=file:/app/data/local.db
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- novalon-network
|
||||
|
||||
networks:
|
||||
novalon-network:
|
||||
external: true
|
||||
EOF
|
||||
|
||||
# 创建数据目录
|
||||
mkdir -p data
|
||||
```
|
||||
|
||||
### 步骤5: 提交代码并触发CI/CD
|
||||
|
||||
```bash
|
||||
# 在本地项目目录
|
||||
cd /Users/zhangxiang/Codes/Gitee/home-page/novalon-website
|
||||
|
||||
# 添加所有文件
|
||||
git add .
|
||||
|
||||
# 提交代码
|
||||
git commit -m "feat: 配置全自动CI/CD工作流
|
||||
|
||||
- 添加完整的CI/CD流水线配置
|
||||
- 配置代码质量检查(lint, type-check, security)
|
||||
- 配置分层测试策略(fast, standard, deep)
|
||||
- 配置Docker镜像构建和推送
|
||||
- 配置自动部署到staging和production环境
|
||||
- 配置健康检查和自动回滚
|
||||
- 配置成功/失败通知
|
||||
- 添加健康检查API端点
|
||||
- 创建CI/CD配置文档"
|
||||
|
||||
# 推送到develop分支
|
||||
git push -u origin develop
|
||||
|
||||
# 或者推送到main分支
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
## 📊 验证CI/CD流水线
|
||||
|
||||
### 1. 查看构建状态
|
||||
|
||||
```bash
|
||||
# 访问Woodpecker CI
|
||||
https://ci.f.novalon.cn/novalon/website
|
||||
|
||||
# 查看构建日志
|
||||
# 每个步骤都有详细的日志输出
|
||||
```
|
||||
|
||||
### 2. 验证部署
|
||||
|
||||
#### Staging环境 (develop分支)
|
||||
```bash
|
||||
# 检查容器状态
|
||||
ssh root@139.155.109.62
|
||||
docker ps | grep novalon-website
|
||||
|
||||
# 查看容器日志
|
||||
docker logs novalon-website -f
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
#### Production环境 (main分支)
|
||||
```bash
|
||||
# 检查容器状态
|
||||
ssh root@139.155.109.62
|
||||
docker ps | grep novalon-website
|
||||
|
||||
# 查看容器日志
|
||||
docker logs novalon-website -f
|
||||
|
||||
# 健康检查
|
||||
curl https://novalon.cn/api/health
|
||||
```
|
||||
|
||||
### 3. 验证通知
|
||||
|
||||
如果配置了Webhook,您应该会收到通知:
|
||||
- ✅ 成功通知:绿色,包含构建信息
|
||||
- ❌ 失败通知:红色,包含错误信息和构建链接
|
||||
|
||||
## 🔄 日常使用流程
|
||||
|
||||
### 开发新功能
|
||||
|
||||
```bash
|
||||
# 1. 创建功能分支
|
||||
git checkout -b feature/new-feature
|
||||
|
||||
# 2. 开发并提交
|
||||
git add .
|
||||
git commit -m "feat: 添加新功能"
|
||||
|
||||
# 3. 推送到远程
|
||||
git push origin feature/new-feature
|
||||
|
||||
# 4. 在Gitea创建Pull Request
|
||||
# 访问: https://git.f.novalon.cn/novalon/novalon-website/pulls
|
||||
|
||||
# 5. CI自动运行测试
|
||||
# - Lint检查
|
||||
# - 类型检查
|
||||
# - 单元测试
|
||||
# - Smoke测试
|
||||
|
||||
# 6. 代码审查通过后合并到develop
|
||||
# - 自动触发完整测试
|
||||
# - 自动构建Docker镜像
|
||||
# - 自动部署到Staging环境
|
||||
|
||||
# 7. 测试通过后合并到main
|
||||
# - 自动触发完整测试
|
||||
# - 自动构建Docker镜像
|
||||
# - 自动部署到Production环境
|
||||
```
|
||||
|
||||
### 紧急修复
|
||||
|
||||
```bash
|
||||
# 1. 创建hotfix分支
|
||||
git checkout -b hotfix/critical-fix main
|
||||
|
||||
# 2. 修复并提交
|
||||
git add .
|
||||
git commit -m "fix: 修复关键问题"
|
||||
|
||||
# 3. 推送并创建PR
|
||||
git push origin hotfix/critical-fix
|
||||
|
||||
# 4. 快速审查并合并到main
|
||||
# - 自动部署到Production
|
||||
# - 自动回滚机制保障
|
||||
```
|
||||
|
||||
## 🛠️ 故障排查
|
||||
|
||||
### 构建失败
|
||||
|
||||
```bash
|
||||
# 1. 查看Woodpecker CI日志
|
||||
https://ci.f.novalon.cn/novalon/novalon-website
|
||||
|
||||
# 2. 常见原因
|
||||
# - 依赖安装失败
|
||||
# - TypeScript类型错误
|
||||
# - 测试失败
|
||||
# - Docker构建失败
|
||||
|
||||
# 3. 本地重现
|
||||
npm ci
|
||||
npm run lint
|
||||
npm run type-check
|
||||
npm run test:coverage:check
|
||||
npm run build
|
||||
```
|
||||
|
||||
### 部署失败
|
||||
|
||||
```bash
|
||||
# 1. SSH到服务器
|
||||
ssh root@139.155.109.62
|
||||
|
||||
# 2. 检查容器状态
|
||||
docker ps -a | grep novalon-website
|
||||
|
||||
# 3. 查看容器日志
|
||||
docker logs novalon-website
|
||||
|
||||
# 4. 检查健康状态
|
||||
curl http://localhost:3000/api/health
|
||||
|
||||
# 5. 手动回滚
|
||||
docker images | grep novalon-website
|
||||
docker tag novalon-website:backup-<commit-sha> novalon-website:latest
|
||||
cd /home/novalon/docker-app/novalon-website
|
||||
docker-compose up -d --no-deps novalon-website
|
||||
```
|
||||
|
||||
### 测试失败
|
||||
|
||||
```bash
|
||||
# 1. 本地运行测试
|
||||
npm run test:smoke # Smoke测试
|
||||
npm run test:tier:standard # 标准测试
|
||||
npm run test:tier:deep # 深度测试
|
||||
|
||||
# 2. 查看测试报告
|
||||
npm run test:allure:open
|
||||
|
||||
# 3. 调试特定测试
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
## 📈 性能优化建议
|
||||
|
||||
### 1. 加速构建
|
||||
|
||||
```yaml
|
||||
# 在.woodpecker.yml中添加缓存
|
||||
cache:
|
||||
- name: npm-cache
|
||||
paths:
|
||||
- node_modules
|
||||
- e2e/node_modules
|
||||
```
|
||||
|
||||
### 2. 并行执行
|
||||
|
||||
```yaml
|
||||
# Woodpecker CI自动并行执行独立步骤
|
||||
# 无需额外配置
|
||||
```
|
||||
|
||||
### 3. 增量构建
|
||||
|
||||
```yaml
|
||||
# 利用Docker层缓存
|
||||
# 在Dockerfile中优化层顺序
|
||||
```
|
||||
|
||||
## 🔐 安全最佳实践
|
||||
|
||||
### 1. 密钥管理
|
||||
|
||||
- ✅ 所有密钥存储在Woodpecker CI中
|
||||
- ✅ 不在代码中硬编码
|
||||
- ✅ 定期轮换密钥
|
||||
|
||||
### 2. 访问控制
|
||||
|
||||
- ✅ main分支受保护
|
||||
- ✅ PR需要代码审查
|
||||
- ✅ 部署需要审批
|
||||
|
||||
### 3. 安全扫描
|
||||
|
||||
- ✅ npm audit自动扫描
|
||||
- ✅ 定期更新依赖
|
||||
- ✅ 修复高危漏洞
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
如有问题,请:
|
||||
1. 查看 [CI/CD配置文档](./CICD_GUIDE.md)
|
||||
2. 检查Woodpecker CI日志
|
||||
3. 联系运维团队: ops@novalon.cn
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-03-27
|
||||
**版本**: 1.0.0
|
||||
@@ -1,164 +0,0 @@
|
||||
# 联系方式配置说明
|
||||
|
||||
## 📋 联系方式总结
|
||||
|
||||
### 实际联系方式(对外)
|
||||
|
||||
| 联系类型 | 邮箱 | 用途 |
|
||||
|----------|------|------|
|
||||
| **运维告警** | ops@novalon.cn | 监控告警、系统故障通知 |
|
||||
| **业务咨询** | contact@novalon.cn | 用户联系、业务咨询、表单提交 |
|
||||
|
||||
### 系统内部配置(不对)
|
||||
|
||||
| 配置项 | 邮箱 | 用途 |
|
||||
|--------|------|------|
|
||||
| **管理员账号** | contact@novalon.cn | CMS后台登录、系统管理 |
|
||||
| **公司邮箱** | contact@novalon.cn | 接收联系表单邮件 |
|
||||
| **Resend API** | re_72PzbVrr_DiwTnB1ZDT7TyqCsgLoAfKfU | 邮件发送服务 |
|
||||
|
||||
## 📧 配置文件更新
|
||||
|
||||
### 1. 生产环境配置
|
||||
|
||||
文件: `.env.production`
|
||||
|
||||
```env
|
||||
# 管理员账号(CMS后台登录)
|
||||
ADMIN_EMAIL=contact@novalon.cn
|
||||
|
||||
# 公司邮箱(接收联系表单邮件)
|
||||
COMPANY_EMAIL=contact@novalon.cn
|
||||
|
||||
# Resend API(邮件发送服务)
|
||||
RESEND_API_KEY=re_72PzbVrr_DiwTnB1ZDT7TyqCsgLoAfKfU
|
||||
```
|
||||
|
||||
### 2. 测试配置更新
|
||||
|
||||
文件: `e2e/global-setup.ts`
|
||||
|
||||
```typescript
|
||||
// 测试登录账号
|
||||
await page.locator('#email').fill('contact@novalon.cn');
|
||||
```
|
||||
|
||||
### 3. 文档更新
|
||||
|
||||
所有文档已更新,移除了不存在的"技术支持"联系方式。
|
||||
|
||||
## 📊 监控和告警配置
|
||||
|
||||
### Sentry 错误监控
|
||||
- **告警邮箱**: ops@novalon.cn
|
||||
- **告警类型**: Critical Errors
|
||||
- **响应时间**: 立即
|
||||
|
||||
### UptimeRobot 可用性监控
|
||||
- **告警邮箱**: ops@novalon.cn
|
||||
- **告警类型**: Down, Up, SSL Expiry
|
||||
- **监控频率**: 5分钟
|
||||
|
||||
### Google Analytics 访问统计
|
||||
- **测量 ID**: G-LGTLCR15KM
|
||||
- **追踪类型**: 用户行为、页面浏览、事件追踪
|
||||
|
||||
## 📝 业务流程
|
||||
|
||||
### 用户联系流程
|
||||
|
||||
1. **用户访问联系页面**
|
||||
- 填写联系表单
|
||||
- 提交表单
|
||||
|
||||
2. **系统处理**
|
||||
- 表单提交到 `/api/contact`
|
||||
- 使用 Resend API 发送邮件
|
||||
- 邮件发送到: contact@novalon.cn
|
||||
|
||||
3. **管理员处理**
|
||||
- 管理员登录: contact@novalon.cn
|
||||
- 查看收到的邮件
|
||||
- 回复用户咨询
|
||||
|
||||
### 系统监控流程
|
||||
|
||||
1. **错误发生**
|
||||
- Sentry 捕获错误
|
||||
- 发送告警到: ops@novalon.cn
|
||||
|
||||
2. **网站故障**
|
||||
- UptimeRobot 检测到故障
|
||||
- 发送告警到: ops@novalon.cn
|
||||
|
||||
3. **运维响应**
|
||||
- 运维团队收到告警
|
||||
- 检查系统状态
|
||||
- 修复问题
|
||||
- 通知相关人员
|
||||
|
||||
## 🔐 安全考虑
|
||||
|
||||
### 账号分离
|
||||
- **管理员账号**: contact@novalon.cn(仅用于系统管理)
|
||||
- **运维告警**: ops@novalon.cn(用于系统监控)
|
||||
- **业务咨询**: contact@novalon.cn(用于用户联系)
|
||||
|
||||
### 权限控制
|
||||
- 管理员账号仅限内部使用
|
||||
- 不对外公开管理员登录信息
|
||||
- 定期更换密码
|
||||
|
||||
## 📞 联系方式使用指南
|
||||
|
||||
### 对于用户
|
||||
- **业务咨询**: contact@novalon.cn
|
||||
- 通过网站联系表单提交
|
||||
- 邮件会在 24 小时内回复
|
||||
|
||||
### 对于运维团队
|
||||
- **系统告警**: ops@novalon.cn
|
||||
- 监控系统自动发送告警
|
||||
- 需要立即响应和处理
|
||||
|
||||
### 对于管理员
|
||||
- **系统登录**: contact@novalon.cn
|
||||
- 访问 CMS 管理后台
|
||||
- 管理网站内容和用户
|
||||
|
||||
## ✅ 配置检查清单
|
||||
|
||||
- [x] 生产环境配置更新
|
||||
- [x] 测试配置更新
|
||||
- [x] 文档联系方式统一
|
||||
- [x] 监控告警配置
|
||||
- [x] 邮件服务配置
|
||||
- [x] 账号权限分离
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [轻量级监控配置](LIGHTWEIGHT_MONITORING.md)
|
||||
- [生产部署指南](PRODUCTION_DEPLOYMENT_LIGHTWEIGHT.md)
|
||||
- [Google Analytics 集成](GOOGLE_ANALYTICS_SETUP.md)
|
||||
- [项目 README](../README.md)
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
现在所有联系方式已经统一配置完成:
|
||||
|
||||
1. **对外联系**: contact@novalon.cn
|
||||
- 用户联系表单
|
||||
- 业务咨询
|
||||
- 管理员登录
|
||||
|
||||
2. **运维告警**: ops@novalon.cn
|
||||
- Sentry 错误告警
|
||||
- UptimeRobot 可用性告警
|
||||
- 系统故障通知
|
||||
|
||||
3. **邮件服务**: Resend API
|
||||
- API Key: re_72PzbVrr_DiwTnB1ZDT7TyqCsgLoAfKfU
|
||||
- 发件人: alertmanager@novalon.cn / contact@novalon.cn
|
||||
- SMTP: smtp.resend.com:587
|
||||
|
||||
所有配置文件和文档都已经更新完成,联系方式现在统一且准确!
|
||||
@@ -1,512 +0,0 @@
|
||||
# API版本控制指南
|
||||
|
||||
## 概述
|
||||
|
||||
API版本控制是API设计的重要部分,它允许我们在不破坏现有客户端的情况下演进API。本项目采用URL路径版本控制策略。
|
||||
|
||||
## 版本控制策略
|
||||
|
||||
### URL路径版本控制
|
||||
|
||||
使用URL路径中的版本号来区分不同版本的API:
|
||||
|
||||
```
|
||||
/api/v1/endpoint # 版本1
|
||||
/api/v2/endpoint # 版本2
|
||||
```
|
||||
|
||||
**优点**:
|
||||
- ✅ 清晰明了,易于理解
|
||||
- ✅ 便于缓存和路由
|
||||
- ✅ 支持多版本并存
|
||||
- ✅ 客户端易于使用
|
||||
|
||||
**缺点**:
|
||||
- ❌ URL较长
|
||||
- ❌ 需要维护多个版本
|
||||
|
||||
### 版本命名规则
|
||||
|
||||
- **主版本号**:`v1`, `v2`, `v3`...
|
||||
- **格式**:`/api/v{major}/`
|
||||
- **示例**:
|
||||
- `/api/v1/content`
|
||||
- `/api/v1/admin/users`
|
||||
|
||||
## 目录结构
|
||||
|
||||
### 当前结构(向后兼容)
|
||||
|
||||
```
|
||||
src/app/api/
|
||||
├── admin/
|
||||
│ ├── config/
|
||||
│ ├── content/
|
||||
│ ├── upload/
|
||||
│ └── users/
|
||||
├── auth/
|
||||
├── config/
|
||||
├── content/
|
||||
├── docs/
|
||||
└── health/
|
||||
```
|
||||
|
||||
### 版本化结构(推荐)
|
||||
|
||||
```
|
||||
src/app/api/
|
||||
├── v1/ # 版本1 API
|
||||
│ ├── admin/
|
||||
│ │ ├── config/
|
||||
│ │ ├── content/
|
||||
│ │ ├── upload/
|
||||
│ │ └── users/
|
||||
│ ├── auth/
|
||||
│ ├── config/
|
||||
│ ├── content/
|
||||
│ └── health/
|
||||
├── admin/ # 向后兼容(重定向到v1)
|
||||
├── auth/
|
||||
├── config/
|
||||
├── content/
|
||||
├── docs/ # OpenAPI文档(无版本)
|
||||
└── health/
|
||||
```
|
||||
|
||||
## 实施步骤
|
||||
|
||||
### 步骤1:创建版本化API
|
||||
|
||||
#### 创建v1目录
|
||||
|
||||
```bash
|
||||
mkdir -p src/app/api/v1
|
||||
```
|
||||
|
||||
#### 迁移现有API
|
||||
|
||||
将现有API复制到v1目录:
|
||||
|
||||
```bash
|
||||
# 复制admin API
|
||||
cp -r src/app/api/admin src/app/api/v1/
|
||||
|
||||
# 复制其他API
|
||||
cp -r src/app/api/auth src/app/api/v1/
|
||||
cp -r src/app/api/config src/app/api/v1/
|
||||
cp -r src/app/api/content src/app/api/v1/
|
||||
cp -r src/app/api/health src/app/api/v1/
|
||||
```
|
||||
|
||||
### 步骤2:更新API路由
|
||||
|
||||
#### 更新v1 API路由
|
||||
|
||||
在v1版本的API中,更新路由路径:
|
||||
|
||||
```typescript
|
||||
// src/app/api/v1/admin/content/route.ts
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /api/v1/admin/content:
|
||||
* get:
|
||||
* tags:
|
||||
* - Admin
|
||||
* - Content
|
||||
* summary: 获取内容列表 (v1)
|
||||
* description: 管理员获取内容列表,支持分页、筛选和搜索
|
||||
* operationId: getAdminContentV1
|
||||
* ...
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
// 实现代码
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤3:创建向后兼容层
|
||||
|
||||
#### 创建重定向中间件
|
||||
|
||||
```typescript
|
||||
// src/middleware.ts
|
||||
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
|
||||
// 如果访问旧API路径,重定向到v1版本
|
||||
const legacyApiPaths = [
|
||||
'/api/admin',
|
||||
'/api/auth',
|
||||
'/api/config',
|
||||
'/api/content',
|
||||
'/api/health',
|
||||
];
|
||||
|
||||
const isLegacyApi = legacyApiPaths.some(path =>
|
||||
pathname.startsWith(path) && !pathname.includes('/v1/')
|
||||
);
|
||||
|
||||
if (isLegacyApi) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = pathname.replace('/api/', '/api/v1/');
|
||||
|
||||
// 返回重定向响应(可选:也可以内部重写)
|
||||
// return NextResponse.redirect(url);
|
||||
|
||||
// 或者内部重写(URL不变,但使用新路径)
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/api/:path*',
|
||||
};
|
||||
```
|
||||
|
||||
### 步骤4:更新客户端代码
|
||||
|
||||
#### 更新API客户端
|
||||
|
||||
```typescript
|
||||
// src/lib/api-client.ts
|
||||
|
||||
const API_VERSION = 'v1';
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || '';
|
||||
|
||||
export class ApiClient {
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(version: string = API_VERSION) {
|
||||
this.baseUrl = `${API_BASE_URL}/api/${version}`;
|
||||
}
|
||||
|
||||
async get(endpoint: string, options?: RequestInit) {
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
method: 'GET',
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async post(endpoint: string, data: any, options?: RequestInit) {
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const apiClient = new ApiClient('v1');
|
||||
const content = await apiClient.get('/admin/content');
|
||||
```
|
||||
|
||||
## 版本生命周期
|
||||
|
||||
### 版本状态
|
||||
|
||||
| 状态 | 描述 | 持续时间 |
|
||||
|------|------|----------|
|
||||
| **Current** | 当前推荐版本 | 无限期 |
|
||||
| **Supported** | 仍受支持,但不推荐新功能 | 6-12个月 |
|
||||
| **Deprecated** | 即将废弃,计划移除 | 3-6个月 |
|
||||
| **Sunset** | 已移除,不再可用 | - |
|
||||
|
||||
### 版本废弃流程
|
||||
|
||||
1. **公告**:提前6个月通知废弃计划
|
||||
2. **警告**:在响应头中添加`Deprecation`和`Sunset`头
|
||||
3. **迁移期**:提供迁移指南和工具
|
||||
4. **移除**:在预定日期移除旧版本
|
||||
|
||||
#### 添加废弃头
|
||||
|
||||
```typescript
|
||||
// src/app/api/v1/admin/content/route.ts
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const response = NextResponse.json(data);
|
||||
|
||||
// 添加废弃警告
|
||||
response.headers.set('Deprecation', 'true');
|
||||
response.headers.set('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT');
|
||||
response.headers.set('Link', '</api/v2/admin/content>; rel="successor-version"');
|
||||
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
## 版本间差异处理
|
||||
|
||||
### 向后兼容的变更
|
||||
|
||||
以下变更不需要增加主版本号:
|
||||
|
||||
- ✅ 添加新的可选参数
|
||||
- ✅ 添加新的响应字段
|
||||
- ✅ 添加新的端点
|
||||
- ✅ 修复bug
|
||||
|
||||
### 需要新版本的变更
|
||||
|
||||
以下变更需要增加主版本号:
|
||||
|
||||
- ❌ 移除或重命名端点
|
||||
- ❌ 移除或重命名请求/响应字段
|
||||
- ❌ 修改必填参数
|
||||
- ❌ 修改认证方式
|
||||
- ❌ 修改错误响应格式
|
||||
|
||||
## 多版本并存示例
|
||||
|
||||
### 场景:修改内容API响应格式
|
||||
|
||||
#### v1版本(旧)
|
||||
|
||||
```typescript
|
||||
// src/app/api/v1/admin/content/route.ts
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /api/v1/admin/content:
|
||||
* get:
|
||||
* responses:
|
||||
* 200:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* items:
|
||||
* type: array
|
||||
* pagination:
|
||||
* type: object
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const items = await db.select().from(content);
|
||||
|
||||
return NextResponse.json({
|
||||
items,
|
||||
pagination: { page: 1, limit: 20, total: items.length },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### v2版本(新)
|
||||
|
||||
```typescript
|
||||
// src/app/api/v2/admin/content/route.ts
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /api/v2/admin/content:
|
||||
* get:
|
||||
* responses:
|
||||
* 200:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* data:
|
||||
* type: array
|
||||
* meta:
|
||||
* type: object
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const items = await db.select().from(content);
|
||||
|
||||
return NextResponse.json({
|
||||
data: items, // 改名:items -> data
|
||||
meta: { // 改名:pagination -> meta
|
||||
page: 1,
|
||||
limit: 20,
|
||||
total: items.length,
|
||||
hasNext: items.length === 20,
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 版本兼容性测试
|
||||
|
||||
```typescript
|
||||
// src/app/api/__tests__/version-compatibility.test.ts
|
||||
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
|
||||
describe('API Version Compatibility', () => {
|
||||
it('should return same data structure in v1 and v2', async () => {
|
||||
const v1Response = await fetch('/api/v1/admin/content');
|
||||
const v2Response = await fetch('/api/v2/admin/content');
|
||||
|
||||
const v1Data = await v1Response.json();
|
||||
const v2Data = await v2Response.json();
|
||||
|
||||
// 验证数据一致性
|
||||
expect(v1Data.items.length).toBe(v2Data.data.length);
|
||||
expect(v1Data.pagination.total).toBe(v2Data.meta.total);
|
||||
});
|
||||
|
||||
it('should redirect legacy API to v1', async () => {
|
||||
const response = await fetch('/api/admin/content');
|
||||
expect(response.url).toContain('/api/v1/admin/content');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 文档更新
|
||||
|
||||
### 更新OpenAPI文档
|
||||
|
||||
```typescript
|
||||
// src/app/api/docs/route.ts
|
||||
|
||||
const options = {
|
||||
definition: {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: '睿新致远 API',
|
||||
version: '1.0.0',
|
||||
description: `
|
||||
## API版本
|
||||
|
||||
当前支持以下版本:
|
||||
|
||||
- **v1** (Current): 当前推荐版本
|
||||
- **v2** (Beta): 测试版本,包含新功能
|
||||
|
||||
### 版本状态
|
||||
|
||||
| 版本 | 状态 | 发布日期 | 废弃日期 |
|
||||
|------|------|----------|----------|
|
||||
| v1 | Current | 2024-01-01 | - |
|
||||
| v2 | Beta | 2024-06-01 | - |
|
||||
`,
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: '/api/v1',
|
||||
description: 'API v1 (Current)',
|
||||
},
|
||||
{
|
||||
url: '/api/v2',
|
||||
description: 'API v2 (Beta)',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### ✅ 推荐做法
|
||||
|
||||
1. **提前规划版本策略**
|
||||
- 在API设计初期就考虑版本控制
|
||||
- 为未来变更预留空间
|
||||
|
||||
2. **保持向后兼容**
|
||||
- 尽可能保持旧版本可用
|
||||
- 提供充足的迁移时间
|
||||
|
||||
3. **清晰的文档**
|
||||
- 明确标注版本差异
|
||||
- 提供迁移指南
|
||||
|
||||
4. **版本废弃通知**
|
||||
- 提前通知用户
|
||||
- 使用HTTP头传递废弃信息
|
||||
|
||||
### ❌ 避免的做法
|
||||
|
||||
1. **不要频繁变更主版本**
|
||||
- 主版本变更应该谨慎
|
||||
- 考虑向后兼容的替代方案
|
||||
|
||||
2. **不要突然移除旧版本**
|
||||
- 给用户足够的迁移时间
|
||||
- 提供迁移工具和文档
|
||||
|
||||
3. **不要忽略版本测试**
|
||||
- 确保多版本并存时功能正常
|
||||
- 测试版本兼容性
|
||||
|
||||
## 监控和分析
|
||||
|
||||
### 版本使用统计
|
||||
|
||||
```typescript
|
||||
// src/lib/api-analytics.ts
|
||||
|
||||
export async function trackApiVersion(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl;
|
||||
const version = pathname.match(/\/api\/v(\d+)\//)?.[1] || 'legacy';
|
||||
|
||||
// 发送到分析服务
|
||||
await analytics.track('api_request', {
|
||||
version,
|
||||
endpoint: pathname,
|
||||
method: request.method,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 版本使用报告
|
||||
|
||||
定期生成版本使用报告:
|
||||
|
||||
```markdown
|
||||
## API版本使用报告(2024年6月)
|
||||
|
||||
### 请求分布
|
||||
|
||||
| 版本 | 请求数 | 占比 | 趋势 |
|
||||
|------|--------|------|------|
|
||||
| v1 | 150,000 | 75% | ↓ |
|
||||
| v2 | 50,000 | 25% | ↑ |
|
||||
|
||||
### 废弃版本使用
|
||||
|
||||
| 版本 | 请求数 | 废弃日期 | 建议 |
|
||||
|------|--------|----------|------|
|
||||
| legacy | 1,000 | 2024-12-31 | 尽快迁移到v1 |
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [API版本控制最佳实践](https://www.postman.com/api-platform/api-versioning/)
|
||||
- [REST API版本控制](https://restfulapi.net/versioning/)
|
||||
- [语义化版本控制](https://semver.org/)
|
||||
- [HTTP废弃头规范](https://datatracker.ietf.org/doc/html/rfc8594)
|
||||
|
||||
## 总结
|
||||
|
||||
API版本控制已集成到项目中,提供了:
|
||||
|
||||
✅ **清晰的版本管理**
|
||||
✅ **向后兼容支持**
|
||||
✅ **平滑的版本迁移**
|
||||
✅ **版本使用监控**
|
||||
✅ **完善的文档支持**
|
||||
|
||||
通过合理的版本控制策略,可以:
|
||||
- 保护现有客户端
|
||||
- 安全地演进API
|
||||
- 提供良好的开发者体验
|
||||
- 维护API的长期健康
|
||||
-382
@@ -1,382 +0,0 @@
|
||||
# API 文档
|
||||
|
||||
## API 概述
|
||||
|
||||
项目使用 Next.js API Routes 实现服务端接口,主要用于处理联系表单提交等后端逻辑。
|
||||
|
||||
## 基础信息
|
||||
|
||||
- **基础 URL**: `/api`
|
||||
- **内容类型**: `application/json`
|
||||
- **字符编码**: `UTF-8`
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 联系表单 API
|
||||
|
||||
#### 提交联系表单
|
||||
|
||||
**接口地址**
|
||||
|
||||
```
|
||||
POST /api/contact
|
||||
```
|
||||
|
||||
**请求头**
|
||||
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
|------|------|------|------|
|
||||
| Content-Type | string | 是 | application/json |
|
||||
|
||||
**请求参数**
|
||||
|
||||
| 参数 | 类型 | 必填 | 描述 |
|
||||
|------|------|------|------|
|
||||
| name | string | 是 | 联系人姓名 |
|
||||
| email | string | 是 | 联系人邮箱 |
|
||||
| phone | string | 否 | 联系人电话 |
|
||||
| subject | string | 是 | 咨询主题 |
|
||||
| message | string | 是 | 咨询内容 |
|
||||
| website | string | 否 | 蜜罐字段(用于反垃圾) |
|
||||
| submitTime | string | 否 | 表单提交时间戳 |
|
||||
| mathHash | string | 否 | 数学验证码哈希 |
|
||||
| mathTimestamp | string | 否 | 数学验证码时间戳 |
|
||||
| mathAnswer | number | 否 | 数学验证码答案 |
|
||||
|
||||
**请求示例**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "张三",
|
||||
"email": "zhangsan@example.com",
|
||||
"phone": "13800138000",
|
||||
"subject": "产品咨询",
|
||||
"message": "我想了解贵公司的软件开发服务。",
|
||||
"submitTime": "1709827200000",
|
||||
"mathHash": "MTAwLTE3MDk4MjcxMDAwMDA=",
|
||||
"mathTimestamp": "1709827100000",
|
||||
"mathAnswer": 100
|
||||
}
|
||||
```
|
||||
|
||||
**响应参数**
|
||||
|
||||
| 参数 | 类型 | 描述 |
|
||||
|------|------|------|
|
||||
| success | boolean | 请求是否成功 |
|
||||
| message | string | 成功消息(成功时) |
|
||||
| error | string | 错误消息(失败时) |
|
||||
|
||||
**成功响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "消息已发送,我们会尽快与您联系!"
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "请填写必填字段"
|
||||
}
|
||||
```
|
||||
|
||||
**错误码说明**
|
||||
|
||||
| HTTP 状态码 | 错误信息 | 描述 |
|
||||
|-------------|----------|------|
|
||||
| 200 | - | 蜜罐字段被填充(静默拒绝) |
|
||||
| 400 | 请填写必填字段 | 缺少必填字段 |
|
||||
| 400 | 请输入有效的邮箱地址 | 邮箱格式不正确 |
|
||||
| 400 | 提交过快,请稍后再试 | 提交时间间隔过短 |
|
||||
| 400 | 验证码错误,请重新计算 | 数学验证码错误 |
|
||||
| 500 | 发送失败,请稍后重试 | 邮件发送失败 |
|
||||
|
||||
## 安全机制
|
||||
|
||||
### 1. 蜜罐字段 (Honeypot)
|
||||
|
||||
通过隐藏字段 `website` 检测机器人提交:
|
||||
|
||||
```tsx
|
||||
// 前端隐藏字段
|
||||
<input name="website" className="hidden" tabIndex={-1} autoComplete="off" />
|
||||
|
||||
// 后端检测
|
||||
if (website) {
|
||||
// 检测到机器人,静默返回成功
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 提交时间验证
|
||||
|
||||
验证表单提交时间间隔,防止快速自动提交:
|
||||
|
||||
```tsx
|
||||
if (submitTime) {
|
||||
const timeDiff = Date.now() - parseInt(submitTime);
|
||||
if (timeDiff < 2000) {
|
||||
// 提交过快,拒绝请求
|
||||
return NextResponse.json({ success: false, error: '提交过快' });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 数学验证码
|
||||
|
||||
使用数学运算验证码防止机器人:
|
||||
|
||||
```tsx
|
||||
// 前端生成验证码
|
||||
const num1 = Math.floor(Math.random() * 10) + 1;
|
||||
const num2 = Math.floor(Math.random() * 10) + 1;
|
||||
const answer = num1 + num2;
|
||||
const timestamp = Date.now();
|
||||
const hash = btoa(`${answer}-${timestamp}`);
|
||||
|
||||
// 后端验证
|
||||
const expectedHash = btoa(`${mathAnswer}-${mathTimestamp}`);
|
||||
if (expectedHash !== mathHash) {
|
||||
return NextResponse.json({ success: false, error: '验证码错误' });
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 邮箱验证
|
||||
|
||||
验证邮箱格式:
|
||||
|
||||
```tsx
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json({ success: false, error: '请输入有效的邮箱地址' });
|
||||
}
|
||||
```
|
||||
|
||||
### 5. XSS 防护
|
||||
|
||||
使用 DOMPurify 清理用户输入:
|
||||
|
||||
```tsx
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
const sanitizedName = DOMPurify.sanitize(name);
|
||||
```
|
||||
|
||||
## 邮件服务
|
||||
|
||||
### Resend 配置
|
||||
|
||||
项目使用 Resend 服务发送邮件:
|
||||
|
||||
```env
|
||||
RESEND_API_KEY=re_xxxxx
|
||||
COMPANY_EMAIL=contact@novalon.cn
|
||||
```
|
||||
|
||||
### 邮件模板
|
||||
|
||||
邮件使用 HTML 模板,包含:
|
||||
|
||||
- 公司品牌头部
|
||||
- 表单数据展示
|
||||
- 时间戳信息
|
||||
- 响应式设计
|
||||
|
||||
**邮件模板结构**
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
/* 响应式样式 */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<!-- 公司品牌 -->
|
||||
</div>
|
||||
<div class="content">
|
||||
<!-- 表单数据 -->
|
||||
</div>
|
||||
<div class="footer">
|
||||
<!-- 页脚信息 -->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Server Actions
|
||||
|
||||
### 联系表单 Server Action
|
||||
|
||||
位于 `src/app/(marketing)/contact/actions.ts`:
|
||||
|
||||
```tsx
|
||||
'use server';
|
||||
|
||||
export async function submitContactForm(formData: FormData) {
|
||||
// 服务端表单处理逻辑
|
||||
}
|
||||
```
|
||||
|
||||
**优势:**
|
||||
- 无需创建 API 路由
|
||||
- 自动 CSRF 保护
|
||||
- 类型安全
|
||||
- 更简洁的错误处理
|
||||
|
||||
## 环境变量
|
||||
|
||||
### 必需配置
|
||||
|
||||
```env
|
||||
# Resend API 密钥
|
||||
RESEND_API_KEY=re_xxxxx
|
||||
|
||||
# 公司邮箱
|
||||
COMPANY_EMAIL=contact@novalon.cn
|
||||
```
|
||||
|
||||
### 可选配置
|
||||
|
||||
```env
|
||||
# 环境
|
||||
NODE_ENV=production
|
||||
|
||||
# 站点 URL
|
||||
NEXT_PUBLIC_SITE_URL=https://www.novalon.cn
|
||||
```
|
||||
|
||||
## 请求限制
|
||||
|
||||
### 速率限制
|
||||
|
||||
建议在生产环境配置速率限制:
|
||||
|
||||
```typescript
|
||||
// 示例:使用 Upstash Redis
|
||||
import { Ratelimit } from '@upstash/ratelimit';
|
||||
import { Redis } from '@upstash/redis';
|
||||
|
||||
const ratelimit = new Ratelimit({
|
||||
redis: Redis.fromEnv(),
|
||||
limiter: Ratelimit.slidingWindow(10, '1 m'),
|
||||
});
|
||||
|
||||
const { success } = await ratelimit.limit(ip);
|
||||
if (!success) {
|
||||
return NextResponse.json({ error: '请求过于频繁' }, { status: 429 });
|
||||
}
|
||||
```
|
||||
|
||||
## CORS 配置
|
||||
|
||||
API 路由默认不允许跨域请求。如需配置 CORS:
|
||||
|
||||
```typescript
|
||||
export async function POST(request: NextRequest) {
|
||||
const response = NextResponse.json({ success: true });
|
||||
|
||||
response.headers.set('Access-Control-Allow-Origin', 'https://www.novalon.cn');
|
||||
response.headers.set('Access-Control-Allow-Methods', 'POST');
|
||||
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 统一错误响应格式
|
||||
|
||||
```typescript
|
||||
interface ApiResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
error?: string;
|
||||
data?: unknown;
|
||||
}
|
||||
```
|
||||
|
||||
### 错误日志
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await resend.emails.send({ ... });
|
||||
} catch (error) {
|
||||
console.error('邮件发送失败:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '发送失败,请稍后重试' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 测试接口
|
||||
|
||||
### 使用 cURL
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/contact \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "测试用户",
|
||||
"email": "test@example.com",
|
||||
"subject": "测试主题",
|
||||
"message": "测试消息内容"
|
||||
}'
|
||||
```
|
||||
|
||||
### 使用 Playwright
|
||||
|
||||
```typescript
|
||||
// e2e/src/tests/api/contact.spec.ts
|
||||
test('提交联系表单', async ({ request }) => {
|
||||
const response = await request.post('/api/contact', {
|
||||
data: {
|
||||
name: '测试用户',
|
||||
email: 'test@example.com',
|
||||
subject: '测试主题',
|
||||
message: '测试消息内容',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.ok()).toBeTruthy();
|
||||
const data = await response.json();
|
||||
expect(data.success).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
## API 版本控制
|
||||
|
||||
当前项目 API 不包含版本号。如需版本控制,建议:
|
||||
|
||||
```
|
||||
/api/v1/contact
|
||||
/api/v2/contact
|
||||
```
|
||||
|
||||
## 监控与日志
|
||||
|
||||
### 推荐集成
|
||||
|
||||
- **Sentry** - 错误监控
|
||||
- **LogRocket** - 会话回放
|
||||
- **Vercel Analytics** - 性能监控
|
||||
|
||||
### 日志格式
|
||||
|
||||
```typescript
|
||||
console.log({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'info',
|
||||
message: '联系表单提交',
|
||||
data: { email, subject },
|
||||
});
|
||||
```
|
||||
@@ -1,104 +0,0 @@
|
||||
# CI/CD中的质量门禁
|
||||
|
||||
## 概述
|
||||
|
||||
质量门禁不仅在本地的Git hooks中运行,也在CI/CD流水线中执行,确保所有合并到主分支的代码都符合质量标准。
|
||||
|
||||
## Woodpecker CI配置
|
||||
|
||||
### 质量检查步骤
|
||||
|
||||
在 `.woodpecker.yml` 中添加质量检查步骤:
|
||||
|
||||
```yaml
|
||||
pipeline:
|
||||
quality-check:
|
||||
image: node:18-alpine
|
||||
environment:
|
||||
NODE_ENV: test
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run lint
|
||||
- npm run type-check
|
||||
- npm run test:coverage
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
- pull_request
|
||||
|
||||
coverage-report:
|
||||
image: node:18-alpine
|
||||
environment:
|
||||
NODE_ENV: test
|
||||
commands:
|
||||
- npm ci
|
||||
- npm run test:coverage
|
||||
# 上传覆盖率报告到Codecov或其他服务
|
||||
secrets: [codecov_token]
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
```
|
||||
|
||||
### 质量门禁规则
|
||||
|
||||
CI/CD中的质量门禁规则:
|
||||
|
||||
1. **代码检查**: ESLint必须通过,无错误
|
||||
2. **类型检查**: TypeScript编译必须成功
|
||||
3. **测试通过**: 所有测试必须通过
|
||||
4. **覆盖率达标**: 代码覆盖率必须≥70%
|
||||
|
||||
### 失败处理
|
||||
|
||||
如果质量检查失败:
|
||||
|
||||
1. CI/CD流水线失败
|
||||
2. 阻止合并到主分支
|
||||
3. 发送通知给开发者
|
||||
4. 显示详细的错误信息
|
||||
|
||||
## 本地开发 vs CI/CD
|
||||
|
||||
### 本地开发
|
||||
|
||||
- **优点**: 快速反馈,立即发现问题
|
||||
- **缺点**: 可能被绕过(--no-verify)
|
||||
|
||||
### CI/CD
|
||||
|
||||
- **优点**: 强制执行,无法绕过
|
||||
- **缺点**: 反馈延迟,需要等待CI运行
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **本地优先**: 在本地运行质量检查,确保通过后再推送
|
||||
2. **CI兜底**: CI/CD作为最后一道防线,确保质量
|
||||
3. **快速反馈**: CI/CD配置为快速失败,尽早发现问题
|
||||
|
||||
## 持续改进
|
||||
|
||||
### 监控质量指标
|
||||
|
||||
定期监控以下指标:
|
||||
|
||||
- 代码覆盖率趋势
|
||||
- ESLint错误数量
|
||||
- TypeScript错误数量
|
||||
- 测试失败率
|
||||
- CI/CD通过率
|
||||
|
||||
### 优化质量门禁
|
||||
|
||||
根据监控数据优化质量门禁:
|
||||
|
||||
1. 调整覆盖率阈值
|
||||
2. 添加新的质量检查
|
||||
3. 优化检查性能
|
||||
4. 改进错误提示
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Woodpecker CI文档](https://woodpecker-ci.org/)
|
||||
- [Codecov文档](https://docs.codecov.com/)
|
||||
- [GitHub Actions文档](https://docs.github.com/en/actions)
|
||||
@@ -1,466 +0,0 @@
|
||||
# OpenAPI文档使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
OpenAPI(原名Swagger)是一个用于描述、生成、消费和可视化RESTful Web服务的规范。本项目已集成OpenAPI文档,提供交互式API文档界面。
|
||||
|
||||
## 访问文档
|
||||
|
||||
### 开发环境
|
||||
|
||||
启动开发服务器后,访问:
|
||||
|
||||
```
|
||||
http://localhost:3000/api-docs
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
部署后访问:
|
||||
|
||||
```
|
||||
https://your-domain.com/api-docs
|
||||
```
|
||||
|
||||
## 文档结构
|
||||
|
||||
### API端点
|
||||
|
||||
| 端点 | 方法 | 描述 | 认证 |
|
||||
|------|------|------|------|
|
||||
| `/api/health` | GET | 健康检查 | 无 |
|
||||
| `/api/admin/content` | GET | 获取内容列表 | 需要管理员权限 |
|
||||
| `/api/admin/content` | POST | 创建新内容 | 需要管理员权限 |
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### Content(内容)
|
||||
|
||||
```typescript
|
||||
interface Content {
|
||||
id: number;
|
||||
type: 'news' | 'case' | 'product' | 'service';
|
||||
title: string;
|
||||
content: string;
|
||||
status: 'draft' | 'published' | 'archived';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
#### User(用户)
|
||||
|
||||
```typescript
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
role: 'admin' | 'editor' | 'viewer';
|
||||
}
|
||||
```
|
||||
|
||||
#### Config(配置)
|
||||
|
||||
```typescript
|
||||
interface Config {
|
||||
key: string;
|
||||
value: string;
|
||||
description: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 使用Swagger UI
|
||||
|
||||
### 浏览API
|
||||
|
||||
1. 访问 `/api-docs`
|
||||
2. 点击任意API端点展开详情
|
||||
3. 查看请求参数、响应格式和示例
|
||||
|
||||
### 测试API
|
||||
|
||||
#### 无需认证的API
|
||||
|
||||
1. 点击"Try it out"按钮
|
||||
2. 填写必要参数
|
||||
3. 点击"Execute"执行请求
|
||||
4. 查看响应结果
|
||||
|
||||
示例:健康检查API
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:3000/api/health" -H "accept: application/json"
|
||||
```
|
||||
|
||||
#### 需要认证的API
|
||||
|
||||
1. 先登录获取访问令牌
|
||||
2. 点击页面右上角的"Authorize"按钮
|
||||
3. 输入Bearer令牌:`Bearer your-access-token`
|
||||
4. 点击"Authorize"确认
|
||||
5. 现在可以测试需要认证的API
|
||||
|
||||
示例:获取内容列表
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:3000/api/admin/content?page=1&limit=20" \
|
||||
-H "accept: application/json" \
|
||||
-H "Authorization: Bearer your-access-token"
|
||||
```
|
||||
|
||||
## 为API添加文档
|
||||
|
||||
### 步骤1:添加JSDoc注释
|
||||
|
||||
在API路由文件中添加JSDoc注释:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @openapi
|
||||
* /api/your-endpoint:
|
||||
* get:
|
||||
* tags:
|
||||
* - YourTag
|
||||
* summary: 简短描述
|
||||
* description: 详细描述
|
||||
* operationId: getYourData
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: 成功响应
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* success:
|
||||
* type: boolean
|
||||
* data:
|
||||
* type: object
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
// API实现
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤2:定义请求体
|
||||
|
||||
对于POST/PUT请求:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @openapi
|
||||
* /api/your-endpoint:
|
||||
* post:
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - email
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* email:
|
||||
* type: string
|
||||
* format: email
|
||||
*/
|
||||
```
|
||||
|
||||
### 步骤3:引用共享Schema
|
||||
|
||||
使用`$ref`引用共享数据模型:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @openapi
|
||||
* /api/admin/content:
|
||||
* get:
|
||||
* responses:
|
||||
* 200:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/Content'
|
||||
*/
|
||||
```
|
||||
|
||||
## OpenAPI规范文件
|
||||
|
||||
### 获取规范文件
|
||||
|
||||
访问以下端点获取原始OpenAPI规范:
|
||||
|
||||
```
|
||||
GET /api/docs
|
||||
```
|
||||
|
||||
### 使用规范文件
|
||||
|
||||
1. **导入到Postman**
|
||||
- 打开Postman
|
||||
- 点击"Import"
|
||||
- 选择"Link"
|
||||
- 输入:`http://localhost:3000/api/docs`
|
||||
- 点击"Import"
|
||||
|
||||
2. **生成客户端代码**
|
||||
- 使用OpenAPI Generator
|
||||
- 支持多种语言:TypeScript, Python, Java等
|
||||
|
||||
3. **API测试**
|
||||
- 导入到测试工具
|
||||
- 自动生成测试用例
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### ✅ 推荐做法
|
||||
|
||||
1. **完整的描述**
|
||||
- 提供清晰的summary和description
|
||||
- 说明参数的作用和限制
|
||||
- 提供示例值
|
||||
|
||||
2. **准确的类型定义**
|
||||
- 使用正确的数据类型
|
||||
- 标注必填字段
|
||||
- 定义枚举值
|
||||
|
||||
3. **完整的响应定义**
|
||||
- 定义所有可能的响应状态码
|
||||
- 提供错误响应格式
|
||||
- 包含示例数据
|
||||
|
||||
4. **合理的标签分组**
|
||||
- 按功能模块分组
|
||||
- 使用一致的命名
|
||||
- 避免过多标签
|
||||
|
||||
### ❌ 避免的做法
|
||||
|
||||
1. **不要省略错误响应**
|
||||
```typescript
|
||||
// ❌ 不好
|
||||
responses:
|
||||
* 200:
|
||||
* description: 成功
|
||||
|
||||
// ✅ 好
|
||||
responses:
|
||||
* 200:
|
||||
* description: 成功
|
||||
* 400:
|
||||
* description: 参数错误
|
||||
* 401:
|
||||
* description: 未授权
|
||||
* 500:
|
||||
* description: 服务器错误
|
||||
```
|
||||
|
||||
2. **不要使用模糊的描述**
|
||||
```typescript
|
||||
// ❌ 不好
|
||||
summary: 获取数据
|
||||
|
||||
// ✅ 好
|
||||
summary: 获取内容列表
|
||||
description: 管理员获取内容列表,支持分页、筛选和搜索
|
||||
```
|
||||
|
||||
3. **不要忽略认证要求**
|
||||
```typescript
|
||||
// ✅ 始终标注认证要求
|
||||
security:
|
||||
* - bearerAuth: []
|
||||
```
|
||||
|
||||
## 高级功能
|
||||
|
||||
### 添加示例
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @openapi
|
||||
* /api/admin/content:
|
||||
* post:
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* examples:
|
||||
* newsExample:
|
||||
* summary: 新闻示例
|
||||
* value:
|
||||
* type: news
|
||||
* title: 新闻标题
|
||||
* content: 新闻内容
|
||||
*/
|
||||
```
|
||||
|
||||
### 添加标签描述
|
||||
|
||||
在`/api/docs/route.ts`中:
|
||||
|
||||
```typescript
|
||||
tags: [
|
||||
{
|
||||
name: 'Content',
|
||||
description: '内容管理相关接口',
|
||||
},
|
||||
{
|
||||
name: 'Admin',
|
||||
description: '管理员相关接口',
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
### 添加服务器配置
|
||||
|
||||
```typescript
|
||||
servers: [
|
||||
{
|
||||
url: 'http://localhost:3000',
|
||||
description: '开发服务器',
|
||||
},
|
||||
{
|
||||
url: 'https://api.novalon.cn',
|
||||
description: '生产服务器',
|
||||
},
|
||||
],
|
||||
```
|
||||
|
||||
## CI/CD集成
|
||||
|
||||
### 验证OpenAPI规范
|
||||
|
||||
```bash
|
||||
# 安装验证工具
|
||||
npm install -g @redocly/cli
|
||||
|
||||
# 验证规范
|
||||
redocly lint http://localhost:3000/api/docs
|
||||
```
|
||||
|
||||
### 生成文档
|
||||
|
||||
```bash
|
||||
# 安装Redoc
|
||||
npm install -g redoc
|
||||
|
||||
# 生成静态HTML文档
|
||||
redocly build-docs http://localhost:3000/api/docs -o api-docs.html
|
||||
```
|
||||
|
||||
### GitHub Actions示例
|
||||
|
||||
```yaml
|
||||
name: API Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Start server
|
||||
run: npm run dev &
|
||||
env:
|
||||
CI: true
|
||||
|
||||
- name: Wait for server
|
||||
run: npx wait-on http://localhost:3000/api/docs
|
||||
|
||||
- name: Validate OpenAPI spec
|
||||
run: npx @redocly/cli lint http://localhost:3000/api/docs
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1:文档页面无法加载
|
||||
|
||||
**症状**:访问`/api-docs`显示加载中或空白页
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 检查API端点是否正常
|
||||
curl http://localhost:3000/api/docs
|
||||
|
||||
# 检查浏览器控制台错误
|
||||
# 打开开发者工具查看Network和Console标签
|
||||
```
|
||||
|
||||
### 问题2:API不显示在文档中
|
||||
|
||||
**症状**:某些API端点未出现在文档中
|
||||
|
||||
**解决方案**:
|
||||
```typescript
|
||||
// 检查JSDoc注释格式
|
||||
// 确保使用 @openapi 标签
|
||||
/**
|
||||
* @openapi // ← 必须是这个标签
|
||||
* /api/your-endpoint:
|
||||
* get:
|
||||
*/
|
||||
|
||||
// 检查apis路径配置
|
||||
apis: [
|
||||
'./src/app/api/**/route.ts', // ← 确保路径正确
|
||||
],
|
||||
```
|
||||
|
||||
### 问题3:认证失败
|
||||
|
||||
**症状**:使用Authorize按钮后仍然无法访问需要认证的API
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 确保令牌格式正确
|
||||
Bearer your-access-token # ← 注意Bearer前缀
|
||||
|
||||
# 检查令牌是否有效
|
||||
curl -H "Authorization: Bearer your-token" http://localhost:3000/api/admin/content
|
||||
```
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [OpenAPI规范](https://swagger.io/specification/)
|
||||
- [Swagger UI文档](https://swagger.io/tools/swagger-ui/)
|
||||
- [swagger-jsdoc文档](https://github.com/surnet/swagger-jsdoc)
|
||||
- [OpenAPI Generator](https://openapi-generator.tech/)
|
||||
- [Redoc文档](https://redocly.com/docs/redoc/)
|
||||
|
||||
## 总结
|
||||
|
||||
OpenAPI文档已完全集成到项目中,提供了:
|
||||
|
||||
✅ **交互式API文档**
|
||||
✅ **自动生成规范**
|
||||
✅ **在线测试功能**
|
||||
✅ **认证支持**
|
||||
✅ **多格式导出**
|
||||
|
||||
通过合理使用OpenAPI文档,可以:
|
||||
- 提升API可用性
|
||||
- 减少沟通成本
|
||||
- 自动化API测试
|
||||
- 生成客户端SDK
|
||||
@@ -1,450 +0,0 @@
|
||||
# 分层测试最佳实践
|
||||
|
||||
## 概述
|
||||
|
||||
本文档提供分层测试系统的最佳实践,帮助团队构建高效、可靠的测试体系。
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 质量左移
|
||||
|
||||
在需求分析和设计阶段就考虑测试策略,而不是在开发完成后才补充测试。
|
||||
|
||||
**实践:**
|
||||
- 在需求文档中明确测试要求
|
||||
- 在设计评审中讨论可测试性
|
||||
- 开发过程中同步编写测试
|
||||
|
||||
### 2. 测试金字塔
|
||||
|
||||
遵循测试金字塔原则,保持合理的测试比例:
|
||||
|
||||
```
|
||||
/\
|
||||
/ \ E2E测试 (10%)
|
||||
/____\
|
||||
/ \ 集成测试 (30%)
|
||||
/________\
|
||||
/ \ 单元测试 (60%)
|
||||
/____________\
|
||||
```
|
||||
|
||||
**实践:**
|
||||
- 单元测试:快速、独立、覆盖核心逻辑
|
||||
- 集成测试:验证组件间交互
|
||||
- E2E测试:验证关键用户流程
|
||||
|
||||
### 3. 快速反馈
|
||||
|
||||
确保测试能够快速提供反馈,帮助开发人员快速定位问题。
|
||||
|
||||
**实践:**
|
||||
- 快速层测试在5分钟内完成
|
||||
- 标准层测试在30分钟内完成
|
||||
- 深度层测试可以接受较长执行时间
|
||||
|
||||
## 测试分层策略
|
||||
|
||||
### 快速层设计
|
||||
|
||||
**目标:** 在5分钟内验证核心功能
|
||||
|
||||
**包含内容:**
|
||||
1. **冒烟测试** (Smoke Tests)
|
||||
- 验证应用能够正常启动
|
||||
- 验证关键页面能够加载
|
||||
- 验证核心API能够响应
|
||||
|
||||
2. **API测试**
|
||||
- 验证API端点的正确性
|
||||
- 验证数据格式和结构
|
||||
- 验证错误处理
|
||||
|
||||
3. **基础功能测试**
|
||||
- 验证用户登录/登出
|
||||
- 验证基本CRUD操作
|
||||
- 验证权限控制
|
||||
|
||||
**最佳实践:**
|
||||
- 每个测试文件不超过3个测试用例
|
||||
- 每个测试用例执行时间不超过10秒
|
||||
- 使用mock数据替代真实数据库
|
||||
|
||||
**示例:**
|
||||
```typescript
|
||||
test.describe('用户认证快速测试 @smoke @critical', () => {
|
||||
test('应该能够成功登录', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('[data-testid="email"]', 'admin@example.com');
|
||||
await page.fill('[data-testid="password"]', 'password123');
|
||||
await page.click('[data-testid="login-btn"]');
|
||||
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
});
|
||||
|
||||
test('应该能够成功登出', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
await page.click('[data-testid="logout-btn"]');
|
||||
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 标准层设计
|
||||
|
||||
**目标:** 在30分钟内验证大部分功能
|
||||
|
||||
**包含内容:**
|
||||
1. **功能测试** (Functional Tests)
|
||||
- 验证完整的用户流程
|
||||
- 验证表单验证
|
||||
- 验证业务规则
|
||||
|
||||
2. **响应式测试** (Responsive Tests)
|
||||
- 验证不同屏幕尺寸下的布局
|
||||
- 验证移动端和桌面端的交互
|
||||
- 验证触摸和鼠标事件
|
||||
|
||||
3. **管理后台测试** (Admin Tests)
|
||||
- 验证内容管理功能
|
||||
- 验证用户管理功能
|
||||
- 验证系统配置
|
||||
|
||||
**最佳实践:**
|
||||
- 每个测试文件包含5-10个测试用例
|
||||
- 每个测试用例执行时间不超过30秒
|
||||
- 使用Page Object Model模式
|
||||
|
||||
**示例:**
|
||||
```typescript
|
||||
test.describe('新闻管理功能测试 @admin @regression', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/admin/news');
|
||||
});
|
||||
|
||||
test('应该能够创建新闻', async ({ page }) => {
|
||||
await page.click('[data-testid="create-news-btn"]');
|
||||
await page.fill('[data-testid="news-title"]', '测试新闻');
|
||||
await page.fill('[data-testid="news-content"]', '新闻内容');
|
||||
await page.click('[data-testid="save-btn"]');
|
||||
|
||||
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('应该能够编辑新闻', async ({ page }) => {
|
||||
await page.click('[data-testid="edit-news-1"]');
|
||||
await page.fill('[data-testid="news-title"]', '更新后的标题');
|
||||
await page.click('[data-testid="save-btn"]');
|
||||
|
||||
await expect(page.locator('[data-testid="news-title"]')).toHaveValue('更新后的标题');
|
||||
});
|
||||
|
||||
test('应该能够删除新闻', async ({ page }) => {
|
||||
await page.click('[data-testid="delete-news-1"]');
|
||||
await page.click('[data-testid="confirm-btn"]');
|
||||
|
||||
await expect(page.locator('[data-testid="news-1"]')).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 深度层设计
|
||||
|
||||
**目标:** 在发布前进行全面验证
|
||||
|
||||
**包含内容:**
|
||||
1. **视觉回归测试** (Visual Regression Tests)
|
||||
- 验证UI与设计稿一致
|
||||
- 验证样式和布局
|
||||
- 验证跨浏览器一致性
|
||||
|
||||
2. **性能测试** (Performance Tests)
|
||||
- 验证页面加载时间
|
||||
- 验证API响应时间
|
||||
- 验证资源加载优化
|
||||
|
||||
3. **完整回归测试** (Full Regression Tests)
|
||||
- 验证所有已知功能
|
||||
- 验证边界情况
|
||||
- 验证错误处理
|
||||
|
||||
**最佳实践:**
|
||||
- 使用截图对比工具
|
||||
- 使用性能监控工具
|
||||
- 在夜间或周末执行
|
||||
|
||||
**示例:**
|
||||
```typescript
|
||||
test.describe('首页视觉回归测试 @visual @regression', () => {
|
||||
test('桌面端首页应该与基准一致', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page).toHaveScreenshot('homepage-desktop.png');
|
||||
});
|
||||
|
||||
test('移动端首页应该与基准一致', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page).toHaveScreenshot('homepage-mobile.png');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 测试标记策略
|
||||
|
||||
### 标记分类
|
||||
|
||||
#### 优先级标记
|
||||
- `@critical` - 关键测试,必须通过
|
||||
- `@high` - 高优先级测试
|
||||
- `@medium` - 中等优先级测试
|
||||
- `@low` - 低优先级测试
|
||||
|
||||
#### 类型标记
|
||||
- `@smoke` - 冒烟测试
|
||||
- `@regression` - 回归测试
|
||||
- `@functional` - 功能测试
|
||||
- `@api` - API测试
|
||||
- `@visual` - 视觉测试
|
||||
- `@performance` - 性能测试
|
||||
|
||||
#### 平台标记
|
||||
- `@desktop` - 桌面端测试
|
||||
- `@mobile` - 移动端测试
|
||||
- `@tablet` - 平板端测试
|
||||
|
||||
#### 功能标记
|
||||
- `@auth` - 认证相关测试
|
||||
- `@admin` - 管理后台测试
|
||||
- `@content` - 内容管理测试
|
||||
- `@user` - 用户功能测试
|
||||
|
||||
### 标记使用规则
|
||||
|
||||
1. **每个测试套件至少有一个标记**
|
||||
2. **关键测试必须标记为 `@critical`**
|
||||
3. **冒烟测试必须标记为 `@smoke`**
|
||||
4. **回归测试必须标记为 `@regression`**
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 减少测试执行时间
|
||||
|
||||
#### 1. 并行执行
|
||||
```typescript
|
||||
// playwright.config.tiered.ts
|
||||
{
|
||||
fullyParallel: true,
|
||||
workers: '75%',
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 减少等待时间
|
||||
```typescript
|
||||
// 不推荐
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 推荐
|
||||
await page.waitForSelector('[data-testid="result"]', { timeout: 5000 });
|
||||
```
|
||||
|
||||
#### 3. 使用快速选择器
|
||||
```typescript
|
||||
// 不推荐
|
||||
await page.click('div > div > button');
|
||||
|
||||
// 推荐
|
||||
await page.click('[data-testid="submit-btn"]');
|
||||
```
|
||||
|
||||
#### 4. 复用浏览器上下文
|
||||
```typescript
|
||||
test.describe('用户管理测试', () => {
|
||||
test.use({ storageState: '.auth/admin.json' });
|
||||
|
||||
test('应该能够创建用户', async ({ page }) => {
|
||||
// 测试逻辑
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 优化测试数据
|
||||
|
||||
#### 1. 使用固定数据
|
||||
```typescript
|
||||
const testUser = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
};
|
||||
|
||||
test('应该能够登录', async ({ page }) => {
|
||||
await page.fill('[data-testid="email"]', testUser.email);
|
||||
await page.fill('[data-testid="password"]', testUser.password);
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. 使用测试数据库
|
||||
```typescript
|
||||
test.beforeEach(async () => {
|
||||
await db.reset();
|
||||
await db.seed(testData);
|
||||
});
|
||||
```
|
||||
|
||||
#### 3. 清理测试数据
|
||||
```typescript
|
||||
test.afterEach(async () => {
|
||||
await db.cleanup();
|
||||
});
|
||||
```
|
||||
|
||||
## 可维护性
|
||||
|
||||
### Page Object Model
|
||||
|
||||
使用Page Object Model模式提高测试的可维护性:
|
||||
|
||||
```typescript
|
||||
// pages/LoginPage.ts
|
||||
export class LoginPage {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
await this.page.fill('[data-testid="email"]', email);
|
||||
await this.page.fill('[data-testid="password"]', password);
|
||||
await this.page.click('[data-testid="login-btn"]');
|
||||
}
|
||||
|
||||
async expectLoggedIn() {
|
||||
await expect(this.page).toHaveURL('/dashboard');
|
||||
}
|
||||
}
|
||||
|
||||
// tests/login.spec.ts
|
||||
test('应该能够登录', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
|
||||
await loginPage.login('admin@example.com', 'password123');
|
||||
await loginPage.expectLoggedIn();
|
||||
});
|
||||
```
|
||||
|
||||
### 测试数据管理
|
||||
|
||||
使用专门的测试数据管理器:
|
||||
|
||||
```typescript
|
||||
// utils/test-data.ts
|
||||
export const TestData = {
|
||||
users: {
|
||||
admin: {
|
||||
email: 'admin@example.com',
|
||||
password: 'password123',
|
||||
role: 'admin',
|
||||
},
|
||||
user: {
|
||||
email: 'user@example.com',
|
||||
password: 'password123',
|
||||
role: 'user',
|
||||
},
|
||||
},
|
||||
news: {
|
||||
valid: {
|
||||
title: '测试新闻',
|
||||
content: '新闻内容',
|
||||
},
|
||||
invalid: {
|
||||
title: '',
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
使用环境变量管理测试配置:
|
||||
|
||||
```typescript
|
||||
// config/environments.ts
|
||||
export const getEnvironment = () => {
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
|
||||
return {
|
||||
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
||||
timeout: parseInt(process.env.TEST_TIMEOUT || '30000'),
|
||||
retries: parseInt(process.env.TEST_RETRIES || '2'),
|
||||
headless: process.env.HEADLESS !== 'false',
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## 持续改进
|
||||
|
||||
### 定期审查
|
||||
|
||||
每月进行一次测试审查:
|
||||
1. 检查测试覆盖率
|
||||
2. 识别慢速测试
|
||||
3. 评估测试有效性
|
||||
4. 清理无用测试
|
||||
|
||||
### 性能监控
|
||||
|
||||
持续监控测试性能:
|
||||
1. 记录测试执行时间
|
||||
2. 识别性能趋势
|
||||
3. 优化慢速测试
|
||||
4. 调整测试分层
|
||||
|
||||
### 反馈收集
|
||||
|
||||
收集测试反馈:
|
||||
1. 开发人员反馈
|
||||
2. 测试失败分析
|
||||
3. 用户反馈
|
||||
4. 生产问题追踪
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 如何确定测试应该放在哪一层?
|
||||
|
||||
A: 根据测试的执行时间和重要性:
|
||||
- 执行时间<30秒且是关键功能 → 快速层
|
||||
- 执行时间<60秒 → 标准层
|
||||
- 执行时间>60秒或需要完整回归 → 深度层
|
||||
|
||||
### Q: 测试失败时如何处理?
|
||||
|
||||
A: 按照以下优先级处理:
|
||||
1. 快速层测试失败 → 立即修复
|
||||
2. 标准层测试失败 → 在合并PR前修复
|
||||
3. 深度层测试失败 → 在发布前修复
|
||||
|
||||
### Q: 如何减少测试执行时间?
|
||||
|
||||
A: 采用以下策略:
|
||||
1. 并行执行测试
|
||||
2. 减少不必要的等待
|
||||
3. 优化选择器
|
||||
4. 拆分大测试
|
||||
5. 使用mock数据
|
||||
|
||||
### Q: 如何提高测试稳定性?
|
||||
|
||||
A: 遵循以下原则:
|
||||
1. 使用稳定的等待策略
|
||||
2. 避免硬编码的等待时间
|
||||
3. 使用data-testid选择器
|
||||
4. 清理测试数据
|
||||
5. 增加重试次数
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Playwright最佳实践](https://playwright.dev/docs/best-practices)
|
||||
- [测试金字塔](https://martinfowler.com/articles/practical-test-pyramid.html)
|
||||
- [Page Object Model](https://playwright.dev/docs/pom)
|
||||
- [测试驱动开发](https://martinfowler.com/bliki/TestDrivenDevelopment.html)
|
||||
@@ -1,295 +0,0 @@
|
||||
# 分层测试快速入门指南
|
||||
|
||||
## 什么是分层测试?
|
||||
|
||||
分层测试是一种测试策略,将测试按照执行时间和重要性分为三个层级:
|
||||
|
||||
- **快速层**:5分钟内完成,验证核心功能
|
||||
- **标准层**:30分钟内完成,验证大部分功能
|
||||
- **深度层**:可接受较长执行时间,进行全面验证
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 本地运行测试
|
||||
|
||||
#### 运行快速层测试(推荐日常开发使用)
|
||||
```bash
|
||||
npm run test:tier:fast
|
||||
```
|
||||
|
||||
#### 运行标准层测试
|
||||
```bash
|
||||
npm run test:tier:standard
|
||||
```
|
||||
|
||||
#### 运行深度层测试
|
||||
```bash
|
||||
npm run test:tier:deep
|
||||
```
|
||||
|
||||
#### 运行所有层级测试
|
||||
```bash
|
||||
npm run test:tier:all
|
||||
```
|
||||
|
||||
### 2. 编写分层测试
|
||||
|
||||
#### 快速层测试示例
|
||||
```typescript
|
||||
test.describe('API快速测试 @smoke @critical', () => {
|
||||
test('应该能够获取内容列表', async ({ request }) => {
|
||||
const response = await request.get('/api/admin/content');
|
||||
expect(response.status()).toBe(200);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 标准层测试示例
|
||||
```typescript
|
||||
test.describe('管理后台功能测试 @admin @regression', () => {
|
||||
test('应该能够创建新闻', async ({ page }) => {
|
||||
await page.goto('/admin/news');
|
||||
await page.click('[data-testid="create-news-btn"]');
|
||||
await page.fill('[data-testid="news-title"]', '测试新闻');
|
||||
await page.click('[data-testid="save-btn"]');
|
||||
|
||||
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 深度层测试示例
|
||||
```typescript
|
||||
test.describe('首页视觉回归测试 @visual @regression', () => {
|
||||
test('桌面端首页应该与基准一致', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page).toHaveScreenshot('homepage-desktop.png');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 使用测试标记
|
||||
|
||||
为测试添加标记以便分类和管理:
|
||||
|
||||
```typescript
|
||||
test.describe('测试套件 @smoke @critical', () => {
|
||||
test('测试用例 @api @regression', async ({ page }) => {
|
||||
// 测试逻辑
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**常用标记:**
|
||||
- `@smoke` - 冒烟测试
|
||||
- `@critical` - 关键测试
|
||||
- `@regression` - 回归测试
|
||||
- `@visual` - 视觉测试
|
||||
- `@api` - API测试
|
||||
- `@mobile` - 移动端测试
|
||||
|
||||
## CI/CD集成
|
||||
|
||||
项目已配置Woodpecker CI,自动执行分层测试:
|
||||
|
||||
### 分支策略
|
||||
|
||||
- **main分支**:执行所有层级测试
|
||||
- **develop分支**:执行快速层和标准层测试
|
||||
- **其他分支**:仅执行快速层测试
|
||||
|
||||
### 工作流程
|
||||
|
||||
1. 提交代码到分支
|
||||
2. Woodpecker CI自动触发
|
||||
3. 依次执行快速层、标准层、深度层测试
|
||||
4. 前一层失败则停止后续执行
|
||||
5. 生成测试报告并上传
|
||||
6. 发送通知
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 识别慢速测试
|
||||
|
||||
运行性能优化工具:
|
||||
|
||||
```bash
|
||||
cd e2e && node test-optimizer-simple-test.js
|
||||
```
|
||||
|
||||
工具会生成优化报告,包含:
|
||||
- 慢速测试列表
|
||||
- 优化建议
|
||||
- 潜在时间节省
|
||||
|
||||
### 优化建议
|
||||
|
||||
1. **减少等待时间**
|
||||
```typescript
|
||||
// 不推荐
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 推荐
|
||||
await page.waitForSelector('[data-testid="result"]', { timeout: 5000 });
|
||||
```
|
||||
|
||||
2. **使用data-testid选择器**
|
||||
```typescript
|
||||
// 不推荐
|
||||
await page.click('div > div > button');
|
||||
|
||||
// 推荐
|
||||
await page.click('[data-testid="submit-btn"]');
|
||||
```
|
||||
|
||||
3. **拆分大测试**
|
||||
```typescript
|
||||
// 不推荐:单个大测试
|
||||
test('完整的用户注册流程', async ({ page }) => {
|
||||
// 100+ 行代码
|
||||
});
|
||||
|
||||
// 推荐:拆分为多个小测试
|
||||
test.describe('用户注册流程', () => {
|
||||
test('应该能够填写注册表单', async ({ page }) => {
|
||||
// 20 行代码
|
||||
});
|
||||
|
||||
test('应该能够提交注册', async ({ page }) => {
|
||||
// 20 行代码
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 监控和告警
|
||||
|
||||
### 测试执行历史
|
||||
|
||||
系统自动记录测试执行历史,存储在 `e2e/test-history.json`。
|
||||
|
||||
### 告警规则
|
||||
|
||||
系统会根据以下规则触发告警:
|
||||
|
||||
1. 测试通过率低于80% (Critical)
|
||||
2. 测试通过率低于90% (High)
|
||||
3. 测试执行时间超过30分钟 (Medium)
|
||||
4. 失败测试数量超过10个 (High)
|
||||
5. 深度层测试存在失败 (Critical)
|
||||
|
||||
### 查看告警
|
||||
|
||||
告警信息会输出到控制台,并保存在 `test-results/alerts.json`。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 测试超时怎么办?
|
||||
|
||||
A: 检查以下几点:
|
||||
1. 是否有不必要的等待时间
|
||||
2. 选择器是否正确
|
||||
3. 网络请求是否正常
|
||||
4. 是否需要增加超时时间
|
||||
|
||||
### Q: 测试不稳定怎么办?
|
||||
|
||||
A: 采用以下策略:
|
||||
1. 增加重试次数
|
||||
2. 使用更稳定的等待策略
|
||||
3. 检查是否有竞态条件
|
||||
4. 使用data-testid选择器
|
||||
|
||||
### Q: 如何确定测试应该放在哪一层?
|
||||
|
||||
A: 根据执行时间和重要性:
|
||||
- 执行时间<30秒且是关键功能 → 快速层
|
||||
- 执行时间<60秒 → 标准层
|
||||
- 执行时间>60秒或需要完整回归 → 深度层
|
||||
|
||||
### Q: 如何减少测试执行时间?
|
||||
|
||||
A: 采用以下策略:
|
||||
1. 并行执行测试
|
||||
2. 减少不必要的等待
|
||||
3. 优化选择器
|
||||
4. 拆分大测试
|
||||
5. 使用mock数据
|
||||
|
||||
## 进阶使用
|
||||
|
||||
### 自定义测试层级
|
||||
|
||||
编辑 `e2e/src/config/test-tiers.ts`:
|
||||
|
||||
```typescript
|
||||
export const TEST_TIERS: Record<string, TestTierConfig> = {
|
||||
fast: {
|
||||
name: '快速层',
|
||||
description: '冒烟测试、API测试、基础功能验证',
|
||||
testMatch: /.*\.smoke\.spec\.ts$|.*\.api\.spec\.ts$/,
|
||||
timeout: 30000,
|
||||
retries: 1,
|
||||
workers: process.env.CI ? 6 : '75%',
|
||||
fullyParallel: true,
|
||||
failFast: true,
|
||||
},
|
||||
// ... 其他层级
|
||||
};
|
||||
```
|
||||
|
||||
### 添加自定义告警规则
|
||||
|
||||
编辑 `e2e/src/utils/test-monitor.ts`:
|
||||
|
||||
```typescript
|
||||
this.alertRules.push({
|
||||
name: 'custom-alert',
|
||||
condition: (m) => m.failedTests > 5 && m.tier === 'fast',
|
||||
severity: 'critical',
|
||||
message: '快速层测试失败超过5个',
|
||||
});
|
||||
```
|
||||
|
||||
### 自定义优化规则
|
||||
|
||||
编辑 `e2e/src/utils/test-optimizer.ts`:
|
||||
|
||||
```typescript
|
||||
this.rules.push({
|
||||
name: 'custom-rule',
|
||||
condition: (p) => p.duration > 90000 && p.tier === 'standard',
|
||||
suggestions: [
|
||||
'标准层测试不应超过90秒',
|
||||
'考虑拆分测试或优化执行流程',
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## 文档资源
|
||||
|
||||
- [测试优化指南](./test-optimization-guide.md) - 详细的优化策略和技巧
|
||||
- [分层测试最佳实践](./test-tiering-best-practices.md) - 完整的最佳实践指南
|
||||
- [Playwright文档](https://playwright.dev/) - Playwright官方文档
|
||||
- [Woodpecker CI文档](https://woodpecker-ci.org/docs/) - Woodpecker CI官方文档
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果遇到问题:
|
||||
|
||||
1. 查看文档资源
|
||||
2. 检查测试日志
|
||||
3. 运行性能优化工具
|
||||
4. 联系团队成员
|
||||
|
||||
## 总结
|
||||
|
||||
分层测试系统通过以下方式提高测试效率:
|
||||
|
||||
1. **快速反馈**:快速层测试在5分钟内完成
|
||||
2. **合理分配**:根据重要性分配测试资源
|
||||
3. **持续优化**:通过历史数据持续优化
|
||||
4. **自动化**:CI/CD自动执行和报告
|
||||
|
||||
开始使用分层测试,提高测试效率,缩短反馈周期!
|
||||
Reference in New Issue
Block a user