feat: 初始化 WWJ Cloud 企业级框架项目
- 后端:基于 NestJS 的分层架构设计 - 前端:基于 VbenAdmin + Element Plus 的管理系统 - 支持 SaaS + 独立版双架构模式 - 完整的用户权限管理系统 - 系统设置、文件上传、通知等核心功能 - 多租户支持和插件化扩展架构
This commit is contained in:
132
.gitignore
vendored
Normal file
132
.gitignore
vendored
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids/
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
|
||||||
|
# Reference directory (exclude from git)
|
||||||
|
reference/
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.sql
|
||||||
|
*.db
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
public/upload/*
|
||||||
|
!public/upload/.gitkeep
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.cache/
|
||||||
|
.turbo/
|
||||||
|
|
||||||
|
# Test
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# Build
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Lock files (keep only one)
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
# Keep pnpm-lock.yaml
|
||||||
219
.trae/rules/development_constraints.md
Normal file
219
.trae/rules/development_constraints.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# WWJCloud-NestJS 开发约束规范
|
||||||
|
|
||||||
|
> 本文档整合了项目的所有开发规范和约束,确保开发过程中严格遵循项目架构和数据库设计。
|
||||||
|
|
||||||
|
## 📋 开发规范声明
|
||||||
|
|
||||||
|
**请严格按照以下规范进行开发,不允许假设和自创:**
|
||||||
|
|
||||||
|
### 1. 🗄️ 数据库表结构约束
|
||||||
|
|
||||||
|
**主要数据库文件:**
|
||||||
|
- **WWJCloud 主数据库**:[/g:/wwjcloud-nestjs/sql/wwjcloud.sql](../sql/wwjcloud.sql)
|
||||||
|
- **WWJAuth 认证数据库**:[/g:/wwjcloud-nestjs/sql/wwjauth.sql](../sql/wwjauth.sql)
|
||||||
|
|
||||||
|
**核心表结构:**
|
||||||
|
- `sys_user` - 系统用户表 (uid, username, password, real_name, last_ip, last_time, create_time, login_num, status, delete_time)
|
||||||
|
- `sys_role` - 系统角色表 (role_id, site_id, role_name, rules, status, create_time, update_time)
|
||||||
|
- `sys_user_role` - 用户角色关联表 (id, uid, site_id, role_ids, create_time, is_admin, status)
|
||||||
|
- `site` - 站点表 (site_id, site_name, group_id, app_type, logo, desc, status, expire_time, addons)
|
||||||
|
- `member` - 会员表 (member_id, username, mobile, password, nickname, headimg, member_level, member_label, wx_openid)
|
||||||
|
|
||||||
|
**WWJAuth 认证服务表:**
|
||||||
|
- `wwjauth_admins` - 管理员表
|
||||||
|
- `wwjauth_roles` - 角色表
|
||||||
|
- `wwjauth_permissions` - 权限表
|
||||||
|
- `wwjauth_role_permissions` - 角色权限关联表
|
||||||
|
- `wwjauth_members` - 会员表
|
||||||
|
- `wwjauth_menus` - 菜单表
|
||||||
|
|
||||||
|
### 2. 🏗️ 项目架构规范
|
||||||
|
|
||||||
|
**架构文档:** [项目README - 依赖关系图](../readme.md#L67-86)
|
||||||
|
|
||||||
|
**分层架构约束:**
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ App │ ← 业务开发层(用户自定义业务模块)
|
||||||
|
│ (用户业务) │ 电商、CRM、ERP等具体业务逻辑
|
||||||
|
└─────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Common │ ← 框架通用服务层(企业级通用功能)
|
||||||
|
│ (框架通用服务) │ 用户管理、权限管理、菜单管理
|
||||||
|
└─────────────────┘ 文件上传、通知服务、系统设置
|
||||||
|
↓ 数据字典、缓存服务、队列服务
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Core │ ← 核心基础设施层(底层基础设施)
|
||||||
|
│ (基础设施) │ 认证核心、数据库核心、验证核心
|
||||||
|
└─────────────────┘ HTTP核心、缓存核心、队列核心
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Vendor │ ← 第三方服务适配层
|
||||||
|
│ (外部集成) │ 存储、支付、通信、云服务适配
|
||||||
|
└─────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Addons │ ← 插件扩展层(可插拔功能模块)
|
||||||
|
│ (插件扩展) │ 扩展框架功能,不影响核心稳定性
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**依赖约束:**
|
||||||
|
- ✅ 允许:App → Common → Core → Vendor(严格单向)
|
||||||
|
- ❌ 禁止:反向依赖、跨层耦合
|
||||||
|
- ❌ 禁止:Common → App、Core → App/Common、App/Common → Vendor
|
||||||
|
|
||||||
|
### 3. 📁 目录结构规范
|
||||||
|
|
||||||
|
**标准目录结构:** [项目README - 目录结构](../readme.md#L155-210)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # 🏢 业务开发层
|
||||||
|
│ ├── demo/ # Demo 模块(标准模板示例)
|
||||||
|
│ └── index.ts # App 层统一导出
|
||||||
|
├── common/ # 🔧 框架通用服务层
|
||||||
|
│ ├── users/ # 用户管理服务
|
||||||
|
│ ├── rbac/ # 权限管理服务
|
||||||
|
│ ├── menu/ # 菜单管理服务
|
||||||
|
│ ├── settings/ # 系统设置服务
|
||||||
|
│ └── ...
|
||||||
|
├── core/ # 🏗️ 核心基础设施层
|
||||||
|
│ ├── auth/ # 认证核心
|
||||||
|
│ ├── database/ # 数据库核心
|
||||||
|
│ ├── validation/ # 验证核心
|
||||||
|
│ └── ...
|
||||||
|
├── vendor/ # 🔌 第三方服务适配层
|
||||||
|
│ ├── storage/ # 存储服务适配
|
||||||
|
│ ├── payment/ # 支付服务适配
|
||||||
|
│ └── ...
|
||||||
|
└── addons/ # 🧩 插件扩展层
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 💻 开发指南
|
||||||
|
|
||||||
|
**开发规范:** [项目README - 开发指南](../readme.md#L301-400)
|
||||||
|
|
||||||
|
**业务模块开发流程:**
|
||||||
|
1. **创建新模块**:参考 `src/app/demo` 模块结构
|
||||||
|
2. **遵循分层架构**:Controller → Service → Repository → Entity
|
||||||
|
3. **使用框架服务**:充分利用 Common 层提供的通用服务
|
||||||
|
4. **统一错误处理**:使用框架提供的异常处理机制
|
||||||
|
5. **API 文档**:使用 Swagger 注解生成 API 文档
|
||||||
|
6. **单元测试**:编写完整的单元测试和集成测试
|
||||||
|
|
||||||
|
**模块结构规范:**
|
||||||
|
```
|
||||||
|
your-module/
|
||||||
|
├── your-module.module.ts # 模块定义
|
||||||
|
├── controllers/ # 控制器层
|
||||||
|
├── services/ # 服务层
|
||||||
|
├── entities/ # 实体层
|
||||||
|
├── dto/ # 数据传输对象
|
||||||
|
├── repositories/ # 仓储层(可选)
|
||||||
|
├── interfaces/ # 接口定义(可选)
|
||||||
|
└── README.md # 模块文档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 🔧 代码规范
|
||||||
|
|
||||||
|
**代码风格:** [项目自定义指令 - 代码风格指南](./project_rules.md)
|
||||||
|
|
||||||
|
**核心约束:**
|
||||||
|
- **TypeScript 严格模式**:启用所有严格类型检查
|
||||||
|
- **ESLint + Prettier**:遵循代码格式化和质量检查
|
||||||
|
- **命名规范**:使用驼峰命名法和语义化命名
|
||||||
|
- **注释规范**:使用 JSDoc 格式编写注释
|
||||||
|
- **Git 提交规范**:使用 Conventional Commits 规范
|
||||||
|
|
||||||
|
**导入顺序:**
|
||||||
|
1. Node.js 内置模块
|
||||||
|
2. 第三方依赖(npm 包)
|
||||||
|
3. 项目内部模块
|
||||||
|
4. 父级目录(../)
|
||||||
|
5. 同级目录(./)
|
||||||
|
6. 索引文件(index)
|
||||||
|
|
||||||
|
### 6. 🔒 API 开发规范
|
||||||
|
|
||||||
|
**RESTful API 设计:** [项目README - API 开发规范](../readme.md#L340-365)
|
||||||
|
|
||||||
|
**控制器示例:**
|
||||||
|
```typescript
|
||||||
|
@Controller('users')
|
||||||
|
@ApiTags('用户管理')
|
||||||
|
export class UsersController {
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取用户列表' })
|
||||||
|
@ApiResponse({ status: 200, description: '成功获取用户列表' })
|
||||||
|
async findAll(@Query() query: QueryUserDto) {
|
||||||
|
return this.usersService.findAll(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: '创建用户' })
|
||||||
|
@ApiResponse({ status: 201, description: '用户创建成功' })
|
||||||
|
async create(@Body() createUserDto: CreateUserDto) {
|
||||||
|
return this.usersService.create(createUserDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. 🧪 测试规范
|
||||||
|
|
||||||
|
**测试指南:** [项目README - 测试](../readme.md#L370-400)
|
||||||
|
|
||||||
|
**测试要求:**
|
||||||
|
- **单元测试**:每个服务和控制器都应有对应的单元测试
|
||||||
|
- **集成测试**:测试模块间的集成功能
|
||||||
|
- **端到端测试**:测试完整的用户场景
|
||||||
|
- **测试覆盖率**:保持 80% 以上的代码覆盖率
|
||||||
|
|
||||||
|
### 8. 🚀 部署规范
|
||||||
|
|
||||||
|
**构建和部署:** [项目README - 构建和部署](../readme.md#L410-450)
|
||||||
|
|
||||||
|
**环境配置:** [项目README - 配置说明](../readme.md#L460-520)
|
||||||
|
|
||||||
|
### 9. 📚 参考文档链接
|
||||||
|
|
||||||
|
**核心文档:**
|
||||||
|
- [项目主README](../readme.md) - 完整的项目介绍和使用指南
|
||||||
|
- [WWJCloud数据库结构](../sql/wwjcloud.sql) - 主数据库表结构
|
||||||
|
- [WWJAuth数据库结构](../sql/wwjauth.sql) - 认证服务数据库
|
||||||
|
- [项目代码规范](./project_rules.md) - 详细的代码风格和开发约束
|
||||||
|
|
||||||
|
**前端文档:**
|
||||||
|
- [前端开发指南](../admin/docs/src/guide/essentials/development.md)
|
||||||
|
- [前端目录说明](../admin/docs/src/guide/project/dir.md)
|
||||||
|
|
||||||
|
**参考实现:**
|
||||||
|
- [NiuCloud PHP实现](../reference/niucloud-php/) - 参考架构和最佳实践
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 重要提醒
|
||||||
|
|
||||||
|
1. **严格遵循数据库表结构**:所有实体类必须与真实数据库表字段一一对应
|
||||||
|
2. **禁止假设和自创**:不允许创建数据库中不存在的字段或表
|
||||||
|
3. **遵循分层架构**:严格按照 App → Common → Core → Vendor 的依赖关系开发
|
||||||
|
4. **使用现有服务**:优先使用 Common 层已有的通用服务,避免重复造轮子
|
||||||
|
5. **保持代码一致性**:遵循项目既定的代码风格和命名规范
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 使用方式
|
||||||
|
|
||||||
|
**在每次开发对话开始时,请引用此文件:**
|
||||||
|
|
||||||
|
```
|
||||||
|
请严格按照开发约束规范进行开发:
|
||||||
|
/g:/wwjcloud-nestjs/.trae/rules/development_constraints.md
|
||||||
|
|
||||||
|
当前任务:[具体描述您的开发需求]
|
||||||
|
|
||||||
|
请确认您已理解上述规范,并严格按照真实的数据库表结构和项目架构进行开发。
|
||||||
|
```
|
||||||
|
|
||||||
|
通过引用此单一文件,AI助手将自动获取所有相关的开发约束和规范链接,确保开发过程的一致性和规范性。
|
||||||
2105
.trae/rules/project_rules.md
Normal file
2105
.trae/rules/project_rules.md
Normal file
File diff suppressed because it is too large
Load Diff
1
admin
Submodule
1
admin
Submodule
Submodule admin added at cf6c4c9aae
252
niucloud-frontend-migration-strategy.md
Normal file
252
niucloud-frontend-migration-strategy.md
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
# Niucloud 前端到 Vben Admin 迁移策略
|
||||||
|
|
||||||
|
## 📋 迁移方式对比分析
|
||||||
|
|
||||||
|
### 方式一:直接复制 + 转换规律
|
||||||
|
**优点:**
|
||||||
|
- 迁移速度快,保留原有业务逻辑
|
||||||
|
- 减少重新理解业务需求的时间
|
||||||
|
- 降低功能遗漏风险
|
||||||
|
|
||||||
|
**缺点:**
|
||||||
|
- 代码质量可能不够现代化
|
||||||
|
- 可能携带技术债务
|
||||||
|
- UI 组件使用方式需要大量调整
|
||||||
|
|
||||||
|
### 方式二:完全重写
|
||||||
|
**优点:**
|
||||||
|
- 代码质量更高,符合现代化标准
|
||||||
|
- 充分利用 Vben Admin 的架构优势
|
||||||
|
- 更好的类型安全和开发体验
|
||||||
|
|
||||||
|
**缺点:**
|
||||||
|
- 开发周期长
|
||||||
|
- 需要重新理解所有业务逻辑
|
||||||
|
- 功能遗漏风险较高
|
||||||
|
|
||||||
|
## 🎯 推荐策略:混合式迁移
|
||||||
|
|
||||||
|
基于代码分析,建议采用**混合式迁移策略**:
|
||||||
|
|
||||||
|
### 1. 核心架构层面:完全重写
|
||||||
|
- 使用 Vben Admin 的 `useVbenForm`、`useVbenTable` 等现代化 Hooks
|
||||||
|
- 采用 Composition API + TypeScript
|
||||||
|
- 遵循 Vben Admin 的目录结构和命名规范
|
||||||
|
|
||||||
|
### 2. 业务逻辑层面:复制 + 重构
|
||||||
|
- 保留核心业务逻辑(API 调用、数据处理、业务规则)
|
||||||
|
- 重构为符合 Vben Admin 规范的代码结构
|
||||||
|
- 优化错误处理和用户体验
|
||||||
|
|
||||||
|
### 3. UI 层面:重新设计
|
||||||
|
- 使用 Vben Admin 的组件体系
|
||||||
|
- 统一设计语言和交互规范
|
||||||
|
- 提升用户体验和视觉效果
|
||||||
|
|
||||||
|
## 🔄 具体转换规律
|
||||||
|
|
||||||
|
### Template 层转换
|
||||||
|
|
||||||
|
#### Niucloud 原始结构:
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="main-container">
|
||||||
|
<el-card class="box-card !border-none" shadow="never">
|
||||||
|
<el-table :data="userTableData.data" v-loading="userTableData.loading">
|
||||||
|
<!-- 表格列定义 -->
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="userTableData.page"
|
||||||
|
v-model:page-size="userTableData.limit"
|
||||||
|
:total="userTableData.total" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vben Admin 目标结构:
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<Page :title="pageName">
|
||||||
|
<VbenTable @register="registerTable">
|
||||||
|
<template #toolbar>
|
||||||
|
<ElButton type="primary" @click="handleAdd">
|
||||||
|
{{ t('common.add') }}
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</VbenTable>
|
||||||
|
</Page>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Script 层转换
|
||||||
|
|
||||||
|
#### Niucloud 原始结构:
|
||||||
|
```typescript
|
||||||
|
// Options API 风格,手动管理状态
|
||||||
|
const userTableData = reactive({
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
total: 0,
|
||||||
|
loading: true,
|
||||||
|
data: [],
|
||||||
|
searchParam: { search: '' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadUserList = (page: number = 1) => {
|
||||||
|
userTableData.loading = true
|
||||||
|
getUserList({
|
||||||
|
page: userTableData.page,
|
||||||
|
limit: userTableData.limit,
|
||||||
|
username: userTableData.searchParam.search
|
||||||
|
}).then(res => {
|
||||||
|
userTableData.data = res.data.data
|
||||||
|
userTableData.total = res.data.total
|
||||||
|
userTableData.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Vben Admin 目标结构:
|
||||||
|
```typescript
|
||||||
|
// 使用 Vben 的 Hook,自动管理状态
|
||||||
|
const [registerTable, { reload, getForm }] = useVbenTable({
|
||||||
|
api: getUserListApi,
|
||||||
|
columns: getColumns(),
|
||||||
|
formConfig: getFormConfig(),
|
||||||
|
useSearchForm: true,
|
||||||
|
actionColumn: {
|
||||||
|
width: 160,
|
||||||
|
title: t('common.action'),
|
||||||
|
dataIndex: 'action',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 目录结构映射
|
||||||
|
|
||||||
|
### Niucloud → Vben Admin 路径映射
|
||||||
|
|
||||||
|
```
|
||||||
|
Niucloud → Vben Admin
|
||||||
|
─────────────────────────────────────────────────────────────
|
||||||
|
app/views/auth/user.vue → common/system/auth/user/index.vue
|
||||||
|
app/views/auth/role.vue → common/system/auth/role/index.vue
|
||||||
|
app/views/auth/menu.vue → common/system/auth/menu/index.vue
|
||||||
|
app/views/setting/ → common/system/setting/
|
||||||
|
app/views/member/ → common/system/member/
|
||||||
|
app/views/auth/components/ → common/system/auth/components/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组件文件命名规范
|
||||||
|
|
||||||
|
```
|
||||||
|
Niucloud → Vben Admin
|
||||||
|
─────────────────────────────────────────────────────────────
|
||||||
|
edit-user.vue → user-modal.vue
|
||||||
|
user.vue → index.vue
|
||||||
|
components/edit-*.vue → components/*-modal.vue
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ 迁移实施步骤
|
||||||
|
|
||||||
|
### 阶段一:基础架构搭建(1-2天)
|
||||||
|
1. 创建目录结构:`common/system/auth/user/`
|
||||||
|
2. 设置基础路由配置
|
||||||
|
3. 创建基础页面框架
|
||||||
|
|
||||||
|
### 阶段二:核心功能迁移(3-5天)
|
||||||
|
1. **用户管理页面**
|
||||||
|
- 用户列表(表格 + 搜索 + 分页)
|
||||||
|
- 用户新增/编辑弹窗
|
||||||
|
- 用户状态管理(锁定/解锁/删除)
|
||||||
|
|
||||||
|
2. **角色管理页面**
|
||||||
|
- 角色列表管理
|
||||||
|
- 权限分配界面
|
||||||
|
|
||||||
|
3. **菜单管理页面**
|
||||||
|
- 菜单树形结构
|
||||||
|
- 菜单编辑功能
|
||||||
|
|
||||||
|
### 阶段三:高级功能迁移(2-3天)
|
||||||
|
1. 系统设置页面
|
||||||
|
2. 会员管理功能
|
||||||
|
3. 其他业务模块
|
||||||
|
|
||||||
|
### 阶段四:优化和测试(1-2天)
|
||||||
|
1. 代码优化和重构
|
||||||
|
2. 类型安全检查
|
||||||
|
3. 功能测试和 UI 调优
|
||||||
|
|
||||||
|
## 📋 迁移检查清单
|
||||||
|
|
||||||
|
### 功能完整性
|
||||||
|
- [ ] 所有 CRUD 操作正常
|
||||||
|
- [ ] 搜索和过滤功能
|
||||||
|
- [ ] 分页功能
|
||||||
|
- [ ] 表单验证
|
||||||
|
- [ ] 权限控制
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
- [ ] TypeScript 类型完整
|
||||||
|
- [ ] 组件复用性良好
|
||||||
|
- [ ] 错误处理完善
|
||||||
|
- [ ] 国际化支持
|
||||||
|
- [ ] 响应式设计
|
||||||
|
|
||||||
|
### 用户体验
|
||||||
|
- [ ] 加载状态提示
|
||||||
|
- [ ] 操作反馈
|
||||||
|
- [ ] 界面美观统一
|
||||||
|
- [ ] 交互流畅
|
||||||
|
|
||||||
|
## 🎨 UI/UX 改进建议
|
||||||
|
|
||||||
|
### 1. 统一设计语言
|
||||||
|
- 使用 Vben Admin 的设计规范
|
||||||
|
- 统一色彩、字体、间距
|
||||||
|
- 保持组件风格一致性
|
||||||
|
|
||||||
|
### 2. 交互体验优化
|
||||||
|
- 添加骨架屏加载效果
|
||||||
|
- 优化表单验证提示
|
||||||
|
- 增加操作确认和撤销功能
|
||||||
|
|
||||||
|
### 3. 响应式设计
|
||||||
|
- 适配移动端显示
|
||||||
|
- 优化大屏显示效果
|
||||||
|
- 支持暗色主题
|
||||||
|
|
||||||
|
## 🔧 技术栈对比
|
||||||
|
|
||||||
|
| 特性 | Niucloud | Vben Admin |
|
||||||
|
|------|----------|------------|
|
||||||
|
| 框架 | Vue 3 + Element Plus | Vue 3 + Element Plus + Vben |
|
||||||
|
| 状态管理 | Reactive API | Pinia + Vben Hooks |
|
||||||
|
| 类型安全 | 基础 TypeScript | 完整 TypeScript |
|
||||||
|
| 表单处理 | 手动管理 | useVbenForm Hook |
|
||||||
|
| 表格处理 | 手动管理 | useVbenTable Hook |
|
||||||
|
| 路由管理 | Vue Router | Vue Router + 权限路由 |
|
||||||
|
| 国际化 | 基础 i18n | 完整 i18n 方案 |
|
||||||
|
|
||||||
|
## 📈 预期收益
|
||||||
|
|
||||||
|
### 开发效率提升
|
||||||
|
- 减少 60% 的重复代码
|
||||||
|
- 提升 40% 的开发速度
|
||||||
|
- 降低 50% 的维护成本
|
||||||
|
|
||||||
|
### 用户体验改善
|
||||||
|
- 更现代化的 UI 设计
|
||||||
|
- 更流畅的交互体验
|
||||||
|
- 更好的响应式支持
|
||||||
|
|
||||||
|
### 代码质量提升
|
||||||
|
- 更好的类型安全
|
||||||
|
- 更规范的代码结构
|
||||||
|
- 更完善的错误处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结:建议采用混合式迁移策略,既保留业务逻辑的完整性,又充分利用 Vben Admin 的现代化架构优势,实现高效、高质量的前端迁移。**
|
||||||
935
readme.md
Normal file
935
readme.md
Normal file
@@ -0,0 +1,935 @@
|
|||||||
|
# WWJ Cloud 企业级框架 - NestJS + VbenAdmin 实现
|
||||||
|
|
||||||
|
> 一款支持插件化+云安装+云编译 快速开发SAAS多用户系统后台管理框架!
|
||||||
|
|
||||||
|
使用 WWJ Cloud 企业级框架,我们开发一个软件系统,**一切插件化**!!!= WWJ Cloud 框架 + 应用1 + 应用2 + 应用N + 插件1 + 插件2 + 插件N + ...
|
||||||
|
|
||||||
|
如果对您有帮助,您可以点右上角 ⭐"Star" 收藏一下,获取第一时间更新,谢谢!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 框架介绍
|
||||||
|
|
||||||
|
WWJ Cloud 企业级框架是一款快速开发 SaaS 通用管理系统后台框架,基于 **NestJS + TypeORM + Redis + MySQL** 后端技术架构和 **VbenAdmin + Vue3 + TypeScript + Element Plus** 前端技术栈精心设计,易读易懂,没有任何其它重度依赖,架构设计小巧灵活,没有采用过度设计模式。是一款快速可以开发企业级应用的软件系统。
|
||||||
|
|
||||||
|
**【您不需要重复造轮子 – 框架内置已经实现基础组件功能,您只需要开发业务模块即可】!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 快速链接
|
||||||
|
|
||||||
|
- **Gitee 下载地址**:https://gitee.com/wwjauth/wwjcloud-nestjs
|
||||||
|
- **GitHub 下载地址**:https://github.com/wwjauth/wwjcloud-nestjs
|
||||||
|
- **演示地址**:http://demo.wwjauth.com/admin/ (账号:admin 密码:123456)
|
||||||
|
- **文档地址**:https://docs.wwjauth.com
|
||||||
|
- **云应用市场**:https://market.wwjauth.com
|
||||||
|
- **VbenAdmin 官网**:https://vben.pro
|
||||||
|
- **NestJS 官网**:https://nestjs.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 WWJ Cloud 开发者生态圈
|
||||||
|
|
||||||
|
WWJ Cloud 框架,目前已经实现有 **NestJS + VbenAdmin** 版本功能实现。整个 WWJ Cloud 开发者生态圈目前已经有众多用户。其中开发者上千人。WWJ Cloud 生态圈众多代理商、经销商、中介商都会采购插件及应用,自己运营或者分销给第三方商家用户。您只需要用心开发插件或应用,并发布到 WWJ Cloud 云应用市场,即会有人购买。依靠 WWJ Cloud 强大的生态圈,实现市场、资源、产品的研发销售闭环。从今天开始,加入 WWJ Cloud 生态圈,实现程序员创业梦想!付出就有回报。心动不如行动!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 设计理念
|
||||||
|
|
||||||
|
### 强大的多应用+插件组合设计理念,低耦合,高内聚
|
||||||
|
|
||||||
|
基于**企业级框架底层设计**,采用**分层架构 + 领域驱动设计**,实现:
|
||||||
|
- **清晰的依赖层次**:`App(业务开发层)` → `Common(框架通用服务层)` → `Core(核心基础设施层)` → `Vendor(第三方适配层)`
|
||||||
|
- **框架化设计**:Common 层提供完整的企业级通用服务,App 层专注用户业务开发
|
||||||
|
- **高内聚低耦合**:每层职责明确,接口清晰,支持插拔式扩展
|
||||||
|
- **可扩展性**:支持 Addons 插件化扩展和微服务拆分
|
||||||
|
- **企业级特性**:完整的配置系统、监控、日志、安全机制
|
||||||
|
- **微服务就绪**:为未来微服务架构演进奠定基础
|
||||||
|
|
||||||
|
### 全新生态设计,多应用聚合+多插件组合运营模式全新升级
|
||||||
|
|
||||||
|
支持共同会员体系下多种应用+插件组合,DIY装修出最强的软件系统。
|
||||||
|
|
||||||
|
### 插件化,完全为开发者二次开发而生
|
||||||
|
|
||||||
|
WWJ Cloud 框架采用插件化模式设计,可以做到多种插件共存,组合使用。**一切皆为插件(应用)!** 比如您有一个项目是电商的项目,这个项目的要求是,既有商城的功能,又有CRM客户管理,还需要进行会员的管理,甚至于还要客服系统。传统的实现方式是,找多个源码,东拼西凑,二次开发,或者部署多套独立的系统,配合起来。而今天,使用 WWJ Cloud 框架,可以通过组装的方式,在一套体系中实现,随着发展,会有越来越多的各行各业的插件和应用上架。您对于项目的定制,可能只需要简单组装,装修页面,就可以最终实现功能交付。
|
||||||
|
|
||||||
|
### 首创强大的一键云安装,云编译,云发布,升级引擎
|
||||||
|
|
||||||
|
- WWJ Cloud 框架内置简单方便的一键云安装,云编译工具
|
||||||
|
- WWJ Cloud 内置在线升级功能,系统会全自动化帮您升级文件。产品的更新只需一键完成
|
||||||
|
- VSCode,WebStorm,微信小程序开发工具,打包,上传,发布!WWJ Cloud 框架强大的小程序一键傻瓜式发布系统,任何开发环境都不再需要搭建!鼠标一点完成小程序升级发布
|
||||||
|
|
||||||
|
### 🏗️ SaaS + 独立版双架构设计
|
||||||
|
|
||||||
|
WWJ Cloud 框架采用创新的 **SaaS + 独立版双架构设计**,一套代码同时支持两种部署模式:
|
||||||
|
|
||||||
|
#### 🔄 SaaS 多租户模式
|
||||||
|
- **适用场景**:云服务商、多客户管理系统
|
||||||
|
- **架构特点**:通过 `site_id` 字段实现多租户数据隔离
|
||||||
|
- **数据隔离**:每个租户拥有独立的 `site_id`,数据完全隔离
|
||||||
|
- **资源共享**:共享系统基础设施,降低运营成本
|
||||||
|
- **扩展性**:支持无限租户扩展,满足 SaaS 服务商需求
|
||||||
|
|
||||||
|
#### 🏢 独立版部署模式
|
||||||
|
- **适用场景**:企业内部系统、单客户项目、私有化部署
|
||||||
|
- **架构特点**:所有数据 `site_id` 统一为 0
|
||||||
|
- **数据独立**:完全独立的数据环境,无租户概念
|
||||||
|
- **部署灵活**:支持私有化部署,数据完全自主可控
|
||||||
|
- **定制化**:可根据客户需求进行深度定制
|
||||||
|
|
||||||
|
#### 🔧 技术实现
|
||||||
|
```sql
|
||||||
|
-- 数据库表结构示例
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
site_id INT NOT NULL DEFAULT 0, -- 租户ID,0表示独立版
|
||||||
|
username VARCHAR(50) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_site_id (site_id) -- 租户索引,提升查询性能
|
||||||
|
);
|
||||||
|
|
||||||
|
-- SaaS模式:每个租户有独立的site_id
|
||||||
|
INSERT INTO users (site_id, username, email) VALUES (1, 'user1', 'user1@tenant1.com');
|
||||||
|
INSERT INTO users (site_id, username, email) VALUES (2, 'user2', 'user2@tenant2.com');
|
||||||
|
|
||||||
|
-- 独立版模式:所有数据site_id为0
|
||||||
|
INSERT INTO users (site_id, username, email) VALUES (0, 'admin', 'admin@company.com');
|
||||||
|
INSERT INTO users (site_id, username, email) VALUES (0, 'user', 'user@company.com');
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🎯 架构优势
|
||||||
|
- **一套代码,两种模式**:无需维护两套代码,降低开发成本
|
||||||
|
- **平滑切换**:可在 SaaS 和独立版之间平滑切换
|
||||||
|
- **数据安全**:多租户数据完全隔离,独立版数据完全自主
|
||||||
|
- **部署灵活**:支持云部署和私有化部署
|
||||||
|
- **成本优化**:SaaS 模式资源共享,独立版模式完全控制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 依赖关系图
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ App │ ← 业务开发层(用户自定义业务模块)
|
||||||
|
│ (用户业务) │ 电商、CRM、ERP等具体业务逻辑
|
||||||
|
└─────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Common │ ← 框架通用服务层(企业级通用功能)
|
||||||
|
│ (框架通用服务) │ 用户管理、权限管理、菜单管理
|
||||||
|
└─────────────────┘ 文件上传、通知服务、系统设置
|
||||||
|
↓ 数据字典、缓存服务、队列服务
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Core │ ← 核心基础设施层(底层基础设施)
|
||||||
|
│ (基础设施) │ 认证核心、数据库核心、验证核心
|
||||||
|
└─────────────────┘ HTTP核心、缓存核心、队列核心
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Vendor │ ← 第三方服务适配层
|
||||||
|
│ (外部集成) │ 存储、支付、通信、云服务适配
|
||||||
|
└─────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Addons │ ← 插件扩展层(可插拔功能模块)
|
||||||
|
│ (插件扩展) │ 扩展框架功能,不影响核心稳定性
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 技术亮点
|
||||||
|
|
||||||
|
### 🏗️ 后端技术栈(NestJS 生态)
|
||||||
|
|
||||||
|
- **NestJS + TypeORM + MySQL8**:采用 SaaS + 独立版双架构设计,支持多租户 SaaS 模式和独立部署模式,通过 `site_id` 字段区分租户,当 `site_id = 0` 时为独立版模式,能够提供企业级软件服务运营,同时满足用户多站点,多商户,多门店等系统开发需求
|
||||||
|
- **严格的 RESTful API 设计**:后端开发采用严格的 RESTful 的 API 设计开发,支持多语言设计开发
|
||||||
|
- **Redis 分布式缓存**:高性能缓存系统,支持集群部署
|
||||||
|
- **Bull 队列系统**:可靠的消息队列处理,支持任务调度和异步处理
|
||||||
|
- **JWT + RBAC 权限系统**:完整的认证授权体系
|
||||||
|
- **Winston 日志系统**:结构化日志记录和监控
|
||||||
|
- **Swagger API 文档**:自动生成 API 文档,支持在线调试
|
||||||
|
|
||||||
|
### 🎨 前端技术栈(VbenAdmin 生态)
|
||||||
|
|
||||||
|
- **VbenAdmin + Vue3 + TypeScript + Vite**:采用最新的前端技术栈,VbenAdmin 是基于 Vue3、Vite、TypeScript 的现代化管理系统
|
||||||
|
- **Element Plus UI 组件库**:丰富的企业级 UI 组件,开发者不需要详细了解前端,只需要用标准的 Element 组件就可以
|
||||||
|
- **Pinia 状态管理**:现代化的 Vue 状态管理方案
|
||||||
|
- **Vue Router 路由管理**:支持动态路由和权限控制
|
||||||
|
- **Tailwind CSS**:原子化 CSS 框架,快速构建现代化界面
|
||||||
|
- **i18n 国际化**:支持多语言切换,真正意义上实现多语言的开发需求
|
||||||
|
- **响应式设计**:支持桌面端、平板端、移动端自适应
|
||||||
|
|
||||||
|
### 🏢 企业级特性
|
||||||
|
|
||||||
|
- **SaaS + 独立版双架构支持**:支持多租户 SaaS 架构和独立部署模式,通过 `site_id` 字段区分,当 `site_id = 0` 时为独立版模式
|
||||||
|
- **多租户 SaaS 架构**:支持多租户隔离,满足 SaaS 服务商需求,每个租户拥有独立的 `site_id`
|
||||||
|
- **独立版部署**:支持独立部署模式,适用于企业内部系统或单客户项目,所有数据 `site_id` 统一为 0
|
||||||
|
- **微服务就绪**:分层架构设计,支持未来微服务拆分
|
||||||
|
- **插件化扩展**:Addons 插件系统,支持功能模块热插拔
|
||||||
|
- **云原生部署**:支持 Docker 容器化部署和 Kubernetes 编排
|
||||||
|
- **监控告警**:完整的系统监控和告警机制
|
||||||
|
- **数据备份**:自动化数据备份和恢复机制
|
||||||
|
- **安全防护**:SQL 注入防护、XSS 防护、CSRF 防护等
|
||||||
|
|
||||||
|
### 🌐 多语言支持
|
||||||
|
|
||||||
|
WWJ Cloud 前端以及后端采用严格的多语言开发规范,包括前端展示,API 接口返回,数据验证,错误返回等全部使用多语言设计规范,使开发者能够真正意义上实现多语言的开发需求。
|
||||||
|
|
||||||
|
### 🛠️ 开发者友好
|
||||||
|
|
||||||
|
WWJ Cloud 结合当前市面上很多框架结构不规范,导致基础结构不稳定等情况,严格定义了分层设计的开发规范,同时 API 接口严格采用 RESTful 的开发规范,能够满足大型业务系统或者微服务的开发需求。
|
||||||
|
|
||||||
|
### 📦 内置功能模块
|
||||||
|
|
||||||
|
WWJ Cloud 已经搭建好常规系统的开发底层,具体功能包括:
|
||||||
|
- **管理员管理**:完整的管理员账户体系
|
||||||
|
- **权限管理**:基于 RBAC 的权限控制系统
|
||||||
|
- **菜单管理**:动态菜单配置和权限绑定
|
||||||
|
- **用户管理**:前台用户管理和会员体系
|
||||||
|
- **应用管理**:多应用管理和配置
|
||||||
|
- **文件管理**:文件上传、存储和管理
|
||||||
|
- **系统设置**:灵活的系统配置管理
|
||||||
|
- **数据字典**:系统数据字典管理
|
||||||
|
- **通知消息**:站内消息和推送通知
|
||||||
|
- **操作日志**:完整的操作审计日志
|
||||||
|
- **定时任务**:计划任务管理和调度
|
||||||
|
- **API 接口**:对外开放接口管理
|
||||||
|
- **健康检查**:系统健康状态监控
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 项目目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── app/ # 🏢 业务开发层(用户自定义业务模块)
|
||||||
|
│ ├── demo/ # Demo 模块(标准模板示例)
|
||||||
|
│ │ ├── demo.module.ts
|
||||||
|
│ │ ├── controllers/
|
||||||
|
│ │ │ └── demo.controller.ts
|
||||||
|
│ │ ├── services/
|
||||||
|
│ │ │ └── demo.service.ts
|
||||||
|
│ │ ├── entities/
|
||||||
|
│ │ │ └── demo.entity.ts
|
||||||
|
│ │ ├── dto/
|
||||||
|
│ │ │ └── demo.dto.ts
|
||||||
|
│ │ └── README.md # 模块开发指南
|
||||||
|
│ └── index.ts # App 层统一导出
|
||||||
|
│
|
||||||
|
├── common/ # 🔧 框架通用服务层(企业级通用功能)
|
||||||
|
│ ├── users/ # 用户管理服务
|
||||||
|
│ ├── rbac/ # 权限管理服务
|
||||||
|
│ ├── menu/ # 菜单管理服务
|
||||||
|
│ ├── apps/ # 应用管理服务
|
||||||
|
│ ├── upload/ # 文件上传服务
|
||||||
|
│ ├── notification/ # 通知服务
|
||||||
|
│ ├── settings/ # 系统设置服务
|
||||||
|
│ ├── dictionary/ # 数据字典服务
|
||||||
|
│ ├── cache/ # 缓存服务
|
||||||
|
│ ├── queue/ # 队列服务
|
||||||
|
│ ├── health/ # 健康检查服务
|
||||||
|
│ └── openapi/ # 对外开放接口服务
|
||||||
|
│
|
||||||
|
├── config/ # ⚙️ 配置层(运行时配置)
|
||||||
|
│ ├── env/ # 环境配置
|
||||||
|
│ ├── database/ # 数据库配置
|
||||||
|
│ ├── cache/ # 缓存配置
|
||||||
|
│ ├── queue/ # 队列配置
|
||||||
|
│ ├── http/ # HTTP 配置
|
||||||
|
│ ├── security/ # 安全配置
|
||||||
|
│ ├── logger/ # 日志配置
|
||||||
|
│ └── third-party/ # 第三方服务配置
|
||||||
|
│
|
||||||
|
├── core/ # 🏗️ 核心基础设施层(跨域通用)
|
||||||
|
│ ├── auth/ # 认证核心
|
||||||
|
│ ├── database/ # 数据库核心
|
||||||
|
│ ├── validation/ # 验证核心
|
||||||
|
│ ├── http/ # HTTP 核心
|
||||||
|
│ ├── cache/ # 缓存核心
|
||||||
|
│ ├── queue/ # 队列核心
|
||||||
|
│ ├── logger/ # 日志核心
|
||||||
|
│ ├── monitoring/ # 监控核心
|
||||||
|
│ └── exceptions/ # 异常处理核心
|
||||||
|
│
|
||||||
|
├── vendor/ # 🔌 第三方服务适配层
|
||||||
|
│ ├── storage/ # 存储服务适配
|
||||||
|
│ ├── payment/ # 支付服务适配
|
||||||
|
│ ├── communication/ # 通信服务适配
|
||||||
|
│ ├── sms/ # 短信服务适配
|
||||||
|
│ ├── email/ # 邮件服务适配
|
||||||
|
|
||||||
|
│
|
||||||
|
├── addons/ # 🧩 插件扩展层(可插拔功能模块)
|
||||||
|
│ └── README.md # 插件开发指南
|
||||||
|
│
|
||||||
|
├── app.module.ts # 根模块
|
||||||
|
└── main.ts # 应用入口
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
|
||||||
|
- **Node.js** >= 18.0.0
|
||||||
|
- **npm** >= 8.0.0 或 **pnpm** >= 7.0.0
|
||||||
|
- **MySQL** >= 8.0 或 **PostgreSQL** >= 13
|
||||||
|
- **Redis** >= 6.0
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 npm
|
||||||
|
$ npm install
|
||||||
|
|
||||||
|
# 或使用 pnpm(推荐)
|
||||||
|
$ pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境配置
|
||||||
|
|
||||||
|
1. 复制环境配置文件:
|
||||||
|
```bash
|
||||||
|
$ cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 配置数据库连接、Redis 连接等必要参数:
|
||||||
|
```bash
|
||||||
|
# 数据库配置
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
DB_DATABASE=wwjauth
|
||||||
|
|
||||||
|
# Redis 配置
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
JWT_SECRET=your_jwt_secret_key
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库迁移
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行数据库迁移
|
||||||
|
$ npm run migration:run
|
||||||
|
|
||||||
|
# 填充种子数据
|
||||||
|
$ npm run seed:run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 启动应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 开发模式
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# 生产模式
|
||||||
|
$ npm run start:prod
|
||||||
|
|
||||||
|
# 调试模式
|
||||||
|
$ npm run start:debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 访问应用
|
||||||
|
|
||||||
|
- **后端 API**:http://localhost:3000
|
||||||
|
- **API 文档**:http://localhost:3000/api
|
||||||
|
- **健康检查**:http://localhost:3000/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 开发指南
|
||||||
|
|
||||||
|
### 业务模块开发
|
||||||
|
|
||||||
|
1. **创建新模块**:参考 `src/app/demo` 模块结构
|
||||||
|
2. **遵循分层架构**:Controller → Service → Repository → Entity
|
||||||
|
3. **使用框架服务**:充分利用 Common 层提供的通用服务
|
||||||
|
4. **统一错误处理**:使用框架提供的异常处理机制
|
||||||
|
5. **API 文档**:使用 Swagger 注解生成 API 文档
|
||||||
|
6. **单元测试**:编写完整的单元测试和集成测试
|
||||||
|
|
||||||
|
### 🏗️ 多租户架构开发规范
|
||||||
|
|
||||||
|
#### 📋 数据库设计规范
|
||||||
|
- **必须包含 `site_id` 字段**:所有业务表都必须包含 `site_id` 字段
|
||||||
|
- **默认值设置**:`site_id` 字段默认值为 0,表示独立版模式
|
||||||
|
- **索引优化**:为 `site_id` 字段创建索引,提升查询性能
|
||||||
|
- **外键约束**:跨表关联时需要考虑租户隔离
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 标准表结构示例
|
||||||
|
CREATE TABLE products (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
site_id INT NOT NULL DEFAULT 0, -- 租户ID,0表示独立版
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
price DECIMAL(10,2) NOT NULL,
|
||||||
|
status TINYINT DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
INDEX idx_site_id (site_id), -- 租户索引
|
||||||
|
INDEX idx_site_status (site_id, status) -- 复合索引
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔧 实体类开发规范
|
||||||
|
```typescript
|
||||||
|
// src/app/demo/entities/demo.entity.ts
|
||||||
|
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('demo')
|
||||||
|
export class DemoEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ name: 'site_id', type: 'int', default: 0, comment: '租户ID,0表示独立版' })
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@Column({ name: 'name', type: 'varchar', length: 100, comment: '名称' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ name: 'status', type: 'tinyint', default: 1, comment: '状态:1启用,0禁用' })
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', comment: '创建时间' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', comment: '更新时间' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🛠️ 服务层开发规范
|
||||||
|
```typescript
|
||||||
|
// src/app/demo/services/demo.service.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { DemoEntity } from '../entities/demo.entity';
|
||||||
|
import { CreateDemoDto } from '../dto/create-demo.dto';
|
||||||
|
import { UpdateDemoDto } from '../dto/update-demo.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DemoService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(DemoEntity)
|
||||||
|
private readonly demoRepository: Repository<DemoEntity>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// 创建时自动设置租户ID
|
||||||
|
async create(createDemoDto: CreateDemoDto, siteId: number = 0): Promise<DemoEntity> {
|
||||||
|
const demo = this.demoRepository.create({
|
||||||
|
...createDemoDto,
|
||||||
|
siteId, // 自动设置租户ID
|
||||||
|
});
|
||||||
|
return this.demoRepository.save(demo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询时自动过滤租户数据
|
||||||
|
async findAll(siteId: number = 0): Promise<DemoEntity[]> {
|
||||||
|
return this.demoRepository.find({
|
||||||
|
where: { siteId },
|
||||||
|
order: { createdAt: 'DESC' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据ID查询时验证租户权限
|
||||||
|
async findOne(id: number, siteId: number = 0): Promise<DemoEntity> {
|
||||||
|
const demo = await this.demoRepository.findOne({
|
||||||
|
where: { id, siteId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!demo) {
|
||||||
|
throw new NotFoundException('数据不存在或无权限访问');
|
||||||
|
}
|
||||||
|
|
||||||
|
return demo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新时验证租户权限
|
||||||
|
async update(id: number, updateDemoDto: UpdateDemoDto, siteId: number = 0): Promise<DemoEntity> {
|
||||||
|
const demo = await this.findOne(id, siteId);
|
||||||
|
|
||||||
|
Object.assign(demo, updateDemoDto);
|
||||||
|
return this.demoRepository.save(demo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除时验证租户权限
|
||||||
|
async remove(id: number, siteId: number = 0): Promise<void> {
|
||||||
|
const demo = await this.findOne(id, siteId);
|
||||||
|
await this.demoRepository.remove(demo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🎯 控制器层开发规范
|
||||||
|
```typescript
|
||||||
|
// src/app/demo/controllers/demo.controller.ts
|
||||||
|
import { Controller, Get, Post, Body, Param, Delete, UseGuards, Req } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { DemoService } from '../services/demo.service';
|
||||||
|
import { CreateDemoDto } from '../dto/create-demo.dto';
|
||||||
|
import { UpdateDemoDto } from '../dto/update-demo.dto';
|
||||||
|
import { JwtAuthGuard } from '../../common/auth/guards/jwt-auth.guard';
|
||||||
|
|
||||||
|
@Controller('demo')
|
||||||
|
@ApiTags('Demo管理')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class DemoController {
|
||||||
|
constructor(private readonly demoService: DemoService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: '创建Demo' })
|
||||||
|
async create(@Body() createDemoDto: CreateDemoDto, @Req() req: any) {
|
||||||
|
// 从JWT token中获取租户ID,独立版默认为0
|
||||||
|
const siteId = req.user?.siteId || 0;
|
||||||
|
return this.demoService.create(createDemoDto, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取Demo列表' })
|
||||||
|
async findAll(@Req() req: any) {
|
||||||
|
const siteId = req.user?.siteId || 0;
|
||||||
|
return this.demoService.findAll(siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: '根据ID获取Demo' })
|
||||||
|
async findOne(@Param('id') id: string, @Req() req: any) {
|
||||||
|
const siteId = req.user?.siteId || 0;
|
||||||
|
return this.demoService.findOne(+id, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@ApiOperation({ summary: '更新Demo' })
|
||||||
|
async update(@Param('id') id: string, @Body() updateDemoDto: UpdateDemoDto, @Req() req: any) {
|
||||||
|
const siteId = req.user?.siteId || 0;
|
||||||
|
return this.demoService.update(+id, updateDemoDto, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: '删除Demo' })
|
||||||
|
async remove(@Param('id') id: string, @Req() req: any) {
|
||||||
|
const siteId = req.user?.siteId || 0;
|
||||||
|
return this.demoService.remove(+id, siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🔐 认证授权规范
|
||||||
|
```typescript
|
||||||
|
// src/common/auth/strategies/jwt.strategy.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: process.env.JWT_SECRET,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: any) {
|
||||||
|
return {
|
||||||
|
userId: payload.sub,
|
||||||
|
username: payload.username,
|
||||||
|
siteId: payload.siteId || 0, // 租户ID,默认为0(独立版)
|
||||||
|
roles: payload.roles,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 📊 数据迁移规范
|
||||||
|
```typescript
|
||||||
|
// src/migrations/1234567890-AddSiteIdToDemo.ts
|
||||||
|
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddSiteIdToDemo1234567890 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// 添加site_id字段
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
'demo',
|
||||||
|
new TableColumn({
|
||||||
|
name: 'site_id',
|
||||||
|
type: 'int',
|
||||||
|
default: 0,
|
||||||
|
comment: '租户ID,0表示独立版',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 创建索引
|
||||||
|
await queryRunner.createIndex('demo', 'idx_site_id', ['site_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn('demo', 'site_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 模块结构规范
|
||||||
|
|
||||||
|
```
|
||||||
|
your-module/
|
||||||
|
├── your-module.module.ts # 模块定义
|
||||||
|
├── controllers/ # 控制器层
|
||||||
|
│ └── your-module.controller.ts
|
||||||
|
├── services/ # 服务层
|
||||||
|
│ └── your-module.service.ts
|
||||||
|
├── entities/ # 实体层
|
||||||
|
│ └── your-module.entity.ts
|
||||||
|
├── dto/ # 数据传输对象
|
||||||
|
│ ├── create-your-module.dto.ts
|
||||||
|
│ ├── update-your-module.dto.ts
|
||||||
|
│ └── query-your-module.dto.ts
|
||||||
|
├── repositories/ # 仓储层(可选)
|
||||||
|
│ └── your-module.repository.ts
|
||||||
|
├── interfaces/ # 接口定义(可选)
|
||||||
|
│ └── your-module.interface.ts
|
||||||
|
└── README.md # 模块文档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
|
||||||
|
- **TypeScript 严格模式**:启用所有严格类型检查
|
||||||
|
- **ESLint + Prettier**:遵循代码格式化和质量检查
|
||||||
|
- **命名规范**:使用驼峰命名法和语义化命名
|
||||||
|
- **注释规范**:使用 JSDoc 格式编写注释
|
||||||
|
- **Git 提交规范**:使用 Conventional Commits 规范
|
||||||
|
|
||||||
|
### API 开发规范
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 控制器示例
|
||||||
|
@Controller('users')
|
||||||
|
@ApiTags('用户管理')
|
||||||
|
export class UsersController {
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取用户列表' })
|
||||||
|
@ApiResponse({ status: 200, description: '成功获取用户列表' })
|
||||||
|
async findAll(@Query() query: QueryUserDto) {
|
||||||
|
return this.usersService.findAll(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: '创建用户' })
|
||||||
|
@ApiResponse({ status: 201, description: '用户创建成功' })
|
||||||
|
async create(@Body() createUserDto: CreateUserDto) {
|
||||||
|
return this.usersService.create(createUserDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 单元测试
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# 端到端测试
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# 测试覆盖率
|
||||||
|
$ npm run test:cov
|
||||||
|
|
||||||
|
# 监听模式测试
|
||||||
|
$ npm run test:watch
|
||||||
|
|
||||||
|
# 调试模式测试
|
||||||
|
$ npm run test:debug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试规范
|
||||||
|
|
||||||
|
- **单元测试**:每个服务和控制器都应有对应的单元测试
|
||||||
|
- **集成测试**:测试模块间的集成功能
|
||||||
|
- **端到端测试**:测试完整的用户场景
|
||||||
|
- **测试覆盖率**:保持 80% 以上的代码覆盖率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 构建和部署
|
||||||
|
|
||||||
|
### 本地构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建生产版本
|
||||||
|
$ npm run build
|
||||||
|
|
||||||
|
# 构建并启动
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建 Docker 镜像
|
||||||
|
$ docker build -t wwjauth/wwjcloud-nestjs .
|
||||||
|
|
||||||
|
# 运行容器
|
||||||
|
$ docker run -p 3000:3000 wwjauth/wwjcloud-nestjs
|
||||||
|
|
||||||
|
# 使用 Docker Compose
|
||||||
|
$ docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境部署
|
||||||
|
|
||||||
|
1. **环境准备**:配置生产环境变量
|
||||||
|
2. **数据库迁移**:运行数据库迁移脚本
|
||||||
|
3. **应用启动**:使用 PM2 进程管理器启动应用
|
||||||
|
4. **反向代理**:配置 Nginx 反向代理
|
||||||
|
5. **SSL 证书**:配置 HTTPS 证书
|
||||||
|
6. **监控告警**:配置系统监控和告警
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PM2 部署
|
||||||
|
$ pm2 start ecosystem.config.js --env production
|
||||||
|
|
||||||
|
# Nginx 配置示例
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name your-domain.com;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 配置说明
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
| 变量名 | 描述 | 默认值 | 必填 |
|
||||||
|
|--------|------|--------|------|
|
||||||
|
| `NODE_ENV` | 运行环境 | `development` | ❌ |
|
||||||
|
| `PORT` | 服务端口 | `3000` | ❌ |
|
||||||
|
| `DB_HOST` | 数据库主机 | `localhost` | ✅ |
|
||||||
|
| `DB_PORT` | 数据库端口 | `3306` | ❌ |
|
||||||
|
| `DB_USERNAME` | 数据库用户名 | - | ✅ |
|
||||||
|
| `DB_PASSWORD` | 数据库密码 | - | ✅ |
|
||||||
|
| `DB_DATABASE` | 数据库名称 | - | ✅ |
|
||||||
|
| `REDIS_HOST` | Redis 主机 | `localhost` | ✅ |
|
||||||
|
| `REDIS_PORT` | Redis 端口 | `6379` | ❌ |
|
||||||
|
| `REDIS_PASSWORD` | Redis 密码 | - | ❌ |
|
||||||
|
| `JWT_SECRET` | JWT 密钥 | - | ✅ |
|
||||||
|
| `JWT_EXPIRES_IN` | JWT 过期时间 | `7d` | ❌ |
|
||||||
|
| `UPLOAD_PATH` | 文件上传路径 | `./uploads` | ❌ |
|
||||||
|
| `LOG_LEVEL` | 日志级别 | `info` | ❌ |
|
||||||
|
|
||||||
|
### 功能特性配置
|
||||||
|
|
||||||
|
- **认证系统**:JWT + RBAC 权限控制,支持多租户隔离
|
||||||
|
- **文件上传**:支持本地存储、阿里云 OSS、腾讯云 COS、AWS S3
|
||||||
|
- **缓存系统**:Redis 分布式缓存,支持集群模式
|
||||||
|
- **队列系统**:Bull 队列处理,支持任务调度和重试机制
|
||||||
|
- **日志系统**:Winston 结构化日志,支持多种输出格式
|
||||||
|
- **监控系统**:健康检查、性能监控、错误追踪
|
||||||
|
- **API 文档**:Swagger 自动生成,支持在线调试
|
||||||
|
- **国际化**:支持多语言切换,前后端统一
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 API 文档
|
||||||
|
|
||||||
|
启动应用后,访问以下地址查看 API 文档:
|
||||||
|
|
||||||
|
- **Swagger UI**:http://localhost:3000/api
|
||||||
|
- **ReDoc**:http://localhost:3000/api-docs
|
||||||
|
- **OpenAPI JSON**:http://localhost:3000/api-json
|
||||||
|
|
||||||
|
### API 规范
|
||||||
|
|
||||||
|
- **RESTful 设计**:遵循 REST 架构风格
|
||||||
|
- **统一响应格式**:标准化的 API 响应结构
|
||||||
|
- **错误处理**:统一的错误码和错误信息
|
||||||
|
- **分页查询**:标准化的分页参数和响应
|
||||||
|
- **版本控制**:支持 API 版本管理
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 统一响应格式
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"data": {},
|
||||||
|
"timestamp": "2024-01-01T00:00:00.000Z",
|
||||||
|
"path": "/api/users"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 插件开发
|
||||||
|
|
||||||
|
### 插件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
addons/your-plugin/
|
||||||
|
├── package.json # 插件配置
|
||||||
|
├── plugin.config.ts # 插件配置文件
|
||||||
|
├── src/
|
||||||
|
│ ├── controllers/ # 控制器
|
||||||
|
│ ├── services/ # 服务
|
||||||
|
│ ├── entities/ # 实体
|
||||||
|
│ └── dto/ # DTO
|
||||||
|
├── migrations/ # 数据库迁移
|
||||||
|
├── seeds/ # 种子数据
|
||||||
|
└── README.md # 插件文档
|
||||||
|
```
|
||||||
|
|
||||||
|
### 插件开发指南
|
||||||
|
|
||||||
|
1. **创建插件**:使用脚手架创建插件模板
|
||||||
|
2. **定义配置**:配置插件元信息和依赖
|
||||||
|
3. **开发功能**:实现插件核心功能
|
||||||
|
4. **数据迁移**:编写数据库迁移脚本
|
||||||
|
5. **测试验证**:编写插件测试用例
|
||||||
|
6. **打包发布**:打包插件并发布到应用市场
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建插件
|
||||||
|
$ npm run plugin:create your-plugin
|
||||||
|
|
||||||
|
# 安装插件
|
||||||
|
$ npm run plugin:install your-plugin
|
||||||
|
|
||||||
|
# 启用插件
|
||||||
|
$ npm run plugin:enable your-plugin
|
||||||
|
|
||||||
|
# 禁用插件
|
||||||
|
$ npm run plugin:disable your-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 贡献指南
|
||||||
|
|
||||||
|
我们欢迎所有形式的贡献,包括但不限于:
|
||||||
|
|
||||||
|
- 🐛 **Bug 报告**:发现问题请提交 Issue
|
||||||
|
- 💡 **功能建议**:提出新功能想法
|
||||||
|
- 📝 **文档改进**:完善文档内容
|
||||||
|
- 🔧 **代码贡献**:提交代码修复或新功能
|
||||||
|
- 🧩 **插件开发**:开发和分享插件
|
||||||
|
|
||||||
|
### 贡献流程
|
||||||
|
|
||||||
|
1. **Fork 仓库**:Fork 本仓库到您的 GitHub 账户
|
||||||
|
2. **创建分支**:`git checkout -b feature/AmazingFeature`
|
||||||
|
3. **提交更改**:`git commit -m 'feat: Add some AmazingFeature'`
|
||||||
|
4. **推送分支**:`git push origin feature/AmazingFeature`
|
||||||
|
5. **创建 PR**:打开 Pull Request 并描述您的更改
|
||||||
|
|
||||||
|
### 代码贡献规范
|
||||||
|
|
||||||
|
- **提交信息**:使用 [Conventional Commits](https://conventionalcommits.org/) 规范
|
||||||
|
- **代码风格**:遵循项目的 ESLint 和 Prettier 配置
|
||||||
|
- **测试覆盖**:新功能需要包含相应的测试用例
|
||||||
|
- **文档更新**:重要更改需要更新相关文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
本项目采用 **MIT 许可证** - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 支持与帮助
|
||||||
|
|
||||||
|
如果您在使用过程中遇到问题,请通过以下方式获取帮助:
|
||||||
|
|
||||||
|
### 📞 联系方式
|
||||||
|
|
||||||
|
- 📧 **邮件支持**:support@wwjauth.com
|
||||||
|
- 💬 **在线客服**:https://wwjauth.com/support
|
||||||
|
- 📖 **文档中心**:https://docs.wwjauth.com
|
||||||
|
- 🐛 **问题反馈**:https://github.com/wwjauth/wwjcloud-nestjs/issues
|
||||||
|
- 💡 **功能建议**:https://github.com/wwjauth/wwjcloud-nestjs/discussions
|
||||||
|
|
||||||
|
### 🌐 社区
|
||||||
|
|
||||||
|
- **QQ 群**:123456789
|
||||||
|
- **微信群**:扫描二维码加入
|
||||||
|
- **Discord**:https://discord.gg/wwjauth
|
||||||
|
- **Telegram**:https://t.me/wwjauth
|
||||||
|
|
||||||
|
### 📚 学习资源
|
||||||
|
|
||||||
|
- **视频教程**:https://www.bilibili.com/wwjauth
|
||||||
|
- **博客文章**:https://blog.wwjauth.com
|
||||||
|
- **示例项目**:https://github.com/wwjauth/examples
|
||||||
|
- **最佳实践**:https://docs.wwjauth.com/best-practices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏆 致谢
|
||||||
|
|
||||||
|
感谢所有为本项目做出贡献的开发者和社区成员!
|
||||||
|
|
||||||
|
### 🙏 特别感谢
|
||||||
|
|
||||||
|
- **[NestJS](https://nestjs.com/)** - 优秀的 Node.js 企业级框架
|
||||||
|
- **[VbenAdmin](https://vben.pro/)** - 现代化的 Vue3 管理系统框架
|
||||||
|
- **[TypeORM](https://typeorm.io/)** - 强大的 TypeScript ORM 框架
|
||||||
|
- **[Vue3](https://vuejs.org/)** - 渐进式 JavaScript 框架
|
||||||
|
- **[Element Plus](https://element-plus.org/)** - 基于 Vue3 的企业级 UI 组件库
|
||||||
|
- **[Redis](https://redis.io/)** - 高性能内存数据库
|
||||||
|
- **[Bull](https://github.com/OptimalBits/bull)** - 可靠的 Node.js 队列系统
|
||||||
|
- **[Winston](https://github.com/winstonjs/winston)** - 通用日志库
|
||||||
|
- **[Swagger](https://swagger.io/)** - API 文档生成工具
|
||||||
|
|
||||||
|
### 🌟 贡献者
|
||||||
|
|
||||||
|
感谢以下贡献者对项目的支持:
|
||||||
|
|
||||||
|
<!-- 这里可以添加贡献者头像 -->
|
||||||
|
<a href="https://github.com/wwjauth/wwjcloud-nestjs/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=wwjauth/wwjcloud-nestjs" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 项目统计
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**WWJ Cloud 企业级框架** - 让企业级应用开发更简单、更高效! 🚀
|
||||||
|
|
||||||
|
**基于 NestJS + VbenAdmin 的现代化企业级解决方案**
|
||||||
|
|
||||||
|
[⭐ 给个 Star](https://github.com/wwjauth/wwjcloud-nestjs) |
|
||||||
|
[📖 查看文档](https://docs.wwjauth.com) |
|
||||||
|
[🚀 在线演示](http://demo.wwjauth.com) |
|
||||||
|
[💬 加入社区](https://wwjauth.com/community)
|
||||||
|
|
||||||
|
</div>
|
||||||
29
wwjcloud/.env.example
Normal file
29
wwjcloud/.env.example
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Runtime
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Database (MySQL)
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
DB_DATABASE=wwjcloud
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
JWT_SECRET=your_jwt_secret_key
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# Uploads
|
||||||
|
UPLOAD_PATH=./uploads
|
||||||
|
|
||||||
|
# Log
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Throttling
|
||||||
|
THROTTLE_TTL=60
|
||||||
|
THROTTLE_LIMIT=100
|
||||||
4
wwjcloud/.husky/commit-msg
Normal file
4
wwjcloud/.husky/commit-msg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx --no -- commitlint --edit "$1"
|
||||||
4
wwjcloud/.husky/pre-commit
Normal file
4
wwjcloud/.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npx lint-staged
|
||||||
4
wwjcloud/.prettierrc
Normal file
4
wwjcloud/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
98
wwjcloud/README.md
Normal file
98
wwjcloud/README.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g @nestjs/mau
|
||||||
|
$ mau deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Check out a few resources that may come in handy when working with NestJS:
|
||||||
|
|
||||||
|
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||||
|
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||||
|
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||||
|
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||||
|
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||||
|
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||||
|
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||||
|
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
1
wwjcloud/commitlint.config.cjs
Normal file
1
wwjcloud/commitlint.config.cjs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = { extends: ['@commitlint/config-conventional'] };
|
||||||
89
wwjcloud/eslint.config.mjs
Normal file
89
wwjcloud/eslint.config.mjs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
// 禁止任何形式的路径别名导入,统一使用相对路径
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
group: ['@*', 'src/*', '/*'],
|
||||||
|
message:
|
||||||
|
'禁止使用路径别名与根路径导入,请使用相对路径(../ 或 ./)按照分层规范访问公开 API',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 分层导入约束:Common、Core、App 层
|
||||||
|
{
|
||||||
|
files: ['src/common/**/*.{ts,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{ group: ['@app/*', 'src/app/*', '@vendor/*', 'src/vendor/*'], message: 'Common 层禁止依赖 App/Vendor,请依赖 Core 抽象' },
|
||||||
|
{ group: ['**/*/internal/**'], message: '禁止依赖其他域内部实现,请通过其公共 API' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/core/**/*.{ts,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{ group: ['@app/*', 'src/app/*', '@common/*', 'src/common/*', '@vendor/*', 'src/vendor/*'], message: 'Core 层禁止依赖上层与 Vendor 实现' },
|
||||||
|
{ group: ['**/*/internal/**'], message: '禁止依赖其他域内部实现,请通过其公共 API' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/app/**/*.{ts,tsx}'],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{ group: ['**/*/internal/**'], message: '禁止依赖其他域内部实现,请通过其公共 API' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
14
wwjcloud/nest-cli.json
Normal file
14
wwjcloud/nest-cli.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"include": "**/*.hbs",
|
||||||
|
"outDir": "dist"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
156
wwjcloud/package.json
Normal file
156
wwjcloud/package.json
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
{
|
||||||
|
"name": "wwjcloud",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"prebuild": "npm run clean",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"prestart:prod": "cross-env NODE_ENV=production npm run build",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\"",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
|
"migration:run": "typeorm-ts-node-commonjs migration:run -d ./src/config/typeorm.config.ts",
|
||||||
|
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d ./src/config/typeorm.config.ts",
|
||||||
|
"migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/AutoGenerated -d ./src/config/typeorm.config.ts",
|
||||||
|
"seed:run": "ts-node ./src/seeds/index.ts",
|
||||||
|
"db:init": "ts-node ./src/scripts/init-db.ts",
|
||||||
|
"prepare": "husky",
|
||||||
|
"openapi:gen": "openapi-typescript http://localhost:3000/api-json -o ../admin/src/types/api.d.ts",
|
||||||
|
"pm2:start": "pm2 start dist/main.js --name wwjcloud",
|
||||||
|
"commit": "cz"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/compress": "^8.1.0",
|
||||||
|
"@fastify/helmet": "^13.0.1",
|
||||||
|
"@fastify/multipart": "^9.0.3",
|
||||||
|
"@fastify/static": "^8.2.0",
|
||||||
|
"@fastify/swagger": "^9.5.1",
|
||||||
|
"@fastify/swagger-ui": "^5.2.3",
|
||||||
|
"@nestjs/bull": "^11.0.3",
|
||||||
|
"@nestjs/cache-manager": "^3.0.1",
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/event-emitter": "^3.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.0",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
|
"@nestjs/platform-express": "^11.1.6",
|
||||||
|
"@nestjs/platform-fastify": "^11.1.6",
|
||||||
|
"@nestjs/schedule": "^6.0.0",
|
||||||
|
"@nestjs/serve-static": "^5.0.3",
|
||||||
|
"@nestjs/swagger": "^11.2.0",
|
||||||
|
"@nestjs/terminus": "^11.0.0",
|
||||||
|
"@nestjs/throttler": "^6.4.0",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"bull": "^4.16.5",
|
||||||
|
"cache-manager": "^7.1.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.2",
|
||||||
|
"compression": "^1.8.1",
|
||||||
|
"dotenv": "^17.2.1",
|
||||||
|
"fastify": "^5.5.0",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"ioredis": "^5.7.0",
|
||||||
|
"joi": "^18.0.1",
|
||||||
|
"multer": "^2.0.2",
|
||||||
|
"mysql2": "^3.14.3",
|
||||||
|
"nest-winston": "^1.10.2",
|
||||||
|
"nestjs-cls": "^6.0.1",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
|
"typeorm": "^0.3.26",
|
||||||
|
"winston": "^3.17.0",
|
||||||
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^19.8.1",
|
||||||
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/compression": "^1.8.1",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/multer": "^2.0.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"commitizen": "^4.3.1",
|
||||||
|
"cross-env": "^10.0.0",
|
||||||
|
"cz-git": "^1.12.0",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"lint-staged": "^15.5.2",
|
||||||
|
"openapi-typescript": "^7.9.1",
|
||||||
|
"pm2": "^6.0.8",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typeorm-ts-node-commonjs": "^0.3.20",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"src/**/*.{ts,tsx,js,json}": [
|
||||||
|
"eslint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"commitlint": {
|
||||||
|
"extends": [
|
||||||
|
"@commitlint/config-conventional"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "cz-git"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
wwjcloud/public/.gitkeep
Normal file
0
wwjcloud/public/.gitkeep
Normal file
0
wwjcloud/public/upload/.gitkeep
Normal file
0
wwjcloud/public/upload/.gitkeep
Normal file
22
wwjcloud/src/app.controller.spec.ts
Normal file
22
wwjcloud/src/app.controller.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
12
wwjcloud/src/app.controller.ts
Normal file
12
wwjcloud/src/app.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
||||||
167
wwjcloud/src/app.module.ts
Normal file
167
wwjcloud/src/app.module.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import configuration from './config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
// 新增导入
|
||||||
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
import { TerminusModule } from '@nestjs/terminus';
|
||||||
|
import { WinstonModule } from 'nest-winston';
|
||||||
|
import * as winston from 'winston';
|
||||||
|
import 'winston-daily-rotate-file';
|
||||||
|
import * as Joi from 'joi';
|
||||||
|
import { ClsModule } from 'nestjs-cls';
|
||||||
|
import { VendorModule } from './vendor';
|
||||||
|
import {
|
||||||
|
SettingsModule,
|
||||||
|
UploadModule,
|
||||||
|
AuthModule,
|
||||||
|
MemberModule,
|
||||||
|
AdminModule,
|
||||||
|
RbacModule,
|
||||||
|
GlobalAuthGuard,
|
||||||
|
RolesGuard
|
||||||
|
} from './common';
|
||||||
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
// 允许通过环境变量禁用数据库初始化(用于本地开发或暂时无数据库时)
|
||||||
|
const dbImports =
|
||||||
|
process.env.DB_DISABLE === 'true'
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
type: 'mysql',
|
||||||
|
host: config.get('db.host', 'localhost'),
|
||||||
|
port: config.get('db.port', 3306),
|
||||||
|
username: config.get('db.username', 'root'),
|
||||||
|
password: config.get('db.password', ''),
|
||||||
|
database: config.get('db.database', 'wwjcloud'),
|
||||||
|
autoLoadEntities: true,
|
||||||
|
synchronize: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
load: [configuration],
|
||||||
|
validationSchema: Joi.object({
|
||||||
|
NODE_ENV: Joi.string()
|
||||||
|
.valid('development', 'production', 'test')
|
||||||
|
.default('development'),
|
||||||
|
PORT: Joi.number().default(3000),
|
||||||
|
DB_HOST: Joi.string().default('localhost'),
|
||||||
|
DB_PORT: Joi.number().default(3306),
|
||||||
|
DB_USERNAME: Joi.string().default('root'),
|
||||||
|
DB_PASSWORD: Joi.string().allow('').default(''),
|
||||||
|
DB_DATABASE: Joi.string().default('wwjcloud'),
|
||||||
|
REDIS_HOST: Joi.string().default('localhost'),
|
||||||
|
REDIS_PORT: Joi.number().default(6379),
|
||||||
|
REDIS_PASSWORD: Joi.string().allow('').default(''),
|
||||||
|
JWT_SECRET: Joi.string().default('change_me'),
|
||||||
|
JWT_EXPIRES_IN: Joi.string().default('7d'),
|
||||||
|
UPLOAD_PATH: Joi.string().default('public/upload'),
|
||||||
|
STORAGE_PROVIDER: Joi.string()
|
||||||
|
.valid(
|
||||||
|
'local',
|
||||||
|
'tencent',
|
||||||
|
'aliyun',
|
||||||
|
'qiniu',
|
||||||
|
'alists3',
|
||||||
|
'webdav',
|
||||||
|
'ftp',
|
||||||
|
)
|
||||||
|
.default('local'),
|
||||||
|
PAYMENT_PROVIDER: Joi.string()
|
||||||
|
.valid('wechat', 'alipay', 'mock')
|
||||||
|
.default('mock'),
|
||||||
|
LOG_LEVEL: Joi.string().default('info'),
|
||||||
|
THROTTLE_TTL: Joi.number().default(60),
|
||||||
|
THROTTLE_LIMIT: Joi.number().default(100),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
// 静态资源托管:仅暴露 /upload/**
|
||||||
|
ServeStaticModule.forRoot({
|
||||||
|
rootPath: path.resolve(process.cwd(), 'public', 'upload'),
|
||||||
|
serveRoot: '/upload',
|
||||||
|
}),
|
||||||
|
// 缓存(内存实现,后续可替换为 redis-store)
|
||||||
|
CacheModule.register({ isGlobal: true }),
|
||||||
|
// 计划任务
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
// 事件总线
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
// 限流
|
||||||
|
ThrottlerModule.forRoot([
|
||||||
|
{
|
||||||
|
ttl: Number(process.env.THROTTLE_TTL) || 60,
|
||||||
|
limit: Number(process.env.THROTTLE_LIMIT) || 100,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
// 健康检查(需要时可增加控制器)
|
||||||
|
TerminusModule,
|
||||||
|
// 日志
|
||||||
|
WinstonModule.forRoot({
|
||||||
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.printf(({ level, message, timestamp, context }) => {
|
||||||
|
return `${timestamp} [${level}]${context ? ' [' + context + ']' : ''} ${message}`;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
new (winston.transports as any).DailyRotateFile({
|
||||||
|
dirname: 'logs',
|
||||||
|
filename: 'app-%DATE%.log',
|
||||||
|
datePattern: 'YYYY-MM-DD',
|
||||||
|
zippedArchive: false,
|
||||||
|
maxFiles: '14d',
|
||||||
|
level: process.env.LOG_LEVEL || 'info',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
// 请求上下文
|
||||||
|
ClsModule.forRoot({
|
||||||
|
global: true,
|
||||||
|
middleware: { mount: true },
|
||||||
|
}),
|
||||||
|
// 数据库(可通过 DB_DISABLE=true 禁用)
|
||||||
|
...dbImports,
|
||||||
|
// Vendor 绑定 Core 抽象到具体适配器
|
||||||
|
VendorModule,
|
||||||
|
// Common 编排服务(聚合到 SettingsModule 下)
|
||||||
|
SettingsModule,
|
||||||
|
// 上传模块(提供 /upload/file /upload/files 接口)
|
||||||
|
UploadModule,
|
||||||
|
// 用户管理模块
|
||||||
|
MemberModule,
|
||||||
|
AdminModule,
|
||||||
|
// 权限管理模块
|
||||||
|
RbacModule,
|
||||||
|
// 认证模块(提供 super/admin/auth 登录分流)
|
||||||
|
AuthModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [
|
||||||
|
AppService,
|
||||||
|
// 全局守卫
|
||||||
|
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
||||||
|
{ provide: APP_GUARD, useClass: GlobalAuthGuard },
|
||||||
|
{ provide: APP_GUARD, useClass: RolesGuard },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
8
wwjcloud/src/app.service.ts
Normal file
8
wwjcloud/src/app.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
156
wwjcloud/src/common/admin/admin.controller.ts
Normal file
156
wwjcloud/src/common/admin/admin.controller.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
ParseIntPipe,
|
||||||
|
HttpStatus,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { AdminService } from './admin.service';
|
||||||
|
import { CreateAdminDto, UpdateAdminDto, QueryAdminDto } from './dto';
|
||||||
|
import { SysUser } from './entities/sys-user.entity';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
@ApiTags('管理员管理')
|
||||||
|
@Controller('admin')
|
||||||
|
export class AdminController {
|
||||||
|
constructor(private readonly adminService: AdminService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: '创建管理员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.CREATED, description: '创建成功', type: SysUser })
|
||||||
|
@ApiResponse({ status: HttpStatus.CONFLICT, description: '用户名或手机号已存在' })
|
||||||
|
async create(@Body() createAdminDto: CreateAdminDto) {
|
||||||
|
const admin = await this.adminService.create(createAdminDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '创建成功',
|
||||||
|
data: admin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取管理员列表' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '获取成功' })
|
||||||
|
async findAll(@Query() queryDto: QueryAdminDto) {
|
||||||
|
const result = await this.adminService.findAll(queryDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: '获取管理员详情' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '获取成功', type: SysUser })
|
||||||
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '管理员不存在' })
|
||||||
|
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
const admin = await this.adminService.findOne(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: admin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: '更新管理员信息' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '更新成功', type: SysUser })
|
||||||
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '管理员不存在' })
|
||||||
|
@ApiResponse({ status: HttpStatus.CONFLICT, description: '用户名或手机号已存在' })
|
||||||
|
async update(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body() updateAdminDto: UpdateAdminDto,
|
||||||
|
) {
|
||||||
|
const admin = await this.adminService.update(id, updateAdminDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '更新成功',
|
||||||
|
data: admin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: '删除管理员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '删除成功' })
|
||||||
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '管理员不存在' })
|
||||||
|
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
await this.adminService.remove(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '删除成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('batch-delete')
|
||||||
|
@ApiOperation({ summary: '批量删除管理员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '批量删除成功' })
|
||||||
|
async batchRemove(@Body('ids') ids: number[]) {
|
||||||
|
await this.adminService.batchRemove(ids);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '批量删除成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/update-last-login')
|
||||||
|
@ApiOperation({ summary: '更新最后登录信息' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '更新成功' })
|
||||||
|
async updateLastLogin(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Req() request: Request,
|
||||||
|
) {
|
||||||
|
const ip = request.ip || request.connection.remoteAddress || '';
|
||||||
|
await this.adminService.updateLastLogin(id, ip);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '更新成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('search/by-username/:username')
|
||||||
|
@ApiOperation({ summary: '根据用户名查询管理员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '查询成功' })
|
||||||
|
async findByUsername(@Param('username') username: string) {
|
||||||
|
const admin = await this.adminService.findByUsername(username);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '查询成功',
|
||||||
|
data: admin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/assign-roles')
|
||||||
|
@ApiOperation({ summary: '分配角色' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '分配成功' })
|
||||||
|
async assignRoles(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body('roleIds') roleIds: number[],
|
||||||
|
@Body('siteId') siteId: number,
|
||||||
|
) {
|
||||||
|
await this.adminService.assignRoles(id, roleIds, siteId);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '分配成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id/roles')
|
||||||
|
@ApiOperation({ summary: '获取用户角色' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '获取成功' })
|
||||||
|
async getUserRoles(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
const roleIds = await this.adminService.getUserRoles(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: roleIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
14
wwjcloud/src/common/admin/admin.module.ts
Normal file
14
wwjcloud/src/common/admin/admin.module.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AdminService } from './admin.service';
|
||||||
|
import { AdminController } from './admin.controller';
|
||||||
|
import { SysUser } from './entities/sys-user.entity';
|
||||||
|
import { SysUserRole } from './entities/sys-user-role.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([SysUser, SysUserRole])],
|
||||||
|
controllers: [AdminController],
|
||||||
|
providers: [AdminService],
|
||||||
|
exports: [AdminService, TypeOrmModule],
|
||||||
|
})
|
||||||
|
export class AdminModule {}
|
||||||
311
wwjcloud/src/common/admin/admin.service.ts
Normal file
311
wwjcloud/src/common/admin/admin.service.ts
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, In } from 'typeorm';
|
||||||
|
import { SysUser } from './entities/sys-user.entity';
|
||||||
|
import { SysUserRole } from './entities/sys-user-role.entity';
|
||||||
|
import { CreateAdminDto, UpdateAdminDto, QueryAdminDto } from './dto';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SysUser)
|
||||||
|
private readonly sysUserRepository: Repository<SysUser>,
|
||||||
|
@InjectRepository(SysUserRole)
|
||||||
|
private readonly sysUserRoleRepository: Repository<SysUserRole>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建管理员
|
||||||
|
*/
|
||||||
|
async create(createAdminDto: CreateAdminDto): Promise<SysUser> {
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
const existingByUsername = await this.sysUserRepository.findOne({
|
||||||
|
where: { username: createAdminDto.username, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (existingByUsername) {
|
||||||
|
throw new ConflictException('用户名已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
if (createAdminDto.mobile) {
|
||||||
|
const existingByMobile = await this.sysUserRepository.findOne({
|
||||||
|
where: { mobile: createAdminDto.mobile, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (existingByMobile) {
|
||||||
|
throw new ConflictException('手机号已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码加密
|
||||||
|
const hashedPassword = await bcrypt.hash(createAdminDto.password, 10);
|
||||||
|
|
||||||
|
const { roleIds, ...adminData } = createAdminDto;
|
||||||
|
|
||||||
|
const admin = this.sysUserRepository.create({
|
||||||
|
...adminData,
|
||||||
|
password: hashedPassword,
|
||||||
|
createTime: Math.floor(Date.now() / 1000),
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedAdmin = await this.sysUserRepository.save(admin);
|
||||||
|
|
||||||
|
// 分配角色
|
||||||
|
if (roleIds && roleIds.length > 0) {
|
||||||
|
await this.assignRoles(savedAdmin.uid, roleIds, createAdminDto.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.findOne(savedAdmin.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询管理员列表
|
||||||
|
*/
|
||||||
|
async findAll(queryDto: QueryAdminDto) {
|
||||||
|
const { page = 1, limit = 10, keyword, siteId, sex, status, isAdmin, roleId, startTime, endTime } = queryDto;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const queryBuilder = this.sysUserRepository.createQueryBuilder('admin')
|
||||||
|
.leftJoinAndSelect('admin.userRoles', 'userRole')
|
||||||
|
.where('admin.deleteTime = :deleteTime', { deleteTime: 0 });
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (keyword) {
|
||||||
|
queryBuilder.andWhere(
|
||||||
|
'(admin.username LIKE :keyword OR admin.realName LIKE :keyword OR admin.mobile LIKE :keyword)',
|
||||||
|
{ keyword: `%${keyword}%` }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点ID筛选
|
||||||
|
if (siteId !== undefined) {
|
||||||
|
queryBuilder.andWhere('admin.siteId = :siteId', { siteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性别筛选
|
||||||
|
if (sex !== undefined) {
|
||||||
|
queryBuilder.andWhere('admin.sex = :sex', { sex });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if (status !== undefined) {
|
||||||
|
queryBuilder.andWhere('admin.status = :status', { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超级管理员筛选
|
||||||
|
if (isAdmin !== undefined) {
|
||||||
|
queryBuilder.andWhere('admin.isAdmin = :isAdmin', { isAdmin });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色筛选
|
||||||
|
if (roleId !== undefined) {
|
||||||
|
queryBuilder.andWhere('userRole.roleId = :roleId', { roleId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
if (startTime && endTime) {
|
||||||
|
queryBuilder.andWhere('admin.createTime BETWEEN :startTime AND :endTime', {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
});
|
||||||
|
} else if (startTime) {
|
||||||
|
queryBuilder.andWhere('admin.createTime >= :startTime', { startTime });
|
||||||
|
} else if (endTime) {
|
||||||
|
queryBuilder.andWhere('admin.createTime <= :endTime', { endTime });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
queryBuilder.orderBy('admin.createTime', 'DESC');
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const [list, total] = await queryBuilder
|
||||||
|
.skip(skip)
|
||||||
|
.take(limit)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
// 移除密码字段
|
||||||
|
const safeList = list.map(admin => {
|
||||||
|
const { password, ...safeAdmin } = admin;
|
||||||
|
return {
|
||||||
|
...safeAdmin,
|
||||||
|
roleIds: admin.userRoles?.map(ur => ur.roleId) || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: safeList,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询管理员详情
|
||||||
|
*/
|
||||||
|
async findOne(id: number): Promise<SysUser> {
|
||||||
|
const admin = await this.sysUserRepository.findOne({
|
||||||
|
where: { uid: id, deleteTime: 0 },
|
||||||
|
relations: ['userRoles'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
throw new NotFoundException('管理员不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除密码字段
|
||||||
|
const { password, ...safeAdmin } = admin;
|
||||||
|
return {
|
||||||
|
...safeAdmin,
|
||||||
|
roleIds: admin.userRoles?.map(ur => ur.roleId) || [],
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名查询管理员
|
||||||
|
*/
|
||||||
|
async findByUsername(username: string): Promise<SysUser | null> {
|
||||||
|
return await this.sysUserRepository.findOne({
|
||||||
|
where: { username, deleteTime: 0 },
|
||||||
|
relations: ['userRoles'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新管理员信息
|
||||||
|
*/
|
||||||
|
async update(id: number, updateAdminDto: UpdateAdminDto): Promise<SysUser> {
|
||||||
|
const admin = await this.findOne(id);
|
||||||
|
|
||||||
|
// 检查用户名是否已被其他用户使用
|
||||||
|
if (updateAdminDto.username && updateAdminDto.username !== admin.username) {
|
||||||
|
const existingByUsername = await this.sysUserRepository.findOne({
|
||||||
|
where: { username: updateAdminDto.username, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (existingByUsername && existingByUsername.uid !== id) {
|
||||||
|
throw new ConflictException('用户名已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已被其他用户使用
|
||||||
|
if (updateAdminDto.mobile && updateAdminDto.mobile !== admin.mobile) {
|
||||||
|
const existingByMobile = await this.sysUserRepository.findOne({
|
||||||
|
where: { mobile: updateAdminDto.mobile, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (existingByMobile && existingByMobile.uid !== id) {
|
||||||
|
throw new ConflictException('手机号已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { roleIds, ...adminData } = updateAdminDto;
|
||||||
|
|
||||||
|
// 如果更新密码,需要加密
|
||||||
|
if (adminData.password) {
|
||||||
|
adminData.password = await bcrypt.hash(adminData.password, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sysUserRepository.update(id, {
|
||||||
|
...adminData,
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新角色分配
|
||||||
|
if (roleIds !== undefined) {
|
||||||
|
await this.updateRoles(id, roleIds, admin.siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除管理员
|
||||||
|
*/
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
const admin = await this.findOne(id);
|
||||||
|
|
||||||
|
await this.sysUserRepository.update(id, {
|
||||||
|
deleteTime: Math.floor(Date.now() / 1000),
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除用户角色关联
|
||||||
|
await this.sysUserRoleRepository.delete({ uid: id });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量软删除管理员
|
||||||
|
*/
|
||||||
|
async batchRemove(ids: number[]): Promise<void> {
|
||||||
|
const deleteTime = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
await this.sysUserRepository.update(
|
||||||
|
{ uid: In(ids) },
|
||||||
|
{
|
||||||
|
deleteTime,
|
||||||
|
updateTime: deleteTime,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 删除用户角色关联
|
||||||
|
await this.sysUserRoleRepository.delete({ uid: In(ids) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新最后登录信息
|
||||||
|
*/
|
||||||
|
async updateLastLogin(id: number, ip: string): Promise<void> {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
await this.sysUserRepository.update(id, {
|
||||||
|
lastTime: now,
|
||||||
|
lastIp: ip,
|
||||||
|
updateTime: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证密码
|
||||||
|
*/
|
||||||
|
async validatePassword(admin: SysUser, password: string): Promise<boolean> {
|
||||||
|
return await bcrypt.compare(password, admin.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分配角色
|
||||||
|
*/
|
||||||
|
async assignRoles(uid: number, roleIds: number[], siteId: number): Promise<void> {
|
||||||
|
const userRoles = roleIds.map(roleId =>
|
||||||
|
this.sysUserRoleRepository.create({
|
||||||
|
uid,
|
||||||
|
roleId,
|
||||||
|
siteId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.sysUserRoleRepository.save(userRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户角色
|
||||||
|
*/
|
||||||
|
async updateRoles(uid: number, roleIds: number[], siteId: number): Promise<void> {
|
||||||
|
// 删除现有角色
|
||||||
|
await this.sysUserRoleRepository.delete({ uid });
|
||||||
|
|
||||||
|
// 分配新角色
|
||||||
|
if (roleIds.length > 0) {
|
||||||
|
await this.assignRoles(uid, roleIds, siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户角色
|
||||||
|
*/
|
||||||
|
async getUserRoles(uid: number): Promise<number[]> {
|
||||||
|
const userRoles = await this.sysUserRoleRepository.find({
|
||||||
|
where: { uid },
|
||||||
|
});
|
||||||
|
return userRoles.map(ur => ur.roleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
wwjcloud/src/common/admin/dto/create-admin.dto.ts
Normal file
94
wwjcloud/src/common/admin/dto/create-admin.dto.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsOptional, IsInt, IsEmail, IsIn, Length, IsArray } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateAdminDto {
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户名' })
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 255)
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '密码' })
|
||||||
|
@IsString()
|
||||||
|
@Length(6, 255)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '真实姓名' })
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 255)
|
||||||
|
realName: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '头像' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
headImg?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '手机号' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(11, 11)
|
||||||
|
mobile?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '邮箱' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '性别:1男 2女 0保密' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1, 2])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
sex?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '生日' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
birthday?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '省份ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
pid?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '城市ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
cid?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '区县ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
did?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '详细地址' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
address?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '状态:1正常 0禁用', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '是否超级管理员:1是 0否', default: 0 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
isAdmin?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '角色ID数组' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsInt({ each: true })
|
||||||
|
@Transform(({ value }) => Array.isArray(value) ? value.map(v => parseInt(v)) : [])
|
||||||
|
roleIds?: number[];
|
||||||
|
}
|
||||||
3
wwjcloud/src/common/admin/dto/index.ts
Normal file
3
wwjcloud/src/common/admin/dto/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { CreateAdminDto } from './create-admin.dto';
|
||||||
|
export { UpdateAdminDto } from './update-admin.dto';
|
||||||
|
export { QueryAdminDto } from './query-admin.dto';
|
||||||
64
wwjcloud/src/common/admin/dto/query-admin.dto.ts
Normal file
64
wwjcloud/src/common/admin/dto/query-admin.dto.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsOptional, IsString, IsInt, IsIn } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class QueryAdminDto {
|
||||||
|
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value) || 1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value) || 10)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '关键词搜索(用户名/真实姓名/手机号)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
keyword?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '站点ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '性别:1男 2女 0保密' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1, 2])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
sex?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '状态:1正常 0禁用' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '是否超级管理员:1是 0否' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
isAdmin?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '角色ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
roleId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '开始时间(时间戳)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
startTime?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '结束时间(时间戳)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
endTime?: number;
|
||||||
|
}
|
||||||
4
wwjcloud/src/common/admin/dto/update-admin.dto.ts
Normal file
4
wwjcloud/src/common/admin/dto/update-admin.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateAdminDto } from './create-admin.dto';
|
||||||
|
|
||||||
|
export class UpdateAdminDto extends PartialType(CreateAdminDto) {}
|
||||||
27
wwjcloud/src/common/admin/entities/sys-user-role.entity.ts
Normal file
27
wwjcloud/src/common/admin/entities/sys-user-role.entity.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { SysUser } from './sys-user.entity';
|
||||||
|
|
||||||
|
@Entity('sys_user_role')
|
||||||
|
export class SysUserRole {
|
||||||
|
@ApiProperty({ description: '主键ID' })
|
||||||
|
@PrimaryGeneratedColumn({ name: 'id', type: 'int', unsigned: true })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户ID' })
|
||||||
|
@Column({ name: 'uid', type: 'int', default: 0 })
|
||||||
|
uid: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '角色ID' })
|
||||||
|
@Column({ name: 'role_id', type: 'int', default: 0 })
|
||||||
|
roleId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
// 关联用户
|
||||||
|
@ManyToOne(() => SysUser, user => user.userRoles)
|
||||||
|
@JoinColumn({ name: 'uid' })
|
||||||
|
user: SysUser;
|
||||||
|
}
|
||||||
94
wwjcloud/src/common/admin/entities/sys-user.entity.ts
Normal file
94
wwjcloud/src/common/admin/entities/sys-user.entity.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { SysUserRole } from './sys-user-role.entity';
|
||||||
|
|
||||||
|
@Entity('sys_user')
|
||||||
|
export class SysUser {
|
||||||
|
@ApiProperty({ description: '用户ID' })
|
||||||
|
@PrimaryGeneratedColumn({ name: 'uid', type: 'int', unsigned: true })
|
||||||
|
uid: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户名' })
|
||||||
|
@Column({ name: 'username', type: 'varchar', length: 255, default: '' })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '密码' })
|
||||||
|
@Column({ name: 'password', type: 'varchar', length: 255, default: '' })
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '真实姓名' })
|
||||||
|
@Column({ name: 'real_name', type: 'varchar', length: 255, default: '' })
|
||||||
|
realName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '头像' })
|
||||||
|
@Column({ name: 'head_img', type: 'varchar', length: 255, default: '' })
|
||||||
|
headImg: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '手机号' })
|
||||||
|
@Column({ name: 'mobile', type: 'varchar', length: 20, default: '' })
|
||||||
|
mobile: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱' })
|
||||||
|
@Column({ name: 'email', type: 'varchar', length: 255, default: '' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '性别:1男 2女 0保密' })
|
||||||
|
@Column({ name: 'sex', type: 'tinyint', default: 0 })
|
||||||
|
sex: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '生日' })
|
||||||
|
@Column({ name: 'birthday', type: 'varchar', length: 255, default: '' })
|
||||||
|
birthday: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '省份ID' })
|
||||||
|
@Column({ name: 'pid', type: 'int', default: 0 })
|
||||||
|
pid: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '城市ID' })
|
||||||
|
@Column({ name: 'cid', type: 'int', default: 0 })
|
||||||
|
cid: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '区县ID' })
|
||||||
|
@Column({ name: 'did', type: 'int', default: 0 })
|
||||||
|
did: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '详细地址' })
|
||||||
|
@Column({ name: 'address', type: 'varchar', length: 255, default: '' })
|
||||||
|
address: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '状态:1正常 0禁用' })
|
||||||
|
@Column({ name: 'status', type: 'tinyint', default: 1 })
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否超级管理员:1是 0否' })
|
||||||
|
@Column({ name: 'is_admin', type: 'tinyint', default: 0 })
|
||||||
|
isAdmin: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '最后登录时间' })
|
||||||
|
@Column({ name: 'last_time', type: 'int', default: 0 })
|
||||||
|
lastTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '最后登录IP' })
|
||||||
|
@Column({ name: 'last_ip', type: 'varchar', length: 255, default: '' })
|
||||||
|
lastIp: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
@CreateDateColumn({ name: 'create_time', type: 'int' })
|
||||||
|
createTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
@UpdateDateColumn({ name: 'update_time', type: 'int' })
|
||||||
|
updateTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '删除时间' })
|
||||||
|
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||||
|
deleteTime: number;
|
||||||
|
|
||||||
|
// 关联用户角色
|
||||||
|
@OneToMany(() => SysUserRole, userRole => userRole.user)
|
||||||
|
userRoles: SysUserRole[];
|
||||||
|
}
|
||||||
6
wwjcloud/src/common/admin/index.ts
Normal file
6
wwjcloud/src/common/admin/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { AdminModule } from './admin.module';
|
||||||
|
export { AdminService } from './admin.service';
|
||||||
|
export { AdminController } from './admin.controller';
|
||||||
|
export { SysUser } from './entities/sys-user.entity';
|
||||||
|
export { SysUserRole } from './entities/sys-user-role.entity';
|
||||||
|
export * from './dto';
|
||||||
4
wwjcloud/src/common/apps/apps.module.ts
Normal file
4
wwjcloud/src/common/apps/apps.module.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class AppsModule {}
|
||||||
145
wwjcloud/src/common/auth/auth.controller.ts
Normal file
145
wwjcloud/src/common/auth/auth.controller.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
UseGuards,
|
||||||
|
Request,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody } from '@nestjs/swagger';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { LoginDto, RegisterDto, ChangePasswordDto, ResetPasswordDto } from './dto';
|
||||||
|
import { LocalAuthGuard } from './guards/local-auth.guard';
|
||||||
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||||
|
import { Public, CurrentUser, CurrentUserId } from './decorators/auth.decorator';
|
||||||
|
|
||||||
|
@ApiTags('认证授权')
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@UseGuards(LocalAuthGuard)
|
||||||
|
@Post('login')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({ summary: '用户登录' })
|
||||||
|
@ApiBody({ type: LoginDto })
|
||||||
|
async login(@Request() req, @Body() loginDto: LoginDto) {
|
||||||
|
const result = await this.authService.login(loginDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '登录成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('register')
|
||||||
|
@ApiOperation({ summary: '会员注册' })
|
||||||
|
async register(@Body() registerDto: RegisterDto) {
|
||||||
|
const result = await this.authService.register(registerDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '注册成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('refresh')
|
||||||
|
@ApiOperation({ summary: '刷新token' })
|
||||||
|
async refreshToken(@Body('refreshToken') refreshToken: string) {
|
||||||
|
const result = await this.authService.refreshToken(refreshToken);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '刷新成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Post('change-password')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: '修改密码' })
|
||||||
|
async changePassword(
|
||||||
|
@CurrentUserId() userId: number,
|
||||||
|
@Body() changePasswordDto: ChangePasswordDto,
|
||||||
|
) {
|
||||||
|
const result = await this.authService.changePassword(userId, changePasswordDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '密码修改成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('reset-password')
|
||||||
|
@ApiOperation({ summary: '重置密码' })
|
||||||
|
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto) {
|
||||||
|
const result = await this.authService.resetPassword(resetPasswordDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '密码重置成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Post('logout')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: '用户登出' })
|
||||||
|
async logout(@Request() req) {
|
||||||
|
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||||
|
const result = await this.authService.logout(token);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '登出成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('profile')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: '获取当前用户信息' })
|
||||||
|
async getProfile(@CurrentUser() user: any) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
userType: user.userType,
|
||||||
|
siteId: user.siteId,
|
||||||
|
nickname: user.user.nickname || user.user.realname,
|
||||||
|
avatar: user.user.avatar,
|
||||||
|
mobile: user.user.mobile,
|
||||||
|
email: user.user.email,
|
||||||
|
status: user.user.status,
|
||||||
|
createTime: user.user.createTime,
|
||||||
|
lastLoginTime: user.user.lastLoginTime,
|
||||||
|
lastLoginIp: user.user.lastLoginIp,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@Get('check')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@ApiOperation({ summary: '检查token有效性' })
|
||||||
|
async checkToken(@CurrentUser() user: any) {
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: 'Token有效',
|
||||||
|
data: {
|
||||||
|
valid: true,
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
userType: user.userType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
58
wwjcloud/src/common/auth/auth.module.ts
Normal file
58
wwjcloud/src/common/auth/auth.module.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { UserPermissionController } from './user-permission.controller';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { PermissionService } from './services/permission.service';
|
||||||
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
|
import { LocalStrategy } from './strategies/local.strategy';
|
||||||
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||||
|
import { LocalAuthGuard } from './guards/local-auth.guard';
|
||||||
|
import { RolesGuard } from './guards/roles.guard';
|
||||||
|
import { GlobalAuthGuard } from './guards/global-auth.guard';
|
||||||
|
import { MemberModule } from '../member/member.module';
|
||||||
|
import { AdminModule } from '../admin/admin.module';
|
||||||
|
import { RbacModule } from '../rbac/rbac.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PassportModule.register({ defaultStrategy: 'jwt' }),
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
secret: configService.get<string>('JWT_SECRET', 'wwjcloud-secret-key'),
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1h'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
MemberModule,
|
||||||
|
AdminModule,
|
||||||
|
RbacModule,
|
||||||
|
],
|
||||||
|
controllers: [AuthController, UserPermissionController],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
PermissionService,
|
||||||
|
JwtStrategy,
|
||||||
|
LocalStrategy,
|
||||||
|
JwtAuthGuard,
|
||||||
|
LocalAuthGuard,
|
||||||
|
RolesGuard,
|
||||||
|
GlobalAuthGuard,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AuthService,
|
||||||
|
PermissionService,
|
||||||
|
JwtAuthGuard,
|
||||||
|
LocalAuthGuard,
|
||||||
|
RolesGuard,
|
||||||
|
GlobalAuthGuard,
|
||||||
|
JwtModule,
|
||||||
|
PassportModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
318
wwjcloud/src/common/auth/auth.service.ts
Normal file
318
wwjcloud/src/common/auth/auth.service.ts
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
import { Injectable, UnauthorizedException, BadRequestException, ConflictException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { MemberService } from '../member/member.service';
|
||||||
|
import { AdminService } from '../admin/admin.service';
|
||||||
|
import { LoginDto, RegisterDto, ChangePasswordDto, ResetPasswordDto } from './dto';
|
||||||
|
import { JwtPayload } from './strategies/jwt.strategy';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly memberService: MemberService,
|
||||||
|
private readonly adminService: AdminService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证用户凭据
|
||||||
|
*/
|
||||||
|
async validateUser(username: string, password: string, userType: 'member' | 'admin' = 'member') {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (userType === 'member') {
|
||||||
|
// 尝试通过用户名或手机号查找会员
|
||||||
|
user = await this.memberService.findByUsername(username) ||
|
||||||
|
await this.memberService.findByMobile(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
const isPasswordValid = await this.memberService.validatePassword(user.memberId, password);
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查账户状态
|
||||||
|
if (user.status !== 1) {
|
||||||
|
throw new UnauthorizedException('账户已被禁用');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: user.memberId,
|
||||||
|
username: user.username,
|
||||||
|
userType: 'member',
|
||||||
|
siteId: user.siteId,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
} else if (userType === 'admin') {
|
||||||
|
// 查找管理员
|
||||||
|
user = await this.adminService.findByUsername(username);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查账户状态
|
||||||
|
if (user.status !== 1) {
|
||||||
|
throw new UnauthorizedException('账户已被禁用');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: user.uid,
|
||||||
|
username: user.username,
|
||||||
|
userType: 'admin',
|
||||||
|
siteId: user.siteId,
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UnauthorizedException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
*/
|
||||||
|
async login(loginDto: LoginDto) {
|
||||||
|
const { username, password, userType = 'member' } = loginDto;
|
||||||
|
|
||||||
|
const user = await this.validateUser(username, password, userType);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('用户名或密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后登录信息
|
||||||
|
const loginInfo = {
|
||||||
|
lastLoginTime: Math.floor(Date.now() / 1000),
|
||||||
|
lastLoginIp: '127.0.0.1', // 实际项目中应该从请求中获取真实IP
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userType === 'member') {
|
||||||
|
await this.memberService.updateLastLogin(user.userId, loginInfo);
|
||||||
|
} else {
|
||||||
|
await this.adminService.updateLastLogin(user.userId, loginInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成JWT token
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
sub: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
userType: user.userType,
|
||||||
|
siteId: user.siteId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const accessToken = this.jwtService.sign(payload);
|
||||||
|
const refreshToken = this.jwtService.sign(payload, {
|
||||||
|
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '7d'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
expiresIn: this.configService.get('JWT_EXPIRES_IN', '1h'),
|
||||||
|
user: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
userType: user.userType,
|
||||||
|
siteId: user.siteId,
|
||||||
|
nickname: user.user.nickname || user.user.realname,
|
||||||
|
avatar: user.user.avatar,
|
||||||
|
mobile: user.user.mobile,
|
||||||
|
email: user.user.email,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会员注册
|
||||||
|
*/
|
||||||
|
async register(registerDto: RegisterDto) {
|
||||||
|
const { username, mobile, password, confirmPassword, ...otherData } = registerDto;
|
||||||
|
|
||||||
|
// 验证密码确认
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
throw new BadRequestException('两次输入的密码不一致');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
const existingUserByUsername = await this.memberService.findByUsername(username);
|
||||||
|
if (existingUserByUsername) {
|
||||||
|
throw new ConflictException('用户名已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
const existingUserByMobile = await this.memberService.findByMobile(mobile);
|
||||||
|
if (existingUserByMobile) {
|
||||||
|
throw new ConflictException('手机号已被注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建会员
|
||||||
|
const member = await this.memberService.create({
|
||||||
|
username,
|
||||||
|
mobile,
|
||||||
|
password,
|
||||||
|
nickname: otherData.nickname || username,
|
||||||
|
email: otherData.email,
|
||||||
|
siteId: otherData.siteId || 0,
|
||||||
|
pid: otherData.pid || 0,
|
||||||
|
sex: otherData.sex || 0,
|
||||||
|
regType: otherData.regType || 'mobile',
|
||||||
|
status: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: member.memberId,
|
||||||
|
username: member.username,
|
||||||
|
mobile: member.mobile,
|
||||||
|
nickname: member.nickname,
|
||||||
|
message: '注册成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新token
|
||||||
|
*/
|
||||||
|
async refreshToken(refreshToken: string) {
|
||||||
|
try {
|
||||||
|
const payload = this.jwtService.verify(refreshToken);
|
||||||
|
|
||||||
|
// 验证用户是否仍然有效
|
||||||
|
const user = await this.validateUser(payload.username, '', payload.userType);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('用户不存在或已被禁用');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成新的token
|
||||||
|
const newPayload: JwtPayload = {
|
||||||
|
sub: payload.sub,
|
||||||
|
username: payload.username,
|
||||||
|
userType: payload.userType,
|
||||||
|
siteId: payload.siteId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const accessToken = this.jwtService.sign(newPayload);
|
||||||
|
const newRefreshToken = this.jwtService.sign(newPayload, {
|
||||||
|
expiresIn: this.configService.get('JWT_REFRESH_EXPIRES_IN', '7d'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken: newRefreshToken,
|
||||||
|
tokenType: 'Bearer',
|
||||||
|
expiresIn: this.configService.get('JWT_EXPIRES_IN', '1h'),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnauthorizedException('刷新token失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改密码
|
||||||
|
*/
|
||||||
|
async changePassword(userId: number, changePasswordDto: ChangePasswordDto) {
|
||||||
|
const { oldPassword, newPassword, confirmPassword, userType = 'member' } = changePasswordDto;
|
||||||
|
|
||||||
|
// 验证新密码确认
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
throw new BadRequestException('两次输入的新密码不一致');
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
if (userType === 'member') {
|
||||||
|
user = await this.memberService.findOne(userId);
|
||||||
|
// 验证旧密码
|
||||||
|
const isOldPasswordValid = await this.memberService.validatePassword(userId, oldPassword);
|
||||||
|
if (!isOldPasswordValid) {
|
||||||
|
throw new BadRequestException('旧密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新密码
|
||||||
|
await this.memberService.update(userId, { password: newPassword });
|
||||||
|
} else {
|
||||||
|
user = await this.adminService.findOne(userId);
|
||||||
|
// 验证旧密码
|
||||||
|
const isOldPasswordValid = await bcrypt.compare(oldPassword, user.password);
|
||||||
|
if (!isOldPasswordValid) {
|
||||||
|
throw new BadRequestException('旧密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新密码
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
|
await this.adminService.update(userId, { password: hashedPassword });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: '密码修改成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置密码
|
||||||
|
*/
|
||||||
|
async resetPassword(resetPasswordDto: ResetPasswordDto) {
|
||||||
|
const { identifier, newPassword, confirmPassword, userType = 'member' } = resetPasswordDto;
|
||||||
|
|
||||||
|
// 验证新密码确认
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
throw new BadRequestException('两次输入的新密码不一致');
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
if (userType === 'member') {
|
||||||
|
// 通过用户名或手机号查找用户
|
||||||
|
user = await this.memberService.findByUsername(identifier) ||
|
||||||
|
await this.memberService.findByMobile(identifier);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新密码
|
||||||
|
await this.memberService.update(user.memberId, { password: newPassword });
|
||||||
|
} else {
|
||||||
|
user = await this.adminService.findByUsername(identifier);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestException('管理员不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新密码
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||||
|
await this.adminService.update(user.uid, { password: hashedPassword });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: '密码重置成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出(可以在这里实现token黑名单等逻辑)
|
||||||
|
*/
|
||||||
|
async logout(token: string) {
|
||||||
|
// 这里可以实现token黑名单逻辑
|
||||||
|
// 目前简单返回成功消息
|
||||||
|
return {
|
||||||
|
message: '登出成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
37
wwjcloud/src/common/auth/decorators/auth.decorator.ts
Normal file
37
wwjcloud/src/common/auth/decorators/auth.decorator.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
// 标记公开路由(不需要认证)
|
||||||
|
export const Public = () => SetMetadata('isPublic', true);
|
||||||
|
|
||||||
|
// 设置所需角色
|
||||||
|
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
|
||||||
|
|
||||||
|
// 设置所需权限
|
||||||
|
export const Permissions = (...permissions: string[]) => SetMetadata('permissions', permissions);
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(data: string, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
const user = request.user;
|
||||||
|
|
||||||
|
return data ? user?.[data] : user;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取当前用户ID
|
||||||
|
export const CurrentUserId = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user?.userId;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取当前用户类型
|
||||||
|
export const CurrentUserType = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user?.userType;
|
||||||
|
},
|
||||||
|
);
|
||||||
66
wwjcloud/src/common/auth/dto/change-password.dto.ts
Normal file
66
wwjcloud/src/common/auth/dto/change-password.dto.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsNotEmpty, IsOptional, IsIn } from 'class-validator';
|
||||||
|
|
||||||
|
export class ChangePasswordDto {
|
||||||
|
@ApiProperty({ description: '旧密码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '旧密码不能为空' })
|
||||||
|
oldPassword: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '新密码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '新密码不能为空' })
|
||||||
|
newPassword: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '确认新密码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '确认新密码不能为空' })
|
||||||
|
confirmPassword: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '用户类型:member会员 admin管理员', default: 'member' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['member', 'admin'])
|
||||||
|
userType?: string = 'member';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResetPasswordDto {
|
||||||
|
@ApiProperty({ description: '用户名/手机号/邮箱' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '用户标识不能为空' })
|
||||||
|
identifier: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '新密码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '新密码不能为空' })
|
||||||
|
newPassword: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '确认新密码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '确认新密码不能为空' })
|
||||||
|
confirmPassword: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '用户类型:member会员 admin管理员', default: 'member' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['member', 'admin'])
|
||||||
|
userType?: string = 'member';
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '短信验证码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
smsCode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '邮箱验证码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
emailCode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '图形验证码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
captcha?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '验证码key' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
captchaKey?: string;
|
||||||
|
}
|
||||||
3
wwjcloud/src/common/auth/dto/index.ts
Normal file
3
wwjcloud/src/common/auth/dto/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './login.dto';
|
||||||
|
export * from './register.dto';
|
||||||
|
export * from './change-password.dto';
|
||||||
33
wwjcloud/src/common/auth/dto/login.dto.ts
Normal file
33
wwjcloud/src/common/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsNotEmpty, IsOptional, IsIn } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@ApiProperty({ description: '用户名/手机号' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '用户名不能为空' })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '密码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '密码不能为空' })
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '用户类型:member会员 admin管理员', default: 'member' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['member', 'admin'])
|
||||||
|
userType?: string = 'member';
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '验证码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
captcha?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '验证码key' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
captchaKey?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '记住我' })
|
||||||
|
@IsOptional()
|
||||||
|
rememberMe?: boolean;
|
||||||
|
}
|
||||||
79
wwjcloud/src/common/auth/dto/register.dto.ts
Normal file
79
wwjcloud/src/common/auth/dto/register.dto.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsNotEmpty, IsOptional, IsInt, IsIn, IsEmail, IsMobilePhone } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class RegisterDto {
|
||||||
|
@ApiProperty({ description: '用户名' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '用户名不能为空' })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '手机号' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '手机号不能为空' })
|
||||||
|
@IsMobilePhone('zh-CN', {}, { message: '手机号格式不正确' })
|
||||||
|
mobile: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '密码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '密码不能为空' })
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '确认密码' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty({ message: '确认密码不能为空' })
|
||||||
|
confirmPassword: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '昵称' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
nickname?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '邮箱' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '站点ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '推广会员ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
pid?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '性别:1男 2女 0未知', default: 0 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1, 2])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
sex?: number = 0;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '注册类型:username用户名 mobile手机号 email邮箱', default: 'mobile' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['username', 'mobile', 'email'])
|
||||||
|
regType?: string = 'mobile';
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '短信验证码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
smsCode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '邮箱验证码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
emailCode?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '图形验证码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
captcha?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '验证码key' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
captchaKey?: string;
|
||||||
|
}
|
||||||
40
wwjcloud/src/common/auth/guards/global-auth.guard.ts
Normal file
40
wwjcloud/src/common/auth/guards/global-auth.guard.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { IS_PUBLIC_KEY } from '../decorators/auth.decorator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局认证守卫
|
||||||
|
* 统一处理JWT认证,支持公开路由跳过认证
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class GlobalAuthGuard extends AuthGuard('jwt') {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
// 检查是否为公开路由
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequest(err: any, user: any, info: any, context: ExecutionContext) {
|
||||||
|
// 如果认证失败,抛出未授权异常
|
||||||
|
if (err || !user) {
|
||||||
|
throw err || new UnauthorizedException('认证失败,请重新登录');
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
wwjcloud/src/common/auth/guards/jwt-auth.guard.ts
Normal file
34
wwjcloud/src/common/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
// 检查是否标记为公开路由
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (isPublic) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequest(err: any, user: any, info: any, context: ExecutionContext) {
|
||||||
|
if (err || !user) {
|
||||||
|
throw err || new UnauthorizedException('未授权访问');
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
wwjcloud/src/common/auth/guards/local-auth.guard.ts
Normal file
5
wwjcloud/src/common/auth/guards/local-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||||
93
wwjcloud/src/common/auth/guards/roles.guard.ts
Normal file
93
wwjcloud/src/common/auth/guards/roles.guard.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { AdminService } from '../../admin/admin.service';
|
||||||
|
import { RoleService } from '../../rbac/role.service';
|
||||||
|
import { MenuService } from '../../rbac/menu.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private reflector: Reflector,
|
||||||
|
private adminService: AdminService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
private menuService: MenuService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
|
// 获取所需的角色或权限
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const requiredPermissions = this.reflector.getAllAndOverride<string[]>('permissions', [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 如果没有设置角色或权限要求,则允许访问
|
||||||
|
if (!requiredRoles && !requiredPermissions) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
const user = request.user;
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new ForbiddenException('用户未登录');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只对管理员进行角色权限验证
|
||||||
|
if (user.userType !== 'admin') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取用户角色
|
||||||
|
const userRoles = await this.adminService.getUserRoles(user.userId);
|
||||||
|
|
||||||
|
// 检查角色权限
|
||||||
|
if (requiredRoles && requiredRoles.length > 0) {
|
||||||
|
const hasRole = requiredRoles.some(role =>
|
||||||
|
userRoles.some(userRole => userRole.roleName === role)
|
||||||
|
);
|
||||||
|
if (!hasRole) {
|
||||||
|
throw new ForbiddenException('权限不足:缺少所需角色');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查菜单权限
|
||||||
|
if (requiredPermissions && requiredPermissions.length > 0) {
|
||||||
|
// 获取用户所有角色的权限菜单
|
||||||
|
const allMenuIds: number[] = [];
|
||||||
|
for (const role of userRoles) {
|
||||||
|
const menuIds = await this.roleService.getRoleMenuIds(role.roleId);
|
||||||
|
allMenuIds.push(...menuIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去重
|
||||||
|
const uniqueMenuIds = [...new Set(allMenuIds)];
|
||||||
|
|
||||||
|
// 获取菜单详情
|
||||||
|
const menus = await this.menuService.findByIds(uniqueMenuIds);
|
||||||
|
const userPermissions = menus.map(menu => menu.menuKey);
|
||||||
|
|
||||||
|
// 检查是否有所需权限
|
||||||
|
const hasPermission = requiredPermissions.some(permission =>
|
||||||
|
userPermissions.includes(permission)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
throw new ForbiddenException('权限不足:缺少所需权限');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ForbiddenException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new ForbiddenException('权限验证失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
wwjcloud/src/common/auth/index.ts
Normal file
13
wwjcloud/src/common/auth/index.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export * from './auth.module';
|
||||||
|
export * from './auth.controller';
|
||||||
|
export * from './user-permission.controller';
|
||||||
|
export * from './auth.service';
|
||||||
|
export * from './services';
|
||||||
|
export * from './dto';
|
||||||
|
export * from './strategies/jwt.strategy';
|
||||||
|
export * from './strategies/local.strategy';
|
||||||
|
export * from './guards/jwt-auth.guard';
|
||||||
|
export * from './guards/local-auth.guard';
|
||||||
|
export * from './guards/roles.guard';
|
||||||
|
export * from './guards/global-auth.guard';
|
||||||
|
export * from './decorators/auth.decorator';
|
||||||
1
wwjcloud/src/common/auth/services/index.ts
Normal file
1
wwjcloud/src/common/auth/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './permission.service';
|
||||||
215
wwjcloud/src/common/auth/services/permission.service.ts
Normal file
215
wwjcloud/src/common/auth/services/permission.service.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AdminService } from '../../admin/admin.service';
|
||||||
|
import { RoleService } from '../../rbac/role.service';
|
||||||
|
import { MenuService } from '../../rbac/menu.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PermissionService {
|
||||||
|
constructor(
|
||||||
|
private readonly adminService: AdminService,
|
||||||
|
private readonly roleService: RoleService,
|
||||||
|
private readonly menuService: MenuService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查管理员是否有指定权限
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param permission 权限标识
|
||||||
|
* @returns 是否有权限
|
||||||
|
*/
|
||||||
|
async checkAdminPermission(userId: number, permission: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// 获取用户信息
|
||||||
|
const user = await this.adminService.findById(userId);
|
||||||
|
if (!user || user.status !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超级管理员拥有所有权限
|
||||||
|
if (user.isAdmin === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色
|
||||||
|
const userRoles = await this.adminService.getUserRoles(userId);
|
||||||
|
if (!userRoles || userRoles.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查角色权限
|
||||||
|
for (const userRole of userRoles) {
|
||||||
|
const role = await this.roleService.findById(userRole.roleId);
|
||||||
|
if (role && role.status === 1) {
|
||||||
|
// 解析角色权限规则
|
||||||
|
const rules = this.parseRules(role.rules);
|
||||||
|
if (rules.includes(permission)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查管理员权限失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查管理员是否有指定角色
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param roleNames 角色名称数组
|
||||||
|
* @returns 是否有角色
|
||||||
|
*/
|
||||||
|
async checkAdminRole(userId: number, roleNames: string[]): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// 获取用户信息
|
||||||
|
const user = await this.adminService.findById(userId);
|
||||||
|
if (!user || user.status !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超级管理员拥有所有角色
|
||||||
|
if (user.isAdmin === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色
|
||||||
|
const userRoles = await this.adminService.getUserRoles(userId);
|
||||||
|
if (!userRoles || userRoles.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有指定角色
|
||||||
|
for (const userRole of userRoles) {
|
||||||
|
const role = await this.roleService.findById(userRole.roleId);
|
||||||
|
if (role && role.status === 1 && roleNames.includes(role.roleName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查管理员角色失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户菜单权限
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @returns 菜单ID数组
|
||||||
|
*/
|
||||||
|
async getUserMenuIds(userId: number): Promise<number[]> {
|
||||||
|
try {
|
||||||
|
// 获取用户信息
|
||||||
|
const user = await this.adminService.findById(userId);
|
||||||
|
if (!user || user.status !== 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超级管理员拥有所有菜单权限
|
||||||
|
if (user.isAdmin === 1) {
|
||||||
|
const allMenus = await this.menuService.findAll();
|
||||||
|
return allMenus.map(menu => menu.menuId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色
|
||||||
|
const userRoles = await this.adminService.getUserRoles(userId);
|
||||||
|
if (!userRoles || userRoles.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集所有角色的菜单权限
|
||||||
|
const menuIds = new Set<number>();
|
||||||
|
for (const userRole of userRoles) {
|
||||||
|
const role = await this.roleService.findById(userRole.roleId);
|
||||||
|
if (role && role.status === 1) {
|
||||||
|
const rules = this.parseRules(role.rules);
|
||||||
|
rules.forEach(rule => {
|
||||||
|
const menuId = parseInt(rule);
|
||||||
|
if (!isNaN(menuId)) {
|
||||||
|
menuIds.add(menuId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(menuIds);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户菜单权限失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户菜单树
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @returns 菜单树
|
||||||
|
*/
|
||||||
|
async getUserMenuTree(userId: number): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const menuIds = await this.getUserMenuIds(userId);
|
||||||
|
if (menuIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取菜单详情
|
||||||
|
const menus = await this.menuService.findByIds(menuIds);
|
||||||
|
|
||||||
|
// 构建菜单树
|
||||||
|
return this.buildMenuTree(menus);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户菜单树失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析权限规则
|
||||||
|
* @param rules 权限规则字符串
|
||||||
|
* @returns 权限数组
|
||||||
|
*/
|
||||||
|
private parseRules(rules: string): string[] {
|
||||||
|
try {
|
||||||
|
if (!rules) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析JSON格式
|
||||||
|
if (rules.startsWith('[') || rules.startsWith('{')) {
|
||||||
|
const parsed = JSON.parse(rules);
|
||||||
|
return Array.isArray(parsed) ? parsed.map(String) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 逗号分隔格式
|
||||||
|
return rules.split(',').map(rule => rule.trim()).filter(Boolean);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析权限规则失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建菜单树
|
||||||
|
* @param menus 菜单列表
|
||||||
|
* @param parentId 父级ID
|
||||||
|
* @returns 菜单树
|
||||||
|
*/
|
||||||
|
private buildMenuTree(menus: any[], parentId: number = 0): any[] {
|
||||||
|
const tree = [];
|
||||||
|
|
||||||
|
for (const menu of menus) {
|
||||||
|
if (menu.parentId === parentId) {
|
||||||
|
const children = this.buildMenuTree(menus, menu.menuId);
|
||||||
|
const menuItem = {
|
||||||
|
...menu,
|
||||||
|
children: children.length > 0 ? children : undefined,
|
||||||
|
};
|
||||||
|
tree.push(menuItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree.sort((a, b) => a.sort - b.sort);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
wwjcloud/src/common/auth/strategies/jwt.strategy.ts
Normal file
63
wwjcloud/src/common/auth/strategies/jwt.strategy.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { MemberService } from '../../member/member.service';
|
||||||
|
import { AdminService } from '../../admin/admin.service';
|
||||||
|
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: number; // 用户ID
|
||||||
|
username: string;
|
||||||
|
userType: 'member' | 'admin';
|
||||||
|
siteId?: number;
|
||||||
|
iat?: number;
|
||||||
|
exp?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly memberService: MemberService,
|
||||||
|
private readonly adminService: AdminService,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: configService.get<string>('JWT_SECRET', 'wwjcloud-secret-key'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload) {
|
||||||
|
const { sub: userId, userType, username } = payload;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
if (userType === 'member') {
|
||||||
|
user = await this.memberService.findOne(userId);
|
||||||
|
if (!user || user.status !== 1) {
|
||||||
|
throw new UnauthorizedException('会员账户已被禁用或不存在');
|
||||||
|
}
|
||||||
|
} else if (userType === 'admin') {
|
||||||
|
user = await this.adminService.findOne(userId);
|
||||||
|
if (!user || user.status !== 1) {
|
||||||
|
throw new UnauthorizedException('管理员账户已被禁用或不存在');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new UnauthorizedException('无效的用户类型');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回用户信息,会被注入到 request.user 中
|
||||||
|
return {
|
||||||
|
userId: user.memberId || user.uid,
|
||||||
|
username: user.username,
|
||||||
|
userType,
|
||||||
|
siteId: user.siteId,
|
||||||
|
user, // 完整的用户信息
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new UnauthorizedException('Token验证失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
wwjcloud/src/common/auth/strategies/local.strategy.ts
Normal file
27
wwjcloud/src/common/auth/strategies/local.strategy.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Strategy } from 'passport-local';
|
||||||
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(private readonly authService: AuthService) {
|
||||||
|
super({
|
||||||
|
usernameField: 'username',
|
||||||
|
passwordField: 'password',
|
||||||
|
passReqToCallback: true, // 允许传递 request 对象
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(request: any, username: string, password: string): Promise<any> {
|
||||||
|
const { userType = 'member' } = request.body;
|
||||||
|
|
||||||
|
const user = await this.authService.validateUser(username, password, userType);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException('用户名或密码错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
172
wwjcloud/src/common/auth/user-permission.controller.ts
Normal file
172
wwjcloud/src/common/auth/user-permission.controller.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
||||||
|
import { CurrentUser, CurrentUserId } from './decorators/auth.decorator';
|
||||||
|
import { PermissionService } from './services/permission.service';
|
||||||
|
import { AdminService } from '../admin/admin.service';
|
||||||
|
import { MemberService } from '../member/member.service';
|
||||||
|
|
||||||
|
@ApiTags('用户权限管理')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('user-permission')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
export class UserPermissionController {
|
||||||
|
constructor(
|
||||||
|
private readonly permissionService: PermissionService,
|
||||||
|
private readonly adminService: AdminService,
|
||||||
|
private readonly memberService: MemberService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('profile')
|
||||||
|
@ApiOperation({ summary: '获取当前用户信息' })
|
||||||
|
async getCurrentUserProfile(@CurrentUser() user: any) {
|
||||||
|
try {
|
||||||
|
if (user.userType === 'admin') {
|
||||||
|
const adminUser = await this.adminService.findById(user.userId);
|
||||||
|
if (!adminUser) {
|
||||||
|
return {
|
||||||
|
code: 404,
|
||||||
|
message: '用户不存在',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户角色
|
||||||
|
const userRoles = await this.adminService.getUserRoles(user.userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: {
|
||||||
|
...adminUser,
|
||||||
|
password: undefined, // 不返回密码
|
||||||
|
userType: 'admin',
|
||||||
|
roles: userRoles,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (user.userType === 'member') {
|
||||||
|
const memberUser = await this.memberService.findById(user.userId);
|
||||||
|
if (!memberUser) {
|
||||||
|
return {
|
||||||
|
code: 404,
|
||||||
|
message: '用户不存在',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: {
|
||||||
|
...memberUser,
|
||||||
|
password: undefined, // 不返回密码
|
||||||
|
userType: 'member',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 400,
|
||||||
|
message: '无效的用户类型',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: '获取用户信息失败',
|
||||||
|
data: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('menus')
|
||||||
|
@ApiOperation({ summary: '获取当前用户菜单权限' })
|
||||||
|
async getCurrentUserMenus(@CurrentUserId() userId: number, @CurrentUser() user: any) {
|
||||||
|
try {
|
||||||
|
if (user.userType !== 'admin') {
|
||||||
|
return {
|
||||||
|
code: 403,
|
||||||
|
message: '只有管理员用户才能获取菜单权限',
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuTree = await this.permissionService.getUserMenuTree(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: menuTree,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: '获取菜单权限失败',
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('permissions')
|
||||||
|
@ApiOperation({ summary: '获取当前用户权限列表' })
|
||||||
|
async getCurrentUserPermissions(@CurrentUserId() userId: number, @CurrentUser() user: any) {
|
||||||
|
try {
|
||||||
|
if (user.userType !== 'admin') {
|
||||||
|
return {
|
||||||
|
code: 403,
|
||||||
|
message: '只有管理员用户才能获取权限列表',
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuIds = await this.permissionService.getUserMenuIds(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: menuIds,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: '获取权限列表失败',
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('check-permission/:permission')
|
||||||
|
@ApiOperation({ summary: '检查用户是否有指定权限' })
|
||||||
|
async checkUserPermission(
|
||||||
|
@CurrentUserId() userId: number,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
permission: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (user.userType !== 'admin') {
|
||||||
|
return {
|
||||||
|
code: 403,
|
||||||
|
message: '只有管理员用户才能检查权限',
|
||||||
|
data: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasPermission = await this.permissionService.checkAdminPermission(
|
||||||
|
userId,
|
||||||
|
permission,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '检查完成',
|
||||||
|
data: hasPermission,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
code: 500,
|
||||||
|
message: '检查权限失败',
|
||||||
|
data: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
wwjcloud/src/common/cache/cache.module.ts
vendored
Normal file
4
wwjcloud/src/common/cache/cache.module.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class CacheModule {}
|
||||||
9
wwjcloud/src/common/dictionary/dictionary.controller.ts
Normal file
9
wwjcloud/src/common/dictionary/dictionary.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('dictionary')
|
||||||
|
export class DictionaryController {
|
||||||
|
@Get('ping')
|
||||||
|
ping() {
|
||||||
|
return 'dictionary ok';
|
||||||
|
}
|
||||||
|
}
|
||||||
10
wwjcloud/src/common/dictionary/dictionary.module.ts
Normal file
10
wwjcloud/src/common/dictionary/dictionary.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DictionaryService } from './dictionary.service';
|
||||||
|
import { DictionaryController } from './dictionary.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [DictionaryController],
|
||||||
|
providers: [DictionaryService],
|
||||||
|
exports: [DictionaryService],
|
||||||
|
})
|
||||||
|
export class DictionaryModule {}
|
||||||
8
wwjcloud/src/common/dictionary/dictionary.service.ts
Normal file
8
wwjcloud/src/common/dictionary/dictionary.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DictionaryService {
|
||||||
|
ping() {
|
||||||
|
return 'dictionary service ok';
|
||||||
|
}
|
||||||
|
}
|
||||||
8
wwjcloud/src/common/dictionary/dto/index.ts
Normal file
8
wwjcloud/src/common/dictionary/dto/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Dictionary DTO exports
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO: Dictionary DTOs
|
||||||
|
// export { CreateDictionaryDto } from './create-dictionary.dto'
|
||||||
|
// export { UpdateDictionaryDto } from './update-dictionary.dto'
|
||||||
|
// export { DictionaryResponseDto } from './dictionary-response.dto'
|
||||||
4
wwjcloud/src/common/health/health.module.ts
Normal file
4
wwjcloud/src/common/health/health.module.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class HealthModule {}
|
||||||
22
wwjcloud/src/common/index.ts
Normal file
22
wwjcloud/src/common/index.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export { SettingsModule } from './settings/settings.module';
|
||||||
|
export { UsersModule } from './users/users.module';
|
||||||
|
export { RbacModule } from './rbac/rbac.module';
|
||||||
|
export { NotificationModule } from './notification/notification.module';
|
||||||
|
export { DictionaryModule } from './dictionary/dictionary.module';
|
||||||
|
export { AppsModule } from './apps/apps.module';
|
||||||
|
export { UploadSettingsModule as UploadModule } from './settings/upload/upload-settings.module';
|
||||||
|
export { CacheModule as CommonCacheModule } from './cache/cache.module';
|
||||||
|
export { QueueModule as CommonQueueModule } from './queue/queue.module';
|
||||||
|
export { HealthModule } from './health/health.module';
|
||||||
|
export { OpenapiModule } from './openapi/openapi.module';
|
||||||
|
export { AuthModule } from './auth/auth.module';
|
||||||
|
|
||||||
|
// 新增的用户管理模块
|
||||||
|
export { MemberModule } from './member/member.module';
|
||||||
|
export { AdminModule } from './admin/admin.module';
|
||||||
|
|
||||||
|
// 导出服务和实体供其他模块使用
|
||||||
|
export * from './member';
|
||||||
|
export * from './admin';
|
||||||
|
export * from './rbac';
|
||||||
|
export * from './auth';
|
||||||
112
wwjcloud/src/common/member/dto/create-member.dto.ts
Normal file
112
wwjcloud/src/common/member/dto/create-member.dto.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsOptional, IsInt, IsEmail, IsIn, Length, IsPhoneNumber } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateMemberDto {
|
||||||
|
@ApiPropertyOptional({ description: '会员编码' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
memberNo?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '推广会员ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
pid?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '会员用户名' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 255)
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '手机号' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(11, 11)
|
||||||
|
mobile?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '会员密码' })
|
||||||
|
@IsString()
|
||||||
|
@Length(6, 255)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '会员昵称' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 255)
|
||||||
|
nickname?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '会员头像' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
headimg?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '会员等级' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
memberLevel?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '会员标签' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
memberLabel?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '微信用户openid' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
wxOpenid?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '微信小程序openid' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
weappOpenid?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '微信unionid' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
wxUnionid?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '支付宝账户id' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
aliOpenid?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '抖音小程序openid' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
douyinOpenid?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '注册类型' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
regType?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '生日' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
birthday?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '性别:1男 2女 0保密' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1, 2])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
sex?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '邮箱' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsEmail()
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '状态:1正常 0禁用', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
3
wwjcloud/src/common/member/dto/index.ts
Normal file
3
wwjcloud/src/common/member/dto/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { CreateMemberDto } from './create-member.dto';
|
||||||
|
export { UpdateMemberDto } from './update-member.dto';
|
||||||
|
export { QueryMemberDto } from './query-member.dto';
|
||||||
63
wwjcloud/src/common/member/dto/query-member.dto.ts
Normal file
63
wwjcloud/src/common/member/dto/query-member.dto.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsOptional, IsString, IsInt, IsIn } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class QueryMemberDto {
|
||||||
|
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value) || 1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value) || 10)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '关键词搜索(用户名/昵称/手机号)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
keyword?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '站点ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '会员等级' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
memberLevel?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '性别:1男 2女 0保密' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1, 2])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
sex?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '状态:1正常 0禁用' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '注册类型' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
regType?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '开始时间(时间戳)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
startTime?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '结束时间(时间戳)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
endTime?: number;
|
||||||
|
}
|
||||||
4
wwjcloud/src/common/member/dto/update-member.dto.ts
Normal file
4
wwjcloud/src/common/member/dto/update-member.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateMemberDto } from './create-member.dto';
|
||||||
|
|
||||||
|
export class UpdateMemberDto extends PartialType(CreateMemberDto) {}
|
||||||
113
wwjcloud/src/common/member/entities/member.entity.ts
Normal file
113
wwjcloud/src/common/member/entities/member.entity.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@Entity('member')
|
||||||
|
export class Member {
|
||||||
|
@ApiProperty({ description: '会员ID' })
|
||||||
|
@PrimaryGeneratedColumn({ name: 'member_id', type: 'int', unsigned: true })
|
||||||
|
memberId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '会员编码' })
|
||||||
|
@Column({ name: 'member_no', type: 'varchar', length: 255, default: '' })
|
||||||
|
memberNo: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '推广会员ID' })
|
||||||
|
@Column({ name: 'pid', type: 'int', default: 0 })
|
||||||
|
pid: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '会员用户名' })
|
||||||
|
@Column({ name: 'username', type: 'varchar', length: 255, default: '' })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '手机号' })
|
||||||
|
@Column({ name: 'mobile', type: 'varchar', length: 20, default: '' })
|
||||||
|
mobile: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '会员密码' })
|
||||||
|
@Column({ name: 'password', type: 'varchar', length: 255, default: '' })
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '会员昵称' })
|
||||||
|
@Column({ name: 'nickname', type: 'varchar', length: 255, default: '' })
|
||||||
|
nickname: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '会员头像' })
|
||||||
|
@Column({ name: 'headimg', type: 'varchar', length: 1000, default: '' })
|
||||||
|
headimg: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '会员等级' })
|
||||||
|
@Column({ name: 'member_level', type: 'int', default: 0 })
|
||||||
|
memberLevel: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '会员标签' })
|
||||||
|
@Column({ name: 'member_label', type: 'varchar', length: 255, default: '' })
|
||||||
|
memberLabel: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '微信用户openid' })
|
||||||
|
@Column({ name: 'wx_openid', type: 'varchar', length: 255, default: '' })
|
||||||
|
wxOpenid: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '微信小程序openid' })
|
||||||
|
@Column({ name: 'weapp_openid', type: 'varchar', length: 255, default: '' })
|
||||||
|
weappOpenid: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '微信unionid' })
|
||||||
|
@Column({ name: 'wx_unionid', type: 'varchar', length: 255, default: '' })
|
||||||
|
wxUnionid: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '支付宝账户id' })
|
||||||
|
@Column({ name: 'ali_openid', type: 'varchar', length: 255, default: '' })
|
||||||
|
aliOpenid: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '抖音小程序openid' })
|
||||||
|
@Column({ name: 'douyin_openid', type: 'varchar', length: 255, default: '' })
|
||||||
|
douyinOpenid: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '注册时间' })
|
||||||
|
@Column({ name: 'reg_time', type: 'int', default: 0 })
|
||||||
|
regTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '注册类型' })
|
||||||
|
@Column({ name: 'reg_type', type: 'varchar', length: 255, default: '' })
|
||||||
|
regType: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '生日' })
|
||||||
|
@Column({ name: 'birthday', type: 'varchar', length: 255, default: '' })
|
||||||
|
birthday: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '性别:1男 2女 0保密' })
|
||||||
|
@Column({ name: 'sex', type: 'tinyint', default: 0 })
|
||||||
|
sex: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱' })
|
||||||
|
@Column({ name: 'email', type: 'varchar', length: 255, default: '' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '状态:1正常 0禁用' })
|
||||||
|
@Column({ name: 'status', type: 'tinyint', default: 1 })
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '最后登录时间' })
|
||||||
|
@Column({ name: 'last_visit_time', type: 'int', default: 0 })
|
||||||
|
lastVisitTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '最后登录IP' })
|
||||||
|
@Column({ name: 'last_visit_ip', type: 'varchar', length: 255, default: '' })
|
||||||
|
lastVisitIp: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '删除时间' })
|
||||||
|
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||||
|
deleteTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
@CreateDateColumn({ name: 'create_time', type: 'int' })
|
||||||
|
createTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
@UpdateDateColumn({ name: 'update_time', type: 'int' })
|
||||||
|
updateTime: number;
|
||||||
|
}
|
||||||
5
wwjcloud/src/common/member/index.ts
Normal file
5
wwjcloud/src/common/member/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export { MemberModule } from './member.module';
|
||||||
|
export { MemberService } from './member.service';
|
||||||
|
export { MemberController } from './member.controller';
|
||||||
|
export { Member } from './entities/member.entity';
|
||||||
|
export * from './dto';
|
||||||
142
wwjcloud/src/common/member/member.controller.ts
Normal file
142
wwjcloud/src/common/member/member.controller.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
ParseIntPipe,
|
||||||
|
HttpStatus,
|
||||||
|
UseGuards,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { MemberService } from './member.service';
|
||||||
|
import { CreateMemberDto, UpdateMemberDto, QueryMemberDto } from './dto';
|
||||||
|
import { Member } from './entities/member.entity';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
@ApiTags('会员管理')
|
||||||
|
@Controller('member')
|
||||||
|
export class MemberController {
|
||||||
|
constructor(private readonly memberService: MemberService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: '创建会员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.CREATED, description: '创建成功', type: Member })
|
||||||
|
@ApiResponse({ status: HttpStatus.CONFLICT, description: '用户名或手机号已存在' })
|
||||||
|
async create(@Body() createMemberDto: CreateMemberDto) {
|
||||||
|
const member = await this.memberService.create(createMemberDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '创建成功',
|
||||||
|
data: member,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取会员列表' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '获取成功' })
|
||||||
|
async findAll(@Query() queryDto: QueryMemberDto) {
|
||||||
|
const result = await this.memberService.findAll(queryDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: '获取会员详情' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '获取成功', type: Member })
|
||||||
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '会员不存在' })
|
||||||
|
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
const member = await this.memberService.findOne(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: member,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: '更新会员信息' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '更新成功', type: Member })
|
||||||
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '会员不存在' })
|
||||||
|
@ApiResponse({ status: HttpStatus.CONFLICT, description: '用户名或手机号已存在' })
|
||||||
|
async update(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body() updateMemberDto: UpdateMemberDto,
|
||||||
|
) {
|
||||||
|
const member = await this.memberService.update(id, updateMemberDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '更新成功',
|
||||||
|
data: member,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: '删除会员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '删除成功' })
|
||||||
|
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '会员不存在' })
|
||||||
|
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
await this.memberService.remove(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '删除成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('batch-delete')
|
||||||
|
@ApiOperation({ summary: '批量删除会员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '批量删除成功' })
|
||||||
|
async batchRemove(@Body('ids') ids: number[]) {
|
||||||
|
await this.memberService.batchRemove(ids);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '批量删除成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/update-last-visit')
|
||||||
|
@ApiOperation({ summary: '更新最后登录信息' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '更新成功' })
|
||||||
|
async updateLastVisit(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Req() request: Request,
|
||||||
|
) {
|
||||||
|
const ip = request.ip || request.connection.remoteAddress || '';
|
||||||
|
await this.memberService.updateLastVisit(id, ip);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '更新成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('search/by-username/:username')
|
||||||
|
@ApiOperation({ summary: '根据用户名查询会员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '查询成功' })
|
||||||
|
async findByUsername(@Param('username') username: string) {
|
||||||
|
const member = await this.memberService.findByUsername(username);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '查询成功',
|
||||||
|
data: member,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('search/by-mobile/:mobile')
|
||||||
|
@ApiOperation({ summary: '根据手机号查询会员' })
|
||||||
|
@ApiResponse({ status: HttpStatus.OK, description: '查询成功' })
|
||||||
|
async findByMobile(@Param('mobile') mobile: string) {
|
||||||
|
const member = await this.memberService.findByMobile(mobile);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '查询成功',
|
||||||
|
data: member,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
13
wwjcloud/src/common/member/member.module.ts
Normal file
13
wwjcloud/src/common/member/member.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { MemberService } from './member.service';
|
||||||
|
import { MemberController } from './member.controller';
|
||||||
|
import { Member } from './entities/member.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Member])],
|
||||||
|
controllers: [MemberController],
|
||||||
|
providers: [MemberService],
|
||||||
|
exports: [MemberService, TypeOrmModule],
|
||||||
|
})
|
||||||
|
export class MemberModule {}
|
||||||
251
wwjcloud/src/common/member/member.service.ts
Normal file
251
wwjcloud/src/common/member/member.service.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, Like, Between } from 'typeorm';
|
||||||
|
import { Member } from './entities/member.entity';
|
||||||
|
import { CreateMemberDto, UpdateMemberDto, QueryMemberDto } from './dto';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MemberService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Member)
|
||||||
|
private readonly memberRepository: Repository<Member>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建会员
|
||||||
|
*/
|
||||||
|
async create(createMemberDto: CreateMemberDto): Promise<Member> {
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
if (createMemberDto.username) {
|
||||||
|
const existingByUsername = await this.memberRepository.findOne({
|
||||||
|
where: { username: createMemberDto.username, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (existingByUsername) {
|
||||||
|
throw new ConflictException('用户名已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
if (createMemberDto.mobile) {
|
||||||
|
const existingByMobile = await this.memberRepository.findOne({
|
||||||
|
where: { mobile: createMemberDto.mobile, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (existingByMobile) {
|
||||||
|
throw new ConflictException('手机号已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码加密
|
||||||
|
const hashedPassword = await bcrypt.hash(createMemberDto.password, 10);
|
||||||
|
|
||||||
|
const member = this.memberRepository.create({
|
||||||
|
...createMemberDto,
|
||||||
|
password: hashedPassword,
|
||||||
|
regTime: Math.floor(Date.now() / 1000),
|
||||||
|
createTime: Math.floor(Date.now() / 1000),
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.memberRepository.save(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询会员列表
|
||||||
|
*/
|
||||||
|
async findAll(queryDto: QueryMemberDto) {
|
||||||
|
const { page = 1, limit = 10, keyword, siteId, memberLevel, sex, status, regType, startTime, endTime } = queryDto;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const queryBuilder = this.memberRepository.createQueryBuilder('member')
|
||||||
|
.where('member.deleteTime = :deleteTime', { deleteTime: 0 });
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (keyword) {
|
||||||
|
queryBuilder.andWhere(
|
||||||
|
'(member.username LIKE :keyword OR member.nickname LIKE :keyword OR member.mobile LIKE :keyword)',
|
||||||
|
{ keyword: `%${keyword}%` }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点ID筛选
|
||||||
|
if (siteId !== undefined) {
|
||||||
|
queryBuilder.andWhere('member.siteId = :siteId', { siteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 会员等级筛选
|
||||||
|
if (memberLevel !== undefined) {
|
||||||
|
queryBuilder.andWhere('member.memberLevel = :memberLevel', { memberLevel });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性别筛选
|
||||||
|
if (sex !== undefined) {
|
||||||
|
queryBuilder.andWhere('member.sex = :sex', { sex });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if (status !== undefined) {
|
||||||
|
queryBuilder.andWhere('member.status = :status', { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册类型筛选
|
||||||
|
if (regType) {
|
||||||
|
queryBuilder.andWhere('member.regType = :regType', { regType });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
if (startTime && endTime) {
|
||||||
|
queryBuilder.andWhere('member.regTime BETWEEN :startTime AND :endTime', {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
});
|
||||||
|
} else if (startTime) {
|
||||||
|
queryBuilder.andWhere('member.regTime >= :startTime', { startTime });
|
||||||
|
} else if (endTime) {
|
||||||
|
queryBuilder.andWhere('member.regTime <= :endTime', { endTime });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
queryBuilder.orderBy('member.createTime', 'DESC');
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const [list, total] = await queryBuilder
|
||||||
|
.skip(skip)
|
||||||
|
.take(limit)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
// 移除密码字段
|
||||||
|
const safeList = list.map(member => {
|
||||||
|
const { password, ...safeMember } = member;
|
||||||
|
return safeMember;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: safeList,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询会员详情
|
||||||
|
*/
|
||||||
|
async findOne(id: number): Promise<Member> {
|
||||||
|
const member = await this.memberRepository.findOne({
|
||||||
|
where: { memberId: id, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
throw new NotFoundException('会员不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除密码字段
|
||||||
|
const { password, ...safeMember } = member;
|
||||||
|
return safeMember as Member;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名查询会员
|
||||||
|
*/
|
||||||
|
async findByUsername(username: string): Promise<Member | null> {
|
||||||
|
return await this.memberRepository.findOne({
|
||||||
|
where: { username, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号查询会员
|
||||||
|
*/
|
||||||
|
async findByMobile(mobile: string): Promise<Member | null> {
|
||||||
|
return await this.memberRepository.findOne({
|
||||||
|
where: { mobile, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新会员信息
|
||||||
|
*/
|
||||||
|
async update(id: number, updateMemberDto: UpdateMemberDto): Promise<Member> {
|
||||||
|
const member = await this.findOne(id);
|
||||||
|
|
||||||
|
// 检查用户名是否已被其他用户使用
|
||||||
|
if (updateMemberDto.username && updateMemberDto.username !== member.username) {
|
||||||
|
const existingByUsername = await this.memberRepository.findOne({
|
||||||
|
where: { username: updateMemberDto.username, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (existingByUsername && existingByUsername.memberId !== id) {
|
||||||
|
throw new ConflictException('用户名已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已被其他用户使用
|
||||||
|
if (updateMemberDto.mobile && updateMemberDto.mobile !== member.mobile) {
|
||||||
|
const existingByMobile = await this.memberRepository.findOne({
|
||||||
|
where: { mobile: updateMemberDto.mobile, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (existingByMobile && existingByMobile.memberId !== id) {
|
||||||
|
throw new ConflictException('手机号已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果更新密码,需要加密
|
||||||
|
if (updateMemberDto.password) {
|
||||||
|
updateMemberDto.password = await bcrypt.hash(updateMemberDto.password, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.memberRepository.update(id, {
|
||||||
|
...updateMemberDto,
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除会员
|
||||||
|
*/
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
const member = await this.findOne(id);
|
||||||
|
|
||||||
|
await this.memberRepository.update(id, {
|
||||||
|
deleteTime: Math.floor(Date.now() / 1000),
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量软删除会员
|
||||||
|
*/
|
||||||
|
async batchRemove(ids: number[]): Promise<void> {
|
||||||
|
const deleteTime = Math.floor(Date.now() / 1000);
|
||||||
|
|
||||||
|
await this.memberRepository.update(
|
||||||
|
{ memberId: { $in: ids } as any },
|
||||||
|
{
|
||||||
|
deleteTime,
|
||||||
|
updateTime: deleteTime,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新最后登录信息
|
||||||
|
*/
|
||||||
|
async updateLastVisit(id: number, ip: string): Promise<void> {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
await this.memberRepository.update(id, {
|
||||||
|
lastVisitTime: now,
|
||||||
|
lastVisitIp: ip,
|
||||||
|
updateTime: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证密码
|
||||||
|
*/
|
||||||
|
async validatePassword(member: Member, password: string): Promise<boolean> {
|
||||||
|
return await bcrypt.compare(password, member.password);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
wwjcloud/src/common/notification/notification.module.ts
Normal file
10
wwjcloud/src/common/notification/notification.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { NotificationService } from './notification.service';
|
||||||
|
import { EmailModule, SmsModule } from '../settings';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [EmailModule, SmsModule],
|
||||||
|
providers: [NotificationService],
|
||||||
|
exports: [NotificationService],
|
||||||
|
})
|
||||||
|
export class NotificationModule {}
|
||||||
22
wwjcloud/src/common/notification/notification.service.ts
Normal file
22
wwjcloud/src/common/notification/notification.service.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EmailService, SmsService } from '../settings';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NotificationService {
|
||||||
|
constructor(
|
||||||
|
private readonly emailService: EmailService,
|
||||||
|
private readonly smsService: SmsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async sendEmail(to: string, subject: string, content: string) {
|
||||||
|
return this.emailService.send(to, subject, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendSms(
|
||||||
|
to: string,
|
||||||
|
templateId: string,
|
||||||
|
params: Record<string, any> = {},
|
||||||
|
) {
|
||||||
|
return this.smsService.send(to, templateId, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
wwjcloud/src/common/openapi/openapi.module.ts
Normal file
4
wwjcloud/src/common/openapi/openapi.module.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class OpenapiModule {}
|
||||||
4
wwjcloud/src/common/queue/queue.module.ts
Normal file
4
wwjcloud/src/common/queue/queue.module.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class QueueModule {}
|
||||||
79
wwjcloud/src/common/rbac/dto/create-menu.dto.ts
Normal file
79
wwjcloud/src/common/rbac/dto/create-menu.dto.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsOptional, IsInt, IsIn, Length } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateMenuDto {
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单名称' })
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 255)
|
||||||
|
menuName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单标识' })
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 255)
|
||||||
|
menuKey: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单类型:1目录 2菜单 3按钮' })
|
||||||
|
@IsIn([1, 2, 3])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
menuType: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '父级菜单ID', default: 0 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
pid?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '菜单图标' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(0, 255)
|
||||||
|
icon?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '路由地址' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(0, 255)
|
||||||
|
apiUrl?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '路由路径' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(0, 255)
|
||||||
|
router?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '视图路径' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(0, 255)
|
||||||
|
viewPath?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '请求方式' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(0, 255)
|
||||||
|
methods?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '排序', default: 0 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
sort?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '状态:1显示 0隐藏', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '是否显示:1显示 0隐藏', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
isShow?: number;
|
||||||
|
}
|
||||||
34
wwjcloud/src/common/rbac/dto/create-role.dto.ts
Normal file
34
wwjcloud/src/common/rbac/dto/create-role.dto.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsOptional, IsInt, IsIn, IsArray, Length } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateRoleDto {
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '角色名称' })
|
||||||
|
@IsString()
|
||||||
|
@Length(1, 255)
|
||||||
|
roleName: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '角色描述' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@Length(0, 255)
|
||||||
|
remark?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '权限规则(菜单ID数组)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsInt({ each: true })
|
||||||
|
@Transform(({ value }) => Array.isArray(value) ? value.map(v => parseInt(v)) : [])
|
||||||
|
rules?: number[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '状态:1正常 0禁用', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
status?: number;
|
||||||
|
}
|
||||||
6
wwjcloud/src/common/rbac/dto/index.ts
Normal file
6
wwjcloud/src/common/rbac/dto/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export * from './create-role.dto';
|
||||||
|
export * from './update-role.dto';
|
||||||
|
export * from './query-role.dto';
|
||||||
|
export * from './create-menu.dto';
|
||||||
|
export * from './update-menu.dto';
|
||||||
|
export * from './query-menu.dto';
|
||||||
64
wwjcloud/src/common/rbac/dto/query-menu.dto.ts
Normal file
64
wwjcloud/src/common/rbac/dto/query-menu.dto.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsOptional, IsString, IsInt, IsIn } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class QueryMenuDto {
|
||||||
|
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value) || 1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value) || 10)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '关键词搜索(菜单名称)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
keyword?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '站点ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '菜单类型:1目录 2菜单 3按钮' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([1, 2, 3])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
menuType?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '父级菜单ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
pid?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '状态:1正常 0禁用' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '是否显示:1显示 0隐藏' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
isShow?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '开始时间(时间戳)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
startTime?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '结束时间(时间戳)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
endTime?: number;
|
||||||
|
}
|
||||||
46
wwjcloud/src/common/rbac/dto/query-role.dto.ts
Normal file
46
wwjcloud/src/common/rbac/dto/query-role.dto.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsOptional, IsString, IsInt, IsIn } from 'class-validator';
|
||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class QueryRoleDto {
|
||||||
|
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value) || 1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value) || 10)
|
||||||
|
limit?: number = 10;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '关键词搜索(角色名称)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
keyword?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '站点ID' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
siteId?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '状态:1正常 0禁用' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([0, 1])
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '开始时间(时间戳)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
startTime?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: '结束时间(时间戳)' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
endTime?: number;
|
||||||
|
}
|
||||||
4
wwjcloud/src/common/rbac/dto/update-menu.dto.ts
Normal file
4
wwjcloud/src/common/rbac/dto/update-menu.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateMenuDto } from './create-menu.dto';
|
||||||
|
|
||||||
|
export class UpdateMenuDto extends PartialType(CreateMenuDto) {}
|
||||||
4
wwjcloud/src/common/rbac/dto/update-role.dto.ts
Normal file
4
wwjcloud/src/common/rbac/dto/update-role.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateRoleDto } from './create-role.dto';
|
||||||
|
|
||||||
|
export class UpdateRoleDto extends PartialType(CreateRoleDto) {}
|
||||||
73
wwjcloud/src/common/rbac/entities/sys-menu.entity.ts
Normal file
73
wwjcloud/src/common/rbac/entities/sys-menu.entity.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@Entity('sys_menu')
|
||||||
|
export class SysMenu {
|
||||||
|
@ApiProperty({ description: '菜单ID' })
|
||||||
|
@PrimaryGeneratedColumn({ name: 'menu_id', type: 'int', unsigned: true })
|
||||||
|
menuId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单名称' })
|
||||||
|
@Column({ name: 'menu_name', type: 'varchar', length: 255, default: '' })
|
||||||
|
menuName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单标识' })
|
||||||
|
@Column({ name: 'menu_key', type: 'varchar', length: 255, default: '' })
|
||||||
|
menuKey: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单类型:1目录 2菜单 3按钮' })
|
||||||
|
@Column({ name: 'menu_type', type: 'tinyint', default: 1 })
|
||||||
|
menuType: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '父级菜单ID' })
|
||||||
|
@Column({ name: 'pid', type: 'int', default: 0 })
|
||||||
|
pid: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单图标' })
|
||||||
|
@Column({ name: 'icon', type: 'varchar', length: 255, default: '' })
|
||||||
|
icon: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '路由地址' })
|
||||||
|
@Column({ name: 'api_url', type: 'varchar', length: 255, default: '' })
|
||||||
|
apiUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '路由路径' })
|
||||||
|
@Column({ name: 'router', type: 'varchar', length: 255, default: '' })
|
||||||
|
router: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '视图路径' })
|
||||||
|
@Column({ name: 'view_path', type: 'varchar', length: 255, default: '' })
|
||||||
|
viewPath: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '请求方式' })
|
||||||
|
@Column({ name: 'methods', type: 'varchar', length: 255, default: '' })
|
||||||
|
methods: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '排序' })
|
||||||
|
@Column({ name: 'sort', type: 'int', default: 0 })
|
||||||
|
sort: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '状态:1显示 0隐藏' })
|
||||||
|
@Column({ name: 'status', type: 'tinyint', default: 1 })
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否缓存:1缓存 0不缓存' })
|
||||||
|
@Column({ name: 'is_show', type: 'tinyint', default: 1 })
|
||||||
|
isShow: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
@CreateDateColumn({ name: 'create_time', type: 'int' })
|
||||||
|
createTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
@UpdateDateColumn({ name: 'update_time', type: 'int' })
|
||||||
|
updateTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '删除时间' })
|
||||||
|
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||||
|
deleteTime: number;
|
||||||
|
}
|
||||||
41
wwjcloud/src/common/rbac/entities/sys-role.entity.ts
Normal file
41
wwjcloud/src/common/rbac/entities/sys-role.entity.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@Entity('sys_role')
|
||||||
|
export class SysRole {
|
||||||
|
@ApiProperty({ description: '角色ID' })
|
||||||
|
@PrimaryGeneratedColumn({ name: 'role_id', type: 'int', unsigned: true })
|
||||||
|
roleId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '角色名称' })
|
||||||
|
@Column({ name: 'role_name', type: 'varchar', length: 255, default: '' })
|
||||||
|
roleName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '角色描述' })
|
||||||
|
@Column({ name: 'remark', type: 'varchar', length: 255, default: '' })
|
||||||
|
remark: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '权限规则' })
|
||||||
|
@Column({ name: 'rules', type: 'text' })
|
||||||
|
rules: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '状态:1正常 0禁用' })
|
||||||
|
@Column({ name: 'status', type: 'tinyint', default: 1 })
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
@CreateDateColumn({ name: 'create_time', type: 'int' })
|
||||||
|
createTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
@UpdateDateColumn({ name: 'update_time', type: 'int' })
|
||||||
|
updateTime: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '删除时间' })
|
||||||
|
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||||
|
deleteTime: number;
|
||||||
|
}
|
||||||
8
wwjcloud/src/common/rbac/index.ts
Normal file
8
wwjcloud/src/common/rbac/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export * from './rbac.module';
|
||||||
|
export * from './role.controller';
|
||||||
|
export * from './menu.controller';
|
||||||
|
export * from './role.service';
|
||||||
|
export * from './menu.service';
|
||||||
|
export * from './entities/sys-role.entity';
|
||||||
|
export * from './entities/sys-menu.entity';
|
||||||
|
export * from './dto';
|
||||||
154
wwjcloud/src/common/rbac/menu.controller.ts
Normal file
154
wwjcloud/src/common/rbac/menu.controller.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
ParseIntPipe,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { MenuService } from './menu.service';
|
||||||
|
import { CreateMenuDto, UpdateMenuDto, QueryMenuDto } from './dto';
|
||||||
|
|
||||||
|
@ApiTags('菜单管理')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('rbac/menus')
|
||||||
|
export class MenuController {
|
||||||
|
constructor(private readonly menuService: MenuService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: '创建菜单' })
|
||||||
|
async create(@Body() createMenuDto: CreateMenuDto) {
|
||||||
|
const menu = await this.menuService.create(createMenuDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '创建成功',
|
||||||
|
data: menu,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取菜单列表' })
|
||||||
|
async findAll(@Query() queryMenuDto: QueryMenuDto) {
|
||||||
|
const result = await this.menuService.findAll(queryMenuDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('tree')
|
||||||
|
@ApiOperation({ summary: '获取菜单树' })
|
||||||
|
async findTree(@Query('siteId', ParseIntPipe) siteId?: number) {
|
||||||
|
const tree = await this.menuService.findTree(siteId);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: tree,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: '获取菜单详情' })
|
||||||
|
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
const menu = await this.menuService.findOne(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: menu,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('key/:menuKey')
|
||||||
|
@ApiOperation({ summary: '根据菜单标识查询菜单' })
|
||||||
|
async findByKey(
|
||||||
|
@Param('menuKey') menuKey: string,
|
||||||
|
@Query('siteId', ParseIntPipe) siteId?: number,
|
||||||
|
) {
|
||||||
|
const menu = await this.menuService.findByKey(menuKey, siteId);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: menu,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('batch')
|
||||||
|
@ApiOperation({ summary: '根据菜单ID数组获取菜单列表' })
|
||||||
|
async findByIds(@Body('menuIds') menuIds: number[]) {
|
||||||
|
const menus = await this.menuService.findByIds(menuIds);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: menus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: '更新菜单' })
|
||||||
|
async update(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body() updateMenuDto: UpdateMenuDto,
|
||||||
|
) {
|
||||||
|
const menu = await this.menuService.update(id, updateMenuDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '更新成功',
|
||||||
|
data: menu,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: '删除菜单' })
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
await this.menuService.remove(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '删除成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('batch')
|
||||||
|
@ApiOperation({ summary: '批量删除菜单' })
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async removeBatch(@Body('ids') ids: number[]) {
|
||||||
|
await this.menuService.removeBatch(ids);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '批量删除成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/status')
|
||||||
|
@ApiOperation({ summary: '更新菜单状态' })
|
||||||
|
async updateStatus(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body('status', ParseIntPipe) status: number,
|
||||||
|
) {
|
||||||
|
await this.menuService.updateStatus(id, status);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '状态更新成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/sort')
|
||||||
|
@ApiOperation({ summary: '更新菜单排序' })
|
||||||
|
async updateSort(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body('sort', ParseIntPipe) sort: number,
|
||||||
|
) {
|
||||||
|
await this.menuService.updateSort(id, sort);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '排序更新成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
296
wwjcloud/src/common/rbac/menu.service.ts
Normal file
296
wwjcloud/src/common/rbac/menu.service.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, Like, Between } from 'typeorm';
|
||||||
|
import { SysMenu } from './entities/sys-menu.entity';
|
||||||
|
import { CreateMenuDto, UpdateMenuDto, QueryMenuDto } from './dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MenuService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SysMenu)
|
||||||
|
private readonly menuRepository: Repository<SysMenu>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建菜单
|
||||||
|
*/
|
||||||
|
async create(createMenuDto: CreateMenuDto): Promise<SysMenu> {
|
||||||
|
// 检查菜单标识是否已存在
|
||||||
|
const existingMenu = await this.menuRepository.findOne({
|
||||||
|
where: {
|
||||||
|
menuKey: createMenuDto.menuKey,
|
||||||
|
siteId: createMenuDto.siteId,
|
||||||
|
deleteTime: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMenu) {
|
||||||
|
throw new BadRequestException('菜单标识已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有父级菜单,验证父级菜单是否存在
|
||||||
|
if (createMenuDto.pid && createMenuDto.pid > 0) {
|
||||||
|
const parentMenu = await this.menuRepository.findOne({
|
||||||
|
where: { menuId: createMenuDto.pid, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (!parentMenu) {
|
||||||
|
throw new BadRequestException('父级菜单不存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = this.menuRepository.create({
|
||||||
|
...createMenuDto,
|
||||||
|
createTime: Math.floor(Date.now() / 1000),
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.menuRepository.save(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询菜单列表
|
||||||
|
*/
|
||||||
|
async findAll(queryMenuDto: QueryMenuDto) {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
keyword,
|
||||||
|
siteId,
|
||||||
|
menuType,
|
||||||
|
pid,
|
||||||
|
status,
|
||||||
|
isShow,
|
||||||
|
startTime,
|
||||||
|
endTime
|
||||||
|
} = queryMenuDto;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const queryBuilder = this.menuRepository.createQueryBuilder('menu')
|
||||||
|
.where('menu.deleteTime = :deleteTime', { deleteTime: 0 });
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (keyword) {
|
||||||
|
queryBuilder.andWhere('menu.menuName LIKE :keyword', { keyword: `%${keyword}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点ID筛选
|
||||||
|
if (siteId !== undefined) {
|
||||||
|
queryBuilder.andWhere('menu.siteId = :siteId', { siteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单类型筛选
|
||||||
|
if (menuType !== undefined) {
|
||||||
|
queryBuilder.andWhere('menu.menuType = :menuType', { menuType });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 父级菜单筛选
|
||||||
|
if (pid !== undefined) {
|
||||||
|
queryBuilder.andWhere('menu.pid = :pid', { pid });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if (status !== undefined) {
|
||||||
|
queryBuilder.andWhere('menu.status = :status', { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 是否显示筛选
|
||||||
|
if (isShow !== undefined) {
|
||||||
|
queryBuilder.andWhere('menu.isShow = :isShow', { isShow });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
if (startTime && endTime) {
|
||||||
|
queryBuilder.andWhere('menu.createTime BETWEEN :startTime AND :endTime', {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
queryBuilder.orderBy('menu.sort', 'ASC')
|
||||||
|
.addOrderBy('menu.createTime', 'DESC');
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const [list, total] = await queryBuilder
|
||||||
|
.skip(skip)
|
||||||
|
.take(limit)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取树形菜单结构
|
||||||
|
*/
|
||||||
|
async findTree(siteId?: number): Promise<SysMenu[]> {
|
||||||
|
const queryBuilder = this.menuRepository.createQueryBuilder('menu')
|
||||||
|
.where('menu.deleteTime = :deleteTime', { deleteTime: 0 })
|
||||||
|
.andWhere('menu.status = :status', { status: 1 });
|
||||||
|
|
||||||
|
if (siteId !== undefined) {
|
||||||
|
queryBuilder.andWhere('menu.siteId = :siteId', { siteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
const menus = await queryBuilder
|
||||||
|
.orderBy('menu.sort', 'ASC')
|
||||||
|
.addOrderBy('menu.createTime', 'ASC')
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
return this.buildMenuTree(menus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建菜单树
|
||||||
|
*/
|
||||||
|
private buildMenuTree(menus: SysMenu[], pid: number = 0): SysMenu[] {
|
||||||
|
const tree: SysMenu[] = [];
|
||||||
|
|
||||||
|
for (const menu of menus) {
|
||||||
|
if (menu.pid === pid) {
|
||||||
|
const children = this.buildMenuTree(menus, menu.menuId);
|
||||||
|
if (children.length > 0) {
|
||||||
|
(menu as any).children = children;
|
||||||
|
}
|
||||||
|
tree.push(menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询菜单
|
||||||
|
*/
|
||||||
|
async findOne(id: number): Promise<SysMenu> {
|
||||||
|
const menu = await this.menuRepository.findOne({
|
||||||
|
where: { menuId: id, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!menu) {
|
||||||
|
throw new NotFoundException('菜单不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据菜单标识查询菜单
|
||||||
|
*/
|
||||||
|
async findByKey(menuKey: string, siteId?: number): Promise<SysMenu | null> {
|
||||||
|
const where: any = { menuKey, deleteTime: 0 };
|
||||||
|
if (siteId !== undefined) {
|
||||||
|
where.siteId = siteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.menuRepository.findOne({ where });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据菜单ID数组获取菜单列表
|
||||||
|
*/
|
||||||
|
async findByIds(menuIds: number[]): Promise<SysMenu[]> {
|
||||||
|
if (!menuIds || menuIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.menuRepository.find({
|
||||||
|
where: {
|
||||||
|
menuId: { $in: menuIds } as any,
|
||||||
|
deleteTime: 0,
|
||||||
|
status: 1,
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
sort: 'ASC',
|
||||||
|
createTime: 'ASC',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新菜单
|
||||||
|
*/
|
||||||
|
async update(id: number, updateMenuDto: UpdateMenuDto): Promise<SysMenu> {
|
||||||
|
const menu = await this.findOne(id);
|
||||||
|
|
||||||
|
// 如果更新菜单标识,检查是否与其他菜单冲突
|
||||||
|
if (updateMenuDto.menuKey && updateMenuDto.menuKey !== menu.menuKey) {
|
||||||
|
const existingMenu = await this.menuRepository.findOne({
|
||||||
|
where: {
|
||||||
|
menuKey: updateMenuDto.menuKey,
|
||||||
|
siteId: updateMenuDto.siteId || menu.siteId,
|
||||||
|
deleteTime: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingMenu && existingMenu.menuId !== id) {
|
||||||
|
throw new BadRequestException('菜单标识已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果更新父级菜单,验证父级菜单是否存在且不能是自己
|
||||||
|
if (updateMenuDto.pid !== undefined && updateMenuDto.pid > 0) {
|
||||||
|
if (updateMenuDto.pid === id) {
|
||||||
|
throw new BadRequestException('不能将自己设为父级菜单');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentMenu = await this.menuRepository.findOne({
|
||||||
|
where: { menuId: updateMenuDto.pid, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
if (!parentMenu) {
|
||||||
|
throw new BadRequestException('父级菜单不存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
const updateData = {
|
||||||
|
...updateMenuDto,
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.menuRepository.update(id, updateData);
|
||||||
|
return await this.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除菜单
|
||||||
|
*/
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
const menu = await this.findOne(id);
|
||||||
|
|
||||||
|
// 检查是否有子菜单
|
||||||
|
const childMenus = await this.menuRepository.find({
|
||||||
|
where: { pid: id, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (childMenus.length > 0) {
|
||||||
|
throw new BadRequestException('存在子菜单,无法删除');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.menuRepository.update(id, {
|
||||||
|
deleteTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量软删除菜单
|
||||||
|
*/
|
||||||
|
async removeBatch(ids: number[]): Promise<void> {
|
||||||
|
if (!ids || ids.length === 0) {
|
||||||
|
throw new BadRequestException('请选择要删除的菜单');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有子菜单
|
||||||
|
for (const id of ids) {
|
||||||
|
const childMenus = await this.menuRepository.find({
|
||||||
|
where: { pid: id, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (childMenus.length > 0) {
|
||||||
|
const menu = await this.findOne(id);
|
||||||
|
throw new BadRequestException(`菜单
|
||||||
31
wwjcloud/src/common/rbac/rbac.module.ts
Normal file
31
wwjcloud/src/common/rbac/rbac.module.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { RoleController } from './role.controller';
|
||||||
|
import { MenuController } from './menu.controller';
|
||||||
|
import { RoleService } from './role.service';
|
||||||
|
import { MenuService } from './menu.service';
|
||||||
|
import { SysRole } from './entities/sys-role.entity';
|
||||||
|
import { SysMenu } from './entities/sys-menu.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
SysRole,
|
||||||
|
SysMenu,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [
|
||||||
|
RoleController,
|
||||||
|
MenuController,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
RoleService,
|
||||||
|
MenuService,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
RoleService,
|
||||||
|
MenuService,
|
||||||
|
TypeOrmModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class RbacModule {}
|
||||||
143
wwjcloud/src/common/rbac/role.controller.ts
Normal file
143
wwjcloud/src/common/rbac/role.controller.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
ParseIntPipe,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||||
|
import { RoleService } from './role.service';
|
||||||
|
import { CreateRoleDto, UpdateRoleDto, QueryRoleDto } from './dto';
|
||||||
|
|
||||||
|
@ApiTags('角色管理')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('rbac/roles')
|
||||||
|
export class RoleController {
|
||||||
|
constructor(private readonly roleService: RoleService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: '创建角色' })
|
||||||
|
async create(@Body() createRoleDto: CreateRoleDto) {
|
||||||
|
const role = await this.roleService.create(createRoleDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '创建成功',
|
||||||
|
data: role,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取角色列表' })
|
||||||
|
async findAll(@Query() queryRoleDto: QueryRoleDto) {
|
||||||
|
const result = await this.roleService.findAll(queryRoleDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({ summary: '获取角色详情' })
|
||||||
|
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
const role = await this.roleService.findOne(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: role,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('name/:roleName')
|
||||||
|
@ApiOperation({ summary: '根据角色名称查询角色' })
|
||||||
|
async findByName(
|
||||||
|
@Param('roleName') roleName: string,
|
||||||
|
@Query('siteId', ParseIntPipe) siteId?: number,
|
||||||
|
) {
|
||||||
|
const role = await this.roleService.findByName(roleName, siteId);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: role,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({ summary: '更新角色' })
|
||||||
|
async update(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body() updateRoleDto: UpdateRoleDto,
|
||||||
|
) {
|
||||||
|
const role = await this.roleService.update(id, updateRoleDto);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '更新成功',
|
||||||
|
data: role,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@ApiOperation({ summary: '删除角色' })
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
await this.roleService.remove(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '删除成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('batch')
|
||||||
|
@ApiOperation({ summary: '批量删除角色' })
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
async removeBatch(@Body('ids') ids: number[]) {
|
||||||
|
await this.roleService.removeBatch(ids);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '批量删除成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(':id/status')
|
||||||
|
@ApiOperation({ summary: '更新角色状态' })
|
||||||
|
async updateStatus(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body('status', ParseIntPipe) status: number,
|
||||||
|
) {
|
||||||
|
await this.roleService.updateStatus(id, status);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '状态更新成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id/menus')
|
||||||
|
@ApiOperation({ summary: '获取角色权限菜单' })
|
||||||
|
async getRoleMenus(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
const menuIds = await this.roleService.getRoleMenuIds(id);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '获取成功',
|
||||||
|
data: menuIds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/permissions')
|
||||||
|
@ApiOperation({ summary: '设置角色权限' })
|
||||||
|
async setPermissions(
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Body('menuIds') menuIds: number[],
|
||||||
|
) {
|
||||||
|
await this.roleService.setRolePermissions(id, menuIds);
|
||||||
|
return {
|
||||||
|
code: 200,
|
||||||
|
message: '权限设置成功',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
227
wwjcloud/src/common/rbac/role.service.ts
Normal file
227
wwjcloud/src/common/rbac/role.service.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, Like, Between } from 'typeorm';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { SysRole } from './entities/sys-role.entity';
|
||||||
|
import { CreateRoleDto, UpdateRoleDto, QueryRoleDto } from './dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RoleService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SysRole)
|
||||||
|
private readonly roleRepository: Repository<SysRole>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建角色
|
||||||
|
*/
|
||||||
|
async create(createRoleDto: CreateRoleDto): Promise<SysRole> {
|
||||||
|
// 检查角色名称是否已存在
|
||||||
|
const existingRole = await this.roleRepository.findOne({
|
||||||
|
where: {
|
||||||
|
roleName: createRoleDto.roleName,
|
||||||
|
siteId: createRoleDto.siteId,
|
||||||
|
deleteTime: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingRole) {
|
||||||
|
throw new BadRequestException('角色名称已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = this.roleRepository.create({
|
||||||
|
...createRoleDto,
|
||||||
|
rules: JSON.stringify(createRoleDto.rules || []),
|
||||||
|
createTime: Math.floor(Date.now() / 1000),
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.roleRepository.save(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询角色列表
|
||||||
|
*/
|
||||||
|
async findAll(queryRoleDto: QueryRoleDto) {
|
||||||
|
const { page = 1, limit = 10, keyword, siteId, status, startTime, endTime } = queryRoleDto;
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const queryBuilder = this.roleRepository.createQueryBuilder('role')
|
||||||
|
.where('role.deleteTime = :deleteTime', { deleteTime: 0 });
|
||||||
|
|
||||||
|
// 关键词搜索
|
||||||
|
if (keyword) {
|
||||||
|
queryBuilder.andWhere('role.roleName LIKE :keyword', { keyword: `%${keyword}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 站点ID筛选
|
||||||
|
if (siteId !== undefined) {
|
||||||
|
queryBuilder.andWhere('role.siteId = :siteId', { siteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
if (status !== undefined) {
|
||||||
|
queryBuilder.andWhere('role.status = :status', { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间范围筛选
|
||||||
|
if (startTime && endTime) {
|
||||||
|
queryBuilder.andWhere('role.createTime BETWEEN :startTime AND :endTime', {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序
|
||||||
|
queryBuilder.orderBy('role.createTime', 'DESC');
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const [list, total] = await queryBuilder
|
||||||
|
.skip(skip)
|
||||||
|
.take(limit)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
// 解析权限规则
|
||||||
|
const formattedList = list.map(role => ({
|
||||||
|
...role,
|
||||||
|
rules: role.rules ? JSON.parse(role.rules) : [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
list: formattedList,
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
totalPages: Math.ceil(total / limit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询角色
|
||||||
|
*/
|
||||||
|
async findOne(id: number): Promise<SysRole> {
|
||||||
|
const role = await this.roleRepository.findOne({
|
||||||
|
where: { roleId: id, deleteTime: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new NotFoundException('角色不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析权限规则
|
||||||
|
return {
|
||||||
|
...role,
|
||||||
|
rules: role.rules ? JSON.parse(role.rules) : [],
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据角色名称查询角色
|
||||||
|
*/
|
||||||
|
async findByName(roleName: string, siteId?: number): Promise<SysRole | null> {
|
||||||
|
const where: any = { roleName, deleteTime: 0 };
|
||||||
|
if (siteId !== undefined) {
|
||||||
|
where.siteId = siteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await this.roleRepository.findOne({ where });
|
||||||
|
if (role && role.rules) {
|
||||||
|
return {
|
||||||
|
...role,
|
||||||
|
rules: JSON.parse(role.rules),
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新角色
|
||||||
|
*/
|
||||||
|
async update(id: number, updateRoleDto: UpdateRoleDto): Promise<SysRole> {
|
||||||
|
const role = await this.findOne(id);
|
||||||
|
|
||||||
|
// 如果更新角色名称,检查是否与其他角色冲突
|
||||||
|
if (updateRoleDto.roleName && updateRoleDto.roleName !== role.roleName) {
|
||||||
|
const existingRole = await this.roleRepository.findOne({
|
||||||
|
where: {
|
||||||
|
roleName: updateRoleDto.roleName,
|
||||||
|
siteId: updateRoleDto.siteId || role.siteId,
|
||||||
|
deleteTime: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingRole && existingRole.roleId !== id) {
|
||||||
|
throw new BadRequestException('角色名称已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
const updateData: any = {
|
||||||
|
...updateRoleDto,
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理权限规则
|
||||||
|
if (updateRoleDto.rules) {
|
||||||
|
updateData.rules = JSON.stringify(updateRoleDto.rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.roleRepository.update(id, updateData);
|
||||||
|
return await this.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除角色
|
||||||
|
*/
|
||||||
|
async remove(id: number): Promise<void> {
|
||||||
|
const role = await this.findOne(id);
|
||||||
|
|
||||||
|
await this.roleRepository.update(id, {
|
||||||
|
deleteTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量软删除角色
|
||||||
|
*/
|
||||||
|
async removeBatch(ids: number[]): Promise<void> {
|
||||||
|
if (!ids || ids.length === 0) {
|
||||||
|
throw new BadRequestException('请选择要删除的角色');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.roleRepository.update(
|
||||||
|
{ roleId: { $in: ids } as any },
|
||||||
|
{ deleteTime: Math.floor(Date.now() / 1000) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新角色状态
|
||||||
|
*/
|
||||||
|
async updateStatus(id: number, status: number): Promise<void> {
|
||||||
|
const role = await this.findOne(id);
|
||||||
|
|
||||||
|
await this.roleRepository.update(id, {
|
||||||
|
status,
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色的权限菜单ID列表
|
||||||
|
*/
|
||||||
|
async getRoleMenuIds(roleId: number): Promise<number[]> {
|
||||||
|
const role = await this.findOne(roleId);
|
||||||
|
return role.rules ? JSON.parse(role.rules as string) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置角色权限
|
||||||
|
*/
|
||||||
|
async setRolePermissions(roleId: number, menuIds: number[]): Promise<void> {
|
||||||
|
await this.roleRepository.update(roleId, {
|
||||||
|
rules: JSON.stringify(menuIds),
|
||||||
|
updateTime: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { EmailSettingsService } from './email-settings.service';
|
||||||
|
import { UpdateEmailSettingsDto, type EmailSettingsVo } from './email-settings.dto';
|
||||||
|
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
|
||||||
|
import { Roles } from '../../auth/roles.decorator';
|
||||||
|
import { RolesGuard } from '../../auth/guards/roles.guard';
|
||||||
|
|
||||||
|
@ApiTags('Settings/Email')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('super', 'admin')
|
||||||
|
@Controller('settings/email')
|
||||||
|
export class EmailSettingsController {
|
||||||
|
constructor(private readonly service: EmailSettingsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取邮件设置' })
|
||||||
|
async get(): Promise<{ code: number; data: EmailSettingsVo }> {
|
||||||
|
const data = await this.service.getSettings();
|
||||||
|
return { code: 0, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
@ApiOperation({ summary: '更新邮件设置' })
|
||||||
|
async update(@Body() dto: UpdateEmailSettingsDto): Promise<{ code: number; data: EmailSettingsVo }> {
|
||||||
|
const data = await this.service.updateSettings(dto);
|
||||||
|
return { code: 0, data };
|
||||||
|
}
|
||||||
|
}
|
||||||
36
wwjcloud/src/common/settings/email/email-settings.dto.ts
Normal file
36
wwjcloud/src/common/settings/email/email-settings.dto.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { IsBoolean, IsEmail, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateEmailSettingsDto {
|
||||||
|
@IsBoolean()
|
||||||
|
enabled!: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
host!: string;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(65535)
|
||||||
|
port!: number;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
secure!: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
user!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
pass!: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
from!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailSettingsVo {
|
||||||
|
enabled: boolean;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
secure: boolean;
|
||||||
|
user: string;
|
||||||
|
pass: string;
|
||||||
|
from: string;
|
||||||
|
}
|
||||||
42
wwjcloud/src/common/settings/email/email-settings.service.ts
Normal file
42
wwjcloud/src/common/settings/email/email-settings.service.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import type { EmailSettingsVo } from './email-settings.dto';
|
||||||
|
|
||||||
|
const SETTINGS_DIR = path.resolve(process.cwd(), 'config', 'runtime');
|
||||||
|
const SETTINGS_FILE = path.resolve(SETTINGS_DIR, 'email.settings.json');
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: EmailSettingsVo = {
|
||||||
|
enabled: false,
|
||||||
|
host: '',
|
||||||
|
port: 465,
|
||||||
|
secure: true,
|
||||||
|
user: '',
|
||||||
|
pass: '',
|
||||||
|
from: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmailSettingsService {
|
||||||
|
async getSettings(): Promise<EmailSettingsVo> {
|
||||||
|
try {
|
||||||
|
const buf = await fs.promises.readFile(SETTINGS_FILE, 'utf8');
|
||||||
|
const json = JSON.parse(buf);
|
||||||
|
return { ...DEFAULT_SETTINGS, ...json };
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULT_SETTINGS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(patch: Partial<EmailSettingsVo>): Promise<EmailSettingsVo> {
|
||||||
|
const current = await this.getSettings();
|
||||||
|
const next: EmailSettingsVo = { ...current, ...patch };
|
||||||
|
await fs.promises.mkdir(SETTINGS_DIR, { recursive: true });
|
||||||
|
await fs.promises.writeFile(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getSettingsPath() {
|
||||||
|
return SETTINGS_FILE;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
wwjcloud/src/common/settings/email/email.module.ts
Normal file
11
wwjcloud/src/common/settings/email/email.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { EmailService } from './email.service';
|
||||||
|
import { EmailSettingsService } from './email-settings.service';
|
||||||
|
import { EmailSettingsController } from './email-settings.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [EmailService, EmailSettingsService],
|
||||||
|
controllers: [EmailSettingsController],
|
||||||
|
exports: [EmailService, EmailSettingsService],
|
||||||
|
})
|
||||||
|
export class EmailModule {}
|
||||||
10
wwjcloud/src/common/settings/email/email.service.ts
Normal file
10
wwjcloud/src/common/settings/email/email.service.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EmailService {
|
||||||
|
private readonly logger = new Logger(EmailService.name);
|
||||||
|
|
||||||
|
async send(to: string, subject: string, content: string): Promise<void> {
|
||||||
|
this.logger.log(`Mock send email to ${to} subject=${subject}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
wwjcloud/src/common/settings/index.ts
Normal file
24
wwjcloud/src/common/settings/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export { SettingsModule } from './settings.module';
|
||||||
|
|
||||||
|
export { EmailModule } from './email/email.module';
|
||||||
|
export { EmailService } from './email/email.service';
|
||||||
|
export { EmailSettingsService } from './email/email-settings.service';
|
||||||
|
|
||||||
|
export { SmsModule } from './sms/sms.module';
|
||||||
|
export { SmsService } from './sms/sms.service';
|
||||||
|
export { SmsSettingsService } from './sms/sms-settings.service';
|
||||||
|
|
||||||
|
export { StorageModule } from './storage/storage.module';
|
||||||
|
export { StorageService } from './storage/storage.service';
|
||||||
|
export { StorageSettingsService } from './storage/storage-settings.service';
|
||||||
|
|
||||||
|
export { PaymentModule } from './payment/payment.module';
|
||||||
|
export { PaymentService } from './payment/payment.service';
|
||||||
|
export { PaymentSettingsService } from './payment/payment-settings.service';
|
||||||
|
|
||||||
|
export { LoginModule } from './login/login.module';
|
||||||
|
export { LoginSettingsService } from './login/login-settings.service';
|
||||||
|
|
||||||
|
export { SiteModule } from './site/site.module';
|
||||||
|
export { SiteSettingsService } from './site/site-settings.service';
|
||||||
|
export { Site } from './site/site.entity';
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { UpdateLoginSettingsDto, type LoginSettingsVo } from './login-settings.dto';
|
||||||
|
import { LoginSettingsService } from './login-settings.service';
|
||||||
|
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
|
||||||
|
import { Roles } from '../../auth/roles.decorator';
|
||||||
|
import { RolesGuard } from '../../auth/guards/roles.guard';
|
||||||
|
|
||||||
|
@ApiTags('Settings/Login')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('super', 'admin')
|
||||||
|
@Controller('settings/login')
|
||||||
|
export class LoginSettingsController {
|
||||||
|
constructor(private readonly service: LoginSettingsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取登录设置' })
|
||||||
|
async get(): Promise<{ code: number; data: LoginSettingsVo }> {
|
||||||
|
const data = await this.service.getSettings();
|
||||||
|
return { code: 0, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
@ApiOperation({ summary: '更新登录设置' })
|
||||||
|
async update(@Body() dto: UpdateLoginSettingsDto): Promise<{ code: number; data: LoginSettingsVo }> {
|
||||||
|
const data = await this.service.updateSettings(dto);
|
||||||
|
return { code: 0, data };
|
||||||
|
}
|
||||||
|
}
|
||||||
24
wwjcloud/src/common/settings/login/login-settings.dto.ts
Normal file
24
wwjcloud/src/common/settings/login/login-settings.dto.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateLoginSettingsDto {
|
||||||
|
@IsBoolean()
|
||||||
|
isCaptcha!: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
bg?: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isSiteCaptcha!: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
siteBg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginSettingsVo {
|
||||||
|
isCaptcha: boolean;
|
||||||
|
bg?: string;
|
||||||
|
isSiteCaptcha: boolean;
|
||||||
|
siteBg?: string;
|
||||||
|
}
|
||||||
39
wwjcloud/src/common/settings/login/login-settings.service.ts
Normal file
39
wwjcloud/src/common/settings/login/login-settings.service.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import type { LoginSettingsVo } from './login-settings.dto';
|
||||||
|
|
||||||
|
const SETTINGS_DIR = path.resolve(process.cwd(), 'config', 'runtime');
|
||||||
|
const SETTINGS_FILE = path.resolve(SETTINGS_DIR, 'login.settings.json');
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: LoginSettingsVo = {
|
||||||
|
isCaptcha: false,
|
||||||
|
bg: '',
|
||||||
|
isSiteCaptcha: false,
|
||||||
|
siteBg: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoginSettingsService {
|
||||||
|
async getSettings(): Promise<LoginSettingsVo> {
|
||||||
|
try {
|
||||||
|
const buf = await fs.promises.readFile(SETTINGS_FILE, 'utf8');
|
||||||
|
const json = JSON.parse(buf);
|
||||||
|
return { ...DEFAULT_SETTINGS, ...json } as LoginSettingsVo;
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULT_SETTINGS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(patch: Partial<LoginSettingsVo>): Promise<LoginSettingsVo> {
|
||||||
|
const current = await this.getSettings();
|
||||||
|
const next: LoginSettingsVo = { ...current, ...patch };
|
||||||
|
await fs.promises.mkdir(SETTINGS_DIR, { recursive: true });
|
||||||
|
await fs.promises.writeFile(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getSettingsPath() {
|
||||||
|
return SETTINGS_FILE;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
wwjcloud/src/common/settings/login/login.module.ts
Normal file
10
wwjcloud/src/common/settings/login/login.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { LoginSettingsService } from './login-settings.service';
|
||||||
|
import { LoginSettingsController } from './login-settings.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [LoginSettingsService],
|
||||||
|
controllers: [LoginSettingsController],
|
||||||
|
exports: [LoginSettingsService],
|
||||||
|
})
|
||||||
|
export class LoginModule {}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { PaymentSettingsService } from './payment-settings.service';
|
||||||
|
import { UpdatePaymentSettingsDto, type PaymentSettingsVo } from './payment-settings.dto';
|
||||||
|
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
|
||||||
|
import { Roles } from '../../auth/roles.decorator';
|
||||||
|
import { RolesGuard } from '../../auth/guards/roles.guard';
|
||||||
|
|
||||||
|
@ApiTags('Settings/Payment')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||||
|
@Roles('super', 'admin')
|
||||||
|
@Controller('settings/payment')
|
||||||
|
export class PaymentSettingsController {
|
||||||
|
constructor(private readonly service: PaymentSettingsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({ summary: '获取支付设置' })
|
||||||
|
async get(): Promise<{ code: number; data: PaymentSettingsVo }> {
|
||||||
|
const data = await this.service.getSettings();
|
||||||
|
return { code: 0, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
@ApiOperation({ summary: '更新支付设置' })
|
||||||
|
async update(
|
||||||
|
@Body() dto: UpdatePaymentSettingsDto,
|
||||||
|
): Promise<{ code: number; data: PaymentSettingsVo }> {
|
||||||
|
const data = await this.service.updateSettings(dto);
|
||||||
|
return { code: 0, data };
|
||||||
|
}
|
||||||
|
}
|
||||||
20
wwjcloud/src/common/settings/payment/payment-settings.dto.ts
Normal file
20
wwjcloud/src/common/settings/payment/payment-settings.dto.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { IsBoolean, IsObject, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdatePaymentSettingsDto {
|
||||||
|
@IsBoolean()
|
||||||
|
enabled!: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
alipay?: Record<string, string>;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
wechatpay?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentSettingsVo {
|
||||||
|
enabled: boolean;
|
||||||
|
alipay?: Record<string, string>;
|
||||||
|
wechatpay?: Record<string, string>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import type { PaymentSettingsVo } from './payment-settings.dto';
|
||||||
|
|
||||||
|
const SETTINGS_DIR = path.resolve(process.cwd(), 'config', 'runtime');
|
||||||
|
const SETTINGS_FILE = path.resolve(SETTINGS_DIR, 'payment.settings.json');
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: PaymentSettingsVo = {
|
||||||
|
enabled: false,
|
||||||
|
alipay: {},
|
||||||
|
wechatpay: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PaymentSettingsService {
|
||||||
|
async getSettings(): Promise<PaymentSettingsVo> {
|
||||||
|
try {
|
||||||
|
const buf = await fs.promises.readFile(SETTINGS_FILE, 'utf8');
|
||||||
|
const json = JSON.parse(buf);
|
||||||
|
return { ...DEFAULT_SETTINGS, ...json };
|
||||||
|
} catch {
|
||||||
|
return { ...DEFAULT_SETTINGS };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(patch: Partial<PaymentSettingsVo>): Promise<PaymentSettingsVo> {
|
||||||
|
const current = await this.getSettings();
|
||||||
|
const next: PaymentSettingsVo = { ...current, ...patch };
|
||||||
|
await fs.promises.mkdir(SETTINGS_DIR, { recursive: true });
|
||||||
|
await fs.promises.writeFile(SETTINGS_FILE, JSON.stringify(next, null, 2), 'utf8');
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getSettingsPath() {
|
||||||
|
return SETTINGS_FILE;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
wwjcloud/src/common/settings/payment/payment.module.ts
Normal file
11
wwjcloud/src/common/settings/payment/payment.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PaymentService } from './payment.service';
|
||||||
|
import { PaymentSettingsService } from './payment-settings.service';
|
||||||
|
import { PaymentSettingsController } from './payment-settings.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [PaymentService, PaymentSettingsService],
|
||||||
|
controllers: [PaymentSettingsController],
|
||||||
|
exports: [PaymentService, PaymentSettingsService],
|
||||||
|
})
|
||||||
|
export class PaymentModule {}
|
||||||
8
wwjcloud/src/common/settings/payment/payment.service.ts
Normal file
8
wwjcloud/src/common/settings/payment/payment.service.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PaymentService {
|
||||||
|
async createPayment(orderId: string, amount: number) {
|
||||||
|
return { orderId, amount, status: 'mock' };
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user