feat: 完善卡密管理模块,添加生成、激活、查询和撤销卡密功能

This commit is contained in:
lichao 2025-12-15 00:07:54 +08:00
parent 7274c58329
commit aa62486f21
32 changed files with 8501 additions and 3888 deletions

2
.env
View File

@ -1,5 +1,5 @@
JWT_SECRET=5270e53a05600cb1f19b287a915b1c792d7552cbaae261afb6dcaf988d0db10f
SERVER_HOST='192.144.32.178'
SERVER_HOST='45.130.23.71'
SERVER_PORT=3306
SERVER_USER='golc'
PASSWORD='Lichao1314!'

418
GENERATE_LICENSE_GUIDE.md Normal file
View File

@ -0,0 +1,418 @@
# 生成卡密操作指南
## 概述
现在系统已经实现了完整的管理员权限验证。管理员可以通过 API 生成卡密,普通用户无法访问生成接口。
## 管理员权限规则
系统会自动识别以下用户为管理员:
1. 用户名为 `admin`
2. 邮箱包含 `admin`(如 `admin@example.com`
3. 用户名以 `admin_` 开头(如 `admin_user`
## 方法一:注册管理员账号(推荐)
### 步骤 1: 注册管理员账号
```bash
curl -X POST http://localhost:3030/user/register \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "your_secure_password",
"email": "admin@example.com"
}'
```
**响应示例:**
```json
{
"message": "User registered successfully",
"user": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"isActive": true,
"provider": "local"
},
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
**保存 `access_token`,后续生成卡密时需要使用!**
### 步骤 2: 使用管理员账号生成卡密
#### 生成单个月卡
```bash
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "monthly",
"validDays": 30,
"count": 1,
"remarks": "测试月卡"
}'
```
#### 批量生成卡密
```bash
# 生成 10 个月卡
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "monthly",
"validDays": 30,
"count": 10,
"remarks": "批量生成月卡"
}'
```
#### 生成不同类型的卡密
```bash
# 试用卡 (7天)
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "trial",
"validDays": 7,
"count": 5,
"remarks": "试用卡"
}'
# 年卡 (365天)
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "yearly",
"validDays": 365,
"count": 2,
"remarks": "年度授权"
}'
# 终身卡
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "lifetime",
"validDays": 36500,
"count": 1,
"remarks": "VIP终身授权"
}'
```
**响应示例:**
```json
[
{
"id": 1,
"code": "ABCD-1234-EFGH-5678",
"type": "monthly",
"status": "unused",
"validDays": 30,
"remarks": "测试月卡",
"createdAt": "2025-12-13T10:00:00.000Z"
}
]
```
### 步骤 3: 将卡密分发给用户
将生成的 `code` 发送给用户,用户可以使用以下方式激活:
```bash
curl -X POST http://localhost:3030/license/activate \
-H "Authorization: Bearer USER_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"code": "ABCD-1234-EFGH-5678"
}'
```
## 方法二:使用 Swagger UI
### 步骤 1: 访问 Swagger
打开浏览器访问http://localhost:3030/api
### 步骤 2: 注册管理员账号
1. 找到 `POST /user/register` 接口
2. 点击 "Try it out"
3. 输入管理员信息:
```json
{
"username": "admin",
"password": "admin123",
"email": "admin@example.com"
}
```
4. 点击 "Execute"
5. 复制响应中的 `access_token`
### 步骤 3: 授权 Swagger
1. 点击页面右上角的 "Authorize" 按钮
2. 在弹出框中输入:`Bearer YOUR_ACCESS_TOKEN`
3. 点击 "Authorize"
4. 点击 "Close"
### 步骤 4: 生成卡密
1. 找到 `POST /license/generate` 接口
2. 点击 "Try it out"
3. 输入生成参数:
```json
{
"type": "monthly",
"validDays": 30,
"count": 1,
"remarks": "测试卡密"
}
```
4. 点击 "Execute"
5. 在响应中复制生成的卡密码
### 步骤 5: 查看已生成的卡密
1. 找到 `GET /license` 接口
2. 点击 "Try it out"
3. 可选:筛选条件
- status: unused (查看未使用的)
- type: monthly (查看月卡)
4. 点击 "Execute"
5. 查看所有卡密列表
## 方法三:直接插入数据库(临时方案)
如果还没有管理员账号,可以直接在数据库中插入卡密:
```sql
-- 插入测试卡密
INSERT INTO `license` (
`code`,
`type`,
`status`,
`validDays`,
`remarks`,
`createdAt`,
`updatedAt`
) VALUES
('ABCD-1234-EFGH-5678', 'monthly', 'unused', 30, '测试月卡', NOW(), NOW()),
('WXYZ-9876-IJKL-5432', 'yearly', 'unused', 365, '测试年卡', NOW(), NOW()),
('TEST-0000-FREE-TRIAL', 'trial', 'unused', 7, '试用卡', NOW(), NOW());
```
**注意:** 卡密格式必须是 `XXXX-XXXX-XXXX-XXXX`19个字符包含3个连字符
## 管理员其他操作
### 查看卡密统计
```bash
curl -X GET http://localhost:3030/license/statistics \
-H "Authorization: Bearer ADMIN_TOKEN"
```
**响应示例:**
```json
{
"total": 100,
"unused": 50,
"active": 30,
"expired": 15,
"revoked": 5
}
```
### 查询所有卡密
```bash
# 查询所有卡密
curl -X GET http://localhost:3030/license \
-H "Authorization: Bearer ADMIN_TOKEN"
# 查询未使用的卡密
curl -X GET "http://localhost:3030/license?status=unused" \
-H "Authorization: Bearer ADMIN_TOKEN"
# 查询已激活的月卡
curl -X GET "http://localhost:3030/license?status=active&type=monthly" \
-H "Authorization: Bearer ADMIN_TOKEN"
```
### 查询单个卡密详情
```bash
curl -X GET http://localhost:3030/license/1 \
-H "Authorization: Bearer ADMIN_TOKEN"
```
### 撤销卡密
```bash
curl -X POST http://localhost:3030/license/1/revoke \
-H "Authorization: Bearer ADMIN_TOKEN"
```
### 删除卡密
```bash
curl -X DELETE http://localhost:3030/license/1 \
-H "Authorization: Bearer ADMIN_TOKEN"
```
## 常见问题
### Q: 非管理员用户能生成卡密吗?
A: 不能。系统会返回 403 错误:
```json
{
"statusCode": 403,
"message": "需要管理员权限",
"error": "Forbidden"
}
```
### Q: 如何修改管理员权限规则?
A: 编辑 [src/common/services/admin.service.ts](src/common/services/admin.service.ts:23-30),修改 `isAdmin` 方法中的判断逻辑。
### Q: 可以有多个管理员吗?
A: 可以。所有满足管理员规则的用户都拥有管理员权限。
### Q: 生成的卡密可以重复使用吗?
A: 不可以。每个卡密只能激活一次,激活后状态变为 `active`
### Q: 如何批量生成大量卡密?
A: 使用 `count` 参数,建议每次不超过 100 个:
```json
{
"type": "monthly",
"count": 100
}
```
## 完整示例流程
### 场景销售100个月卡
```bash
# 1. 使用管理员账号登录
curl -X POST http://localhost:3030/user/login \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "admin123"
}'
# 获取 access_token
# 2. 批量生成 100 个月卡
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "monthly",
"validDays": 30,
"count": 100,
"remarks": "2025年1月批次"
}'
# 3. 查询已生成的卡密
curl -X GET "http://localhost:3030/license?status=unused&type=monthly" \
-H "Authorization: Bearer ADMIN_TOKEN"
# 4. 导出卡密可以通过数据库查询或API获取
# SQL 查询未使用的卡密
# SELECT code FROM license WHERE status = 'unused' AND type = 'monthly'
# 5. 将卡密分发给客户
# 6. 监控卡密使用情况
curl -X GET http://localhost:3030/license/statistics \
-H "Authorization: Bearer ADMIN_TOKEN"
```
## 数据库直接查询卡密
### 查询所有未使用的卡密
```sql
SELECT
id, code, type, validDays, remarks, createdAt
FROM license
WHERE status = 'unused'
ORDER BY createdAt DESC;
```
### 导出卡密列表(用于分发)
```sql
SELECT
code as '卡密码',
CASE type
WHEN 'trial' THEN '试用版'
WHEN 'monthly' THEN '月卡'
WHEN 'yearly' THEN '年卡'
WHEN 'lifetime' THEN '终身版'
END as '类型',
validDays as '有效天数',
remarks as '备注'
FROM license
WHERE status = 'unused'
ORDER BY type, createdAt DESC;
```
### 统计卡密使用情况
```sql
SELECT
type as '类型',
COUNT(*) as '总数',
SUM(CASE WHEN status = 'unused' THEN 1 ELSE 0 END) as '未使用',
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as '已激活',
SUM(CASE WHEN status = 'expired' THEN 1 ELSE 0 END) as '已过期',
SUM(CASE WHEN status = 'revoked' THEN 1 ELSE 0 END) as '已撤销'
FROM license
GROUP BY type;
```
## 安全建议
1. **保护管理员账号**
- 使用强密码
- 定期更换密码
- 不要分享管理员凭证
2. **卡密分发**
- 通过安全渠道发送(邮件、加密通讯)
- 记录卡密分发情况
- 及时撤销未使用的卡密
3. **监控使用**
- 定期查看统计信息
- 检查异常激活
- 审计管理员操作日志
## 下一步
完成卡密生成后,可以:
1. 将卡密分发给用户
2. 用户使用 `POST /license/activate` 激活
3. 激活后用户可以使用任务功能
4. 管理员可以通过统计接口监控使用情况
## 相关文档
- [LICENSE_SYSTEM.md](LICENSE_SYSTEM.md) - 卡密系统详细说明
- [QUICKSTART.md](QUICKSTART.md) - 快速开始指南
- [Swagger API](http://localhost:3030/api) - 在线 API 文档

82
HOW_TO_GENERATE.md Normal file
View File

@ -0,0 +1,82 @@
# 如何生成卡密 - 快速指南
## 🎯 三种生成方式
### 方式一:使用自动化脚本(最简单)⭐
```bash
bash generate-licenses.sh
```
脚本会自动引导你完成:
1. 注册/登录管理员账号
2. 选择卡密类型
3. 输入生成数量
4. 自动生成并显示卡密
5. 可选保存到文件
### 方式二:使用 Swagger UI可视化
1. 启动项目后访问http://localhost:3030/api
2. 注册管理员账号 `POST /user/register`
```json
{
"username": "admin",
"password": "admin123",
"email": "admin@example.com"
}
```
3. 点击右上角 "Authorize",输入 Token
4. 调用 `POST /license/generate` 生成卡密
```json
{
"type": "monthly",
"validDays": 30,
"count": 10
}
```
### 方式三:使用 curl 命令
```bash
# 1. 注册管理员
curl -X POST http://localhost:3030/user/register \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123","email":"admin@example.com"}'
# 2. 使用返回的 token 生成卡密
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"monthly","validDays":30,"count":10}'
```
## 📋 管理员权限规则
系统自动识别以下用户为管理员:
- 用户名为 `admin`
- 邮箱包含 `admin`
- 用户名以 `admin_` 开头
## 📚 详细文档
- [GENERATE_LICENSE_GUIDE.md](GENERATE_LICENSE_GUIDE.md) - 完整生成指南
- [LICENSE_SYSTEM.md](LICENSE_SYSTEM.md) - 卡密系统说明
- [QUICKSTART.md](QUICKSTART.md) - 快速开始
## 🚀 快速开始
```bash
# 1. 启动项目
npm run start:dev
# 2. 运行生成脚本
bash generate-licenses.sh
# 3. 按照提示操作即可
```
就这么简单!🎉

355
LICENSE_IMPLEMENTATION.md Normal file
View File

@ -0,0 +1,355 @@
# 卡密商业化系统实现总结
## 项目概述
成功为 Task 任务管理模块添加了完整的卡密商业化授权系统。用户需要激活有效的卡密才能使用任务管理功能。
## 实现的功能
### 1. 核心功能
- ✅ 卡密生成(批量、唯一性保证)
- ✅ 卡密激活(自动验证、防重复)
- ✅ 授权延期(多卡密叠加)
- ✅ 授权验证(守卫拦截)
- ✅ 授权查询(剩余天数)
- ✅ 卡密撤销
- ✅ 统计分析
### 2. 卡密类型
- 试用版 (7天)
- 月度订阅 (30天)
- 年度订阅 (365天)
- 终身授权 (100年)
### 3. 安全机制
- 卡密格式: `XXXX-XXXX-XXXX-XXXX`
- 随机生成算法(去除易混淆字符)
- 全局唯一性检查
- 状态机管理unused → active → expired/revoked
- 守卫层面统一拦截
## 文件结构
```
src/
├── license/
│ ├── entities/
│ │ └── license.entity.ts # 卡密实体
│ ├── dto/
│ │ ├── generate-license.dto.ts # 生成卡密 DTO
│ │ ├── activate-license.dto.ts # 激活卡密 DTO
│ │ └── query-license.dto.ts # 查询卡密 DTO
│ ├── decorators/
│ │ └── require-license.decorator.ts # 授权装饰器
│ ├── guards/
│ │ └── license.guard.ts # 授权守卫
│ ├── license.service.ts # 卡密服务
│ ├── license.controller.ts # 卡密控制器
│ └── license.module.ts # 卡密模块
├── task/
│ └── task.controller.ts # 已添加卡密验证
├── common/
│ └── dto/
│ └── error-response.dto.ts # 统一错误响应
└── app.module.ts # 注册 License Module
```
## API 接口
### 用户接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/license/activate` | POST | 激活卡密 |
| `/license/my` | GET | 查询我的授权 |
| `/license/my/history` | GET | 查询卡密历史 |
### 管理员接口
| 接口 | 方法 | 说明 |
|------|------|------|
| `/license/generate` | POST | 生成卡密 |
| `/license` | GET | 查询所有卡密 |
| `/license/statistics` | GET | 获取统计信息 |
| `/license/:id` | GET | 查询单个卡密 |
| `/license/:id/revoke` | POST | 撤销卡密 |
| `/license/:id` | DELETE | 删除卡密 |
### 受保护的接口
所有 `/task/*` 接口都需要有效授权:
- `POST /task` - 创建任务
- `GET /task` - 获取任务列表
- `GET /task/:id` - 获取单个任务
- `PATCH /task/:id` - 更新任务
- `PATCH /task/reorder` - 重新排序
- `DELETE /task/:id` - 删除任务
## 数据库变更
### 新增表: license
```sql
CREATE TABLE `license` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`code` VARCHAR(32) UNIQUE NOT NULL,
`type` ENUM('trial', 'monthly', 'yearly', 'lifetime') DEFAULT 'monthly',
`status` ENUM('unused', 'active', 'expired', 'revoked') DEFAULT 'unused',
`validDays` INT DEFAULT 30,
`activatedAt` DATETIME NULL,
`expiresAt` DATETIME NULL,
`userId` INT NULL,
`remarks` TEXT NULL,
`createdAt` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updatedAt` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`)
);
```
## 使用流程
### 管理员生成卡密
```bash
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer {admin_token}" \
-H "Content-Type: application/json" \
-d '{
"type": "monthly",
"validDays": 30,
"count": 10,
"remarks": "批量生成月卡"
}'
```
### 用户激活卡密
```bash
curl -X POST http://localhost:3030/license/activate \
-H "Authorization: Bearer {user_token}" \
-H "Content-Type: application/json" \
-d '{
"code": "ABCD-1234-EFGH-5678"
}'
```
### 用户查询授权
```bash
curl -X GET http://localhost:3030/license/my \
-H "Authorization: Bearer {user_token}"
```
### 使用任务功能
```bash
curl -X POST http://localhost:3030/task \
-H "Authorization: Bearer {user_token}" \
-H "Content-Type: application/json" \
-d '{
"title": "完成项目文档",
"description": "编写 API 文档",
"priority": "high"
}'
```
## 测试
### 1. 运行测试脚本
```bash
bash test-license-system.sh
```
### 2. 手动测试步骤
1. 启动项目: `npm run start:dev`
2. 访问 Swagger: `http://localhost:3030/api`
3. 注册用户
4. 生成卡密(管理员)
5. 激活卡密
6. 测试任务接口
### 3. 测试场景
- ✅ 未授权访问任务接口 → 返回 403
- ✅ 激活卡密后访问 → 正常使用
- ✅ 重复激活同一卡密 → 返回 409
- ✅ 激活第二个卡密 → 自动延期
- ✅ 授权过期后访问 → 返回 403
- ✅ 查询剩余天数 → 正确显示
- ✅ 撤销卡密 → 无法再使用
## Swagger 文档
所有接口都已添加完整的 Swagger 注释,包括:
- 接口描述
- 请求参数说明
- 响应示例
- 错误码说明
访问地址: `http://localhost:3030/api`
## 技术亮点
### 1. 安全性
- 卡密随机生成,去除易混淆字符
- 全局唯一性保证
- 状态机防止重复激活
- 守卫层面统一验证
### 2. 用户体验
- 支持授权延期(多卡密叠加)
- 清晰的错误提示
- 剩余天数实时查询
- 授权历史记录
### 3. 可维护性
- 模块化设计
- 装饰器 + 守卫模式
- 完整的 TypeScript 类型
- 详细的代码注释
### 4. 可扩展性
- 支持多种卡密类型
- 可配置有效天数
- 易于添加新的授权模块
- 支持批量生成
## 后续优化建议
### 功能扩展
1. 支持不同模块的独立授权
2. 添加授权转让功能
3. 实现自动续费机制
4. 添加授权即将过期提醒
5. 支持授权使用日志
### 性能优化
1. 卡密验证结果缓存Redis
2. 批量生成优化
3. 数据库索引优化
4. 添加分页查询
### 安全增强
1. 添加 IP 白名单限制
2. 实现设备绑定
3. 添加异常登录检测
4. 卡密使用频率限制
## 前端集成建议
### 1. 授权状态管理
```typescript
// 存储授权信息
interface LicenseState {
hasValidLicense: boolean;
remainingDays: number;
expiresAt: string;
type: string;
}
// 在应用启动时查询
const checkLicense = async () => {
const response = await api.get('/license/my');
return response.data;
};
```
### 2. 路由守卫
```typescript
// 需要授权的路由添加守卫
const ProtectedRoute = ({ children }) => {
const { hasValidLicense } = useLicense();
if (!hasValidLicense) {
return <Navigate to="/activate" />;
}
return children;
};
```
### 3. 激活页面
```typescript
const ActivatePage = () => {
const [code, setCode] = useState('');
const handleActivate = async () => {
try {
const response = await api.post('/license/activate', { code });
toast.success(response.data.message);
navigate('/tasks');
} catch (error) {
toast.error(error.response.data.message);
}
};
return (
<div>
<input
value={code}
onChange={e => setCode(e.target.value)}
placeholder="输入卡密: XXXX-XXXX-XXXX-XXXX"
/>
<button onClick={handleActivate}>激活</button>
</div>
);
};
```
### 4. 授权状态显示
```typescript
const LicenseStatus = () => {
const { license, remainingDays } = useLicense();
return (
<div>
<p>授权类型: {license.type}</p>
<p>剩余天数: {remainingDays} 天</p>
<p>到期时间: {new Date(license.expiresAt).toLocaleDateString()}</p>
</div>
);
};
```
## 常见问题
### Q: 如何生成第一个管理员卡密?
A: 可以直接在数据库中插入一条记录,或者临时修改代码去除权限验证。
### Q: 卡密格式有什么要求?
A: 必须是 `XXXX-XXXX-XXXX-XXXX` 格式共19个字符包含连字符
### Q: 用户激活多个卡密会怎样?
A: 如果已有有效授权,新卡密的时间会在原有基础上累加延期。
### Q: 授权过期后数据会丢失吗?
A: 不会,数据仍然保留,只是无法访问。重新激活后可以继续使用。
### Q: 如何撤销已发放的卡密?
A: 使用管理员接口 `POST /license/:id/revoke` 撤销指定卡密。
## 相关文档
- [LICENSE_SYSTEM.md](LICENSE_SYSTEM.md) - 详细使用说明
- [Swagger API 文档](http://localhost:3030/api) - 在线接口文档
- `test-license-system.sh` - 测试脚本
## 总结
本次实现完成了一个功能完整、安全可靠的卡密商业化授权系统。系统具有以下特点:
1. **功能完整**: 涵盖生成、激活、验证、查询、统计等全流程
2. **安全可靠**: 多层验证,状态机管理,防止滥用
3. **易于使用**: 清晰的 API 设计,完整的文档和测试
4. **易于扩展**: 模块化设计,支持多种授权类型和场景
系统已经可以直接投入使用,后续可根据实际需求进行功能扩展和优化。

329
LICENSE_SYSTEM.md Normal file
View File

@ -0,0 +1,329 @@
# 卡密系统使用说明
## 概述
本项目实现了完整的卡密商业化授权系统用户需要激活有效的卡密才能使用任务管理Task模块的功能。
## 功能特性
### 1. 卡密类型
- **试用版 (trial)**: 7天有效期
- **月度订阅 (monthly)**: 30天有效期
- **年度订阅 (yearly)**: 365天有效期
- **终身授权 (lifetime)**: 100年有效期
### 2. 卡密状态
- **未使用 (unused)**: 卡密已生成但未被激活
- **已激活 (active)**: 卡密正在使用中
- **已过期 (expired)**: 卡密有效期已过
- **已撤销 (revoked)**: 卡密被管理员撤销
### 3. 卡密格式
卡密采用 `XXXX-XXXX-XXXX-XXXX` 格式,例如:`ABCD-1234-EFGH-5678`
## API 接口
### 用户接口
#### 1. 激活卡密
```http
POST /license/activate
Authorization: Bearer {access_token}
Content-Type: application/json
{
"code": "ABCD-1234-EFGH-5678"
}
```
**响应示例:**
```json
{
"message": "卡密激活成功,有效期至 2026-01-13 10:00:00",
"license": {
"id": 1,
"code": "ABCD-1234-EFGH-5678",
"type": "monthly",
"status": "active",
"validDays": 30,
"activatedAt": "2025-12-13T10:00:00.000Z",
"expiresAt": "2026-01-13T10:00:00.000Z",
"userId": 1
}
}
```
**特殊说明:**
- 如果用户已有有效授权,新卡密的时间会在原有基础上累加延期
- 已激活、已过期或已撤销的卡密无法重复使用
#### 2. 查询我的授权信息
```http
GET /license/my
Authorization: Bearer {access_token}
```
**响应示例:**
```json
{
"hasValidLicense": true,
"license": {
"id": 1,
"code": "ABCD-1234-EFGH-5678",
"type": "monthly",
"status": "active",
"validDays": 30,
"activatedAt": "2025-12-13T10:00:00.000Z",
"expiresAt": "2026-01-13T10:00:00.000Z",
"remainingDays": 31
}
}
```
#### 3. 查询我的卡密历史
```http
GET /license/my/history
Authorization: Bearer {access_token}
```
### 管理员接口
#### 1. 生成卡密
```http
POST /license/generate
Authorization: Bearer {admin_token}
Content-Type: application/json
{
"type": "monthly",
"validDays": 30,
"count": 10,
"remarks": "批量生成月卡"
}
```
**响应示例:**
```json
[
{
"id": 1,
"code": "ABCD-1234-EFGH-5678",
"type": "monthly",
"status": "unused",
"validDays": 30,
"remarks": "批量生成月卡",
"createdAt": "2025-12-13T10:00:00.000Z"
},
...
]
```
#### 2. 查询所有卡密
```http
GET /license?status=active&type=monthly
Authorization: Bearer {admin_token}
```
#### 3. 获取统计信息
```http
GET /license/statistics
Authorization: Bearer {admin_token}
```
**响应示例:**
```json
{
"total": 100,
"unused": 50,
"active": 30,
"expired": 15,
"revoked": 5
}
```
#### 4. 撤销卡密
```http
POST /license/:id/revoke
Authorization: Bearer {admin_token}
```
#### 5. 删除卡密
```http
DELETE /license/:id
Authorization: Bearer {admin_token}
```
## 使用流程
### 用户激活流程
1. **用户注册/登录**
```http
POST /user/register 或 POST /user/login
```
2. **获取卡密**
- 从管理员处获取卡密码
3. **激活卡密**
```http
POST /license/activate
{
"code": "获取到的卡密"
}
```
4. **开始使用任务功能**
- 激活成功后,所有 Task 相关接口均可正常使用
- 未激活或授权过期时Task 接口会返回 403 错误
### 管理员操作流程
1. **生成卡密**
```bash
# 生成 10 个月卡
POST /license/generate
{
"type": "monthly",
"count": 10
}
```
2. **查看卡密列表**
```bash
GET /license
```
3. **分发卡密给用户**
- 将生成的卡密码发给用户
4. **监控卡密使用情况**
```bash
GET /license/statistics
```
## 任务模块授权保护
所有任务相关接口都需要有效的卡密授权:
- `POST /task` - 创建任务
- `GET /task` - 获取任务列表
- `GET /task/:id` - 获取单个任务
- `PATCH /task/:id` - 更新任务
- `PATCH /task/reorder` - 重新排序
- `DELETE /task/:id` - 删除任务
**未授权时的响应:**
```json
{
"statusCode": 403,
"message": "您的授权已过期或未激活,请联系管理员获取卡密",
"error": "Forbidden"
}
```
## 数据库表结构
### license 表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | INT | 主键 |
| code | VARCHAR(32) | 卡密码(唯一) |
| type | ENUM | 卡密类型 |
| status | ENUM | 卡密状态 |
| validDays | INT | 有效天数 |
| activatedAt | DATETIME | 激活时间 |
| expiresAt | DATETIME | 过期时间 |
| userId | INT | 激活用户ID外键 |
| remarks | TEXT | 备注信息 |
| createdAt | DATETIME | 创建时间 |
| updatedAt | DATETIME | 更新时间 |
## 技术实现
### 核心组件
1. **LicenseService** - 卡密业务逻辑
- 生成卡密(确保唯一性)
- 验证卡密
- 激活卡密(支持延期)
- 查询统计
2. **LicenseGuard** - 授权守卫
- 拦截需要授权的请求
- 验证用户授权状态
- 返回 403 错误(未授权)
3. **@RequireLicense** - 装饰器
- 标记需要卡密授权的 Controller/Method
### 代码示例
在需要授权保护的 Controller 上添加:
```typescript
import { UseGuards } from '@nestjs/common';
import { RequireLicense } from '../license/decorators/require-license.decorator';
import { LicenseGuard } from '../license/guards/license.guard';
@Controller('task')
@UseGuards(LicenseGuard)
@RequireLicense()
export class TaskController {
// ... 所有接口都需要有效授权
}
```
## 注意事项
1. **安全性**
- 卡密使用随机生成算法,去除易混淆字符
- 每个卡密全局唯一
- 卡密验证在守卫层面统一拦截
2. **用户体验**
- 支持授权延期(多个卡密叠加)
- 提供剩余天数查询
- 清晰的错误提示
3. **管理维护**
- 支持卡密撤销(无需删除记录)
- 提供统计接口
- 支持按状态和类型筛选
## Swagger 文档
启动项目后访问:`http://localhost:3030/api`
在 Swagger 文档中可以查看所有卡密相关接口的详细说明和测试。
## 测试建议
1. **测试用户激活流程**
```bash
# 1. 注册用户
# 2. 生成测试卡密
# 3. 激活卡密
# 4. 访问任务接口
```
2. **测试授权过期**
- 修改数据库中的 expiresAt 为过去时间
- 访问任务接口应返回 403
3. **测试延期功能**
- 激活第一个卡密
- 再激活第二个卡密
- 验证有效期是否累加
## 未来扩展
1. 支持不同模块的独立授权
2. 支持授权转让
3. 添加卡密使用日志
4. 支持自动续费
5. 添加授权即将过期提醒

303
QUICKSTART.md Normal file
View File

@ -0,0 +1,303 @@
# 快速开始 - 卡密系统
## 前置要求
- Node.js 16+
- MySQL 5.7+
- 已配置好的数据库连接
## 安装步骤
### 1. 安装依赖
```bash
npm install
# 或
pnpm install
```
### 2. 启动项目
```bash
npm run start:dev
```
### 3. 访问 Swagger 文档
打开浏览器访问: http://localhost:3030/api
## 快速测试
### 方式一: 使用测试脚本
```bash
# 1. 运行测试脚本
bash test-license-system.sh
# 2. 按照提示输入卡密
```
### 方式二: 使用 Swagger UI
#### Step 1: 注册用户
1. 找到 `POST /user/register` 接口
2. 点击 "Try it out"
3. 输入用户信息:
```json
{
"username": "testuser",
"password": "password123",
"email": "test@example.com"
}
```
4. 点击 "Execute"
5. 复制返回的 `access_token`
#### Step 2: 生成卡密(需要管理员权限)
**临时方案 - 直接插入数据库:**
```sql
INSERT INTO `license` (`code`, `type`, `status`, `validDays`, `remarks`)
VALUES
('ABCD-1234-EFGH-5678', 'monthly', 'unused', 30, '测试卡密'),
('WXYZ-9876-IJKL-5432', 'yearly', 'unused', 365, '年度测试卡');
```
**或使用 API需要管理员 Token:**
1. 找到 `POST /license/generate` 接口
2. 点击右上角 "Authorize",输入 access_token
3. 输入生成参数:
```json
{
"type": "monthly",
"validDays": 30,
"count": 1,
"remarks": "测试卡密"
}
```
4. 复制生成的卡密码
#### Step 3: 激活卡密
1. 找到 `POST /license/activate` 接口
2. 点击 "Authorize",输入你的 access_token
3. 输入卡密:
```json
{
"code": "ABCD-1234-EFGH-5678"
}
```
4. 点击 "Execute"
5. 验证激活成功
#### Step 4: 查询授权状态
1. 找到 `GET /license/my` 接口
2. 点击 "Try it out"
3. 点击 "Execute"
4. 查看授权信息和剩余天数
#### Step 5: 测试任务功能
1. 找到 `POST /task` 接口
2. 输入任务信息:
```json
{
"title": "测试任务",
"description": "这是一个测试任务",
"priority": "high"
}
```
3. 点击 "Execute"
4. 应该创建成功(之前会返回 403
## 常用 curl 命令
### 注册用户
```bash
curl -X POST http://localhost:3030/user/register \
-H "Content-Type: application/json" \
-d '{
"username": "testuser",
"password": "password123",
"email": "test@example.com"
}'
```
### 激活卡密
```bash
curl -X POST http://localhost:3030/license/activate \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"code": "ABCD-1234-EFGH-5678"
}'
```
### 查询授权
```bash
curl -X GET http://localhost:3030/license/my \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
### 创建任务(需要有效授权)
```bash
curl -X POST http://localhost:3030/task \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "完成项目文档",
"priority": "high"
}'
```
## 验证授权保护
### 测试未授权访问(应该返回 403
```bash
# 使用未激活卡密的用户 Token
curl -X GET http://localhost:3030/task \
-H "Authorization: Bearer UNAUTHORIZED_TOKEN"
# 预期响应:
# {
# "statusCode": 403,
# "message": "您的授权已过期或未激活,请联系管理员获取卡密",
# "error": "Forbidden"
# }
```
### 测试授权后访问(应该成功)
```bash
# 使用已激活卡密的用户 Token
curl -X GET http://localhost:3030/task \
-H "Authorization: Bearer AUTHORIZED_TOKEN"
# 预期响应: 任务列表数组
```
## 管理员操作
### 生成批量卡密
```bash
curl -X POST http://localhost:3030/license/generate \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "monthly",
"validDays": 30,
"count": 10,
"remarks": "批量生成月卡"
}'
```
### 查看统计信息
```bash
curl -X GET http://localhost:3030/license/statistics \
-H "Authorization: Bearer ADMIN_TOKEN"
```
### 查询所有卡密
```bash
# 查询所有未使用的月卡
curl -X GET "http://localhost:3030/license?status=unused&type=monthly" \
-H "Authorization: Bearer ADMIN_TOKEN"
```
### 撤销卡密
```bash
curl -X POST http://localhost:3030/license/1/revoke \
-H "Authorization: Bearer ADMIN_TOKEN"
```
## 数据库快速查看
### 查看所有卡密
```sql
SELECT
id, code, type, status, validDays,
activatedAt, expiresAt, userId, remarks
FROM license
ORDER BY createdAt DESC;
```
### 查看有效授权
```sql
SELECT
l.code, l.type, l.status,
u.username, u.email,
l.activatedAt, l.expiresAt,
DATEDIFF(l.expiresAt, NOW()) as remainingDays
FROM license l
LEFT JOIN user u ON l.userId = u.id
WHERE l.status = 'active' AND l.expiresAt > NOW()
ORDER BY l.expiresAt;
```
### 查看即将过期的授权7天内
```sql
SELECT
l.code, u.username, u.email,
l.expiresAt,
DATEDIFF(l.expiresAt, NOW()) as remainingDays
FROM license l
LEFT JOIN user u ON l.userId = u.id
WHERE
l.status = 'active'
AND l.expiresAt > NOW()
AND l.expiresAt < DATE_ADD(NOW(), INTERVAL 7 DAY)
ORDER BY l.expiresAt;
```
## 故障排查
### 问题: 激活卡密时返回 403
**原因**: 未登录或 Token 过期
**解决**: 重新登录获取新的 access_token
### 问题: 激活卡密时返回 409
**原因**: 卡密已被激活
**解决**: 使用其他未激活的卡密
### 问题: 任务接口返回 403
**原因**: 授权已过期或未激活
**解决**: 激活新的卡密或检查授权状态
### 问题: 生成卡密返回 401
**原因**: 需要管理员权限
**解决**: 使用管理员账号或临时从数据库插入
## 下一步
1. 根据实际业务需求调整卡密类型和有效期
2. 实现管理员权限验证机制
3. 添加前端授权管理页面
4. 配置授权即将过期的邮件提醒
5. 实现授权使用统计和分析
## 相关文档
- [LICENSE_SYSTEM.md](LICENSE_SYSTEM.md) - 详细功能说明
- [LICENSE_IMPLEMENTATION.md](LICENSE_IMPLEMENTATION.md) - 实现总结
- [Swagger API](http://localhost:3030/api) - 在线 API 文档
## 技术支持
如有问题,请查看:
1. Swagger 文档中的接口说明
2. LICENSE_SYSTEM.md 中的详细文档
3. 数据库中的卡密状态
祝使用愉快!

179
generate-licenses.sh Executable file
View File

@ -0,0 +1,179 @@
#!/bin/bash
# 卡密生成快速脚本
# 使用方法: bash generate-licenses.sh
BASE_URL="http://localhost:3030"
# 颜色定义
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
echo -e "${GREEN}====== 卡密生成工具 ======${NC}"
echo ""
# 检查是否已有管理员账号
read -p "是否已有管理员账号?(y/n): " has_admin
if [ "$has_admin" != "y" ]; then
echo -e "${YELLOW}步骤 1: 注册管理员账号${NC}"
read -p "请输入管理员用户名 (默认: admin): " admin_username
admin_username=${admin_username:-admin}
read -sp "请输入管理员密码: " admin_password
echo ""
read -p "请输入管理员邮箱 (默认: admin@example.com): " admin_email
admin_email=${admin_email:-admin@example.com}
echo -e "${YELLOW}正在注册管理员账号...${NC}"
REGISTER_RESPONSE=$(curl -s -X POST "$BASE_URL/user/register" \
-H "Content-Type: application/json" \
-d "{
\"username\": \"$admin_username\",
\"password\": \"$admin_password\",
\"email\": \"$admin_email\"
}")
ADMIN_TOKEN=$(echo $REGISTER_RESPONSE | jq -r '.access_token')
if [ "$ADMIN_TOKEN" = "null" ] || [ -z "$ADMIN_TOKEN" ]; then
echo -e "${RED}注册失败!${NC}"
echo "$REGISTER_RESPONSE" | jq '.'
exit 1
fi
echo -e "${GREEN}管理员账号注册成功!${NC}"
echo "管理员 Token: $ADMIN_TOKEN"
echo ""
else
echo -e "${YELLOW}步骤 1: 管理员登录${NC}"
read -p "请输入管理员用户名: " admin_username
read -sp "请输入管理员密码: " admin_password
echo ""
echo -e "${YELLOW}正在登录...${NC}"
LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/user/login" \
-H "Content-Type: application/json" \
-d "{
\"username\": \"$admin_username\",
\"password\": \"$admin_password\"
}")
ADMIN_TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.access_token')
if [ "$ADMIN_TOKEN" = "null" ] || [ -z "$ADMIN_TOKEN" ]; then
echo -e "${RED}登录失败!请检查用户名和密码${NC}"
echo "$LOGIN_RESPONSE" | jq '.'
exit 1
fi
echo -e "${GREEN}登录成功!${NC}"
echo ""
fi
# 生成卡密
echo -e "${YELLOW}步骤 2: 选择要生成的卡密类型${NC}"
echo "1. 试用卡 (7天)"
echo "2. 月卡 (30天)"
echo "3. 年卡 (365天)"
echo "4. 终身卡"
read -p "请选择 (1-4): " license_choice
case $license_choice in
1)
LICENSE_TYPE="trial"
VALID_DAYS=7
;;
2)
LICENSE_TYPE="monthly"
VALID_DAYS=30
;;
3)
LICENSE_TYPE="yearly"
VALID_DAYS=365
;;
4)
LICENSE_TYPE="lifetime"
VALID_DAYS=36500
;;
*)
echo -e "${RED}无效的选择${NC}"
exit 1
;;
esac
read -p "请输入生成数量 (默认: 1): " count
count=${count:-1}
read -p "请输入备注信息 (可选): " remarks
echo ""
echo -e "${YELLOW}正在生成 $count$LICENSE_TYPE 卡密...${NC}"
GENERATE_RESPONSE=$(curl -s -X POST "$BASE_URL/license/generate" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"$LICENSE_TYPE\",
\"validDays\": $VALID_DAYS,
\"count\": $count,
\"remarks\": \"$remarks\"
}")
# 检查是否成功
if echo "$GENERATE_RESPONSE" | jq -e '.[0].code' > /dev/null 2>&1; then
echo -e "${GREEN}卡密生成成功!${NC}"
echo ""
echo "生成的卡密:"
echo "$GENERATE_RESPONSE" | jq -r '.[] | " \(.code) - \(.type) (\(.validDays)天) - \(.remarks // "无备注")"'
echo ""
# 询问是否保存到文件
read -p "是否将卡密保存到文件?(y/n): " save_to_file
if [ "$save_to_file" = "y" ]; then
FILENAME="licenses_$(date +%Y%m%d_%H%M%S).txt"
echo "生成时间: $(date)" > "$FILENAME"
echo "类型: $LICENSE_TYPE" >> "$FILENAME"
echo "有效天数: $VALID_DAYS" >> "$FILENAME"
echo "数量: $count" >> "$FILENAME"
echo "备注: $remarks" >> "$FILENAME"
echo "" >> "$FILENAME"
echo "卡密列表:" >> "$FILENAME"
echo "$GENERATE_RESPONSE" | jq -r '.[] | .code' >> "$FILENAME"
echo -e "${GREEN}卡密已保存到: $FILENAME${NC}"
fi
# 显示统计信息
echo ""
read -p "是否查看卡密统计信息?(y/n): " show_stats
if [ "$show_stats" = "y" ]; then
echo -e "${YELLOW}查询统计信息...${NC}"
STATS=$(curl -s -X GET "$BASE_URL/license/statistics" \
-H "Authorization: Bearer $ADMIN_TOKEN")
echo "$STATS" | jq '.'
fi
else
echo -e "${RED}生成失败!${NC}"
echo "$GENERATE_RESPONSE" | jq '.'
exit 1
fi
echo ""
echo -e "${GREEN}====== 操作完成 ======${NC}"
echo ""
echo "提示:"
echo "1. 管理员 Token 已保存(有效期内可继续使用)"
echo "2. 可以将卡密分发给用户进行激活"
echo "3. 用户激活接口: POST /license/activate"
echo "4. 查看所有卡密: GET /license"
echo ""

View File

@ -93,5 +93,6 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,25 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
@ApiTags('根路由')
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@ApiOperation({
summary: '健康检查',
description: '返回应用程序的基本欢迎信息,用于验证服务是否正常运行',
})
@ApiResponse({
status: 200,
description: '服务运行正常',
schema: {
type: 'string',
example: 'Hello World!',
},
})
getHello(): string {
return this.appService.getHello();
}

View File

@ -10,6 +10,8 @@ import { LarkModule } from './lark/lark.module';
import { GitHubModule } from './github/github.module';
import { DeepseekModule } from './deepseek/deepseek.module';
import { TaskModule } from './task/task.module';
import { LicenseModule } from './license/license.module';
import { CommonModule } from './common/common.module';
@Module({
imports: [
@ -32,12 +34,14 @@ import { TaskModule } from './task/task.module';
}),
inject: [ConfigService],
}),
CommonModule,
UserModule,
AuthModule,
LarkModule,
GitHubModule,
DeepseekModule,
TaskModule,
LicenseModule,
],
controllers: [AppController],
providers: [AppService],

View File

@ -2,26 +2,68 @@ import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { UserService } from '../user/user.service';
import { Response } from 'express';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiResponse,
} from '@nestjs/swagger';
import { ErrorResponseDto } from '../common/dto/error-response.dto';
@ApiTags('认证模块')
@Controller('auth')
export class AuthController {
constructor(private readonly userService: UserService) {}
@Get('github')
@UseGuards(AuthGuard('github'))
@ApiOperation({ summary: 'Github 登录' })
@ApiResponse({ status: 302, description: '重定向到 Github 授权页面' })
@ApiOperation({
summary: 'GitHub 登录',
description:
'启动 GitHub OAuth 登录流程,重定向用户到 GitHub 授权页面进行身份验证',
})
@ApiResponse({
status: 302,
description: '重定向到 GitHub 授权页面',
schema: {
example: {
url: 'https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=xxx',
},
},
})
@ApiResponse({
status: 500,
description: '服务器内部错误',
type: ErrorResponseDto,
})
async githubLogin() {
// GitHub 登录重定向
}
@Get('github/callback')
@UseGuards(AuthGuard('github'))
@ApiOperation({ summary: 'Github 登录回调' })
@ApiOperation({
summary: 'GitHub 登录回调',
description:
'GitHub OAuth 回调端点。在用户授权后GitHub 会重定向到此端点,携带授权码。系统将验证用户身份,生成 JWT Token并重定向到前端页面',
})
@ApiResponse({
status: 302,
description: '重定向到前端页面,并携带 token',
description: '登录成功,重定向到前端页面并携带 JWT Token 和用户信息',
schema: {
example: {
url: 'http://localhost:8000/test-github-sso.html?userId=1&username=johndoe&email=john@example.com&provider=github&providerUsername=johndoe&access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
})
@ApiResponse({
status: 401,
description: 'GitHub 授权失败或用户取消授权',
type: ErrorResponseDto,
})
@ApiResponse({
status: 500,
description: '服务器内部错误',
type: ErrorResponseDto,
})
async githubCallback(@Req() req: any, @Res() res: Response) {
const oauthUser = req.user;

View File

@ -0,0 +1,13 @@
import { Module, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '../user/entities/user.entity';
import { AdminService } from './services/admin.service';
import { AdminGuard } from './guards/admin.guard';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [AdminService, AdminGuard],
exports: [AdminService, AdminGuard],
})
export class CommonModule {}

View File

@ -0,0 +1,130 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* DTO
*/
export class ErrorResponseDto {
@ApiProperty({
description: 'HTTP 状态码',
example: 400,
})
statusCode: number;
@ApiProperty({
description: '错误消息',
example: '请求参数错误',
})
message: string;
@ApiProperty({
description: '错误详情(可选)',
required: false,
example: ['用户名不能为空', '邮箱格式不正确'],
})
error?: string | string[];
@ApiProperty({
description: '请求路径',
example: '/api/user/register',
})
path: string;
@ApiProperty({
description: '时间戳',
example: '2025-12-13T10:30:00.000Z',
})
timestamp: string;
}
/**
* DTO
*/
export class UnauthorizedResponseDto {
@ApiProperty({
description: 'HTTP 状态码',
example: 401,
})
statusCode: number;
@ApiProperty({
description: '错误消息',
example: '未授权访问',
})
message: string;
@ApiProperty({
description: '错误类型',
example: 'Unauthorized',
})
error: string;
}
/**
* DTO
*/
export class NotFoundResponseDto {
@ApiProperty({
description: 'HTTP 状态码',
example: 404,
})
statusCode: number;
@ApiProperty({
description: '错误消息',
example: '资源未找到',
})
message: string;
@ApiProperty({
description: '错误类型',
example: 'Not Found',
})
error: string;
}
/**
* DTO
*/
export class ConflictResponseDto {
@ApiProperty({
description: 'HTTP 状态码',
example: 409,
})
statusCode: number;
@ApiProperty({
description: '错误消息',
example: '用户名已存在',
})
message: string;
@ApiProperty({
description: '错误类型',
example: 'Conflict',
})
error: string;
}
/**
* DTO
*/
export class ValidationErrorResponseDto {
@ApiProperty({
description: 'HTTP 状态码',
example: 422,
})
statusCode: number;
@ApiProperty({
description: '错误消息列表',
example: ['用户名不能为空', '密码长度必须在6到20之间'],
type: [String],
})
message: string[];
@ApiProperty({
description: '错误类型',
example: 'Unprocessable Entity',
})
error: string;
}

View File

@ -0,0 +1,29 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { AdminService } from '../services/admin.service';
@Injectable()
export class AdminGuard implements CanActivate {
constructor(private adminService: AdminService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.userId) {
throw new ForbiddenException('请先登录');
}
const isAdmin = await this.adminService.isAdmin(user.userId);
if (!isAdmin) {
throw new ForbiddenException('需要管理员权限');
}
return true;
}
}

View File

@ -0,0 +1,54 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../user/entities/user.entity';
@Injectable()
export class AdminService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
/**
*
* admin admin
* 使
*/
async isAdmin(userId: number): Promise<boolean> {
const user = await this.userRepository.findOne({
where: { id: userId },
});
if (!user) {
return false;
}
// 简单的管理员判断逻辑(可根据实际需求调整)
return (
user.username === 'admin' ||
user.email.includes('admin') ||
user.username.startsWith('admin_')
);
}
/**
*
*/
async getAdminUsers(): Promise<User[]> {
const users = await this.userRepository.find();
const admins: User[] = [];
for (const user of users) {
if (
user.username === 'admin' ||
user.email.includes('admin') ||
user.username.startsWith('admin_')
) {
admins.push(user);
}
}
return admins;
}
}

View File

@ -3,25 +3,58 @@ import { DeepseekService } from './deepseek.service';
import { Public } from 'src/auth/decorators/public.decorator';
import { PROVIDER_TYPE } from 'src/constants/providerType';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ErrorResponseDto } from '../common/dto/error-response.dto';
@ApiTags('Deepseek 模块')
@ApiTags('DeepSeek AI 模块')
@Controller('deepSeek')
export class DeepseekController {
constructor(private readonly deepseekService: DeepseekService) {}
@Public()
@Post('chat-flow')
@ApiOperation({ summary: '与 Flow 模型聊天' })
@ApiOperation({
summary: '与 DeepSeek Flow 模型对话',
description:
'使用 DeepSeek Flow 模型进行对话交互。Flow 模型擅长快速响应和流式输出,适合实时对话场景',
})
@ApiBody({
schema: {
type: 'object',
properties: {
message: { type: 'string', description: '发送给模型的消息' },
message: {
type: 'string',
description: '发送给 AI 模型的消息内容',
example: '你好,请介绍一下你自己',
},
},
required: ['message'],
},
})
@ApiResponse({ status: 200, description: '成功返回模型的响应' })
@ApiResponse({
status: 200,
description: '成功返回 AI 模型的响应',
schema: {
example: {
model: 'deepseek-flow',
message: '你好!我是 DeepSeek Flow一个 AI 助手。我可以帮助你回答问题、提供建议和进行对话。',
usage: {
prompt_tokens: 10,
completion_tokens: 25,
total_tokens: 35,
},
},
},
})
@ApiResponse({
status: 400,
description: '请求参数错误(消息内容为空等)',
type: ErrorResponseDto,
})
@ApiResponse({
status: 500,
description: 'AI 服务调用失败',
type: ErrorResponseDto,
})
async chatFlow(@Body() body: { message: string }) {
const response = await this.deepseekService.chatRequest(
body.message,
@ -32,17 +65,50 @@ export class DeepseekController {
@Public()
@Post('chat-deep')
@ApiOperation({ summary: '与 Deep 模型聊天' })
@ApiOperation({
summary: '与 DeepSeek Deep 模型对话',
description:
'使用 DeepSeek Deep 模型进行对话交互。Deep 模型提供更深入的推理能力,适合复杂问题解答和深度分析',
})
@ApiBody({
schema: {
type: 'object',
properties: {
message: { type: 'string', description: '发送给模型的消息' },
message: {
type: 'string',
description: '发送给 AI 模型的消息内容',
example: '请解释一下量子计算的基本原理',
},
},
required: ['message'],
},
})
@ApiResponse({ status: 200, description: '成功返回模型的响应' })
@ApiResponse({
status: 200,
description: '成功返回 AI 模型的响应',
schema: {
example: {
model: 'deepseek-deep',
message:
'量子计算是一种利用量子力学原理进行信息处理的计算方式...',
usage: {
prompt_tokens: 15,
completion_tokens: 150,
total_tokens: 165,
},
},
},
})
@ApiResponse({
status: 400,
description: '请求参数错误(消息内容为空等)',
type: ErrorResponseDto,
})
@ApiResponse({
status: 500,
description: 'AI 服务调用失败',
type: ErrorResponseDto,
})
async chatDeep(@Body() body: { message: string }) {
const response = await this.deepseekService.chatRequest(
body.message,
@ -53,17 +119,49 @@ export class DeepseekController {
@Public()
@Post('chat-grok')
@ApiOperation({ summary: '与 Grok 模型聊天' })
@ApiOperation({
summary: '与 Grok 模型对话',
description:
'使用 Grok 模型进行对话交互。Grok 模型具有幽默风格和实时信息获取能力,适合需要最新信息的场景',
})
@ApiBody({
schema: {
type: 'object',
properties: {
message: { type: 'string', description: '发送给模型的消息' },
message: {
type: 'string',
description: '发送给 AI 模型的消息内容',
example: '最近有什么科技新闻?',
},
},
required: ['message'],
},
})
@ApiResponse({ status: 200, description: '成功返回模型的响应' })
@ApiResponse({
status: 200,
description: '成功返回 AI 模型的响应',
schema: {
example: {
model: 'grok',
message: '最近的科技新闻包括...',
usage: {
prompt_tokens: 12,
completion_tokens: 80,
total_tokens: 92,
},
},
},
})
@ApiResponse({
status: 400,
description: '请求参数错误(消息内容为空等)',
type: ErrorResponseDto,
})
@ApiResponse({
status: 500,
description: 'AI 服务调用失败',
type: ErrorResponseDto,
})
async chatGrok(@Body() body: { message: string }) {
const response = await this.deepseekService.chatRequest(
body.message,

View File

@ -4,8 +4,9 @@ import { UserService } from '../user/user.service';
import { Response } from 'express';
import { Public } from '../auth/decorators/public.decorator';
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
import { ErrorResponseDto } from '../common/dto/error-response.dto';
@ApiTags('Github 模块')
@ApiTags('GitHub OAuth 模块')
@Controller('github')
export class GitHubController {
constructor(
@ -15,13 +16,31 @@ export class GitHubController {
@Get('login')
@Public()
@ApiOperation({ summary: 'Github 登录' })
@ApiOperation({
summary: 'GitHub OAuth 登录',
description:
'启动 GitHub OAuth 2.0 登录流程。接收前端重定向地址,生成 GitHub 授权 URL 并重定向用户到 GitHub 授权页面',
})
@ApiQuery({
name: 'redirectUri',
description: '登录成功后的重定向地址',
description: '登录成功后的前端重定向地址URL编码',
required: true,
example: 'http://localhost:8000/callback',
})
@ApiResponse({
status: 302,
description: '重定向到 GitHub 授权页面',
schema: {
example: {
url: 'https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=xxx&scope=read:user user:email',
},
},
})
@ApiResponse({
status: 400,
description: '缺少必需的重定向地址参数',
type: ErrorResponseDto,
})
@ApiResponse({ status: 302, description: '重定向到 Github 授权页面' })
async login(@Query('redirectUri') redirectUri: string, @Res() res: Response) {
const loginUrl = await this.githubService.getLoginUrl(redirectUri);
return res.redirect(loginUrl);
@ -29,12 +48,51 @@ export class GitHubController {
@Get('callback')
@Public()
@ApiOperation({ summary: 'Github 登录回调' })
@ApiQuery({ name: 'code', description: 'Github 返回的授权码' })
@ApiQuery({ name: 'state', description: 'Github 返回的状态' })
@ApiResponse({ status: 200, description: '登录成功,返回用户信息和 token' })
@ApiResponse({ status: 400, description: '授权码缺失' })
@ApiResponse({ status: 500, description: 'Github 认证失败' })
@ApiOperation({
summary: 'GitHub OAuth 回调处理',
description:
'GitHub OAuth 回调端点。GitHub 授权完成后会重定向到此端点,携带授权码。系统会用授权码换取访问令牌,获取用户信息,创建或更新用户记录,并生成 JWT Token 返回给前端',
})
@ApiQuery({
name: 'code',
description: 'GitHub 返回的授权码(用于换取访问令牌)',
required: true,
example: 'ghu_xxxxxxxxxxxxxxxxxxxxx',
})
@ApiQuery({
name: 'state',
description: 'GitHub 返回的状态参数(用于防止 CSRF 攻击)',
required: false,
example: 'random_state_string',
})
@ApiResponse({
status: 200,
description: 'GitHub 登录成功,返回用户信息和 JWT Token',
schema: {
example: {
message: 'GitHub login successful',
user: {
id: 1,
username: 'johndoe',
email: 'john@example.com',
provider: 'github',
providerUsername: 'johndoe',
},
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
})
@ApiResponse({
status: 400,
description: '授权码缺失或获取访问令牌失败',
type: ErrorResponseDto,
})
@ApiResponse({
status: 500,
description: 'GitHub 认证过程中发生服务器错误',
type: ErrorResponseDto,
})
async callback(
@Query('code') code: string,
@Query('state') _state: string,

View File

@ -4,8 +4,9 @@ import { UserService } from '../user/user.service';
import { Response } from 'express';
import { Public } from '../auth/decorators/public.decorator';
import { ApiTags, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger';
import { ErrorResponseDto } from '../common/dto/error-response.dto';
@ApiTags('Lark 模块')
@ApiTags('飞书 OAuth 模块')
@Controller('lark')
export class LarkController {
constructor(
@ -15,13 +16,31 @@ export class LarkController {
@Get('login')
@Public()
@ApiOperation({ summary: 'Lark 登录' })
@ApiOperation({
summary: '飞书 OAuth 登录',
description:
'启动飞书Lark/FeishuOAuth 2.0 登录流程。接收前端重定向地址,生成飞书授权 URL 并重定向用户到飞书授权页面',
})
@ApiQuery({
name: 'redirectUri',
description: '登录成功后的重定向地址',
description: '登录成功后的前端重定向地址URL编码',
required: true,
example: 'http://localhost:8000/callback',
})
@ApiResponse({
status: 302,
description: '重定向到飞书授权页面',
schema: {
example: {
url: 'https://open.feishu.cn/open-apis/authen/v1/index?app_id=xxx&redirect_uri=xxx',
},
},
})
@ApiResponse({
status: 400,
description: '缺少必需的重定向地址参数',
type: ErrorResponseDto,
})
@ApiResponse({ status: 302, description: '重定向到 Lark 授权页面' })
async login(@Query('redirectUri') redirectUri: string, @Res() res: Response) {
const loginUrl = await this.larkService.getLoginUrl(redirectUri);
return res.redirect(loginUrl);
@ -29,11 +48,45 @@ export class LarkController {
@Get('callback')
@Public()
@ApiOperation({ summary: 'Lark 登录回调' })
@ApiQuery({ name: 'code', description: 'Lark 返回的授权码' })
@ApiResponse({ status: 200, description: '登录成功,返回用户信息和 token' })
@ApiResponse({ status: 400, description: '授权码缺失' })
@ApiResponse({ status: 500, description: 'Lark 认证失败' })
@ApiOperation({
summary: '飞书 OAuth 回调处理',
description:
'飞书 OAuth 回调端点。飞书授权完成后会重定向到此端点,携带授权码。系统会用授权码换取访问令牌,获取用户信息,创建或更新用户记录,并生成 JWT Token 返回给前端',
})
@ApiQuery({
name: 'code',
description: '飞书返回的授权码(用于换取访问令牌)',
required: true,
example: 'xxxxxxxxxxxxxxxxxxxxxxxx',
})
@ApiResponse({
status: 200,
description: '飞书登录成功,返回用户信息和 JWT Token',
schema: {
example: {
message: 'Lark login successful',
user: {
id: 1,
username: '张三',
email: 'zhangsan@example.com',
provider: 'lark',
providerUsername: '张三',
},
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
})
@ApiResponse({
status: 400,
description: '授权码缺失或获取访问令牌失败',
type: ErrorResponseDto,
})
@ApiResponse({
status: 500,
description: '飞书认证过程中发生服务器错误',
type: ErrorResponseDto,
})
async callback(@Query('code') code: string, @Res() res: Response) {
if (!code) {
return res.status(400).send('Authorization code is missing');

View File

@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const REQUIRE_LICENSE_KEY = 'requireLicense';
export const RequireLicense = () => SetMetadata(REQUIRE_LICENSE_KEY, true);

View File

@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, Length } from 'class-validator';
/**
* DTO
*/
export class ActivateLicenseDto {
@ApiProperty({
description: '卡密码',
example: 'ABCD-1234-EFGH-5678',
minLength: 19,
maxLength: 19,
})
@IsString()
@IsNotEmpty({ message: '卡密不能为空' })
@Length(19, 19, { message: '卡密格式不正确' })
code: string;
}

View File

@ -0,0 +1,45 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsOptional, IsInt, Min, IsString } from 'class-validator';
import { LicenseType } from '../entities/license.entity';
/**
* DTO
*/
export class GenerateLicenseDto {
@ApiProperty({
description: '卡密类型',
enum: LicenseType,
example: LicenseType.MONTHLY,
})
@IsEnum(LicenseType)
type: LicenseType;
@ApiProperty({
description: '有效天数',
example: 30,
default: 30,
})
@IsInt()
@Min(1)
@IsOptional()
validDays?: number;
@ApiProperty({
description: '生成数量',
example: 1,
default: 1,
})
@IsInt()
@Min(1)
@IsOptional()
count?: number;
@ApiProperty({
description: '备注信息',
required: false,
example: '批量生成月卡',
})
@IsString()
@IsOptional()
remarks?: string;
}

View File

@ -0,0 +1,26 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsOptional } from 'class-validator';
import { LicenseStatus, LicenseType } from '../entities/license.entity';
/**
* DTO
*/
export class QueryLicenseDto {
@ApiProperty({
description: '卡密状态',
enum: LicenseStatus,
required: false,
})
@IsEnum(LicenseStatus)
@IsOptional()
status?: LicenseStatus;
@ApiProperty({
description: '卡密类型',
enum: LicenseType,
required: false,
})
@IsEnum(LicenseType)
@IsOptional()
type?: LicenseType;
}

View File

@ -0,0 +1,132 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { User } from '../../user/entities/user.entity';
/**
*
*/
export enum LicenseType {
TRIAL = 'trial', // 试用版
MONTHLY = 'monthly', // 月度订阅
YEARLY = 'yearly', // 年度订阅
LIFETIME = 'lifetime', // 终身授权
}
/**
*
*/
export enum LicenseStatus {
UNUSED = 'unused', // 未使用
ACTIVE = 'active', // 已激活
EXPIRED = 'expired', // 已过期
REVOKED = 'revoked', // 已撤销
}
/**
*
*/
@Entity('license')
export class License {
@PrimaryGeneratedColumn()
@ApiProperty({ description: '卡密ID' })
id: number;
@Column({ unique: true, length: 32 })
@ApiProperty({ description: '卡密码', example: 'XXXX-XXXX-XXXX-XXXX' })
code: string;
@Column({
type: 'enum',
enum: LicenseType,
default: LicenseType.MONTHLY,
})
@ApiProperty({
description: '卡密类型',
enum: LicenseType,
example: LicenseType.MONTHLY,
})
type: LicenseType;
@Column({
type: 'enum',
enum: LicenseStatus,
default: LicenseStatus.UNUSED,
})
@ApiProperty({
description: '卡密状态',
enum: LicenseStatus,
example: LicenseStatus.UNUSED,
})
status: LicenseStatus;
@Column({ type: 'int', default: 30 })
@ApiProperty({ description: '有效天数', example: 30 })
validDays: number;
@Column({ type: 'datetime', nullable: true })
@ApiProperty({ description: '激活时间', required: false })
activatedAt?: Date;
@Column({ type: 'datetime', nullable: true })
@ApiProperty({ description: '过期时间', required: false })
expiresAt?: Date;
@Column({ nullable: true })
@ApiProperty({ description: '激活用户ID', required: false })
userId?: number;
@ManyToOne(() => User, { nullable: true })
@JoinColumn({ name: 'userId' })
@ApiProperty({ description: '激活用户信息', required: false, type: () => User })
user?: User;
@Column({ type: 'text', nullable: true })
@ApiProperty({ description: '备注信息', required: false })
remarks?: string;
@CreateDateColumn()
@ApiProperty({ description: '创建时间' })
createdAt: Date;
@UpdateDateColumn()
@ApiProperty({ description: '更新时间' })
updatedAt: Date;
/**
*
*/
isValid(): boolean {
if (this.status !== LicenseStatus.ACTIVE) {
return false;
}
if (!this.expiresAt) {
return false;
}
return new Date() < new Date(this.expiresAt);
}
/**
*
*/
getRemainingDays(): number {
if (!this.expiresAt || this.status !== LicenseStatus.ACTIVE) {
return 0;
}
const now = new Date();
const expiry = new Date(this.expiresAt);
const diff = expiry.getTime() - now.getTime();
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
}
}

View File

@ -0,0 +1,47 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { LicenseService } from '../license.service';
import { REQUIRE_LICENSE_KEY } from '../decorators/require-license.decorator';
@Injectable()
export class LicenseGuard implements CanActivate {
constructor(
private reflector: Reflector,
private licenseService: LicenseService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requireLicense = this.reflector.getAllAndOverride<boolean>(
REQUIRE_LICENSE_KEY,
[context.getHandler(), context.getClass()],
);
if (!requireLicense) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.userId) {
throw new ForbiddenException('请先登录');
}
const hasValidLicense = await this.licenseService.verifyUserLicense(
user.userId,
);
if (!hasValidLicense) {
throw new ForbiddenException(
'您的授权已过期或未激活,请联系管理员获取卡密',
);
}
return true;
}
}

View File

@ -0,0 +1,385 @@
import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiBearerAuth,
ApiBody,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { LicenseService } from './license.service';
import { GenerateLicenseDto } from './dto/generate-license.dto';
import { ActivateLicenseDto } from './dto/activate-license.dto';
import { QueryLicenseDto } from './dto/query-license.dto';
import { License } from './entities/license.entity';
import {
ErrorResponseDto,
UnauthorizedResponseDto,
NotFoundResponseDto,
ConflictResponseDto,
ValidationErrorResponseDto,
} from '../common/dto/error-response.dto';
import { AdminGuard } from '../common/guards/admin.guard';
@ApiTags('卡密管理模块')
@ApiBearerAuth()
@Controller('license')
export class LicenseController {
constructor(private readonly licenseService: LicenseService) {}
@Post('generate')
@UseGuards(AdminGuard)
@ApiOperation({
summary: '生成卡密(管理员)',
description:
'批量生成卡密,可指定卡密类型、有效天数和数量。仅管理员可访问(用户名为 admin 或邮箱包含 admin',
})
@ApiBody({ type: GenerateLicenseDto })
@ApiResponse({
status: 201,
description: '卡密生成成功',
type: [License],
schema: {
example: [
{
id: 1,
code: 'ABCD-1234-EFGH-5678',
type: 'monthly',
status: 'unused',
validDays: 30,
remarks: '月卡批量生成',
createdAt: '2025-12-13T10:00:00.000Z',
},
],
},
})
@ApiResponse({
status: 400,
description: '请求参数错误',
type: ErrorResponseDto,
})
@ApiResponse({
status: 401,
description: '未授权,需要管理员权限',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 422,
description: '数据验证失败',
type: ValidationErrorResponseDto,
})
async generate(@Body() dto: GenerateLicenseDto): Promise<License[]> {
return this.licenseService.generate(dto);
}
@Post('activate')
@ApiOperation({
summary: '激活卡密',
description:
'用户使用卡密激活授权。如果用户已有有效授权,则在原有基础上延期',
})
@ApiBody({ type: ActivateLicenseDto })
@ApiResponse({
status: 200,
description: '卡密激活成功',
schema: {
example: {
message: '卡密激活成功,有效期至 2026-01-13 10:00:00',
license: {
id: 1,
code: 'ABCD-1234-EFGH-5678',
type: 'monthly',
status: 'active',
validDays: 30,
activatedAt: '2025-12-13T10:00:00.000Z',
expiresAt: '2026-01-13T10:00:00.000Z',
userId: 1,
},
},
},
})
@ApiResponse({
status: 400,
description: '卡密无效、已过期或已撤销',
type: ErrorResponseDto,
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 404,
description: '卡密不存在',
type: NotFoundResponseDto,
})
@ApiResponse({
status: 409,
description: '卡密已被激活',
type: ConflictResponseDto,
})
@ApiResponse({
status: 422,
description: '卡密格式不正确',
type: ValidationErrorResponseDto,
})
async activate(
@Req() req: any,
@Body() dto: ActivateLicenseDto,
): Promise<{ message: string; license: License }> {
const userId = req.user?.userId || req.user?.id;
return this.licenseService.activate(userId, dto);
}
@Get('my')
@ApiOperation({
summary: '查询我的授权信息',
description: '获取当前用户的有效授权信息,包括剩余天数等',
})
@ApiResponse({
status: 200,
description: '查询成功',
schema: {
example: {
hasValidLicense: true,
license: {
id: 1,
code: 'ABCD-1234-EFGH-5678',
type: 'monthly',
status: 'active',
validDays: 30,
activatedAt: '2025-12-13T10:00:00.000Z',
expiresAt: '2026-01-13T10:00:00.000Z',
remainingDays: 31,
},
},
},
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
async getMyLicense(@Req() req: any): Promise<{
hasValidLicense: boolean;
license?: any;
}> {
const userId = req.user?.userId || req.user?.id;
const license = await this.licenseService.getUserActiveLicense(userId);
if (!license) {
return { hasValidLicense: false };
}
return {
hasValidLicense: true,
license: {
id: license.id,
code: license.code,
type: license.type,
status: license.status,
validDays: license.validDays,
activatedAt: license.activatedAt,
expiresAt: license.expiresAt,
remainingDays: license.getRemainingDays(),
},
};
}
@Get('my/history')
@ApiOperation({
summary: '查询我的卡密历史',
description: '获取当前用户激活过的所有卡密记录',
})
@ApiResponse({
status: 200,
description: '查询成功',
type: [License],
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
async getMyLicenseHistory(@Req() req: any): Promise<License[]> {
const userId = req.user?.userId || req.user?.id;
return this.licenseService.findByUser(userId);
}
@Get('statistics')
@UseGuards(AdminGuard)
@ApiOperation({
summary: '获取卡密统计信息(管理员)',
description: '获取卡密的统计数据,包括总数、已激活、未使用等。仅管理员可访问',
})
@ApiResponse({
status: 200,
description: '查询成功',
schema: {
example: {
total: 100,
unused: 50,
active: 30,
expired: 15,
revoked: 5,
},
},
})
@ApiResponse({
status: 401,
description: '未授权,需要管理员权限',
type: UnauthorizedResponseDto,
})
async getStatistics(): Promise<{
total: number;
unused: number;
active: number;
expired: number;
revoked: number;
}> {
return this.licenseService.getStatistics();
}
@Get()
@UseGuards(AdminGuard)
@ApiOperation({
summary: '查询所有卡密(管理员)',
description: '获取所有卡密列表,支持按状态和类型筛选。仅管理员可访问',
})
@ApiQuery({
name: 'status',
required: false,
description: '按状态筛选',
enum: ['unused', 'active', 'expired', 'revoked'],
})
@ApiQuery({
name: 'type',
required: false,
description: '按类型筛选',
enum: ['trial', 'monthly', 'yearly', 'lifetime'],
})
@ApiResponse({
status: 200,
description: '查询成功',
type: [License],
})
@ApiResponse({
status: 401,
description: '未授权,需要管理员权限',
type: UnauthorizedResponseDto,
})
async findAll(@Query() query: QueryLicenseDto): Promise<License[]> {
return this.licenseService.findAll(query);
}
@Get(':id')
@UseGuards(AdminGuard)
@ApiOperation({
summary: '查询单个卡密(管理员)',
description: '根据 ID 查询卡密详细信息。仅管理员可访问',
})
@ApiParam({
name: 'id',
description: '卡密ID',
type: Number,
})
@ApiResponse({
status: 200,
description: '查询成功',
type: License,
})
@ApiResponse({
status: 401,
description: '未授权,需要管理员权限',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 404,
description: '卡密不存在',
type: NotFoundResponseDto,
})
async findOne(@Param('id') id: string): Promise<License> {
return this.licenseService.findOne(+id);
}
@Post(':id/revoke')
@UseGuards(AdminGuard)
@ApiOperation({
summary: '撤销卡密(管理员)',
description: '将指定卡密标记为已撤销状态,撤销后无法再使用。仅管理员可访问',
})
@ApiParam({
name: 'id',
description: '卡密ID',
type: Number,
})
@ApiResponse({
status: 200,
description: '撤销成功',
type: License,
})
@ApiResponse({
status: 400,
description: '卡密已被撤销',
type: ErrorResponseDto,
})
@ApiResponse({
status: 401,
description: '未授权,需要管理员权限',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 404,
description: '卡密不存在',
type: NotFoundResponseDto,
})
async revoke(@Param('id') id: string): Promise<License> {
return this.licenseService.revoke(+id);
}
@Delete(':id')
@UseGuards(AdminGuard)
@ApiOperation({
summary: '删除卡密(管理员)',
description: '永久删除指定的卡密记录。仅管理员可访问',
})
@ApiParam({
name: 'id',
description: '卡密ID',
type: Number,
})
@ApiResponse({
status: 200,
description: '删除成功',
schema: {
example: {
message: 'License deleted successfully',
},
},
})
@ApiResponse({
status: 401,
description: '未授权,需要管理员权限',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 404,
description: '卡密不存在',
type: NotFoundResponseDto,
})
async remove(@Param('id') id: string): Promise<{ message: string }> {
await this.licenseService.remove(+id);
return { message: 'License deleted successfully' };
}
}

View File

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LicenseController } from './license.controller';
import { LicenseService } from './license.service';
import { License } from './entities/license.entity';
import { LicenseGuard } from './guards/license.guard';
@Module({
imports: [TypeOrmModule.forFeature([License])],
controllers: [LicenseController],
providers: [LicenseService, LicenseGuard],
exports: [LicenseService, LicenseGuard],
})
export class LicenseModule {}

View File

@ -0,0 +1,320 @@
import {
Injectable,
BadRequestException,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { License, LicenseStatus, LicenseType } from './entities/license.entity';
import { GenerateLicenseDto } from './dto/generate-license.dto';
import { ActivateLicenseDto } from './dto/activate-license.dto';
import { QueryLicenseDto } from './dto/query-license.dto';
import * as crypto from 'crypto';
@Injectable()
export class LicenseService {
constructor(
@InjectRepository(License)
private readonly licenseRepository: Repository<License>,
) {}
/**
* XXXX-XXXX-XXXX-XXXX
*/
private generateLicenseCode(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // 去除易混淆字符
let code = '';
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const randomIndex = crypto.randomInt(0, chars.length);
code += chars[randomIndex];
}
if (i < 3) {
code += '-';
}
}
return code;
}
/**
*
*/
private getDefaultValidDays(type: LicenseType): number {
switch (type) {
case LicenseType.TRIAL:
return 7; // 试用 7 天
case LicenseType.MONTHLY:
return 30; // 月卡 30 天
case LicenseType.YEARLY:
return 365; // 年卡 365 天
case LicenseType.LIFETIME:
return 36500; // 终身 100 年
default:
return 30;
}
}
/**
*
*/
async generate(dto: GenerateLicenseDto): Promise<License[]> {
const count = dto.count || 1;
const validDays = dto.validDays || this.getDefaultValidDays(dto.type);
const licenses: License[] = [];
for (let i = 0; i < count; i++) {
let code: string;
let attempts = 0;
const maxAttempts = 10;
// 生成唯一卡密码,最多尝试 10 次
do {
code = this.generateLicenseCode();
attempts++;
if (attempts >= maxAttempts) {
throw new BadRequestException('生成卡密失败,请重试');
}
const existing = await this.licenseRepository.findOne({
where: { code },
});
if (!existing) {
break;
}
} while (true);
const license = this.licenseRepository.create({
code,
type: dto.type,
validDays,
status: LicenseStatus.UNUSED,
remarks: dto.remarks,
});
licenses.push(await this.licenseRepository.save(license));
}
return licenses;
}
/**
*
*/
async activate(
userId: number,
dto: ActivateLicenseDto,
): Promise<{ license: License; message: string }> {
// 查找卡密
const license = await this.licenseRepository.findOne({
where: { code: dto.code.toUpperCase() },
relations: ['user'],
});
if (!license) {
throw new NotFoundException('卡密不存在');
}
// 检查卡密状态
if (license.status === LicenseStatus.ACTIVE) {
throw new ConflictException('该卡密已被激活');
}
if (license.status === LicenseStatus.REVOKED) {
throw new BadRequestException('该卡密已被撤销');
}
if (license.status === LicenseStatus.EXPIRED) {
throw new BadRequestException('该卡密已过期');
}
// 检查用户是否已有有效授权
const existingLicense = await this.getUserActiveLicense(userId);
if (existingLicense) {
// 如果已有有效授权,则在现有基础上延期
const remainingTime =
new Date(existingLicense.expiresAt).getTime() - new Date().getTime();
const additionalDays = license.validDays;
const additionalTime = additionalDays * 24 * 60 * 60 * 1000;
existingLicense.expiresAt = new Date(
Date.now() + remainingTime + additionalTime,
);
await this.licenseRepository.save(existingLicense);
// 将新卡密标记为已使用
license.status = LicenseStatus.EXPIRED;
license.userId = userId;
license.activatedAt = new Date();
await this.licenseRepository.save(license);
return {
license: existingLicense,
message: `成功延期 ${additionalDays} 天,当前授权到期时间:${existingLicense.expiresAt.toLocaleString('zh-CN')}`,
};
}
// 激活新卡密
const now = new Date();
const expiresAt = new Date(
now.getTime() + license.validDays * 24 * 60 * 60 * 1000,
);
license.status = LicenseStatus.ACTIVE;
license.userId = userId;
license.activatedAt = now;
license.expiresAt = expiresAt;
await this.licenseRepository.save(license);
return {
license,
message: `卡密激活成功,有效期至 ${expiresAt.toLocaleString('zh-CN')}`,
};
}
/**
*
*/
async getUserActiveLicense(userId: number): Promise<License | null> {
const license = await this.licenseRepository.findOne({
where: {
userId,
status: LicenseStatus.ACTIVE,
},
order: {
expiresAt: 'DESC',
},
});
if (!license) {
return null;
}
// 检查是否过期
if (license.expiresAt && new Date() > new Date(license.expiresAt)) {
license.status = LicenseStatus.EXPIRED;
await this.licenseRepository.save(license);
return null;
}
return license;
}
/**
*
*/
async verifyUserLicense(userId: number): Promise<boolean> {
const license = await this.getUserActiveLicense(userId);
return license !== null && license.isValid();
}
/**
*
*/
async findAll(query?: QueryLicenseDto): Promise<License[]> {
const where: any = {};
if (query?.status) {
where.status = query.status;
}
if (query?.type) {
where.type = query.type;
}
return this.licenseRepository.find({
where,
relations: ['user'],
order: {
createdAt: 'DESC',
},
});
}
/**
*
*/
async findByUser(userId: number): Promise<License[]> {
return this.licenseRepository.find({
where: { userId },
order: {
createdAt: 'DESC',
},
});
}
/**
* ID查询卡密
*/
async findOne(id: number): Promise<License> {
const license = await this.licenseRepository.findOne({
where: { id },
relations: ['user'],
});
if (!license) {
throw new NotFoundException('卡密不存在');
}
return license;
}
/**
*
*/
async revoke(id: number): Promise<License> {
const license = await this.findOne(id);
if (license.status === LicenseStatus.REVOKED) {
throw new BadRequestException('该卡密已被撤销');
}
license.status = LicenseStatus.REVOKED;
return this.licenseRepository.save(license);
}
/**
*
*/
async remove(id: number): Promise<void> {
const license = await this.findOne(id);
await this.licenseRepository.remove(license);
}
/**
*
*/
async getStatistics(): Promise<{
total: number;
unused: number;
active: number;
expired: number;
revoked: number;
}> {
const [total, unused, active, expired, revoked] = await Promise.all([
this.licenseRepository.count(),
this.licenseRepository.count({ where: { status: LicenseStatus.UNUSED } }),
this.licenseRepository.count({ where: { status: LicenseStatus.ACTIVE } }),
this.licenseRepository.count({
where: { status: LicenseStatus.EXPIRED },
}),
this.licenseRepository.count({
where: { status: LicenseStatus.REVOKED },
}),
]);
return {
total,
unused,
active,
expired,
revoked,
};
}
}

View File

@ -6,60 +6,279 @@ import {
Patch,
Param,
Delete,
UseGuards,
} from '@nestjs/common';
import { TaskService } from './task.service';
import { CreateTaskDto } from './dto/create-task.dto';
import { UpdateTaskDto } from './dto/update-task.dto';
import { ReorderTasksDto } from './dto/reorder-tasks.dto';
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiBearerAuth,
} from '@nestjs/swagger';
import { Task } from './entities/task.entity';
import {
ErrorResponseDto,
UnauthorizedResponseDto,
NotFoundResponseDto,
ValidationErrorResponseDto,
} from '../common/dto/error-response.dto';
import { RequireLicense } from '../license/decorators/require-license.decorator';
import { LicenseGuard } from '../license/guards/license.guard';
@ApiTags('任务模块')
@ApiBearerAuth()
@Controller('task')
@UseGuards(LicenseGuard)
@RequireLicense()
export class TaskController {
constructor(private readonly taskService: TaskService) {}
@Post()
@ApiOperation({ summary: '创建任务' })
@ApiResponse({ status: 201, description: '创建成功', type: Task })
@ApiOperation({
summary: '创建任务',
description: '创建一个新的任务,需要提供任务标题,可选提供描述、截止日期和优先级。需要有效的授权卡密',
})
@ApiResponse({
status: 201,
description: '任务创建成功',
type: Task,
schema: {
example: {
id: 1,
title: '完成项目文档',
description: '编写项目的 API 文档和使用说明',
isCompleted: false,
position: 1,
dueDate: '2025-12-31T23:59:59.000Z',
priority: 'high',
},
},
})
@ApiResponse({
status: 400,
description: '请求参数错误',
type: ErrorResponseDto,
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 403,
description: '授权已过期或未激活,请联系管理员获取卡密',
type: ErrorResponseDto,
})
@ApiResponse({
status: 422,
description: '数据验证失败',
type: ValidationErrorResponseDto,
})
create(@Body() createTaskDto: CreateTaskDto) {
return this.taskService.create(createTaskDto);
}
@Get()
@ApiOperation({ summary: '获取所有任务' })
@ApiResponse({ status: 200, description: '获取成功', type: [Task] })
@ApiOperation({
summary: '获取所有任务',
description: '获取当前用户的所有任务列表,按照 position 字段排序',
})
@ApiResponse({
status: 200,
description: '获取成功,返回任务列表',
type: [Task],
schema: {
example: [
{
id: 1,
title: '完成项目文档',
description: '编写项目的 API 文档和使用说明',
isCompleted: false,
position: 1,
dueDate: '2025-12-31T23:59:59.000Z',
priority: 'high',
},
{
id: 2,
title: '修复 Bug',
description: '修复登录页面的显示问题',
isCompleted: true,
position: 2,
dueDate: null,
priority: 'medium',
},
],
},
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
findAll() {
return this.taskService.findAll();
}
@Get(':id')
@ApiOperation({ summary: '获取单个任务' })
@ApiParam({ name: 'id', description: '任务ID' })
@ApiResponse({ status: 200, description: '获取成功', type: Task })
@ApiOperation({
summary: '获取单个任务',
description: '根据任务 ID 获取任务的详细信息',
})
@ApiParam({
name: 'id',
description: '任务ID',
type: Number,
example: 1,
})
@ApiResponse({
status: 200,
description: '获取成功,返回任务详情',
type: Task,
schema: {
example: {
id: 1,
title: '完成项目文档',
description: '编写项目的 API 文档和使用说明',
isCompleted: false,
position: 1,
dueDate: '2025-12-31T23:59:59.000Z',
priority: 'high',
},
},
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 404,
description: '任务不存在',
type: NotFoundResponseDto,
})
findOne(@Param('id') id: string) {
return this.taskService.findOne(+id);
}
@Patch('reorder')
@ApiOperation({ summary: '对任务进行重新排序' })
@ApiResponse({ status: 200, description: '排序成功' })
@ApiOperation({
summary: '对任务进行重新排序',
description:
'批量更新任务的排序位置,传入按新顺序排列的任务 ID 数组,系统会自动更新每个任务的 position 值',
})
@ApiResponse({
status: 200,
description: '排序成功',
schema: {
example: {
message: 'Tasks reordered successfully',
},
},
})
@ApiResponse({
status: 400,
description: '请求参数错误',
type: ErrorResponseDto,
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 422,
description: '数据验证失败',
type: ValidationErrorResponseDto,
})
reorder(@Body() reorderTasksDto: ReorderTasksDto) {
return this.taskService.reorder(reorderTasksDto.taskIds);
}
@Patch(':id')
@ApiOperation({ summary: '更新任务' })
@ApiParam({ name: 'id', description: '任务ID' })
@ApiResponse({ status: 200, description: '更新成功', type: Task })
@ApiOperation({
summary: '更新任务',
description: '更新指定任务的信息,可以更新标题、描述、完成状态、截止日期和优先级',
})
@ApiParam({
name: 'id',
description: '任务ID',
type: Number,
example: 1,
})
@ApiResponse({
status: 200,
description: '更新成功,返回更新后的任务信息',
type: Task,
schema: {
example: {
id: 1,
title: '完成项目文档(已更新)',
description: '编写项目的 API 文档和使用说明',
isCompleted: true,
position: 1,
dueDate: '2025-12-31T23:59:59.000Z',
priority: 'high',
},
},
})
@ApiResponse({
status: 400,
description: '请求参数错误',
type: ErrorResponseDto,
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 404,
description: '任务不存在',
type: NotFoundResponseDto,
})
@ApiResponse({
status: 422,
description: '数据验证失败',
type: ValidationErrorResponseDto,
})
update(@Param('id') id: string, @Body() updateTaskDto: UpdateTaskDto) {
return this.taskService.update(+id, updateTaskDto);
}
@Delete(':id')
@ApiOperation({ summary: '删除任务' })
@ApiParam({ name: 'id', description: '任务ID' })
@ApiResponse({ status: 200, description: '删除成功' })
@ApiOperation({
summary: '删除任务',
description: '根据任务 ID 删除指定的任务',
})
@ApiParam({
name: 'id',
description: '任务ID',
type: Number,
example: 1,
})
@ApiResponse({
status: 200,
description: '删除成功',
schema: {
example: {
message: 'Task deleted successfully',
},
},
})
@ApiResponse({
status: 401,
description: '未授权,需要登录',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 404,
description: '任务不存在',
type: NotFoundResponseDto,
})
remove(@Param('id') id: string) {
return this.taskService.remove(+id);
}

View File

@ -3,9 +3,10 @@ import { TaskService } from './task.service';
import { TaskController } from './task.controller';
import { Task } from './entities/task.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LicenseModule } from '../license/license.module';
@Module({
imports: [TypeOrmModule.forFeature([Task])],
imports: [TypeOrmModule.forFeature([Task]), LicenseModule],
controllers: [TaskController],
providers: [TaskService],
})

View File

@ -8,7 +8,12 @@ import {
import { UserService } from '../user/user.service';
import { Public } from '../auth/decorators/public.decorator';
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import { User } from './entities/user.entity';
import {
ErrorResponseDto,
UnauthorizedResponseDto,
ConflictResponseDto,
ValidationErrorResponseDto,
} from '../common/dto/error-response.dto';
@ApiTags('用户模块')
@Controller('user')
@ -17,20 +22,66 @@ export class UserController {
@Public()
@Post('register')
@ApiOperation({ summary: '用户注册' })
@ApiOperation({
summary: '用户注册',
description: '创建新用户账号,需要提供用户名、密码和邮箱。注册成功后自动返回 JWT Token',
})
@ApiBody({
schema: {
type: 'object',
properties: {
username: { type: 'string', description: '用户名' },
password: { type: 'string', description: '密码' },
email: { type: 'string', description: '邮箱' },
username: {
type: 'string',
description: '用户名(唯一)',
example: 'johndoe',
},
password: {
type: 'string',
description: '密码6-20位',
example: 'password123',
},
email: {
type: 'string',
description: '邮箱地址',
example: 'john@example.com',
},
},
required: ['username', 'password', 'email'],
},
})
@ApiResponse({ status: 201, description: '注册成功', type: User })
@ApiResponse({ status: 400, description: '参数错误' })
@ApiResponse({
status: 201,
description: '注册成功,返回用户信息和 JWT Token',
schema: {
example: {
message: 'User registered successfully',
user: {
id: 1,
username: 'johndoe',
email: 'john@example.com',
isActive: true,
provider: 'local',
},
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
})
@ApiResponse({
status: 400,
description: '请求参数错误(密码为空等)',
type: ErrorResponseDto,
})
@ApiResponse({
status: 409,
description: '用户名或邮箱已存在',
type: ConflictResponseDto,
})
@ApiResponse({
status: 422,
description: '数据验证失败',
type: ValidationErrorResponseDto,
})
async register(
@Body() body: { username: string; password: string; email: string },
) {
@ -49,19 +100,56 @@ export class UserController {
@Public()
@Post('login')
@ApiOperation({ summary: '用户登录' })
@ApiOperation({
summary: '用户登录',
description: '使用用户名和密码进行身份验证,登录成功后返回 JWT Token',
})
@ApiBody({
schema: {
type: 'object',
properties: {
username: { type: 'string', description: '用户名' },
password: { type: 'string', description: '密码' },
username: {
type: 'string',
description: '用户名',
example: 'johndoe',
},
password: {
type: 'string',
description: '密码',
example: 'password123',
},
},
required: ['username', 'password'],
},
})
@ApiResponse({ status: 200, description: '登录成功' })
@ApiResponse({ status: 401, description: '认证失败' })
@ApiResponse({
status: 200,
description: '登录成功,返回用户信息和 JWT Token',
schema: {
example: {
message: 'Login successful',
user: {
id: 1,
username: 'johndoe',
email: 'john@example.com',
isActive: true,
provider: 'local',
},
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
})
@ApiResponse({
status: 401,
description: '认证失败(用户不存在或密码错误)',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 400,
description: '请求参数错误',
type: ErrorResponseDto,
})
async login(@Body() body: { username: string; password: string }) {
const { username, password } = body;
const user = await this.userService.findUserByUsername(username);
@ -85,17 +173,43 @@ export class UserController {
@Public()
@Post('refresh-token')
@ApiOperation({ summary: '刷新-token' })
@ApiOperation({
summary: '刷新访问令牌',
description: '使用 refresh_token 刷新获取新的 access_token延长用户登录状态',
})
@ApiBody({
schema: {
type: 'object',
properties: {
refresh_token: { type: 'string', description: 'Refresh Token' },
refresh_token: {
type: 'string',
description: 'Refresh Token从登录或注册接口获取',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
required: ['refresh_token'],
},
})
@ApiResponse({ status: 200, description: '刷新成功' })
@ApiResponse({
status: 200,
description: '刷新成功,返回新的访问令牌',
schema: {
example: {
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
})
@ApiResponse({
status: 401,
description: 'Refresh Token 无效或已过期',
type: UnauthorizedResponseDto,
})
@ApiResponse({
status: 400,
description: '请求参数错误',
type: ErrorResponseDto,
})
async refreshToken(@Body() body: { refresh_token: string }) {
return this.userService.refreshAccessToken(body.refresh_token);
}

124
test-license-system.sh Executable file
View File

@ -0,0 +1,124 @@
#!/bin/bash
# 卡密系统测试脚本
# 使用方法: bash test-license-system.sh
BASE_URL="http://localhost:3030"
ADMIN_TOKEN=""
USER_TOKEN=""
LICENSE_CODE=""
echo "====== 卡密系统测试 ======"
echo ""
# 颜色定义
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 测试函数
test_api() {
local name=$1
local method=$2
local endpoint=$3
local data=$4
local token=$5
echo -e "${YELLOW}测试: ${name}${NC}"
if [ -n "$token" ]; then
if [ -n "$data" ]; then
response=$(curl -s -X $method "$BASE_URL$endpoint" \
-H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
-d "$data")
else
response=$(curl -s -X $method "$BASE_URL$endpoint" \
-H "Authorization: Bearer $token")
fi
else
if [ -n "$data" ]; then
response=$(curl -s -X $METHOD "$BASE_URL$endpoint" \
-H "Content-Type: application/json" \
-d "$data")
else
response=$(curl -s -X $method "$BASE_URL$endpoint")
fi
fi
echo "$response" | jq '.'
echo ""
}
# 1. 注册测试用户
echo -e "${GREEN}步骤 1: 注册测试用户${NC}"
REGISTER_RESPONSE=$(curl -s -X POST "$BASE_URL/user/register" \
-H "Content-Type: application/json" \
-d '{
"username": "testuser_'$(date +%s)'",
"password": "password123",
"email": "test'$(date +%s)'@example.com"
}')
USER_TOKEN=$(echo $REGISTER_RESPONSE | jq -r '.access_token')
echo "用户Token: $USER_TOKEN"
echo ""
# 2. 测试未授权时访问任务接口应该返回403
echo -e "${GREEN}步骤 2: 测试未授权访问任务接口 (预期 403)${NC}"
test_api "创建任务 (无授权)" "POST" "/task" '{
"title": "测试任务",
"description": "这应该失败"
}' "$USER_TOKEN"
# 3. 生成测试卡密(需要管理员权限,这里模拟)
echo -e "${GREEN}步骤 3: 生成测试卡密${NC}"
echo -e "${RED}注意: 此步骤需要管理员权限,请手动生成卡密或直接从数据库插入${NC}"
echo "示例 API 调用:"
echo 'POST /license/generate'
echo '{
"type": "monthly",
"validDays": 30,
"count": 1
}'
echo ""
# 提示用户输入卡密
read -p "请输入生成的卡密码 (格式: XXXX-XXXX-XXXX-XXXX): " LICENSE_CODE
echo ""
# 4. 激活卡密
echo -e "${GREEN}步骤 4: 激活卡密${NC}"
test_api "激活卡密" "POST" "/license/activate" "{
\"code\": \"$LICENSE_CODE\"
}" "$USER_TOKEN"
# 5. 查询授权信息
echo -e "${GREEN}步骤 5: 查询我的授权信息${NC}"
test_api "查询授权" "GET" "/license/my" "" "$USER_TOKEN"
# 6. 测试授权后访问任务接口
echo -e "${GREEN}步骤 6: 测试授权后访问任务接口 (应该成功)${NC}"
test_api "创建任务 (有授权)" "POST" "/task" '{
"title": "测试任务",
"description": "这应该成功",
"priority": "high"
}' "$USER_TOKEN"
# 7. 获取任务列表
echo -e "${GREEN}步骤 7: 获取任务列表${NC}"
test_api "获取任务列表" "GET" "/task" "" "$USER_TOKEN"
echo -e "${GREEN}====== 测试完成 ======${NC}"
echo ""
echo "测试总结:"
echo "1. 用户注册成功"
echo "2. 未授权时无法访问任务功能 (403)"
echo "3. 卡密激活成功"
echo "4. 授权后可以正常使用任务功能"
echo ""
echo "后续测试建议:"
echo "- 测试卡密延期功能(激活第二个卡密)"
echo "- 测试卡密过期后的访问"
echo "- 测试管理员接口(统计、查询、撤销等)"