feat: 初始化 WWJ Cloud 企业级框架项目

- 后端:基于 NestJS 的分层架构设计
- 前端:基于 VbenAdmin + Element Plus 的管理系统
- 支持 SaaS + 独立版双架构模式
- 完整的用户权限管理系统
- 系统设置、文件上传、通知等核心功能
- 多租户支持和插件化扩展架构
This commit is contained in:
万物街
2025-08-23 13:20:01 +08:00
commit f30d64e6cc
172 changed files with 10179 additions and 0 deletions

132
.gitignore vendored Normal file
View 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

View 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

File diff suppressed because it is too large Load Diff

1
admin Submodule

Submodule admin added at cf6c4c9aae

View 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
View 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 内置在线升级功能,系统会全自动化帮您升级文件。产品的更新只需一键完成
- VSCodeWebStorm微信小程序开发工具打包上传发布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, -- 租户ID0表示独立版
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, -- 租户ID0表示独立版
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: '租户ID0表示独立版' })
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: '租户ID0表示独立版',
}),
);
// 创建索引
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>
---
## 📈 项目统计
![GitHub stars](https://img.shields.io/github/stars/wwjauth/wwjcloud-nestjs?style=social)
![GitHub forks](https://img.shields.io/github/forks/wwjauth/wwjcloud-nestjs?style=social)
![GitHub issues](https://img.shields.io/github/issues/wwjauth/wwjcloud-nestjs)
![GitHub license](https://img.shields.io/github/license/wwjauth/wwjcloud-nestjs)
![GitHub release](https://img.shields.io/github/v/release/wwjauth/wwjcloud-nestjs)
---
<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
View 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

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit "$1"

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged

4
wwjcloud/.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

98
wwjcloud/README.md Normal file
View 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>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](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).

View File

@@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

View 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
View 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
View 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
View File

View File

View 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!');
});
});
});

View 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
View 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 {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View 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,
};
}
}

View 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 {}

View 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);
}
}

View 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[];
}

View File

@@ -0,0 +1,3 @@
export { CreateAdminDto } from './create-admin.dto';
export { UpdateAdminDto } from './update-admin.dto';
export { QueryAdminDto } from './query-admin.dto';

View 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;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateAdminDto } from './create-admin.dto';
export class UpdateAdminDto extends PartialType(CreateAdminDto) {}

View 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;
}

View 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[];
}

View 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';

View File

@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class AppsModule {}

View 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,
},
};
}
}

View 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 {}

View 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: '登出成功',
};
}
}

View 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;
},
);

View 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;
}

View File

@@ -0,0 +1,3 @@
export * from './login.dto';
export * from './register.dto';
export * from './change-password.dto';

View 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;
}

View 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;
}

View 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {}

View 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('权限验证失败');
}
}
}

View 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';

View File

@@ -0,0 +1 @@
export * from './permission.service';

View 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);
}
}

View 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验证失败');
}
}
}

View 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;
}
}

View 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,
};
}
}
}

View File

@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class CacheModule {}

View File

@@ -0,0 +1,9 @@
import { Controller, Get } from '@nestjs/common';
@Controller('dictionary')
export class DictionaryController {
@Get('ping')
ping() {
return 'dictionary ok';
}
}

View 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 {}

View File

@@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class DictionaryService {
ping() {
return 'dictionary service ok';
}
}

View 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'

View File

@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class HealthModule {}

View 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';

View 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;
}

View File

@@ -0,0 +1,3 @@
export { CreateMemberDto } from './create-member.dto';
export { UpdateMemberDto } from './update-member.dto';
export { QueryMemberDto } from './query-member.dto';

View 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;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateMemberDto } from './create-member.dto';
export class UpdateMemberDto extends PartialType(CreateMemberDto) {}

View 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;
}

View 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';

View 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,
};
}
}

View 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 {}

View 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);
}
}

View 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 {}

View 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);
}
}

View File

@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class OpenapiModule {}

View File

@@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class QueueModule {}

View 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;
}

View 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;
}

View 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';

View 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;
}

View 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;
}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateMenuDto } from './create-menu.dto';
export class UpdateMenuDto extends PartialType(CreateMenuDto) {}

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateRoleDto } from './create-role.dto';
export class UpdateRoleDto extends PartialType(CreateRoleDto) {}

View 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;
}

View 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;
}

View 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';

View 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: '排序更新成功',
};
}
}

View 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(`菜单

View 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 {}

View 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: '权限设置成功',
};
}
}

View 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),
});
}
}

View File

@@ -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 };
}
}

View 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;
}

View 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;
}
}

View 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 {}

View 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}`);
}
}

View 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';

View File

@@ -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 };
}
}

View 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;
}

View 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;
}
}

View 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 {}

View File

@@ -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 };
}
}

View 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>;
}

View File

@@ -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;
}
}

View 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 {}

View 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