From f30d64e6ccd801f5eb80b06f9fbeede0f0bfc768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=87=E7=89=A9=E8=A1=97?= <7729700+wanwujie@user.noreply.gitee.com> Date: Sat, 23 Aug 2025 13:20:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=20WWJ=20Clo?= =?UTF-8?q?ud=20=E4=BC=81=E4=B8=9A=E7=BA=A7=E6=A1=86=E6=9E=B6=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:基于 NestJS 的分层架构设计 - 前端:基于 VbenAdmin + Element Plus 的管理系统 - 支持 SaaS + 独立版双架构模式 - 完整的用户权限管理系统 - 系统设置、文件上传、通知等核心功能 - 多租户支持和插件化扩展架构 --- .gitignore | 132 ++ .trae/rules/development_constraints.md | 219 ++ .trae/rules/project_rules.md | 2105 +++++++++++++++++ admin | 1 + niucloud-frontend-migration-strategy.md | 252 ++ readme.md | 935 ++++++++ wwjcloud/.env.example | 29 + wwjcloud/.husky/commit-msg | 4 + wwjcloud/.husky/pre-commit | 4 + wwjcloud/.prettierrc | 4 + wwjcloud/README.md | 98 + wwjcloud/commitlint.config.cjs | 1 + wwjcloud/eslint.config.mjs | 89 + wwjcloud/nest-cli.json | 14 + wwjcloud/package.json | 156 ++ wwjcloud/public/.gitkeep | 0 wwjcloud/public/upload/.gitkeep | 0 wwjcloud/src/app.controller.spec.ts | 22 + wwjcloud/src/app.controller.ts | 12 + wwjcloud/src/app.module.ts | 167 ++ wwjcloud/src/app.service.ts | 8 + wwjcloud/src/common/admin/admin.controller.ts | 156 ++ wwjcloud/src/common/admin/admin.module.ts | 14 + wwjcloud/src/common/admin/admin.service.ts | 311 +++ .../src/common/admin/dto/create-admin.dto.ts | 94 + wwjcloud/src/common/admin/dto/index.ts | 3 + .../src/common/admin/dto/query-admin.dto.ts | 64 + .../src/common/admin/dto/update-admin.dto.ts | 4 + .../admin/entities/sys-user-role.entity.ts | 27 + .../common/admin/entities/sys-user.entity.ts | 94 + wwjcloud/src/common/admin/index.ts | 6 + wwjcloud/src/common/apps/apps.module.ts | 4 + wwjcloud/src/common/auth/auth.controller.ts | 145 ++ wwjcloud/src/common/auth/auth.module.ts | 58 + wwjcloud/src/common/auth/auth.service.ts | 318 +++ .../common/auth/decorators/auth.decorator.ts | 37 + .../common/auth/dto/change-password.dto.ts | 66 + wwjcloud/src/common/auth/dto/index.ts | 3 + wwjcloud/src/common/auth/dto/login.dto.ts | 33 + wwjcloud/src/common/auth/dto/register.dto.ts | 79 + .../common/auth/guards/global-auth.guard.ts | 40 + .../src/common/auth/guards/jwt-auth.guard.ts | 34 + .../common/auth/guards/local-auth.guard.ts | 5 + .../src/common/auth/guards/roles.guard.ts | 93 + wwjcloud/src/common/auth/index.ts | 13 + wwjcloud/src/common/auth/services/index.ts | 1 + .../auth/services/permission.service.ts | 215 ++ .../common/auth/strategies/jwt.strategy.ts | 63 + .../common/auth/strategies/local.strategy.ts | 27 + .../common/auth/user-permission.controller.ts | 172 ++ wwjcloud/src/common/cache/cache.module.ts | 4 + .../dictionary/dictionary.controller.ts | 9 + .../common/dictionary/dictionary.module.ts | 10 + .../common/dictionary/dictionary.service.ts | 8 + wwjcloud/src/common/dictionary/dto/index.ts | 8 + wwjcloud/src/common/health/health.module.ts | 4 + wwjcloud/src/common/index.ts | 22 + .../common/member/dto/create-member.dto.ts | 112 + wwjcloud/src/common/member/dto/index.ts | 3 + .../src/common/member/dto/query-member.dto.ts | 63 + .../common/member/dto/update-member.dto.ts | 4 + .../common/member/entities/member.entity.ts | 113 + wwjcloud/src/common/member/index.ts | 5 + .../src/common/member/member.controller.ts | 142 ++ wwjcloud/src/common/member/member.module.ts | 13 + wwjcloud/src/common/member/member.service.ts | 251 ++ .../notification/notification.module.ts | 10 + .../notification/notification.service.ts | 22 + wwjcloud/src/common/openapi/openapi.module.ts | 4 + wwjcloud/src/common/queue/queue.module.ts | 4 + .../src/common/rbac/dto/create-menu.dto.ts | 79 + .../src/common/rbac/dto/create-role.dto.ts | 34 + wwjcloud/src/common/rbac/dto/index.ts | 6 + .../src/common/rbac/dto/query-menu.dto.ts | 64 + .../src/common/rbac/dto/query-role.dto.ts | 46 + .../src/common/rbac/dto/update-menu.dto.ts | 4 + .../src/common/rbac/dto/update-role.dto.ts | 4 + .../common/rbac/entities/sys-menu.entity.ts | 73 + .../common/rbac/entities/sys-role.entity.ts | 41 + wwjcloud/src/common/rbac/index.ts | 8 + wwjcloud/src/common/rbac/menu.controller.ts | 154 ++ wwjcloud/src/common/rbac/menu.service.ts | 296 +++ wwjcloud/src/common/rbac/rbac.module.ts | 31 + wwjcloud/src/common/rbac/role.controller.ts | 143 ++ wwjcloud/src/common/rbac/role.service.ts | 227 ++ .../email/email-settings.controller.ts | 30 + .../settings/email/email-settings.dto.ts | 36 + .../settings/email/email-settings.service.ts | 42 + .../src/common/settings/email/email.module.ts | 11 + .../common/settings/email/email.service.ts | 10 + wwjcloud/src/common/settings/index.ts | 24 + .../login/login-settings.controller.ts | 30 + .../settings/login/login-settings.dto.ts | 24 + .../settings/login/login-settings.service.ts | 39 + .../src/common/settings/login/login.module.ts | 10 + .../payment/payment-settings.controller.ts | 32 + .../settings/payment/payment-settings.dto.ts | 20 + .../payment/payment-settings.service.ts | 38 + .../common/settings/payment/payment.module.ts | 11 + .../settings/payment/payment.service.ts | 8 + .../src/common/settings/settings.module.ts | 30 + .../settings/site/site-settings.controller.ts | 50 + .../common/settings/site/site-settings.dto.ts | 91 + .../settings/site/site-settings.service.ts | 133 ++ .../src/common/settings/site/site.entity.ts | 41 + .../src/common/settings/site/site.module.ts | 13 + .../settings/sms/sms-settings.controller.ts | 30 + .../common/settings/sms/sms-settings.dto.ts | 39 + .../settings/sms/sms-settings.service.ts | 42 + .../src/common/settings/sms/sms.module.ts | 11 + .../src/common/settings/sms/sms.service.ts | 16 + .../storage/storage-settings.controller.ts | 32 + .../settings/storage/storage-settings.dto.ts | 54 + .../storage/storage-settings.service.ts | 51 + .../settings/storage/storage.controller.ts | 9 + .../common/settings/storage/storage.module.ts | 12 + .../settings/storage/storage.service.ts | 12 + .../upload/upload-settings.controller.ts | 35 + .../settings/upload/upload-settings.dto.ts | 25 + .../settings/upload/upload-settings.module.ts | 10 + .../upload/upload-settings.service.ts | 44 + wwjcloud/src/config/cache/index.ts | 6 + wwjcloud/src/config/database/index.ts | 8 + wwjcloud/src/config/env/index.ts | 6 + wwjcloud/src/config/http/index.ts | 5 + wwjcloud/src/config/index.ts | 28 + wwjcloud/src/config/logger/index.ts | 4 + wwjcloud/src/config/queue/index.ts | 4 + wwjcloud/src/config/security/index.ts | 7 + wwjcloud/src/config/third-party/index.ts | 5 + wwjcloud/src/config/typeorm.config.ts | 28 + wwjcloud/src/core/cache/cache.module.ts | 7 + wwjcloud/src/core/cache/ports/cache.port.ts | 5 + wwjcloud/src/core/config/config.module.ts | 7 + wwjcloud/src/core/config/schemas/index.ts | 2 + wwjcloud/src/core/context/cls.module.ts | 7 + wwjcloud/src/core/database/base.entity.ts | 6 + wwjcloud/src/core/database/base.repository.ts | 7 + wwjcloud/src/core/database/database.module.ts | 7 + .../boolean-number.transformer.ts | 13 + .../src/core/database/transformers/index.ts | 3 + .../transformers/status-number.transformer.ts | 15 + .../transformers/status-string.transformer.ts | 14 + wwjcloud/src/core/enums/index.ts | 1 + wwjcloud/src/core/enums/status.enum.ts | 21 + .../filters/http-exception.filter.ts | 30 + wwjcloud/src/core/http/http.module.ts | 7 + .../core/interceptor/logging.interceptor.ts | 14 + .../core/interceptor/transform.interceptor.ts | 14 + wwjcloud/src/core/logger/logger.module.ts | 7 + wwjcloud/src/core/queue/ports/queue.port.ts | 11 + wwjcloud/src/core/queue/queue.module.ts | 7 + wwjcloud/src/core/security/guards/index.ts | 2 + wwjcloud/src/core/security/security.module.ts | 7 + .../src/core/security/strategies/index.ts | 2 + wwjcloud/src/core/validation/pipes/index.ts | 2 + wwjcloud/src/main.ts | 58 + wwjcloud/src/migrations/.gitkeep | 0 .../migrations/1755845112842-InitSchema.ts | 52 + wwjcloud/src/scripts/init-db.ts | 46 + wwjcloud/src/vendor/http/axios.adapter.ts | 6 + wwjcloud/src/vendor/index.ts | 1 + .../src/vendor/mailer/nodemailer.adapter.ts | 6 + wwjcloud/src/vendor/payment/mock.adapter.ts | 6 + wwjcloud/src/vendor/redis/redis.provider.ts | 6 + wwjcloud/src/vendor/sms/aliyun-sms.adapter.ts | 6 + wwjcloud/src/vendor/storage/local.adapter.ts | 6 + wwjcloud/src/vendor/vendor.module.ts | 7 + wwjcloud/test/app.e2e-spec.ts | 25 + wwjcloud/test/jest-e2e.json | 9 + wwjcloud/tsconfig.build.json | 4 + wwjcloud/tsconfig.json | 25 + 172 files changed, 10179 insertions(+) create mode 100644 .gitignore create mode 100644 .trae/rules/development_constraints.md create mode 100644 .trae/rules/project_rules.md create mode 160000 admin create mode 100644 niucloud-frontend-migration-strategy.md create mode 100644 readme.md create mode 100644 wwjcloud/.env.example create mode 100644 wwjcloud/.husky/commit-msg create mode 100644 wwjcloud/.husky/pre-commit create mode 100644 wwjcloud/.prettierrc create mode 100644 wwjcloud/README.md create mode 100644 wwjcloud/commitlint.config.cjs create mode 100644 wwjcloud/eslint.config.mjs create mode 100644 wwjcloud/nest-cli.json create mode 100644 wwjcloud/package.json create mode 100644 wwjcloud/public/.gitkeep create mode 100644 wwjcloud/public/upload/.gitkeep create mode 100644 wwjcloud/src/app.controller.spec.ts create mode 100644 wwjcloud/src/app.controller.ts create mode 100644 wwjcloud/src/app.module.ts create mode 100644 wwjcloud/src/app.service.ts create mode 100644 wwjcloud/src/common/admin/admin.controller.ts create mode 100644 wwjcloud/src/common/admin/admin.module.ts create mode 100644 wwjcloud/src/common/admin/admin.service.ts create mode 100644 wwjcloud/src/common/admin/dto/create-admin.dto.ts create mode 100644 wwjcloud/src/common/admin/dto/index.ts create mode 100644 wwjcloud/src/common/admin/dto/query-admin.dto.ts create mode 100644 wwjcloud/src/common/admin/dto/update-admin.dto.ts create mode 100644 wwjcloud/src/common/admin/entities/sys-user-role.entity.ts create mode 100644 wwjcloud/src/common/admin/entities/sys-user.entity.ts create mode 100644 wwjcloud/src/common/admin/index.ts create mode 100644 wwjcloud/src/common/apps/apps.module.ts create mode 100644 wwjcloud/src/common/auth/auth.controller.ts create mode 100644 wwjcloud/src/common/auth/auth.module.ts create mode 100644 wwjcloud/src/common/auth/auth.service.ts create mode 100644 wwjcloud/src/common/auth/decorators/auth.decorator.ts create mode 100644 wwjcloud/src/common/auth/dto/change-password.dto.ts create mode 100644 wwjcloud/src/common/auth/dto/index.ts create mode 100644 wwjcloud/src/common/auth/dto/login.dto.ts create mode 100644 wwjcloud/src/common/auth/dto/register.dto.ts create mode 100644 wwjcloud/src/common/auth/guards/global-auth.guard.ts create mode 100644 wwjcloud/src/common/auth/guards/jwt-auth.guard.ts create mode 100644 wwjcloud/src/common/auth/guards/local-auth.guard.ts create mode 100644 wwjcloud/src/common/auth/guards/roles.guard.ts create mode 100644 wwjcloud/src/common/auth/index.ts create mode 100644 wwjcloud/src/common/auth/services/index.ts create mode 100644 wwjcloud/src/common/auth/services/permission.service.ts create mode 100644 wwjcloud/src/common/auth/strategies/jwt.strategy.ts create mode 100644 wwjcloud/src/common/auth/strategies/local.strategy.ts create mode 100644 wwjcloud/src/common/auth/user-permission.controller.ts create mode 100644 wwjcloud/src/common/cache/cache.module.ts create mode 100644 wwjcloud/src/common/dictionary/dictionary.controller.ts create mode 100644 wwjcloud/src/common/dictionary/dictionary.module.ts create mode 100644 wwjcloud/src/common/dictionary/dictionary.service.ts create mode 100644 wwjcloud/src/common/dictionary/dto/index.ts create mode 100644 wwjcloud/src/common/health/health.module.ts create mode 100644 wwjcloud/src/common/index.ts create mode 100644 wwjcloud/src/common/member/dto/create-member.dto.ts create mode 100644 wwjcloud/src/common/member/dto/index.ts create mode 100644 wwjcloud/src/common/member/dto/query-member.dto.ts create mode 100644 wwjcloud/src/common/member/dto/update-member.dto.ts create mode 100644 wwjcloud/src/common/member/entities/member.entity.ts create mode 100644 wwjcloud/src/common/member/index.ts create mode 100644 wwjcloud/src/common/member/member.controller.ts create mode 100644 wwjcloud/src/common/member/member.module.ts create mode 100644 wwjcloud/src/common/member/member.service.ts create mode 100644 wwjcloud/src/common/notification/notification.module.ts create mode 100644 wwjcloud/src/common/notification/notification.service.ts create mode 100644 wwjcloud/src/common/openapi/openapi.module.ts create mode 100644 wwjcloud/src/common/queue/queue.module.ts create mode 100644 wwjcloud/src/common/rbac/dto/create-menu.dto.ts create mode 100644 wwjcloud/src/common/rbac/dto/create-role.dto.ts create mode 100644 wwjcloud/src/common/rbac/dto/index.ts create mode 100644 wwjcloud/src/common/rbac/dto/query-menu.dto.ts create mode 100644 wwjcloud/src/common/rbac/dto/query-role.dto.ts create mode 100644 wwjcloud/src/common/rbac/dto/update-menu.dto.ts create mode 100644 wwjcloud/src/common/rbac/dto/update-role.dto.ts create mode 100644 wwjcloud/src/common/rbac/entities/sys-menu.entity.ts create mode 100644 wwjcloud/src/common/rbac/entities/sys-role.entity.ts create mode 100644 wwjcloud/src/common/rbac/index.ts create mode 100644 wwjcloud/src/common/rbac/menu.controller.ts create mode 100644 wwjcloud/src/common/rbac/menu.service.ts create mode 100644 wwjcloud/src/common/rbac/rbac.module.ts create mode 100644 wwjcloud/src/common/rbac/role.controller.ts create mode 100644 wwjcloud/src/common/rbac/role.service.ts create mode 100644 wwjcloud/src/common/settings/email/email-settings.controller.ts create mode 100644 wwjcloud/src/common/settings/email/email-settings.dto.ts create mode 100644 wwjcloud/src/common/settings/email/email-settings.service.ts create mode 100644 wwjcloud/src/common/settings/email/email.module.ts create mode 100644 wwjcloud/src/common/settings/email/email.service.ts create mode 100644 wwjcloud/src/common/settings/index.ts create mode 100644 wwjcloud/src/common/settings/login/login-settings.controller.ts create mode 100644 wwjcloud/src/common/settings/login/login-settings.dto.ts create mode 100644 wwjcloud/src/common/settings/login/login-settings.service.ts create mode 100644 wwjcloud/src/common/settings/login/login.module.ts create mode 100644 wwjcloud/src/common/settings/payment/payment-settings.controller.ts create mode 100644 wwjcloud/src/common/settings/payment/payment-settings.dto.ts create mode 100644 wwjcloud/src/common/settings/payment/payment-settings.service.ts create mode 100644 wwjcloud/src/common/settings/payment/payment.module.ts create mode 100644 wwjcloud/src/common/settings/payment/payment.service.ts create mode 100644 wwjcloud/src/common/settings/settings.module.ts create mode 100644 wwjcloud/src/common/settings/site/site-settings.controller.ts create mode 100644 wwjcloud/src/common/settings/site/site-settings.dto.ts create mode 100644 wwjcloud/src/common/settings/site/site-settings.service.ts create mode 100644 wwjcloud/src/common/settings/site/site.entity.ts create mode 100644 wwjcloud/src/common/settings/site/site.module.ts create mode 100644 wwjcloud/src/common/settings/sms/sms-settings.controller.ts create mode 100644 wwjcloud/src/common/settings/sms/sms-settings.dto.ts create mode 100644 wwjcloud/src/common/settings/sms/sms-settings.service.ts create mode 100644 wwjcloud/src/common/settings/sms/sms.module.ts create mode 100644 wwjcloud/src/common/settings/sms/sms.service.ts create mode 100644 wwjcloud/src/common/settings/storage/storage-settings.controller.ts create mode 100644 wwjcloud/src/common/settings/storage/storage-settings.dto.ts create mode 100644 wwjcloud/src/common/settings/storage/storage-settings.service.ts create mode 100644 wwjcloud/src/common/settings/storage/storage.controller.ts create mode 100644 wwjcloud/src/common/settings/storage/storage.module.ts create mode 100644 wwjcloud/src/common/settings/storage/storage.service.ts create mode 100644 wwjcloud/src/common/settings/upload/upload-settings.controller.ts create mode 100644 wwjcloud/src/common/settings/upload/upload-settings.dto.ts create mode 100644 wwjcloud/src/common/settings/upload/upload-settings.module.ts create mode 100644 wwjcloud/src/common/settings/upload/upload-settings.service.ts create mode 100644 wwjcloud/src/config/cache/index.ts create mode 100644 wwjcloud/src/config/database/index.ts create mode 100644 wwjcloud/src/config/env/index.ts create mode 100644 wwjcloud/src/config/http/index.ts create mode 100644 wwjcloud/src/config/index.ts create mode 100644 wwjcloud/src/config/logger/index.ts create mode 100644 wwjcloud/src/config/queue/index.ts create mode 100644 wwjcloud/src/config/security/index.ts create mode 100644 wwjcloud/src/config/third-party/index.ts create mode 100644 wwjcloud/src/config/typeorm.config.ts create mode 100644 wwjcloud/src/core/cache/cache.module.ts create mode 100644 wwjcloud/src/core/cache/ports/cache.port.ts create mode 100644 wwjcloud/src/core/config/config.module.ts create mode 100644 wwjcloud/src/core/config/schemas/index.ts create mode 100644 wwjcloud/src/core/context/cls.module.ts create mode 100644 wwjcloud/src/core/database/base.entity.ts create mode 100644 wwjcloud/src/core/database/base.repository.ts create mode 100644 wwjcloud/src/core/database/database.module.ts create mode 100644 wwjcloud/src/core/database/transformers/boolean-number.transformer.ts create mode 100644 wwjcloud/src/core/database/transformers/index.ts create mode 100644 wwjcloud/src/core/database/transformers/status-number.transformer.ts create mode 100644 wwjcloud/src/core/database/transformers/status-string.transformer.ts create mode 100644 wwjcloud/src/core/enums/index.ts create mode 100644 wwjcloud/src/core/enums/status.enum.ts create mode 100644 wwjcloud/src/core/exception/filters/http-exception.filter.ts create mode 100644 wwjcloud/src/core/http/http.module.ts create mode 100644 wwjcloud/src/core/interceptor/logging.interceptor.ts create mode 100644 wwjcloud/src/core/interceptor/transform.interceptor.ts create mode 100644 wwjcloud/src/core/logger/logger.module.ts create mode 100644 wwjcloud/src/core/queue/ports/queue.port.ts create mode 100644 wwjcloud/src/core/queue/queue.module.ts create mode 100644 wwjcloud/src/core/security/guards/index.ts create mode 100644 wwjcloud/src/core/security/security.module.ts create mode 100644 wwjcloud/src/core/security/strategies/index.ts create mode 100644 wwjcloud/src/core/validation/pipes/index.ts create mode 100644 wwjcloud/src/main.ts create mode 100644 wwjcloud/src/migrations/.gitkeep create mode 100644 wwjcloud/src/migrations/1755845112842-InitSchema.ts create mode 100644 wwjcloud/src/scripts/init-db.ts create mode 100644 wwjcloud/src/vendor/http/axios.adapter.ts create mode 100644 wwjcloud/src/vendor/index.ts create mode 100644 wwjcloud/src/vendor/mailer/nodemailer.adapter.ts create mode 100644 wwjcloud/src/vendor/payment/mock.adapter.ts create mode 100644 wwjcloud/src/vendor/redis/redis.provider.ts create mode 100644 wwjcloud/src/vendor/sms/aliyun-sms.adapter.ts create mode 100644 wwjcloud/src/vendor/storage/local.adapter.ts create mode 100644 wwjcloud/src/vendor/vendor.module.ts create mode 100644 wwjcloud/test/app.e2e-spec.ts create mode 100644 wwjcloud/test/jest-e2e.json create mode 100644 wwjcloud/tsconfig.build.json create mode 100644 wwjcloud/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08f339d --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.trae/rules/development_constraints.md b/.trae/rules/development_constraints.md new file mode 100644 index 0000000..7923b52 --- /dev/null +++ b/.trae/rules/development_constraints.md @@ -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助手将自动获取所有相关的开发约束和规范链接,确保开发过程的一致性和规范性。 \ No newline at end of file diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 0000000..3981eda --- /dev/null +++ b/.trae/rules/project_rules.md @@ -0,0 +1,2105 @@ +# 📊 依赖关系图 + +‍``` +┌─────────────────┐ +│ App │ ← 业务开发层(用户自定义业务模块) +│ (用户业务) │ 电商、CRM、ERP等具体业务逻辑 +└─────────────────┘ + ↓ +┌─────────────────┐ +│ Common │ ← 框架通用服务层(企业级通用功能) +│ (框架通用服务) │ 用户管理、权限管理、菜单管理 +└─────────────────┘ 文件上传、通知服务、系统设置 + ↓ 数据字典、缓存服务、队列服务 +┌─────────────────┐ +│ Core │ ← 核心基础设施层(底层基础设施) +│ (基础设施) │ 认证核心、数据库核心、验证核心 +└─────────────────┘ HTTP核心、缓存核心、队列核心 + ↓ +┌─────────────────┐ +│ Vendor │ ← 第三方服务适配层 +│ (外部集成) │ 存储、支付、通信、云服务适配 +└─────────────────┘ + +┌─────────────────┐ +│ Addons │ ← 插件扩展层(可插拔功能模块) +│ (插件扩展) │ 扩展框架功能,不影响核心稳定性 +└─────────────────┘ +``` + +## 📚 参考文档 + +- **Vben Admin 官方文档**: https://doc.vben.pro/ +- **项目技术栈**: Vue 3 + TypeScript + Vite + Element Plus +- **UI 组件库**: Element Plus (当前项目使用) +- **表单组件**: Vben Form (支持 Element Plus、Ant Design Vue、Naive UI 等多种 UI 库适配) + +--- + +## 📐 项目规范(代码风格与约定) + +本节定义目录结构、导入顺序、命名、方法与引用等统一规范,用于指导日常开发与 Code Review。 + +### 1) 目录结构规范 + +``` +src/ +├─ addon/ # 插件扩展层:可插拔功能模块,不影响核心稳定性 +├─ app/ # 应用层:用户业务开发模块 +│ ├─ ecommerce/ # 电商业务模块 +│ ├─ crm/ # CRM 业务模块 +│ └─ erp/ # ERP 业务模块 +├─ common/ # 框架通用服务层:用户、权限、菜单、文件、通知、系统设置等 +├─ config/ # 配置层:集中化配置与校验 +├─ core/ # 基础设施:数据库、缓存、HTTP、认证、日志等核心能力封装 +├─ vendor/ # 第三方适配:支付、存储、短信、云服务等 +└─ main.ts # 应用入口 +``` + +- 依赖方向:App → Common → Core → Vendor;禁止反向依赖与跨层耦合。 +- Addon 可依赖 Common/Core,但 App/Common/Core 不得依赖 Addon。 +- 模块划分以领域边界为单位,保持高内聚、低耦合。 + +### 2) 导入顺序与分组 + +1. Node.js 内置模块 +2. 第三方依赖(npm 包) +3. 项目内部模块 +4. 父级目录(../) +5. 同级目录(./) +6. 索引文件(index) + +示例: +```ts +// 1) Node 内置 +import * as fs from 'fs'; +import * as path from 'path'; + +// 2) 外部依赖 +import { Controller } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +// 3) 内部模块 +import { LoggerService } from '../../core/logger'; + +// 4) 父级目录 +import { BaseController } from '../base'; + +// 5) 同级目录 +import { UserService } from './user.service'; + +// 6) 索引 +import { CreateUserDto } from './dto'; +``` + +### 3) 命名规范 + +- 文件: + - 控制器 `*.controller.ts`,服务 `*.service.ts`,实体 `*.entity.ts`,DTO `*.dto.ts` + - 接口 `*.interface.ts`,类型 `*.type.ts`,常量 `*.constant.ts`,配置 `*.config.ts`,模块 `*.module.ts` +- 类:`UserController`、`UserService`、`UserEntity`、`CreateUserDto` +- 变量/方法:camelCase;布尔值以 `is/has/can/should` 开头;必要时私有方法可用前缀 `_`(可选) + +### 4) 方法与类设计 + +- 单一职责、短小精悍,每个方法聚焦一个清晰目标 +- 依赖注入优先,避免在方法中直接构造依赖 +- 显式返回类型,避免 any;公共方法尽量无副作用 +- 错误优先返回与早失败(early return),减少嵌套 + +### 5) 引用与依赖约束 + +- 严格遵循分层依赖:App → Common → Core → Vendor +- 禁止跨域(跨子域模块)直接依赖,使用约定接口与适配器 +- 避免循环依赖;必要时通过接口、token 或事件解耦 +- 推荐在每层提供 index.ts 作为 barrel 导出,统一对外 API + +### 6) 类型、实体与 DTO + +- 优先使用 interface,合理使用泛型提升复用 +- DTO 与 Entity 分离:DTO 负责入参校验(class-validator),Entity 负责持久化结构 +- 禁止直接在控制器接收实体,必须使用 DTO 并开启全局 ValidationPipe(已开启) + +### 7) 错误处理与日志 + +- 统一抛出框架异常(如 NotFoundException、BadRequestException),或自定义异常族 +- 记录错误日志,包含 requestId(CLS 已接入)、上下文与栈信息 +- 外部接口与关键链路增加 info 级打点,敏感信息不可入日志 + +### 8) 注释规范 + +- 公共类与复杂方法使用 JSDoc 注释;重要分支与特殊处理写明原因 +- 使用 TODO / FIXME 标注技术债与待优化点 + +### 9) 代码质量与提交 + +- Prettier + ESLint 强制统一风格;提交前 lint-staged 自动修复 +- Git 提交遵循 Conventional Commits;使用 `npm run commit` 触发交互式提交 + +### 10) 模块骨架推荐 + +``` +feature/ +├─ feature.module.ts +├─ feature.controller.ts +├─ feature.service.ts +├─ entities/ +│ └─ feature.entity.ts +├─ dto/ +│ ├─ create-feature.dto.ts +│ └─ update-feature.dto.ts +└─ feature.repository.ts # 如需自定义仓储 +``` + +- Common 层提供通用能力(用户、权限、菜单…),App 层仅组合与编排,尽量避免在 App 层重复造轮子。 + +### 11) API 约定 + +- Swagger 注解最小化:`@ApiTags`、`@ApiOperation`、`@ApiBearerAuth`(如需鉴权) +- 错误码与响应体保持一致性(统一响应封装可在 Common 层提供) + +### 12) 性能与安全 + +- 优先分页与选择性字段;避免 N+1 查询,必要时使用关联加载或查询优化 +- 合理使用缓存与索引;异步/队列处理重任务 +- 输入校验与输出清洗;开启限流与安全中间件;严禁在日志中打印密钥/密码 + +> 以上规范作为默认约束,后续将根据业务与基础设施演进持续完善。 +--- + +## 🔒 层级约束(强制) + +- 允许依赖: + - App(modules) → App(common) → Core(严格单向) + - Vendor 仅提供第三方适配器,不依赖 App/Core 任何实现 + - Addon 可依赖 App(common)/Core,但 App/Core 不得依赖 Addon +- 禁止依赖: + - App(common) → App(modules)(反向依赖) + - Core → App(反向依赖) + - App(common) → Vendor(直接依赖第三方),必须通过 Core 暴露的抽象端口(Port/Token)间接使用 Vendor + - 同层不同域模块严禁相互依赖其内部实现,唯一入口为该域公开的 index.ts 或导出 API +- 访问 Vendor 的约束: + - 第三方 SDK/Client 在 Vendor 层实现具体 Adapter;Core 层定义抽象 Port/Token 并注入;Common 只依赖 Core 的抽象,不直接 import Vendor +- 运行时注册约束: + - 所有外部资源(Redis、OSS、SMTP、SMS 等)统一在 Vendor 模块封装 Provider;由 Core 定义抽象并在应用 root 注册,Common 仅消费抽象 + +## 🧭 导入约束执行(建议自动化) + +- ESLint 约束(示例片段,后续可合并到 eslint.config.mjs): +```js +// import 方向约束,禁止反向与跨层内部实现依赖 +rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: [ + // Common 禁止依赖 App、Vendor + { group: ['@app/*', 'src/app/*', '@vendor/*', 'src/vendor/*'], message: 'Common 层禁止依赖 App/Vendor,请依赖 Core 抽象' }, + // Core 禁止依赖 App/Common/Vendor + { group: ['@app/*', 'src/app/*', '@common/*', 'src/common/*', '@vendor/*', 'src/vendor/*'], message: 'Core 层禁止依赖上层与 Vendor 实现' }, + // 任何层禁止 import 同层其他域的内部文件(建议各域仅通过 index.ts 暴露) + { group: ['**/*/internal/**'], message: '禁止依赖其他域内部实现,请通过其公共 API' }, + ], + }, + ], +} +``` +- 路径别名建议(仅规范,不立即修改): + - @app/*,@common/*,@core/*,@vendor/* + - 同层跨域访问仅允许 import 其公共 API(index.ts 或 public API 文件) + +## 🧩 层级职责与能力清单 + +- Core(核心基础设施) + - 配置系统(ConfigModule + Joi 校验) + - 数据库(TypeORM 基类:BaseEntity/BaseRepository,事务、审计字段) + - 日志(Winston + nest-winston,按日切割,CLS requestId) + - 缓存抽象(Cache Port,默认内存,提供 Redis 端口定义) + - 队列抽象(Bull Port,重试、延时、并发控制) + - HTTP 客户端封装(重试、超时、熔断占位) + - 安全与鉴权抽象(密码策略、加密、加盐、签名、ACL/RBAC 接口) + - 全局管道/过滤器/拦截器(ValidationPipe、异常过滤、响应封装、日志/耗时拦截) + - 限流封装(Throttler) + - 事件总线(EventEmitter 封装) + - 请求上下文(CLS,traceId/requestId) + +- App/Common(通用业务能力,内置功能) + - 账户与组织:User、Dept/Org、Profile + - 认证与授权:Auth(登录/登出/刷新)、JWT、RBAC(Role/Permission/Menu/Route) + - 系统配置:SystemConfig、参数配置、开关项、Settings(Email/SMS/Storage/Payment/Login) + - 字典中心:Dictionary/Enum 管理 + - 文件中心:上传、元数据、存储策略(通过 Core 抽象对接 Vendor) + - 通知中心:邮件/短信/站内信(通过 Core 抽象) + - 审计与操作日志:请求追踪、关键操作记录 + - 任务调度:定时任务编排(cron) + - 健康检查与监控:/health、/metrics(可选) + - 国际化:i18n(可选) + - 多租户(可选,后续版本) + +- App/Modules(具体业务模块) + - 电商模块:商品管理、订单管理、购物车、支付流程 + - CRM 模块:客户管理、销售线索、商机跟进 + - ERP 模块:库存管理、采购管理、财务管理 + - 其他业务模块:根据具体需求扩展 + +- Vendor(第三方适配层) + - Redis 客户端、MySQL 连接适配(由 Core 调用) + - 对象存储:Aliyun OSS / AWS S3 / Qiniu 等适配器 + - 邮件:Nodemailer/SES 适配器 + - 短信:Aliyun SMS / Tencent SMS 适配器 + - 支付:Alipay / WeChat Pay 适配器 + - 验证码:图形/短信验证码服务 + - 第三方 OAuth:GitHub/WeChat 等 + +- Addon(可选插件) + - 雪花 ID / 分布式 ID + - 审批流/流程引擎 + - 特性开关/灰度发布 + - 代码生成/脚手架 + +## 📁 完整目录规范(建议骨架) + +``` +src/ +├─ addon/ +│ ├─ feature-flags/ +│ │ ├─ feature-flags.module.ts +│ │ └─ services/ +│ ├─ id-generator/ +│ │ ├─ id-generator.module.ts +│ │ └─ services/ +│ └─ workflow/ +│ ├─ workflow.module.ts +│ └─ services/ +│ +├─ app/ +│ ├─ ecommerce/ +│ │ ├─ product/ +│ │ │ ├─ product.module.ts +│ │ │ ├─ product.controller.ts +│ │ │ ├─ product.service.ts +│ │ │ ├─ entities/ +│ │ │ └─ dto/ +│ │ ├─ order/ +│ │ │ ├─ order.module.ts +│ │ │ ├─ order.controller.ts +│ │ │ ├─ order.service.ts +│ │ │ ├─ entities/ +│ │ │ └─ dto/ +│ │ └─ cart/ +│ │ ├─ cart.module.ts +│ │ ├─ cart.controller.ts +│ │ ├─ cart.service.ts +│ │ ├─ entities/ +│ │ └─ dto/ +│ ├─ crm/ +│ │ ├─ customer/ +│ │ ├─ lead/ +│ │ └─ opportunity/ +│ └─ erp/ +│ ├─ inventory/ +│ ├─ procurement/ +│ └─ finance/ +│ +├─ common/ +│ ├─ auth/ +│ │ ├─ auth.module.ts +│ │ ├─ auth.controller.ts +│ │ ├─ auth.service.ts +│ │ ├─ strategies/ +│ │ └─ dto/ +│ ├─ user/ +│ │ ├─ user.module.ts +│ │ ├─ user.controller.ts +│ │ ├─ user.service.ts +│ │ ├─ entities/ +│ │ └─ dto/ +│ ├─ rbac/ +│ │ ├─ rbac.module.ts +│ │ ├─ role.service.ts +│ │ ├─ permission.service.ts +│ │ ├─ menu.service.ts +│ │ ├─ entities/ +│ │ └─ dto/ +│ ├─ settings/ +│ │ ├─ settings.module.ts +│ │ ├─ email/ +│ │ ├─ sms/ +│ │ ├─ storage/ +│ │ ├─ payment/ +│ │ └─ login/ +│ ├─ dict/ +│ ├─ file/ +│ ├─ notify/ +│ ├─ audit/ +│ ├─ schedule/ +│ ├─ health/ +│ ├─ i18n/ +│ └─ shared/ +│ ├─ dto/ +│ ├─ constants/ +│ └─ utils/ +│ +├─ core/ +│ ├─ config/ +│ │ ├─ config.module.ts +│ │ └─ schemas/ +│ ├─ database/ +│ │ ├─ database.module.ts +│ │ ├─ base.entity.ts +│ │ └─ base.repository.ts +│ ├─ logger/ +│ │ └─ logger.module.ts +│ ├─ cache/ +│ │ ├─ cache.module.ts +│ │ └─ ports/ +│ │ └─ cache.port.ts +│ ├─ queue/ +│ │ ├─ queue.module.ts +│ │ └─ ports/ +│ │ └─ queue.port.ts +│ ├─ http/ +│ │ └─ http.module.ts +│ ├─ security/ +│ │ ├─ security.module.ts +│ │ ├─ guards/ +│ │ └─ strategies/ +│ ├─ exception/ +│ │ └─ filters/ +│ ├─ interceptor/ +│ │ ├─ logging.interceptor.ts +│ │ └─ transform.interceptor.ts +│ ├─ validation/ +│ │ └─ pipes/ +│ └─ context/ +│ └─ cls.module.ts +│ +├─ vendor/ +│ ├─ redis/ +│ │ ├─ redis.module.ts +│ │ └─ redis.provider.ts +│ ├─ mailer/ +│ │ ├─ mailer.module.ts +│ │ └─ nodemailer.adapter.ts +│ ├─ sms/ +│ │ └─ aliyun-sms.adapter.ts +│ ├─ storage/ +│ │ ├─ oss.adapter.ts +│ │ └─ s3.adapter.ts +│ ├─ payment/ +│ │ ├─ alipay.adapter.ts +│ │ └─ wechatpay.adapter.ts +│ ├─ captcha/ +│ │ └─ captcha.adapter.ts +│ └─ http/ +│ └─ axios.adapter.ts +│ +├─ config/ +│ ├─ database.config.ts +│ ├─ redis.config.ts +│ └─ app.config.ts +│ +└─ main.ts +``` + +## 🔌 依赖倒置与适配器模式约定 + +- Core 只定义 Port/Token(接口/抽象)与领域无关的基础能力 +- Vendor 负责第三方实现(Adapter),通过 Provider 绑定到 Core 的 Token +- Common 仅通过 Core 暴露的 Token 使用能力,禁止直接引用具体 Adapter + +## 🔔 事件与跨层通信 + +- 领域事件优先,使用 EventEmitter;禁止同步强耦合调用导致循环依赖 +- 对跨系统/异步任务,统一走队列(Bull)或消息(后续可扩展) + +## ⚙️ 配置命名约定(节选) + +- LOG_LEVEL、THROTTLE_TTL、THROTTLE_LIMIT +- DB_HOST、DB_PORT、DB_USER、DB_PASS、DB_NAME +- REDIS_HOST、REDIS_PORT、REDIS_DB、REDIS_PASS +- JWT_SECRET、JWT_EXPIRES_IN + +## 🛠️ 待落实的工程化检查(后续可执行) + +- ESLint import 方向约束规则落地 +- tsconfig 路径别名(@app/@common/@core/@vendor) +- 各层 index.ts 统一导出公共 API + +--- + + +## 🎨 前端开发规范(Vben Admin) + +本项目前端基于 **Vben Admin** 框架,参考 **Niucloud** 的业务模式进行开发。前端位于 `admin/` 目录,采用 Vue 3 + TypeScript + Vite 技术栈。 + +**参考目录:** +- Niucloud 前端参考:`g:\wwjcloud-nestjs\reference\niucloud-php\admin\` +- 本项目前端目录:`g:\wwjcloud-nestjs\admin\apps\web-ele\` + +### 1) 前端目录结构规范 + +``` +admin/ +├─ apps/ +│ └─ web-ele/ # 主应用 +│ ├─ src/ +│ │ ├─ addon/ # 插件扩展层(参考 Niucloud) +│ │ │ ├─ shop/ # 商城插件 +│ │ │ ├─ cms/ # 内容管理插件 +│ │ │ └─ marketing/ # 营销插件 +│ │ ├─ app/ # 应用业务层(对应后端 app) +│ │ │ ├─ ecommerce/ # 电商模块 +│ │ │ │ ├─ product/ # 商品管理 +│ │ │ │ ├─ order/ # 订单管理 +│ │ │ │ └─ cart/ # 购物车 +│ │ │ ├─ crm/ # CRM 模块 +│ │ │ │ ├─ customer/ # 客户管理 +│ │ │ │ └─ lead/ # 销售线索 +│ │ │ └─ erp/ # ERP 模块 +│ │ │ ├─ inventory/ # 库存管理 +│ │ │ └─ finance/ # 财务管理 +│ │ ├─ common/ # 通用业务功能(对应后端 common) +│ │ │ ├─ user/ # 用户管理 +│ │ │ │ ├─ api/ # 用户相关 API +│ │ │ │ └─ views/ # 用户页面 +│ │ │ ├─ auth/ # 认证授权 +│ │ │ │ ├─ api/ # 认证相关 API +│ │ │ │ └─ views/ # 认证页面 +│ │ │ ├─ settings/ # 系统设置 +│ │ │ │ ├─ api/ # 设置相关 API +│ │ │ │ │ ├─ email.ts +│ │ │ │ │ ├─ login.ts +│ │ │ │ │ ├─ sms.ts +│ │ │ │ │ └─ storage.ts +│ │ │ │ └─ views/ # 设置页面 +│ │ │ │ ├─ email/ +│ │ │ │ │ └─ index.vue +│ │ │ │ ├─ login/ +│ │ │ │ │ └─ index.vue +│ │ │ │ ├─ sms/ +│ │ │ │ │ └─ index.vue +│ │ │ │ └─ storage/ +│ │ │ │ └─ index.vue +│ │ │ ├─ menu/ # 菜单管理 +│ │ │ │ ├─ api/ # 菜单相关 API +│ │ │ │ └─ views/ # 菜单页面 +│ │ │ ├─ upload/ # 文件上传 +│ │ │ │ ├─ api/ # 上传相关 API +│ │ │ │ └─ views/ # 上传页面 +│ │ │ └─ rbac/ # 角色权限管理 +│ │ │ ├─ api/ # 权限相关 API +│ │ │ └─ views/ # 权限页面 +│ │ ├─ api/ # API 接口层(Vben 规范) +│ │ │ └─ request.ts # 请求客户端配置 +│ │ ├─ views/ # 页面视图层(Vben 规范) +│ │ │ └─ dashboard/ # 仪表板 +│ │ ├─ router/ # 路由配置(Vben 规范) +│ │ │ └─ routes/ +│ │ │ └─ modules/ +│ │ │ ├─ settings.ts +│ │ │ └─ auth.ts +│ │ ├─ stores/ # 状态管理(Vben 规范) +│ │ ├─ components/ # 公共组件(Vben 规范) +│ │ ├─ composables/ # 组合式函数(Vben 规范) +│ │ ├─ utils/ # 工具函数(Vben 规范) +│ │ └─ types/ # 类型定义(Vben 规范) +│ ├─ package.json +│ └─ vite.config.ts +├─ packages/ # 共享包(Vben 规范) +└─ docs/ # 文档 +``` + +### 2) 前端分层架构 + +前端采用分层架构设计,参考 Niucloud 业务模式并结合 Vben 框架规范: + +``` +┌─────────────────┐ +│ Addon │ ← 插件扩展层(参考 Niucloud) +│ (插件扩展) │ 可插拔功能模块,如商城、CMS等 +└─────────────────┘ + ↓ +┌─────────────────┐ +│ App │ ← 应用业务层(对应后端 app 层) +│ (业务应用) │ 电商、CRM、ERP 等具体业务模块 +└─────────────────┘ + ↓ +┌─────────────────┐ +│ Common │ ← 通用功能层(对应后端 common 层) +│ (通用功能) │ 用户、认证、设置、菜单、权限等 +└─────────────────┘ + ↓ +┌─────────────────┐ +│ Views │ ← 视图层(页面组件,Vben 规范) +│ (页面视图) │ 负责用户界面展示和交互 +└─────────────────┘ + ↓ +┌─────────────────┐ +│ Composables │ ← 逻辑层(组合式函数,Vben 规范) +│ (业务逻辑) │ 封装业务逻辑和状态管理 +└─────────────────┘ + ↓ +┌─────────────────┐ +│ API │ ← 接口层(API 调用,Vben 规范) +│ (数据接口) │ 封装后端接口调用 +└─────────────────┘ + ↓ +┌─────────────────┐ +│ Utils │ ← 工具层(通用工具,Vben 规范) +│ (工具函数) │ 提供通用工具和辅助函数 +└─────────────────┘ +``` + +#### 2.1 分层架构详细说明 + +- **插件扩展层 (addon/)**: 可插拔功能模块,参考 Niucloud 插件架构 +- **应用业务层 (app/)**: 对应后端 app 层,具体业务模块 + - **电商模块 (app/ecommerce/)**: 商品、订单、购物车等 + - **CRM 模块 (app/crm/)**: 客户管理、销售线索等 + - **ERP 模块 (app/erp/)**: 库存、财务等 +- **通用功能层 (common/)**: 对应后端 common 层,框架通用功能 + - **用户管理 (common/user/)**: 用户相关功能 + - **认证授权 (common/auth/)**: 登录、权限等 + - **系统设置 (common/settings/)**: 邮件、短信、存储等配置 + - **菜单管理 (common/menu/)**: 菜单配置 + - **文件上传 (common/upload/)**: 文件处理 + - **角色权限 (common/rbac/)**: RBAC 权限管理 +- **API 接口层 (api/)**: 统一管理后端接口调用(Vben 规范) +- **页面视图层 (views/)**: 业务页面组件(Vben 规范) +- **路由配置层 (router/)**: 页面路由管理(Vben 规范) +- **状态管理层 (stores/)**: 全局状态管理(Vben 规范) +- **组件层 (components/)**: 可复用组件(Vben 规范) +- **工具层 (utils/)**: 通用工具函数(Vben 规范) + +### 3) API 接口层规范 + +#### 3.1 接口文件组织 +- 按业务模块划分:`api/settings/`、`api/auth/`、`api/user/` 等 +- 每个模块包含:接口函数、类型定义、响应处理 +- 统一使用 `requestClient` 进行 HTTP 请求 + +#### 3.2 接口命名约定 +```typescript +// 获取数据:get{Module}Api 或 get{Module}{Action}Api +export const getEmailSettingsApi = () => requestClient.get('/settings/email') + +// 更新数据:update{Module}Api 或 update{Module}{Action}Api +export const updateEmailSettingsApi = (data: UpdateEmailSettingsDto) => + requestClient.put('/settings/email', data) + +// 创建数据:create{Module}Api +export const createUserApi = (data: CreateUserDto) => + requestClient.post('/users', data) + +// 删除数据:delete{Module}Api +export const deleteUserApi = (id: string) => + requestClient.delete(`/users/${id}`) +``` + +#### 3.3 类型定义规范 +```typescript +// 响应类型以 Vo 结尾(View Object) +export interface EmailSettingsVo { + host: string + port: number + username: string + password: string + encryption: string + fromAddress: string + fromName: string +} + +// 请求类型以 Dto 结尾(Data Transfer Object) +export interface UpdateEmailSettingsDto { + host?: string + port?: number + username?: string + password?: string + encryption?: string + fromAddress?: string + fromName?: string +} +``` + +### 4) 页面组件规范 + +#### 4.1 页面结构模板(Vben Admin + Element Plus 规范) +```vue + +``` + +#### 4.2 组件脚本结构(Vben Admin + Element Plus 规范) +```vue + +``` + +### 5) 路由配置规范(Vben Admin 规范) + +#### 5.1 路由文件组织 +- 按模块划分:`src/router/routes/modules/settings.ts` +- 路由配置遵循 Vben 路由规范 +- 使用懒加载方式导入组件 +- 路由权限通过 `meta.authority` 配置 + +#### 5.2 路由配置示例 +```typescript +import type { RouteRecordRaw } from 'vue-router' + +const routes: RouteRecordRaw[] = [ + { + path: '/settings', + name: 'Settings', + component: '#/layouts/index.vue', + meta: { + title: '系统设置', + icon: 'lucide:settings', + order: 1000, + }, + children: [ + { + path: '/settings/email', + name: 'EmailSettings', + component: () => import('#/views/settings/email/index.vue'), + meta: { + title: '邮件设置', + icon: 'lucide:mail', + authority: ['admin', 'super'], + }, + }, + { + path: '/settings/sms', + name: 'SmsSettings', + component: () => import('#/views/settings/sms/index.vue'), + meta: { + title: '短信设置', + icon: 'lucide:message-square', + authority: ['admin', 'super'], + }, + }, + { + path: '/settings/storage', + name: 'StorageSettings', + component: () => import('#/views/settings/storage/index.vue'), + meta: { + title: '存储设置', + icon: 'lucide:hard-drive', + authority: ['admin', 'super'], + }, + }, + ], + }, +] + +export default routes +``` + +#### 5.3 路由权限配置 +- 使用 `meta.authority` 配置页面访问权限 +- 支持角色权限:`['admin', 'super']` +- 支持权限码:`['settings:email:read', 'settings:email:write']` +- 无权限配置表示公开访问 + +### 6) 状态管理规范(Vben Admin 规范) + +#### 6.1 使用 Pinia 进行状态管理 +- 按业务模块划分 Store:`src/stores/modules/` +- 优先使用 Composition API 风格 +- 合理使用持久化存储 +- 遵循 Vben 状态管理模式 + +#### 6.2 Store 示例 +```typescript +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { EmailSettingsVo } from '#/api/settings/email' +import { getEmailSettingsApi, updateEmailSettingsApi } from '#/api/settings/email' + +export const useSettingsStore = defineStore( + 'settings', + () => { + // 状态 + const emailSettings = ref(null) + const smsSettings = ref(null) + const storageSettings = ref(null) + const loading = ref(false) + + // 计算属性 + const isEmailConfigured = computed(() => { + return emailSettings.value?.host && emailSettings.value?.username + }) + + const isAnySettingLoading = computed(() => loading.value) + + // 方法 + const fetchEmailSettings = async () => { + loading.value = true + try { + const data = await getEmailSettingsApi() + emailSettings.value = data + return data + } catch (error) { + console.error('Failed to fetch email settings:', error) + throw error + } finally { + loading.value = false + } + } + + const updateEmailSettings = async (settings: EmailSettingsVo) => { + loading.value = true + try { + await updateEmailSettingsApi(settings) + emailSettings.value = settings + return settings + } catch (error) { + console.error('Failed to update email settings:', error) + throw error + } finally { + loading.value = false + } + } + + const resetEmailSettings = () => { + emailSettings.value = null + } + + return { + // 状态 + emailSettings, + smsSettings, + storageSettings, + loading, + // 计算属性 + isEmailConfigured, + isAnySettingLoading, + // 方法 + fetchEmailSettings, + updateEmailSettings, + resetEmailSettings, + } + }, + { + // 持久化配置 + persist: { + key: 'settings-store', + storage: localStorage, + paths: ['emailSettings', 'smsSettings', 'storageSettings'], + }, + }, +) +``` + +### 7) 组件开发规范(Vben Admin 规范) + +#### 7.1 组件命名 +- 使用 PascalCase 命名 +- 组件文件名与组件名保持一致 +- 页面组件放在 `views/` 目录 +- 公共组件放在 `components/` 目录 +- 遵循 Vben 组件命名约定 + +#### 7.2 组件使用优先级(基于 Vben Admin 官方规范) + +**组件选择原则:** +1. **Vben 封装组件**(最高优先级):如 `VbenForm`、`VbenModal`、`VbenDrawer`、`VbenVxeTable` 等 +2. **适配器组件**(中等优先级):通过 `src/adapter/component` 和 `src/adapter/form` 适配的组件 +3. **原生 UI 库组件**(最低优先级):如 Element Plus 的 `el-card`、`el-button` 等 + +**官方指导原则:** +> "如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。" —— Vben Admin 官方文档 + +**具体使用指导:** +- **表单开发**:**必须优先使用** `useVbenForm` 和 `VbenForm`,提供完整的表单解决方案,支持动态表单、验证、联动等高级功能 +- **模态框**:**必须优先使用** `useVbenModal` 和 `VbenModal`,支持拖拽、全屏、自动高度等功能,提供统一的交互体验 +- **抽屉**:**必须优先使用** `useVbenDrawer` 和 `VbenDrawer`,提供更好的用户体验和统一的样式 +- **表格**:优先使用 `VbenVxeTable`,基于 vxe-table 封装,结合 VbenForm 搜索;简单展示可使用 Element Plus 的 `ElTable` +- **基础组件**:可根据需求选择 Vben 组件或通过适配器使用 Element Plus 组件 + +**适配器配置要求:** +- Element Plus 组件适配:在 `src/adapter/component/index.ts` 中配置组件映射 +- 表单适配:在 `src/adapter/form.ts` 中配置表单验证、国际化等 +- 适配器处理:v-model 属性映射、国际化、主题适配、验证规则等 +- 灵活使用:可根据具体需求选择 Vben 组件、适配器组件或原生组件 + +#### 7.3 组件结构模板(Vben 组件优先) +```vue + + + + + +``` + +#### 7.3 组件 Props 定义 +```typescript +interface Props { + title?: string + loading?: boolean + data?: Record +} + +const props = withDefaults(defineProps(), { + title: '', + loading: false, + data: () => ({}) +}) +``` + +#### 7.4 组件事件定义 +```typescript +interface Emits { + save: [data: Record] + cancel: [] + change: [value: string] +} + +const emit = defineEmits() +``` + +### 8) 样式规范(Vben Admin 规范) + +#### 8.1 样式系统 +- 优先使用 Tailwind CSS 工具类 +- 使用 CSS Variables 进行主题定制 +- 避免编写自定义 CSS,除非必要 +- 遵循 Vben 设计系统 + +#### 8.2 Tailwind CSS 使用 +```vue + +``` + +#### 8.3 主题定制 +```css +/* 在 tailwind.config.js 中定义主题 */ +module.exports = { + theme: { + extend: { + colors: { + primary: { + 50: '#eff6ff', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + }, + gray: { + 50: '#f9fafb', + 100: '#f3f4f6', + 900: '#111827', + }, + }, + spacing: { + '18': '4.5rem', + '88': '22rem', + }, + }, + }, +} +``` + +#### 8.4 自定义样式(仅在必要时使用) +```vue + +``` + +### 9) 国际化规范(Vben Admin 规范) + +#### 9.1 语言文件组织 +``` +src/locales/ +├── langs/ +│ ├── zh-CN/ +│ │ ├── common.json +│ │ ├── settings.json +│ │ ├── validation.json +│ │ └── index.ts +│ ├── en-US/ +│ │ ├── common.json +│ │ ├── settings.json +│ │ ├── validation.json +│ │ └── index.ts +│ └── index.ts +├── helper.ts +└── index.ts +``` + +#### 9.2 语言文件示例 +```json +// src/locales/langs/zh-CN/settings.json +{ + "title": "系统设置", + "email": { + "title": "邮件设置", + "description": "配置系统邮件发送相关参数", + "form": { + "host": "SMTP服务器", + "port": "端口", + "username": "用户名", + "password": "密码", + "encryption": "加密方式", + "fromName": "发件人名称", + "fromEmail": "发件人邮箱" + }, + "placeholder": { + "host": "请输入SMTP服务器地址", + "port": "请输入端口号", + "username": "请输入用户名", + "password": "请输入密码" + }, + "validation": { + "hostRequired": "请输入SMTP服务器地址", + "portRequired": "请输入端口号", + "usernameRequired": "请输入用户名", + "passwordRequired": "请输入密码" + } + }, + "sms": { + "title": "短信设置", + "description": "配置短信发送服务商参数", + "form": { + "provider": "服务商", + "accessKey": "Access Key", + "secretKey": "Secret Key", + "signName": "签名" + } + }, + "storage": { + "title": "存储设置", + "description": "配置文件存储相关参数", + "form": { + "driver": "存储驱动", + "bucket": "存储桶", + "region": "地域", + "endpoint": "访问域名" + } + } +} +``` + +#### 9.3 在组件中使用 +```vue + + + +``` + +#### 9.4 国际化配置 +```typescript +// src/locales/index.ts +import { createI18n } from 'vue-i18n' +import { getLocale, setLocale } from './helper' + +// 导入语言包 +import zhCN from './langs/zh-CN' +import enUS from './langs/en-US' + +const messages = { + 'zh-CN': zhCN, + 'en-US': enUS, +} + +export const i18n = createI18n({ + legacy: false, + locale: getLocale(), + fallbackLocale: 'zh-CN', + messages, + globalInjection: true, +}) + +export { setLocale, getLocale } +``` + +### 10) 错误处理规范(Vben Admin 规范) + +#### 10.1 API 错误处理 +```typescript +// src/api/request.ts +import { requestClient } from '#/api/request' +import { useAuthStore } from '#/stores/auth' +import { message } from '#/components' +import { $t } from '#/locales' + +// 请求拦截器 +requestClient.interceptors.request.use( + (config) => { + const authStore = useAuthStore() + const token = authStore.accessToken + + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +requestClient.interceptors.response.use( + (response) => { + const { code, data, message: msg } = response.data + + // 根据业务状态码处理 + if (code === 200 || code === 0) { + return data + } else { + const errorMessage = msg || $t('common.requestFailed') + message.error(errorMessage) + return Promise.reject(new Error(errorMessage)) + } + }, + (error) => { + const { response } = error + + if (response) { + const { status, data } = response + + switch (status) { + case 401: + const authStore = useAuthStore() + authStore.logout() + message.error($t('common.tokenExpired')) + break + case 403: + message.error($t('common.noPermission')) + break + case 404: + message.error($t('common.notFound')) + break + case 500: + message.error($t('common.serverError')) + break + default: + message.error(data?.message || $t('common.requestFailed')) + } + } else { + // 网络错误 + message.error($t('common.networkError')) + } + + return Promise.reject(error) + } +) +``` + +#### 10.2 组件错误处理 +```vue + +``` + +#### 10.3 全局错误处理 +```typescript +// src/main.ts +import { createApp } from 'vue' +import App from './App.vue' + +const app = createApp(App) + +// 全局错误处理 +app.config.errorHandler = (error, instance, info) => { + console.error('Global error:', error) + console.error('Component instance:', instance) + console.error('Error info:', info) + + // 发送错误到监控服务 + // reportError(error, { instance, info }) +} + +// 未捕获的 Promise 错误 +window.addEventListener('unhandledrejection', (event) => { + console.error('Unhandled promise rejection:', event.reason) + // reportError(event.reason) +}) + +app.mount('#app') +``` + +### 11) 性能优化规范(Vben Admin 规范) + +#### 11.1 组件懒加载 +```typescript +// 路由懒加载 +const routes: RouteRecordRaw[] = [ + { + path: '/settings', + name: 'Settings', + component: () => import('#/views/settings/index.vue'), + meta: { + title: '系统设置', + }, + }, +] + +// 组件懒加载 +import { defineAsyncComponent } from 'vue' + +const AsyncHeavyComponent = defineAsyncComponent({ + loader: () => import('./HeavyComponent.vue'), + loadingComponent: () => import('#/components/Loading.vue'), + errorComponent: () => import('#/components/Error.vue'), + delay: 200, + timeout: 3000, +}) +``` + +#### 11.2 虚拟滚动 +```vue + + + +``` + +#### 11.3 图片优化 +```vue + + + +``` + +#### 11.4 状态管理优化 +```typescript +// 使用 computed 缓存计算结果 +import { computed, ref } from 'vue' + +const expensiveData = ref([]) + +// 缓存计算结果 +const processedData = computed(() => { + return expensiveData.value + .filter(item => item.active) + .map(item => ({ + ...item, + displayName: `${item.firstName} ${item.lastName}`, + })) + .sort((a, b) => a.displayName.localeCompare(b.displayName)) +}) + +// 使用 shallowRef 优化大对象 +import { shallowRef, triggerRef } from 'vue' + +const largeObject = shallowRef({ + // 大量数据 +}) + +// 更新时手动触发响应 +const updateLargeObject = (newData: any) => { + largeObject.value = { ...largeObject.value, ...newData } + triggerRef(largeObject) +} +``` + +#### 11.5 网络请求优化 +```typescript +// 请求去重 +import { ref } from 'vue' + +const requestCache = new Map>() + +const cachedRequest = async (url: string, options?: any) => { + const cacheKey = `${url}-${JSON.stringify(options)}` + + if (requestCache.has(cacheKey)) { + return requestCache.get(cacheKey) + } + + const promise = fetch(url, options).then(res => res.json()) + requestCache.set(cacheKey, promise) + + // 请求完成后清除缓存 + promise.finally(() => { + requestCache.delete(cacheKey) + }) + + return promise +} + +// 请求防抖 +import { debounce } from 'lodash-es' + +const searchKeyword = ref('') +const searchResults = ref([]) + +const debouncedSearch = debounce(async (keyword: string) => { + if (!keyword.trim()) { + searchResults.value = [] + return + } + + try { + const results = await searchApi(keyword) + searchResults.value = results + } catch (error) { + console.error('Search failed:', error) + } +}, 300) + +watch(searchKeyword, (newKeyword) => { + debouncedSearch(newKeyword) +}) +``` + +### 12) 测试规范(Vben Admin 规范) + +#### 12.1 单元测试 +```typescript +// tests/unit/components/EmailSettings.spec.ts +import { mount } from '@vue/test-utils' +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { createTestingPinia } from '@pinia/testing' +import EmailSettings from '#/views/settings/email/index.vue' +import { VbenForm } from '#/components/form' + +// Mock API +vi.mock('#/api/settings/email', () => ({ + getEmailSettingsApi: vi.fn(), + updateEmailSettingsApi: vi.fn(), +})) + +describe('EmailSettings', () => { + let wrapper: any + + beforeEach(() => { + wrapper = mount(EmailSettings, { + global: { + plugins: [ + createTestingPinia({ + createSpy: vi.fn, + }), + ], + components: { + VbenForm, + }, + }, + }) + }) + + it('renders correctly', () => { + expect(wrapper.find('[data-testid="email-settings-title"]').text()) + .toBe('邮件设置') + }) + + it('displays form fields correctly', () => { + const formFields = wrapper.findAll('.vben-form-item') + expect(formFields.length).toBeGreaterThan(0) + }) + + it('handles form submission', async () => { + const mockUpdateApi = vi.mocked(updateEmailSettingsApi) + mockUpdateApi.mockResolvedValue({}) + + const formData = { + host: 'smtp.test.com', + port: 587, + username: 'test@test.com', + password: 'password123', + } + + await wrapper.vm.handleSubmit(formData) + + expect(mockUpdateApi).toHaveBeenCalledWith(formData) + // 验证 Element Plus 消息提示 + expect(ElMessage.success).toHaveBeenCalledWith('保存成功') + }) +}) +``` + +#### 12.2 API 测试 +```typescript +// tests/api/settings.spec.ts +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { getEmailSettingsApi, updateEmailSettingsApi } from '#/api/settings/email' +import { requestClient } from '#/api/request' +import type { EmailSettingsVo } from '#/api/settings/email' + +// Mock request client +vi.mock('#/api/request', () => ({ + requestClient: { + get: vi.fn(), + put: vi.fn(), + }, +})) + +describe('Settings API', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('getEmailSettingsApi', () => { + it('should fetch email settings successfully', async () => { + const mockData: EmailSettingsVo = { + host: 'smtp.example.com', + port: 587, + username: 'test@example.com', + password: 'password123', + encryption: 'tls', + fromName: 'System', + fromEmail: 'noreply@example.com', + } + + vi.mocked(requestClient.get).mockResolvedValue(mockData) + + const result = await getEmailSettingsApi() + + expect(requestClient.get).toHaveBeenCalledWith('/settings/email') + expect(result).toEqual(mockData) + }) + }) +}) +``` + +#### 12.3 E2E 测试 +```typescript +// tests/e2e/settings.spec.ts +import { test, expect } from '@playwright/test' + +test.describe('Settings Page', () => { + test.beforeEach(async ({ page }) => { + // 登录 + await page.goto('/login') + await page.fill('[data-testid="username-input"]', 'admin') + await page.fill('[data-testid="password-input"]', 'password') + await page.click('[data-testid="login-button"]') + + // 等待登录完成 + await page.waitForURL('/dashboard') + }) + + test('should navigate to email settings', async ({ page }) => { + await page.goto('/settings/email') + + await expect(page.locator('[data-testid="email-settings-title"]')) + .toHaveText('邮件设置') + }) + + test('should update email settings', async ({ page }) => { + await page.goto('/settings/email') + + // 填写表单(Element Plus 输入框) + await page.fill('[data-testid="host-input"]', 'smtp.test.com') + await page.fill('[data-testid="port-input"]', '587') + + // 提交表单(Element Plus 按钮) + await page.click('[data-testid="save-button"]') + + // 验证 Element Plus 成功消息 + await expect(page.locator('.el-message--success')) + .toHaveText('保存成功') + }) +}) +``` + +#### 12.4 测试配置 +```typescript +// vitest.config.ts +import { defineConfig } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [vue()], + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./tests/setup.ts'], + }, + resolve: { + alias: { + '#': resolve(__dirname, './src'), + }, + }, +}) +``` + +### 13) 开发工具配置 + +#### 13.1 必要的 VSCode 插件 +- Vue Language Features (Volar) +- TypeScript Vue Plugin (Volar) +- Tailwind CSS IntelliSense +- ESLint +- Prettier + +#### 13.2 代码质量检查 +```bash +# 代码格式化 +pnpm format + +# 代码检查 +pnpm lint + +# 类型检查 +pnpm type-check + +# 构建检查 +pnpm build +``` + +### 14) 与后端协作规范 + +#### 14.1 接口约定 +- 前端接口路径与后端保持一致 +- 使用 TypeScript 类型定义确保类型安全 +- 统一错误码和响应格式 + +#### 14.2 数据流约定 +```typescript +// 后端响应格式 +interface ApiResponse { + code: number + data: T + message: string +} + +// 前端自动解包 data 字段 +const data = await getEmailSettingsApi() // 直接返回 EmailSettingsVo +``` + +--- + +## 📖 重要开发说明 + +### Vben Admin 官方文档参考 + +本项目基于 **Vben Admin** 框架开发,所有前端开发规范均应严格遵循 Vben Admin 官方文档: + +- **官方文档地址**: https://doc.vben.pro/ +- **当前使用版本**: Element Plus 适配版本 +- **表单组件**: 使用 Vben Form,支持多种 UI 库适配 +- **组件库**: Element Plus(项目已配置) + +### 开发约束 + +1. **禁止自创规范**: 所有前端开发必须参考 Vben Admin 官方文档,禁止自创或假设性开发 +2. **参考项目文档**: 可参考项目 `docs/` 目录下的代码规范和示例 + +#### 组件使用优先级(基于项目实际代码示例) + +**组件选择原则:** +1. **Element Plus 原生组件**(最高优先级):如 `ElCard`、`ElButton`、`ElTable`、`ElMessage` 等 +2. **Vben 封装组件**(中等优先级):如 `WorkbenchHeader`、`WorkbenchProject`、`AnalysisChartCard`、`Page` 等业务组件 +3. **适配器组件**(最低优先级):自定义适配器组件,仅在特殊需求时使用 + +**实际使用示例:** + +**基础 UI 组件**:直接使用 Element Plus +```vue + + + +``` + +**业务组件**:使用 Vben 封装组件 +```vue + + + +``` + +**官方指导原则:** +> "如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。" —— Vben Admin 官方文档 + +**具体使用指导:** +- **基础 UI**:优先使用 Element Plus 原生组件(`ElCard`、`ElButton`、`ElTable`、`ElForm` 等) +- **页面布局**:使用 Vben 的 `Page` 组件作为页面容器 +- **业务组件**:使用 Vben 封装的业务组件(`WorkbenchHeader`、`WorkbenchProject` 等) +- **表单开发**:可选择 Element Plus 的 `ElForm` 或 Vben 的表单组件,根据复杂度决定 +- **消息提示**:直接使用 Element Plus 的 `ElMessage`、`ElNotification` +- **数据展示**:直接使用 Element Plus 的 `ElTable`、图表组件等 + +### 协同开发要求 + +开发步骤,要前后端一起协同开发,项目的数据库账号、密码、库表均为wwjcloud。前端和后端项目均有规范工具,开发前一定要检查工具是否配置正确,是否有必要的插件,是否正常运行状态。前端开发进行修改框架核心内容,可以参考前端docs目录参考代码规范禁止自创和假设造成,没有规范的去写。 + +### 技术栈确认 + +- **前端框架**: Vue 3 + TypeScript + Vite +- **UI 组件库**: Element Plus +- **表单解决方案**: Vben Form(支持 Element Plus 适配) +- **状态管理**: Pinia +- **路由**: Vue Router +- **构建工具**: Vite +- **包管理器**: pnpm \ No newline at end of file diff --git a/admin b/admin new file mode 160000 index 0000000..cf6c4c9 --- /dev/null +++ b/admin @@ -0,0 +1 @@ +Subproject commit cf6c4c9aae1edf6eb0b7e285a218504ec56ed773 diff --git a/niucloud-frontend-migration-strategy.md b/niucloud-frontend-migration-strategy.md new file mode 100644 index 0000000..3f37feb --- /dev/null +++ b/niucloud-frontend-migration-strategy.md @@ -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 + +``` + +#### Vben Admin 目标结构: +```vue + +``` + +### 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 的现代化架构优势,实现高效、高质量的前端迁移。** \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..4fc4846 --- /dev/null +++ b/readme.md @@ -0,0 +1,935 @@ +# WWJ Cloud 企业级框架 - NestJS + VbenAdmin 实现 + +> 一款支持插件化+云安装+云编译 快速开发SAAS多用户系统后台管理框架! + +使用 WWJ Cloud 企业级框架,我们开发一个软件系统,**一切插件化**!!!= WWJ Cloud 框架 + 应用1 + 应用2 + 应用N + 插件1 + 插件2 + 插件N + ... + +如果对您有帮助,您可以点右上角 ⭐"Star" 收藏一下,获取第一时间更新,谢谢! + +--- + +## 📖 框架介绍 + +WWJ Cloud 企业级框架是一款快速开发 SaaS 通用管理系统后台框架,基于 **NestJS + TypeORM + Redis + MySQL** 后端技术架构和 **VbenAdmin + Vue3 + TypeScript + Element Plus** 前端技术栈精心设计,易读易懂,没有任何其它重度依赖,架构设计小巧灵活,没有采用过度设计模式。是一款快速可以开发企业级应用的软件系统。 + +**【您不需要重复造轮子 – 框架内置已经实现基础组件功能,您只需要开发业务模块即可】!** + +--- + +## 🔗 快速链接 + +- **Gitee 下载地址**:https://gitee.com/wwjauth/wwjcloud-nestjs +- **GitHub 下载地址**:https://github.com/wwjauth/wwjcloud-nestjs +- **演示地址**:http://demo.wwjauth.com/admin/ (账号:admin 密码:123456) +- **文档地址**:https://docs.wwjauth.com +- **云应用市场**:https://market.wwjauth.com +- **VbenAdmin 官网**:https://vben.pro +- **NestJS 官网**:https://nestjs.com + +--- + +## 🌟 WWJ Cloud 开发者生态圈 + +WWJ Cloud 框架,目前已经实现有 **NestJS + VbenAdmin** 版本功能实现。整个 WWJ Cloud 开发者生态圈目前已经有众多用户。其中开发者上千人。WWJ Cloud 生态圈众多代理商、经销商、中介商都会采购插件及应用,自己运营或者分销给第三方商家用户。您只需要用心开发插件或应用,并发布到 WWJ Cloud 云应用市场,即会有人购买。依靠 WWJ Cloud 强大的生态圈,实现市场、资源、产品的研发销售闭环。从今天开始,加入 WWJ Cloud 生态圈,实现程序员创业梦想!付出就有回报。心动不如行动! + +--- + +## 🎯 设计理念 + +### 强大的多应用+插件组合设计理念,低耦合,高内聚 + +基于**企业级框架底层设计**,采用**分层架构 + 领域驱动设计**,实现: +- **清晰的依赖层次**:`App(业务开发层)` → `Common(框架通用服务层)` → `Core(核心基础设施层)` → `Vendor(第三方适配层)` +- **框架化设计**:Common 层提供完整的企业级通用服务,App 层专注用户业务开发 +- **高内聚低耦合**:每层职责明确,接口清晰,支持插拔式扩展 +- **可扩展性**:支持 Addons 插件化扩展和微服务拆分 +- **企业级特性**:完整的配置系统、监控、日志、安全机制 +- **微服务就绪**:为未来微服务架构演进奠定基础 + +### 全新生态设计,多应用聚合+多插件组合运营模式全新升级 + +支持共同会员体系下多种应用+插件组合,DIY装修出最强的软件系统。 + +### 插件化,完全为开发者二次开发而生 + +WWJ Cloud 框架采用插件化模式设计,可以做到多种插件共存,组合使用。**一切皆为插件(应用)!** 比如您有一个项目是电商的项目,这个项目的要求是,既有商城的功能,又有CRM客户管理,还需要进行会员的管理,甚至于还要客服系统。传统的实现方式是,找多个源码,东拼西凑,二次开发,或者部署多套独立的系统,配合起来。而今天,使用 WWJ Cloud 框架,可以通过组装的方式,在一套体系中实现,随着发展,会有越来越多的各行各业的插件和应用上架。您对于项目的定制,可能只需要简单组装,装修页面,就可以最终实现功能交付。 + +### 首创强大的一键云安装,云编译,云发布,升级引擎 + +- WWJ Cloud 框架内置简单方便的一键云安装,云编译工具 +- WWJ Cloud 内置在线升级功能,系统会全自动化帮您升级文件。产品的更新只需一键完成 +- VSCode,WebStorm,微信小程序开发工具,打包,上传,发布!WWJ Cloud 框架强大的小程序一键傻瓜式发布系统,任何开发环境都不再需要搭建!鼠标一点完成小程序升级发布 + +### 🏗️ SaaS + 独立版双架构设计 + +WWJ Cloud 框架采用创新的 **SaaS + 独立版双架构设计**,一套代码同时支持两种部署模式: + +#### 🔄 SaaS 多租户模式 +- **适用场景**:云服务商、多客户管理系统 +- **架构特点**:通过 `site_id` 字段实现多租户数据隔离 +- **数据隔离**:每个租户拥有独立的 `site_id`,数据完全隔离 +- **资源共享**:共享系统基础设施,降低运营成本 +- **扩展性**:支持无限租户扩展,满足 SaaS 服务商需求 + +#### 🏢 独立版部署模式 +- **适用场景**:企业内部系统、单客户项目、私有化部署 +- **架构特点**:所有数据 `site_id` 统一为 0 +- **数据独立**:完全独立的数据环境,无租户概念 +- **部署灵活**:支持私有化部署,数据完全自主可控 +- **定制化**:可根据客户需求进行深度定制 + +#### 🔧 技术实现 +```sql +-- 数据库表结构示例 +CREATE TABLE users ( + id INT PRIMARY KEY AUTO_INCREMENT, + site_id INT NOT NULL DEFAULT 0, -- 租户ID,0表示独立版 + username VARCHAR(50) NOT NULL, + email VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_site_id (site_id) -- 租户索引,提升查询性能 +); + +-- SaaS模式:每个租户有独立的site_id +INSERT INTO users (site_id, username, email) VALUES (1, 'user1', 'user1@tenant1.com'); +INSERT INTO users (site_id, username, email) VALUES (2, 'user2', 'user2@tenant2.com'); + +-- 独立版模式:所有数据site_id为0 +INSERT INTO users (site_id, username, email) VALUES (0, 'admin', 'admin@company.com'); +INSERT INTO users (site_id, username, email) VALUES (0, 'user', 'user@company.com'); +``` + +#### 🎯 架构优势 +- **一套代码,两种模式**:无需维护两套代码,降低开发成本 +- **平滑切换**:可在 SaaS 和独立版之间平滑切换 +- **数据安全**:多租户数据完全隔离,独立版数据完全自主 +- **部署灵活**:支持云部署和私有化部署 +- **成本优化**:SaaS 模式资源共享,独立版模式完全控制 + +--- + +## 📊 依赖关系图 + +‍``` +┌─────────────────┐ +│ App │ ← 业务开发层(用户自定义业务模块) +│ (用户业务) │ 电商、CRM、ERP等具体业务逻辑 +└─────────────────┘ + ↓ +┌─────────────────┐ +│ Common │ ← 框架通用服务层(企业级通用功能) +│ (框架通用服务) │ 用户管理、权限管理、菜单管理 +└─────────────────┘ 文件上传、通知服务、系统设置 + ↓ 数据字典、缓存服务、队列服务 +┌─────────────────┐ +│ Core │ ← 核心基础设施层(底层基础设施) +│ (基础设施) │ 认证核心、数据库核心、验证核心 +└─────────────────┘ HTTP核心、缓存核心、队列核心 + ↓ +┌─────────────────┐ +│ Vendor │ ← 第三方服务适配层 +│ (外部集成) │ 存储、支付、通信、云服务适配 +└─────────────────┘ + +┌─────────────────┐ +│ Addons │ ← 插件扩展层(可插拔功能模块) +│ (插件扩展) │ 扩展框架功能,不影响核心稳定性 +└─────────────────┘ +‍``` + +--- + +## 🚀 技术亮点 + +### 🏗️ 后端技术栈(NestJS 生态) + +- **NestJS + TypeORM + MySQL8**:采用 SaaS + 独立版双架构设计,支持多租户 SaaS 模式和独立部署模式,通过 `site_id` 字段区分租户,当 `site_id = 0` 时为独立版模式,能够提供企业级软件服务运营,同时满足用户多站点,多商户,多门店等系统开发需求 +- **严格的 RESTful API 设计**:后端开发采用严格的 RESTful 的 API 设计开发,支持多语言设计开发 +- **Redis 分布式缓存**:高性能缓存系统,支持集群部署 +- **Bull 队列系统**:可靠的消息队列处理,支持任务调度和异步处理 +- **JWT + RBAC 权限系统**:完整的认证授权体系 +- **Winston 日志系统**:结构化日志记录和监控 +- **Swagger API 文档**:自动生成 API 文档,支持在线调试 + +### 🎨 前端技术栈(VbenAdmin 生态) + +- **VbenAdmin + Vue3 + TypeScript + Vite**:采用最新的前端技术栈,VbenAdmin 是基于 Vue3、Vite、TypeScript 的现代化管理系统 +- **Element Plus UI 组件库**:丰富的企业级 UI 组件,开发者不需要详细了解前端,只需要用标准的 Element 组件就可以 +- **Pinia 状态管理**:现代化的 Vue 状态管理方案 +- **Vue Router 路由管理**:支持动态路由和权限控制 +- **Tailwind CSS**:原子化 CSS 框架,快速构建现代化界面 +- **i18n 国际化**:支持多语言切换,真正意义上实现多语言的开发需求 +- **响应式设计**:支持桌面端、平板端、移动端自适应 + +### 🏢 企业级特性 + +- **SaaS + 独立版双架构支持**:支持多租户 SaaS 架构和独立部署模式,通过 `site_id` 字段区分,当 `site_id = 0` 时为独立版模式 +- **多租户 SaaS 架构**:支持多租户隔离,满足 SaaS 服务商需求,每个租户拥有独立的 `site_id` +- **独立版部署**:支持独立部署模式,适用于企业内部系统或单客户项目,所有数据 `site_id` 统一为 0 +- **微服务就绪**:分层架构设计,支持未来微服务拆分 +- **插件化扩展**:Addons 插件系统,支持功能模块热插拔 +- **云原生部署**:支持 Docker 容器化部署和 Kubernetes 编排 +- **监控告警**:完整的系统监控和告警机制 +- **数据备份**:自动化数据备份和恢复机制 +- **安全防护**:SQL 注入防护、XSS 防护、CSRF 防护等 + +### 🌐 多语言支持 + +WWJ Cloud 前端以及后端采用严格的多语言开发规范,包括前端展示,API 接口返回,数据验证,错误返回等全部使用多语言设计规范,使开发者能够真正意义上实现多语言的开发需求。 + +### 🛠️ 开发者友好 + +WWJ Cloud 结合当前市面上很多框架结构不规范,导致基础结构不稳定等情况,严格定义了分层设计的开发规范,同时 API 接口严格采用 RESTful 的开发规范,能够满足大型业务系统或者微服务的开发需求。 + +### 📦 内置功能模块 + +WWJ Cloud 已经搭建好常规系统的开发底层,具体功能包括: +- **管理员管理**:完整的管理员账户体系 +- **权限管理**:基于 RBAC 的权限控制系统 +- **菜单管理**:动态菜单配置和权限绑定 +- **用户管理**:前台用户管理和会员体系 +- **应用管理**:多应用管理和配置 +- **文件管理**:文件上传、存储和管理 +- **系统设置**:灵活的系统配置管理 +- **数据字典**:系统数据字典管理 +- **通知消息**:站内消息和推送通知 +- **操作日志**:完整的操作审计日志 +- **定时任务**:计划任务管理和调度 +- **API 接口**:对外开放接口管理 +- **健康检查**:系统健康状态监控 + +--- + +## 📁 项目目录结构 + +‍``` +src/ +├── app/ # 🏢 业务开发层(用户自定义业务模块) +│ ├── demo/ # Demo 模块(标准模板示例) +│ │ ├── demo.module.ts +│ │ ├── controllers/ +│ │ │ └── demo.controller.ts +│ │ ├── services/ +│ │ │ └── demo.service.ts +│ │ ├── entities/ +│ │ │ └── demo.entity.ts +│ │ ├── dto/ +│ │ │ └── demo.dto.ts +│ │ └── README.md # 模块开发指南 +│ └── index.ts # App 层统一导出 +│ +├── common/ # 🔧 框架通用服务层(企业级通用功能) +│ ├── users/ # 用户管理服务 +│ ├── rbac/ # 权限管理服务 +│ ├── menu/ # 菜单管理服务 +│ ├── apps/ # 应用管理服务 +│ ├── upload/ # 文件上传服务 +│ ├── notification/ # 通知服务 +│ ├── settings/ # 系统设置服务 +│ ├── dictionary/ # 数据字典服务 +│ ├── cache/ # 缓存服务 +│ ├── queue/ # 队列服务 +│ ├── health/ # 健康检查服务 +│ └── openapi/ # 对外开放接口服务 +│ +├── config/ # ⚙️ 配置层(运行时配置) +│ ├── env/ # 环境配置 +│ ├── database/ # 数据库配置 +│ ├── cache/ # 缓存配置 +│ ├── queue/ # 队列配置 +│ ├── http/ # HTTP 配置 +│ ├── security/ # 安全配置 +│ ├── logger/ # 日志配置 +│ └── third-party/ # 第三方服务配置 +│ +├── core/ # 🏗️ 核心基础设施层(跨域通用) +│ ├── auth/ # 认证核心 +│ ├── database/ # 数据库核心 +│ ├── validation/ # 验证核心 +│ ├── http/ # HTTP 核心 +│ ├── cache/ # 缓存核心 +│ ├── queue/ # 队列核心 +│ ├── logger/ # 日志核心 +│ ├── monitoring/ # 监控核心 +│ └── exceptions/ # 异常处理核心 +│ +├── vendor/ # 🔌 第三方服务适配层 +│ ├── storage/ # 存储服务适配 +│ ├── payment/ # 支付服务适配 +│ ├── communication/ # 通信服务适配 +│ ├── sms/ # 短信服务适配 +│ ├── email/ # 邮件服务适配 + +│ +├── addons/ # 🧩 插件扩展层(可插拔功能模块) +│ └── README.md # 插件开发指南 +│ +├── app.module.ts # 根模块 +└── main.ts # 应用入口 +‍``` + +--- + +## 🚀 快速开始 + +### 环境要求 + +- **Node.js** >= 18.0.0 +- **npm** >= 8.0.0 或 **pnpm** >= 7.0.0 +- **MySQL** >= 8.0 或 **PostgreSQL** >= 13 +- **Redis** >= 6.0 + +### 安装依赖 + +‍```bash +# 使用 npm +$ npm install + +# 或使用 pnpm(推荐) +$ pnpm install +‍``` + +### 环境配置 + +1. 复制环境配置文件: +‍```bash +$ cp .env.example .env +‍``` + +2. 配置数据库连接、Redis 连接等必要参数: +‍```bash +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USERNAME=root +DB_PASSWORD=your_password +DB_DATABASE=wwjauth + +# Redis 配置 +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= + +# JWT 配置 +JWT_SECRET=your_jwt_secret_key +JWT_EXPIRES_IN=7d +‍``` + +### 数据库迁移 + +‍```bash +# 运行数据库迁移 +$ npm run migration:run + +# 填充种子数据 +$ npm run seed:run +‍``` + +### 启动应用 + +‍```bash +# 开发模式 +$ npm run start:dev + +# 生产模式 +$ npm run start:prod + +# 调试模式 +$ npm run start:debug +‍``` + +### 访问应用 + +- **后端 API**:http://localhost:3000 +- **API 文档**:http://localhost:3000/api +- **健康检查**:http://localhost:3000/health + +--- + +## 🏗️ 开发指南 + +### 业务模块开发 + +1. **创建新模块**:参考 `src/app/demo` 模块结构 +2. **遵循分层架构**:Controller → Service → Repository → Entity +3. **使用框架服务**:充分利用 Common 层提供的通用服务 +4. **统一错误处理**:使用框架提供的异常处理机制 +5. **API 文档**:使用 Swagger 注解生成 API 文档 +6. **单元测试**:编写完整的单元测试和集成测试 + +### 🏗️ 多租户架构开发规范 + +#### 📋 数据库设计规范 +- **必须包含 `site_id` 字段**:所有业务表都必须包含 `site_id` 字段 +- **默认值设置**:`site_id` 字段默认值为 0,表示独立版模式 +- **索引优化**:为 `site_id` 字段创建索引,提升查询性能 +- **外键约束**:跨表关联时需要考虑租户隔离 + +```sql +-- 标准表结构示例 +CREATE TABLE products ( + id INT PRIMARY KEY AUTO_INCREMENT, + site_id INT NOT NULL DEFAULT 0, -- 租户ID,0表示独立版 + name VARCHAR(100) NOT NULL, + price DECIMAL(10,2) NOT NULL, + status TINYINT DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_site_id (site_id), -- 租户索引 + INDEX idx_site_status (site_id, status) -- 复合索引 +); +``` + +#### 🔧 实体类开发规范 +```typescript +// src/app/demo/entities/demo.entity.ts +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('demo') +export class DemoEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'site_id', type: 'int', default: 0, comment: '租户ID,0表示独立版' }) + siteId: number; + + @Column({ name: 'name', type: 'varchar', length: 100, comment: '名称' }) + name: string; + + @Column({ name: 'status', type: 'tinyint', default: 1, comment: '状态:1启用,0禁用' }) + status: number; + + @CreateDateColumn({ name: 'created_at', comment: '创建时间' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', comment: '更新时间' }) + updatedAt: Date; +} +``` + +#### 🛠️ 服务层开发规范 +```typescript +// src/app/demo/services/demo.service.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DemoEntity } from '../entities/demo.entity'; +import { CreateDemoDto } from '../dto/create-demo.dto'; +import { UpdateDemoDto } from '../dto/update-demo.dto'; + +@Injectable() +export class DemoService { + constructor( + @InjectRepository(DemoEntity) + private readonly demoRepository: Repository, + ) {} + + // 创建时自动设置租户ID + async create(createDemoDto: CreateDemoDto, siteId: number = 0): Promise { + const demo = this.demoRepository.create({ + ...createDemoDto, + siteId, // 自动设置租户ID + }); + return this.demoRepository.save(demo); + } + + // 查询时自动过滤租户数据 + async findAll(siteId: number = 0): Promise { + return this.demoRepository.find({ + where: { siteId }, + order: { createdAt: 'DESC' }, + }); + } + + // 根据ID查询时验证租户权限 + async findOne(id: number, siteId: number = 0): Promise { + 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 { + const demo = await this.findOne(id, siteId); + + Object.assign(demo, updateDemoDto); + return this.demoRepository.save(demo); + } + + // 删除时验证租户权限 + async remove(id: number, siteId: number = 0): Promise { + 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 { + // 添加site_id字段 + await queryRunner.addColumn( + 'demo', + new TableColumn({ + name: 'site_id', + type: 'int', + default: 0, + comment: '租户ID,0表示独立版', + }), + ); + + // 创建索引 + await queryRunner.createIndex('demo', 'idx_site_id', ['site_id']); + } + + public async down(queryRunner: QueryRunner): Promise { + 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 文档生成工具 + +### 🌟 贡献者 + +感谢以下贡献者对项目的支持: + + + + + + +--- + +## 📈 项目统计 + +![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) + +--- + +
+ +**WWJ Cloud 企业级框架** - 让企业级应用开发更简单、更高效! 🚀 + +**基于 NestJS + VbenAdmin 的现代化企业级解决方案** + +[⭐ 给个 Star](https://github.com/wwjauth/wwjcloud-nestjs) | +[📖 查看文档](https://docs.wwjauth.com) | +[🚀 在线演示](http://demo.wwjauth.com) | +[💬 加入社区](https://wwjauth.com/community) + +
\ No newline at end of file diff --git a/wwjcloud/.env.example b/wwjcloud/.env.example new file mode 100644 index 0000000..0b5a1c9 --- /dev/null +++ b/wwjcloud/.env.example @@ -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 \ No newline at end of file diff --git a/wwjcloud/.husky/commit-msg b/wwjcloud/.husky/commit-msg new file mode 100644 index 0000000..42a70e4 --- /dev/null +++ b/wwjcloud/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx --no -- commitlint --edit "$1" \ No newline at end of file diff --git a/wwjcloud/.husky/pre-commit b/wwjcloud/.husky/pre-commit new file mode 100644 index 0000000..0312b76 --- /dev/null +++ b/wwjcloud/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +npx lint-staged \ No newline at end of file diff --git a/wwjcloud/.prettierrc b/wwjcloud/.prettierrc new file mode 100644 index 0000000..dcb7279 --- /dev/null +++ b/wwjcloud/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} \ No newline at end of file diff --git a/wwjcloud/README.md b/wwjcloud/README.md new file mode 100644 index 0000000..8f0f65f --- /dev/null +++ b/wwjcloud/README.md @@ -0,0 +1,98 @@ +

+ Nest Logo +

+ +[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 +[circleci-url]: https://circleci.com/gh/nestjs/nest + +

A progressive Node.js framework for building efficient and scalable server-side applications.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## 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). diff --git a/wwjcloud/commitlint.config.cjs b/wwjcloud/commitlint.config.cjs new file mode 100644 index 0000000..3d88028 --- /dev/null +++ b/wwjcloud/commitlint.config.cjs @@ -0,0 +1 @@ +module.exports = { extends: ['@commitlint/config-conventional'] }; \ No newline at end of file diff --git a/wwjcloud/eslint.config.mjs b/wwjcloud/eslint.config.mjs new file mode 100644 index 0000000..ec0992b --- /dev/null +++ b/wwjcloud/eslint.config.mjs @@ -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' }, + ], + }, + ], + }, + }, +); \ No newline at end of file diff --git a/wwjcloud/nest-cli.json b/wwjcloud/nest-cli.json new file mode 100644 index 0000000..76335ec --- /dev/null +++ b/wwjcloud/nest-cli.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true, + "assets": [ + { + "include": "**/*.hbs", + "outDir": "dist" + } + ] + } +} diff --git a/wwjcloud/package.json b/wwjcloud/package.json new file mode 100644 index 0000000..106fcd8 --- /dev/null +++ b/wwjcloud/package.json @@ -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" + } + } +} diff --git a/wwjcloud/public/.gitkeep b/wwjcloud/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/wwjcloud/public/upload/.gitkeep b/wwjcloud/public/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/wwjcloud/src/app.controller.spec.ts b/wwjcloud/src/app.controller.spec.ts new file mode 100644 index 0000000..d22f389 --- /dev/null +++ b/wwjcloud/src/app.controller.spec.ts @@ -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); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/wwjcloud/src/app.controller.ts b/wwjcloud/src/app.controller.ts new file mode 100644 index 0000000..cce879e --- /dev/null +++ b/wwjcloud/src/app.controller.ts @@ -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(); + } +} diff --git a/wwjcloud/src/app.module.ts b/wwjcloud/src/app.module.ts new file mode 100644 index 0000000..cb053e1 --- /dev/null +++ b/wwjcloud/src/app.module.ts @@ -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 {} diff --git a/wwjcloud/src/app.service.ts b/wwjcloud/src/app.service.ts new file mode 100644 index 0000000..927d7cc --- /dev/null +++ b/wwjcloud/src/app.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService { + getHello(): string { + return 'Hello World!'; + } +} diff --git a/wwjcloud/src/common/admin/admin.controller.ts b/wwjcloud/src/common/admin/admin.controller.ts new file mode 100644 index 0000000..dcccb8a --- /dev/null +++ b/wwjcloud/src/common/admin/admin.controller.ts @@ -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, + }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/admin/admin.module.ts b/wwjcloud/src/common/admin/admin.module.ts new file mode 100644 index 0000000..eac115b --- /dev/null +++ b/wwjcloud/src/common/admin/admin.module.ts @@ -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 {} \ No newline at end of file diff --git a/wwjcloud/src/common/admin/admin.service.ts b/wwjcloud/src/common/admin/admin.service.ts new file mode 100644 index 0000000..a283c7a --- /dev/null +++ b/wwjcloud/src/common/admin/admin.service.ts @@ -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, + @InjectRepository(SysUserRole) + private readonly sysUserRoleRepository: Repository, + ) {} + + /** + * 创建管理员 + */ + async create(createAdminDto: CreateAdminDto): Promise { + // 检查用户名是否已存在 + 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 { + 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 { + return await this.sysUserRepository.findOne({ + where: { username, deleteTime: 0 }, + relations: ['userRoles'], + }); + } + + /** + * 更新管理员信息 + */ + async update(id: number, updateAdminDto: UpdateAdminDto): Promise { + 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 { + 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 { + 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 { + 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 { + return await bcrypt.compare(password, admin.password); + } + + /** + * 分配角色 + */ + async assignRoles(uid: number, roleIds: number[], siteId: number): Promise { + 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 { + // 删除现有角色 + await this.sysUserRoleRepository.delete({ uid }); + + // 分配新角色 + if (roleIds.length > 0) { + await this.assignRoles(uid, roleIds, siteId); + } + } + + /** + * 获取用户角色 + */ + async getUserRoles(uid: number): Promise { + const userRoles = await this.sysUserRoleRepository.find({ + where: { uid }, + }); + return userRoles.map(ur => ur.roleId); + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/admin/dto/create-admin.dto.ts b/wwjcloud/src/common/admin/dto/create-admin.dto.ts new file mode 100644 index 0000000..bd7de1a --- /dev/null +++ b/wwjcloud/src/common/admin/dto/create-admin.dto.ts @@ -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[]; +} \ No newline at end of file diff --git a/wwjcloud/src/common/admin/dto/index.ts b/wwjcloud/src/common/admin/dto/index.ts new file mode 100644 index 0000000..37348af --- /dev/null +++ b/wwjcloud/src/common/admin/dto/index.ts @@ -0,0 +1,3 @@ +export { CreateAdminDto } from './create-admin.dto'; +export { UpdateAdminDto } from './update-admin.dto'; +export { QueryAdminDto } from './query-admin.dto'; \ No newline at end of file diff --git a/wwjcloud/src/common/admin/dto/query-admin.dto.ts b/wwjcloud/src/common/admin/dto/query-admin.dto.ts new file mode 100644 index 0000000..6976028 --- /dev/null +++ b/wwjcloud/src/common/admin/dto/query-admin.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/admin/dto/update-admin.dto.ts b/wwjcloud/src/common/admin/dto/update-admin.dto.ts new file mode 100644 index 0000000..60c5ecf --- /dev/null +++ b/wwjcloud/src/common/admin/dto/update-admin.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateAdminDto } from './create-admin.dto'; + +export class UpdateAdminDto extends PartialType(CreateAdminDto) {} \ No newline at end of file diff --git a/wwjcloud/src/common/admin/entities/sys-user-role.entity.ts b/wwjcloud/src/common/admin/entities/sys-user-role.entity.ts new file mode 100644 index 0000000..3fe64f1 --- /dev/null +++ b/wwjcloud/src/common/admin/entities/sys-user-role.entity.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/admin/entities/sys-user.entity.ts b/wwjcloud/src/common/admin/entities/sys-user.entity.ts new file mode 100644 index 0000000..5b901bc --- /dev/null +++ b/wwjcloud/src/common/admin/entities/sys-user.entity.ts @@ -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[]; +} \ No newline at end of file diff --git a/wwjcloud/src/common/admin/index.ts b/wwjcloud/src/common/admin/index.ts new file mode 100644 index 0000000..b3ff0d8 --- /dev/null +++ b/wwjcloud/src/common/admin/index.ts @@ -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'; \ No newline at end of file diff --git a/wwjcloud/src/common/apps/apps.module.ts b/wwjcloud/src/common/apps/apps.module.ts new file mode 100644 index 0000000..f84eeff --- /dev/null +++ b/wwjcloud/src/common/apps/apps.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class AppsModule {} diff --git a/wwjcloud/src/common/auth/auth.controller.ts b/wwjcloud/src/common/auth/auth.controller.ts new file mode 100644 index 0000000..da0721b --- /dev/null +++ b/wwjcloud/src/common/auth/auth.controller.ts @@ -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, + }, + }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/auth.module.ts b/wwjcloud/src/common/auth/auth.module.ts new file mode 100644 index 0000000..e702e9b --- /dev/null +++ b/wwjcloud/src/common/auth/auth.module.ts @@ -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('JWT_SECRET', 'wwjcloud-secret-key'), + signOptions: { + expiresIn: configService.get('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 {} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/auth.service.ts b/wwjcloud/src/common/auth/auth.service.ts new file mode 100644 index 0000000..6756389 --- /dev/null +++ b/wwjcloud/src/common/auth/auth.service.ts @@ -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: '登出成功', + }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/decorators/auth.decorator.ts b/wwjcloud/src/common/auth/decorators/auth.decorator.ts new file mode 100644 index 0000000..f75b0d1 --- /dev/null +++ b/wwjcloud/src/common/auth/decorators/auth.decorator.ts @@ -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; + }, +); \ No newline at end of file diff --git a/wwjcloud/src/common/auth/dto/change-password.dto.ts b/wwjcloud/src/common/auth/dto/change-password.dto.ts new file mode 100644 index 0000000..33eaf69 --- /dev/null +++ b/wwjcloud/src/common/auth/dto/change-password.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/dto/index.ts b/wwjcloud/src/common/auth/dto/index.ts new file mode 100644 index 0000000..00f715f --- /dev/null +++ b/wwjcloud/src/common/auth/dto/index.ts @@ -0,0 +1,3 @@ +export * from './login.dto'; +export * from './register.dto'; +export * from './change-password.dto'; \ No newline at end of file diff --git a/wwjcloud/src/common/auth/dto/login.dto.ts b/wwjcloud/src/common/auth/dto/login.dto.ts new file mode 100644 index 0000000..6097384 --- /dev/null +++ b/wwjcloud/src/common/auth/dto/login.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/dto/register.dto.ts b/wwjcloud/src/common/auth/dto/register.dto.ts new file mode 100644 index 0000000..12109b3 --- /dev/null +++ b/wwjcloud/src/common/auth/dto/register.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/guards/global-auth.guard.ts b/wwjcloud/src/common/auth/guards/global-auth.guard.ts new file mode 100644 index 0000000..97d98e1 --- /dev/null +++ b/wwjcloud/src/common/auth/guards/global-auth.guard.ts @@ -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 | Observable { + // 检查是否为公开路由 + const isPublic = this.reflector.getAllAndOverride(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; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/guards/jwt-auth.guard.ts b/wwjcloud/src/common/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..782a1f8 --- /dev/null +++ b/wwjcloud/src/common/auth/guards/jwt-auth.guard.ts @@ -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 | Observable { + // 检查是否标记为公开路由 + const isPublic = this.reflector.getAllAndOverride('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; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/guards/local-auth.guard.ts b/wwjcloud/src/common/auth/guards/local-auth.guard.ts new file mode 100644 index 0000000..189bc34 --- /dev/null +++ b/wwjcloud/src/common/auth/guards/local-auth.guard.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +@Injectable() +export class LocalAuthGuard extends AuthGuard('local') {} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/guards/roles.guard.ts b/wwjcloud/src/common/auth/guards/roles.guard.ts new file mode 100644 index 0000000..a7b0986 --- /dev/null +++ b/wwjcloud/src/common/auth/guards/roles.guard.ts @@ -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 { + // 获取所需的角色或权限 + const requiredRoles = this.reflector.getAllAndOverride('roles', [ + context.getHandler(), + context.getClass(), + ]); + + const requiredPermissions = this.reflector.getAllAndOverride('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('权限验证失败'); + } + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/index.ts b/wwjcloud/src/common/auth/index.ts new file mode 100644 index 0000000..f7c1ed9 --- /dev/null +++ b/wwjcloud/src/common/auth/index.ts @@ -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'; \ No newline at end of file diff --git a/wwjcloud/src/common/auth/services/index.ts b/wwjcloud/src/common/auth/services/index.ts new file mode 100644 index 0000000..30dde10 --- /dev/null +++ b/wwjcloud/src/common/auth/services/index.ts @@ -0,0 +1 @@ +export * from './permission.service'; \ No newline at end of file diff --git a/wwjcloud/src/common/auth/services/permission.service.ts b/wwjcloud/src/common/auth/services/permission.service.ts new file mode 100644 index 0000000..2eacc85 --- /dev/null +++ b/wwjcloud/src/common/auth/services/permission.service.ts @@ -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 { + 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 { + 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 { + 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(); + 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 { + 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); + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/strategies/jwt.strategy.ts b/wwjcloud/src/common/auth/strategies/jwt.strategy.ts new file mode 100644 index 0000000..36d8920 --- /dev/null +++ b/wwjcloud/src/common/auth/strategies/jwt.strategy.ts @@ -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('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验证失败'); + } + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/strategies/local.strategy.ts b/wwjcloud/src/common/auth/strategies/local.strategy.ts new file mode 100644 index 0000000..c55b0ce --- /dev/null +++ b/wwjcloud/src/common/auth/strategies/local.strategy.ts @@ -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 { + const { userType = 'member' } = request.body; + + const user = await this.authService.validateUser(username, password, userType); + + if (!user) { + throw new UnauthorizedException('用户名或密码错误'); + } + + return user; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/auth/user-permission.controller.ts b/wwjcloud/src/common/auth/user-permission.controller.ts new file mode 100644 index 0000000..f722b03 --- /dev/null +++ b/wwjcloud/src/common/auth/user-permission.controller.ts @@ -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, + }; + } + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/cache/cache.module.ts b/wwjcloud/src/common/cache/cache.module.ts new file mode 100644 index 0000000..9adc6c6 --- /dev/null +++ b/wwjcloud/src/common/cache/cache.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class CacheModule {} diff --git a/wwjcloud/src/common/dictionary/dictionary.controller.ts b/wwjcloud/src/common/dictionary/dictionary.controller.ts new file mode 100644 index 0000000..a836200 --- /dev/null +++ b/wwjcloud/src/common/dictionary/dictionary.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('dictionary') +export class DictionaryController { + @Get('ping') + ping() { + return 'dictionary ok'; + } +} diff --git a/wwjcloud/src/common/dictionary/dictionary.module.ts b/wwjcloud/src/common/dictionary/dictionary.module.ts new file mode 100644 index 0000000..216e18d --- /dev/null +++ b/wwjcloud/src/common/dictionary/dictionary.module.ts @@ -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 {} diff --git a/wwjcloud/src/common/dictionary/dictionary.service.ts b/wwjcloud/src/common/dictionary/dictionary.service.ts new file mode 100644 index 0000000..58975a2 --- /dev/null +++ b/wwjcloud/src/common/dictionary/dictionary.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class DictionaryService { + ping() { + return 'dictionary service ok'; + } +} diff --git a/wwjcloud/src/common/dictionary/dto/index.ts b/wwjcloud/src/common/dictionary/dto/index.ts new file mode 100644 index 0000000..a23daec --- /dev/null +++ b/wwjcloud/src/common/dictionary/dto/index.ts @@ -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' diff --git a/wwjcloud/src/common/health/health.module.ts b/wwjcloud/src/common/health/health.module.ts new file mode 100644 index 0000000..20d8c19 --- /dev/null +++ b/wwjcloud/src/common/health/health.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class HealthModule {} diff --git a/wwjcloud/src/common/index.ts b/wwjcloud/src/common/index.ts new file mode 100644 index 0000000..8c1cee8 --- /dev/null +++ b/wwjcloud/src/common/index.ts @@ -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'; diff --git a/wwjcloud/src/common/member/dto/create-member.dto.ts b/wwjcloud/src/common/member/dto/create-member.dto.ts new file mode 100644 index 0000000..def12f9 --- /dev/null +++ b/wwjcloud/src/common/member/dto/create-member.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/member/dto/index.ts b/wwjcloud/src/common/member/dto/index.ts new file mode 100644 index 0000000..275dc88 --- /dev/null +++ b/wwjcloud/src/common/member/dto/index.ts @@ -0,0 +1,3 @@ +export { CreateMemberDto } from './create-member.dto'; +export { UpdateMemberDto } from './update-member.dto'; +export { QueryMemberDto } from './query-member.dto'; \ No newline at end of file diff --git a/wwjcloud/src/common/member/dto/query-member.dto.ts b/wwjcloud/src/common/member/dto/query-member.dto.ts new file mode 100644 index 0000000..64805e6 --- /dev/null +++ b/wwjcloud/src/common/member/dto/query-member.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/member/dto/update-member.dto.ts b/wwjcloud/src/common/member/dto/update-member.dto.ts new file mode 100644 index 0000000..4fae49f --- /dev/null +++ b/wwjcloud/src/common/member/dto/update-member.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateMemberDto } from './create-member.dto'; + +export class UpdateMemberDto extends PartialType(CreateMemberDto) {} \ No newline at end of file diff --git a/wwjcloud/src/common/member/entities/member.entity.ts b/wwjcloud/src/common/member/entities/member.entity.ts new file mode 100644 index 0000000..9517324 --- /dev/null +++ b/wwjcloud/src/common/member/entities/member.entity.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/member/index.ts b/wwjcloud/src/common/member/index.ts new file mode 100644 index 0000000..644baec --- /dev/null +++ b/wwjcloud/src/common/member/index.ts @@ -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'; \ No newline at end of file diff --git a/wwjcloud/src/common/member/member.controller.ts b/wwjcloud/src/common/member/member.controller.ts new file mode 100644 index 0000000..ce99c04 --- /dev/null +++ b/wwjcloud/src/common/member/member.controller.ts @@ -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, + }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/member/member.module.ts b/wwjcloud/src/common/member/member.module.ts new file mode 100644 index 0000000..f8a6a22 --- /dev/null +++ b/wwjcloud/src/common/member/member.module.ts @@ -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 {} \ No newline at end of file diff --git a/wwjcloud/src/common/member/member.service.ts b/wwjcloud/src/common/member/member.service.ts new file mode 100644 index 0000000..fb811c6 --- /dev/null +++ b/wwjcloud/src/common/member/member.service.ts @@ -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, + ) {} + + /** + * 创建会员 + */ + async create(createMemberDto: CreateMemberDto): Promise { + // 检查用户名是否已存在 + 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 { + 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 { + return await this.memberRepository.findOne({ + where: { username, deleteTime: 0 }, + }); + } + + /** + * 根据手机号查询会员 + */ + async findByMobile(mobile: string): Promise { + return await this.memberRepository.findOne({ + where: { mobile, deleteTime: 0 }, + }); + } + + /** + * 更新会员信息 + */ + async update(id: number, updateMemberDto: UpdateMemberDto): Promise { + 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 { + 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 { + 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 { + 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 { + return await bcrypt.compare(password, member.password); + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/notification/notification.module.ts b/wwjcloud/src/common/notification/notification.module.ts new file mode 100644 index 0000000..b945544 --- /dev/null +++ b/wwjcloud/src/common/notification/notification.module.ts @@ -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 {} diff --git a/wwjcloud/src/common/notification/notification.service.ts b/wwjcloud/src/common/notification/notification.service.ts new file mode 100644 index 0000000..6403642 --- /dev/null +++ b/wwjcloud/src/common/notification/notification.service.ts @@ -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 = {}, + ) { + return this.smsService.send(to, templateId, params); + } +} diff --git a/wwjcloud/src/common/openapi/openapi.module.ts b/wwjcloud/src/common/openapi/openapi.module.ts new file mode 100644 index 0000000..26a16be --- /dev/null +++ b/wwjcloud/src/common/openapi/openapi.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class OpenapiModule {} diff --git a/wwjcloud/src/common/queue/queue.module.ts b/wwjcloud/src/common/queue/queue.module.ts new file mode 100644 index 0000000..ea49ead --- /dev/null +++ b/wwjcloud/src/common/queue/queue.module.ts @@ -0,0 +1,4 @@ +import { Module } from '@nestjs/common'; + +@Module({}) +export class QueueModule {} diff --git a/wwjcloud/src/common/rbac/dto/create-menu.dto.ts b/wwjcloud/src/common/rbac/dto/create-menu.dto.ts new file mode 100644 index 0000000..592f864 --- /dev/null +++ b/wwjcloud/src/common/rbac/dto/create-menu.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/dto/create-role.dto.ts b/wwjcloud/src/common/rbac/dto/create-role.dto.ts new file mode 100644 index 0000000..7543280 --- /dev/null +++ b/wwjcloud/src/common/rbac/dto/create-role.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/dto/index.ts b/wwjcloud/src/common/rbac/dto/index.ts new file mode 100644 index 0000000..6766ac4 --- /dev/null +++ b/wwjcloud/src/common/rbac/dto/index.ts @@ -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'; \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/dto/query-menu.dto.ts b/wwjcloud/src/common/rbac/dto/query-menu.dto.ts new file mode 100644 index 0000000..0075920 --- /dev/null +++ b/wwjcloud/src/common/rbac/dto/query-menu.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/dto/query-role.dto.ts b/wwjcloud/src/common/rbac/dto/query-role.dto.ts new file mode 100644 index 0000000..558628d --- /dev/null +++ b/wwjcloud/src/common/rbac/dto/query-role.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/dto/update-menu.dto.ts b/wwjcloud/src/common/rbac/dto/update-menu.dto.ts new file mode 100644 index 0000000..2ce61a6 --- /dev/null +++ b/wwjcloud/src/common/rbac/dto/update-menu.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateMenuDto } from './create-menu.dto'; + +export class UpdateMenuDto extends PartialType(CreateMenuDto) {} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/dto/update-role.dto.ts b/wwjcloud/src/common/rbac/dto/update-role.dto.ts new file mode 100644 index 0000000..3f80ce5 --- /dev/null +++ b/wwjcloud/src/common/rbac/dto/update-role.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateRoleDto } from './create-role.dto'; + +export class UpdateRoleDto extends PartialType(CreateRoleDto) {} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/entities/sys-menu.entity.ts b/wwjcloud/src/common/rbac/entities/sys-menu.entity.ts new file mode 100644 index 0000000..f4058cf --- /dev/null +++ b/wwjcloud/src/common/rbac/entities/sys-menu.entity.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/entities/sys-role.entity.ts b/wwjcloud/src/common/rbac/entities/sys-role.entity.ts new file mode 100644 index 0000000..3fa5ecc --- /dev/null +++ b/wwjcloud/src/common/rbac/entities/sys-role.entity.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/index.ts b/wwjcloud/src/common/rbac/index.ts new file mode 100644 index 0000000..ed04264 --- /dev/null +++ b/wwjcloud/src/common/rbac/index.ts @@ -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'; \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/menu.controller.ts b/wwjcloud/src/common/rbac/menu.controller.ts new file mode 100644 index 0000000..f0185d9 --- /dev/null +++ b/wwjcloud/src/common/rbac/menu.controller.ts @@ -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: '排序更新成功', + }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/menu.service.ts b/wwjcloud/src/common/rbac/menu.service.ts new file mode 100644 index 0000000..ac8e131 --- /dev/null +++ b/wwjcloud/src/common/rbac/menu.service.ts @@ -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, + ) {} + + /** + * 创建菜单 + */ + async create(createMenuDto: CreateMenuDto): Promise { + // 检查菜单标识是否已存在 + 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 { + 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 { + 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 { + const where: any = { menuKey, deleteTime: 0 }; + if (siteId !== undefined) { + where.siteId = siteId; + } + + return await this.menuRepository.findOne({ where }); + } + + /** + * 根据菜单ID数组获取菜单列表 + */ + async findByIds(menuIds: number[]): Promise { + 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 { + 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 { + 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 { + 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(`菜单 \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/rbac.module.ts b/wwjcloud/src/common/rbac/rbac.module.ts new file mode 100644 index 0000000..65f5204 --- /dev/null +++ b/wwjcloud/src/common/rbac/rbac.module.ts @@ -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 {} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/role.controller.ts b/wwjcloud/src/common/rbac/role.controller.ts new file mode 100644 index 0000000..da8fe22 --- /dev/null +++ b/wwjcloud/src/common/rbac/role.controller.ts @@ -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: '权限设置成功', + }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/rbac/role.service.ts b/wwjcloud/src/common/rbac/role.service.ts new file mode 100644 index 0000000..8e2a352 --- /dev/null +++ b/wwjcloud/src/common/rbac/role.service.ts @@ -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, + ) {} + + /** + * 创建角色 + */ + async create(createRoleDto: CreateRoleDto): Promise { + // 检查角色名称是否已存在 + 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 { + 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 { + 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 { + 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 { + const role = await this.findOne(id); + + await this.roleRepository.update(id, { + deleteTime: Math.floor(Date.now() / 1000), + }); + } + + /** + * 批量软删除角色 + */ + async removeBatch(ids: number[]): Promise { + 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 { + const role = await this.findOne(id); + + await this.roleRepository.update(id, { + status, + updateTime: Math.floor(Date.now() / 1000), + }); + } + + /** + * 获取角色的权限菜单ID列表 + */ + async getRoleMenuIds(roleId: number): Promise { + const role = await this.findOne(roleId); + return role.rules ? JSON.parse(role.rules as string) : []; + } + + /** + * 设置角色权限 + */ + async setRolePermissions(roleId: number, menuIds: number[]): Promise { + await this.roleRepository.update(roleId, { + rules: JSON.stringify(menuIds), + updateTime: Math.floor(Date.now() / 1000), + }); + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/email/email-settings.controller.ts b/wwjcloud/src/common/settings/email/email-settings.controller.ts new file mode 100644 index 0000000..437f6ca --- /dev/null +++ b/wwjcloud/src/common/settings/email/email-settings.controller.ts @@ -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 }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/email/email-settings.dto.ts b/wwjcloud/src/common/settings/email/email-settings.dto.ts new file mode 100644 index 0000000..d6410e6 --- /dev/null +++ b/wwjcloud/src/common/settings/email/email-settings.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/email/email-settings.service.ts b/wwjcloud/src/common/settings/email/email-settings.service.ts new file mode 100644 index 0000000..5f1803e --- /dev/null +++ b/wwjcloud/src/common/settings/email/email-settings.service.ts @@ -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 { + 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): Promise { + 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; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/email/email.module.ts b/wwjcloud/src/common/settings/email/email.module.ts new file mode 100644 index 0000000..a7cdd99 --- /dev/null +++ b/wwjcloud/src/common/settings/email/email.module.ts @@ -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 {} diff --git a/wwjcloud/src/common/settings/email/email.service.ts b/wwjcloud/src/common/settings/email/email.service.ts new file mode 100644 index 0000000..eda620c --- /dev/null +++ b/wwjcloud/src/common/settings/email/email.service.ts @@ -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 { + this.logger.log(`Mock send email to ${to} subject=${subject}`); + } +} diff --git a/wwjcloud/src/common/settings/index.ts b/wwjcloud/src/common/settings/index.ts new file mode 100644 index 0000000..e4d65f9 --- /dev/null +++ b/wwjcloud/src/common/settings/index.ts @@ -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'; diff --git a/wwjcloud/src/common/settings/login/login-settings.controller.ts b/wwjcloud/src/common/settings/login/login-settings.controller.ts new file mode 100644 index 0000000..257b862 --- /dev/null +++ b/wwjcloud/src/common/settings/login/login-settings.controller.ts @@ -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 }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/login/login-settings.dto.ts b/wwjcloud/src/common/settings/login/login-settings.dto.ts new file mode 100644 index 0000000..c448889 --- /dev/null +++ b/wwjcloud/src/common/settings/login/login-settings.dto.ts @@ -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; +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/login/login-settings.service.ts b/wwjcloud/src/common/settings/login/login-settings.service.ts new file mode 100644 index 0000000..0bd30db --- /dev/null +++ b/wwjcloud/src/common/settings/login/login-settings.service.ts @@ -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 { + 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): Promise { + 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; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/login/login.module.ts b/wwjcloud/src/common/settings/login/login.module.ts new file mode 100644 index 0000000..45422aa --- /dev/null +++ b/wwjcloud/src/common/settings/login/login.module.ts @@ -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 {} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/payment/payment-settings.controller.ts b/wwjcloud/src/common/settings/payment/payment-settings.controller.ts new file mode 100644 index 0000000..2acf635 --- /dev/null +++ b/wwjcloud/src/common/settings/payment/payment-settings.controller.ts @@ -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 }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/payment/payment-settings.dto.ts b/wwjcloud/src/common/settings/payment/payment-settings.dto.ts new file mode 100644 index 0000000..69c031a --- /dev/null +++ b/wwjcloud/src/common/settings/payment/payment-settings.dto.ts @@ -0,0 +1,20 @@ +import { IsBoolean, IsObject, IsOptional } from 'class-validator'; + +export class UpdatePaymentSettingsDto { + @IsBoolean() + enabled!: boolean; + + @IsOptional() + @IsObject() + alipay?: Record; + + @IsOptional() + @IsObject() + wechatpay?: Record; +} + +export interface PaymentSettingsVo { + enabled: boolean; + alipay?: Record; + wechatpay?: Record; +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/payment/payment-settings.service.ts b/wwjcloud/src/common/settings/payment/payment-settings.service.ts new file mode 100644 index 0000000..4beb50a --- /dev/null +++ b/wwjcloud/src/common/settings/payment/payment-settings.service.ts @@ -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 { + 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): Promise { + 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; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/payment/payment.module.ts b/wwjcloud/src/common/settings/payment/payment.module.ts new file mode 100644 index 0000000..072c7ff --- /dev/null +++ b/wwjcloud/src/common/settings/payment/payment.module.ts @@ -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 {} diff --git a/wwjcloud/src/common/settings/payment/payment.service.ts b/wwjcloud/src/common/settings/payment/payment.service.ts new file mode 100644 index 0000000..d863928 --- /dev/null +++ b/wwjcloud/src/common/settings/payment/payment.service.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class PaymentService { + async createPayment(orderId: string, amount: number) { + return { orderId, amount, status: 'mock' }; + } +} diff --git a/wwjcloud/src/common/settings/settings.module.ts b/wwjcloud/src/common/settings/settings.module.ts new file mode 100644 index 0000000..303c2b6 --- /dev/null +++ b/wwjcloud/src/common/settings/settings.module.ts @@ -0,0 +1,30 @@ +import { Module } from '@nestjs/common'; +import { StorageModule } from './storage/storage.module'; +import { PaymentModule } from './payment/payment.module'; +import { EmailModule } from './email/email.module'; +import { SmsModule } from './sms/sms.module'; +import { UploadSettingsModule } from './upload/upload-settings.module'; +import { LoginModule } from './login/login.module'; +import { SiteModule } from './site/site.module'; + +@Module({ + imports: [ + StorageModule, + PaymentModule, + EmailModule, + SmsModule, + UploadSettingsModule, + LoginModule, + SiteModule, + ], + exports: [ + StorageModule, + PaymentModule, + EmailModule, + SmsModule, + UploadSettingsModule, + LoginModule, + SiteModule, + ], +}) +export class SettingsModule {} diff --git a/wwjcloud/src/common/settings/site/site-settings.controller.ts b/wwjcloud/src/common/settings/site/site-settings.controller.ts new file mode 100644 index 0000000..29a0ded --- /dev/null +++ b/wwjcloud/src/common/settings/site/site-settings.controller.ts @@ -0,0 +1,50 @@ +import { + Controller, + Get, + Put, + Body, + Post, + UseGuards, + HttpCode, + HttpStatus, +} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; +import { RolesGuard } from '../../auth/guards/roles.guard'; +import { Roles } from '../../auth/roles.decorator'; +import { SiteSettingsService } from './site-settings.service'; +import { UpdateSiteSettingsDto } from './site-settings.dto'; + +@ApiTags('站点设置') +@Controller('system/settings/basic') +@UseGuards(JwtAuthGuard, RolesGuard) +@ApiBearerAuth() +export class SiteSettingsController { + constructor(private readonly siteSettingsService: SiteSettingsService) {} + + @Get() + @ApiOperation({ summary: '获取站点设置' }) + @ApiResponse({ status: 200, description: '成功获取站点设置' }) + @Roles('super', 'admin') + async getSiteSettings() { + return this.siteSettingsService.getSiteSettings(); + } + + @Put() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '更新站点设置' }) + @ApiResponse({ status: 200, description: '站点设置更新成功' }) + @Roles('super', 'admin') + async updateSiteSettings(@Body() updateSiteSettingsDto: UpdateSiteSettingsDto) { + return this.siteSettingsService.updateSiteSettings(updateSiteSettingsDto); + } + + @Post('reset') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '重置站点设置为默认值' }) + @ApiResponse({ status: 200, description: '站点设置重置成功' }) + @Roles('super') + async resetSiteSettings() { + return this.siteSettingsService.resetSiteSettings(); + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/site/site-settings.dto.ts b/wwjcloud/src/common/settings/site/site-settings.dto.ts new file mode 100644 index 0000000..b14ba55 --- /dev/null +++ b/wwjcloud/src/common/settings/site/site-settings.dto.ts @@ -0,0 +1,91 @@ +import { IsOptional, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +/** + * 更新站点设置DTO + */ +export class UpdateSiteSettingsDto { + @ApiProperty({ description: '网站名称', required: false }) + @IsOptional() + @IsString() + site_name?: string; + + @ApiProperty({ description: '网站标题', required: false }) + @IsOptional() + @IsString() + site_title?: string; + + @ApiProperty({ description: '网站关键词', required: false }) + @IsOptional() + @IsString() + site_keywords?: string; + + @ApiProperty({ description: '网站描述', required: false }) + @IsOptional() + @IsString() + site_description?: string; + + @ApiProperty({ description: '网站Logo', required: false }) + @IsOptional() + @IsString() + site_logo?: string; + + @ApiProperty({ description: '网站图标', required: false }) + @IsOptional() + @IsString() + site_favicon?: string; + + @ApiProperty({ description: 'ICP备案号', required: false }) + @IsOptional() + @IsString() + icp_number?: string; + + @ApiProperty({ description: '版权信息', required: false }) + @IsOptional() + @IsString() + copyright?: string; + + @ApiProperty({ description: '网站状态', required: false }) + @IsOptional() + site_status?: number; + + @ApiProperty({ description: '关闭原因', required: false }) + @IsOptional() + @IsString() + close_reason?: string; +} + +/** + * 站点设置响应DTO + */ +export class SiteSettingsDto { + @ApiProperty({ description: '网站名称' }) + site_name: string; + + @ApiProperty({ description: '网站标题' }) + site_title: string; + + @ApiProperty({ description: '网站关键词' }) + site_keywords: string; + + @ApiProperty({ description: '网站描述' }) + site_description: string; + + @ApiProperty({ description: '网站Logo' }) + site_logo: string; + + @ApiProperty({ description: '网站图标' }) + site_favicon: string; + + @ApiProperty({ description: 'ICP备案号' }) + icp_number: string; + + @ApiProperty({ description: '版权信息' }) + copyright: string; + + @ApiProperty({ description: '网站状态' }) + site_status: number; + + @ApiProperty({ description: '关闭原因' }) + close_reason: string; +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/site/site-settings.service.ts b/wwjcloud/src/common/settings/site/site-settings.service.ts new file mode 100644 index 0000000..f276085 --- /dev/null +++ b/wwjcloud/src/common/settings/site/site-settings.service.ts @@ -0,0 +1,133 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Site } from './site.entity'; +import { UpdateSiteSettingsDto } from './site-settings.dto'; + +@Injectable() +export class SiteSettingsService { + constructor( + @InjectRepository(Site) + private readonly siteRepository: Repository, + ) {} + + /** + * 获取站点设置 + */ + async getSiteSettings() { + // 获取默认站点(id = 1) + const site = await this.siteRepository.findOne({ + where: { id: 1 }, + }); + + if (!site) { + // 如果没有找到站点,返回默认值 + return { + site_name: 'WWJ Cloud', + site_title: 'WWJ Cloud 企业级框架', + site_keywords: 'WWJ Cloud,企业级框架,NestJS,VbenAdmin', + site_description: 'WWJ Cloud 企业级框架 - 快速开发SAAS多用户系统后台管理框架', + site_logo: '', + site_favicon: '', + icp_number: '', + copyright: '', + site_status: 1, + close_reason: '', + }; + } + + return { + site_name: site.site_name || '', + site_title: site.site_title || '', + site_keywords: site.site_keywords || '', + site_description: site.site_description || '', + site_logo: site.site_logo || '', + site_favicon: site.site_favicon || '', + icp_number: site.icp_number || '', + copyright: site.copyright || '', + site_status: site.site_status || 1, + close_reason: site.close_reason || '', + }; + } + + /** + * 更新站点设置 + */ + async updateSiteSettings(updateSiteSettingsDto: UpdateSiteSettingsDto) { + const { + site_name, + site_title, + site_keywords, + site_description, + site_logo, + site_favicon, + icp_number, + copyright, + site_status, + close_reason, + } = updateSiteSettingsDto; + + // 查找或创建默认站点 + let site = await this.siteRepository.findOne({ + where: { id: 1 }, + }); + + if (!site) { + // 创建默认站点 + site = this.siteRepository.create({ + id: 1, + site_name: site_name || 'WWJ Cloud', + site_title: site_title || 'WWJ Cloud 企业级框架', + site_keywords: site_keywords || '', + site_description: site_description || '', + site_logo: site_logo || '', + site_favicon: site_favicon || '', + icp_number: icp_number || '', + copyright: copyright || '', + site_status: site_status || 1, + close_reason: close_reason || '', + }); + } else { + // 更新现有站点 + if (site_name !== undefined) site.site_name = site_name; + if (site_title !== undefined) site.site_title = site_title; + if (site_keywords !== undefined) site.site_keywords = site_keywords; + if (site_description !== undefined) site.site_description = site_description; + if (site_logo !== undefined) site.site_logo = site_logo; + if (site_favicon !== undefined) site.site_favicon = site_favicon; + if (icp_number !== undefined) site.icp_number = icp_number; + if (copyright !== undefined) site.copyright = copyright; + if (site_status !== undefined) site.site_status = site_status; + if (close_reason !== undefined) site.close_reason = close_reason; + } + + await this.siteRepository.save(site); + return { message: '站点设置更新成功' }; + } + + /** + * 重置站点设置为默认值 + */ + async resetSiteSettings() { + // 删除现有站点配置 + await this.siteRepository.delete({ id: 1 }); + + // 创建默认站点配置 + const defaultSite = this.siteRepository.create({ + id: 1, + site_name: 'WWJ Cloud', + site_title: 'WWJ Cloud 企业级框架', + site_keywords: 'WWJ Cloud,企业级框架,NestJS,VbenAdmin', + site_description: 'WWJ Cloud 企业级框架 - 快速开发SAAS多用户系统后台管理框架', + site_logo: '', + site_favicon: '', + icp_number: '', + copyright: '', + site_status: 1, + close_reason: '', + }); + + await this.siteRepository.save(defaultSite); + return { message: '站点设置已重置为默认值' }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/site/site.entity.ts b/wwjcloud/src/common/settings/site/site.entity.ts new file mode 100644 index 0000000..bf947ea --- /dev/null +++ b/wwjcloud/src/common/settings/site/site.entity.ts @@ -0,0 +1,41 @@ +import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; + +/** + * 站点信息实体 + * 对应数据库表:site + */ +@Entity('site') +export class Site { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'varchar', length: 100, comment: '网站名称' }) + site_name: string; + + @Column({ type: 'varchar', length: 255, comment: '网站标题' }) + site_title: string; + + @Column({ type: 'varchar', length: 255, comment: '网站关键词' }) + site_keywords: string; + + @Column({ type: 'text', comment: '网站描述' }) + site_description: string; + + @Column({ type: 'varchar', length: 255, comment: '网站Logo' }) + site_logo: string; + + @Column({ type: 'varchar', length: 255, comment: '网站图标' }) + site_favicon: string; + + @Column({ type: 'varchar', length: 50, comment: 'ICP备案号' }) + icp_number: string; + + @Column({ type: 'varchar', length: 255, comment: '版权信息' }) + copyright: string; + + @Column({ type: 'tinyint', default: 1, comment: '网站状态 1:开启 0:关闭' }) + site_status: number; + + @Column({ type: 'varchar', length: 255, comment: '关闭原因' }) + close_reason: string; +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/site/site.module.ts b/wwjcloud/src/common/settings/site/site.module.ts new file mode 100644 index 0000000..849f17a --- /dev/null +++ b/wwjcloud/src/common/settings/site/site.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SiteSettingsController } from './site-settings.controller'; +import { SiteSettingsService } from './site-settings.service'; +import { Site } from './site.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Site])], + controllers: [SiteSettingsController], + providers: [SiteSettingsService], + exports: [SiteSettingsService], +}) +export class SiteModule {} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/sms/sms-settings.controller.ts b/wwjcloud/src/common/settings/sms/sms-settings.controller.ts new file mode 100644 index 0000000..6b9ce94 --- /dev/null +++ b/wwjcloud/src/common/settings/sms/sms-settings.controller.ts @@ -0,0 +1,30 @@ +import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { SmsSettingsService } from './sms-settings.service'; +import { UpdateSmsSettingsDto, type SmsSettingsVo } from './sms-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/Sms') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('super', 'admin') +@Controller('settings/sms') +export class SmsSettingsController { + constructor(private readonly service: SmsSettingsService) {} + + @Get() + @ApiOperation({ summary: '获取短信设置' }) + async get(): Promise<{ code: number; data: SmsSettingsVo }> { + const data = await this.service.getSettings(); + return { code: 0, data }; + } + + @Put() + @ApiOperation({ summary: '更新短信设置' }) + async update(@Body() dto: UpdateSmsSettingsDto): Promise<{ code: number; data: SmsSettingsVo }> { + const data = await this.service.updateSettings(dto); + return { code: 0, data }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/sms/sms-settings.dto.ts b/wwjcloud/src/common/settings/sms/sms-settings.dto.ts new file mode 100644 index 0000000..17a80e6 --- /dev/null +++ b/wwjcloud/src/common/settings/sms/sms-settings.dto.ts @@ -0,0 +1,39 @@ +import { IsBoolean, IsIn, IsObject, IsOptional, IsString } from 'class-validator'; + +export class UpdateSmsSettingsDto { + @IsBoolean() + enabled!: boolean; + + @IsIn(['mock', 'aliyun', 'tencent']) + provider!: 'mock' | 'aliyun' | 'tencent'; + + @IsOptional() + @IsString() + signName?: string; + + @IsOptional() + @IsString() + accessKeyId?: string; + + @IsOptional() + @IsString() + accessKeySecret?: string; + + @IsOptional() + @IsString() + region?: string; + + @IsOptional() + @IsObject() + templates?: Record; +} + +export interface SmsSettingsVo { + enabled: boolean; + provider: 'mock' | 'aliyun' | 'tencent'; + signName?: string; + accessKeyId?: string; + accessKeySecret?: string; + region?: string; + templates?: Record; +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/sms/sms-settings.service.ts b/wwjcloud/src/common/settings/sms/sms-settings.service.ts new file mode 100644 index 0000000..ee3a76d --- /dev/null +++ b/wwjcloud/src/common/settings/sms/sms-settings.service.ts @@ -0,0 +1,42 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { Injectable } from '@nestjs/common'; +import type { SmsSettingsVo } from './sms-settings.dto'; + +const SETTINGS_DIR = path.resolve(process.cwd(), 'config', 'runtime'); +const SETTINGS_FILE = path.resolve(SETTINGS_DIR, 'sms.settings.json'); + +const DEFAULT_SETTINGS: SmsSettingsVo = { + enabled: false, + provider: 'mock', + signName: '', + accessKeyId: '', + accessKeySecret: '', + region: 'cn-hangzhou', + templates: {}, +}; + +@Injectable() +export class SmsSettingsService { + async getSettings(): Promise { + 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): Promise { + const current = await this.getSettings(); + const next: SmsSettingsVo = { ...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; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/sms/sms.module.ts b/wwjcloud/src/common/settings/sms/sms.module.ts new file mode 100644 index 0000000..a624a2f --- /dev/null +++ b/wwjcloud/src/common/settings/sms/sms.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { SmsService } from './sms.service'; +import { SmsSettingsService } from './sms-settings.service'; +import { SmsSettingsController } from './sms-settings.controller'; + +@Module({ + providers: [SmsService, SmsSettingsService], + controllers: [SmsSettingsController], + exports: [SmsService, SmsSettingsService], +}) +export class SmsModule {} diff --git a/wwjcloud/src/common/settings/sms/sms.service.ts b/wwjcloud/src/common/settings/sms/sms.service.ts new file mode 100644 index 0000000..28e176b --- /dev/null +++ b/wwjcloud/src/common/settings/sms/sms.service.ts @@ -0,0 +1,16 @@ +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export class SmsService { + private readonly logger = new Logger(SmsService.name); + + async send( + to: string, + templateId: string, + params: Record = {}, + ): Promise { + this.logger.log( + `Mock send sms to ${to} templateId=${templateId} params=${JSON.stringify(params)}`, + ); + } +} diff --git a/wwjcloud/src/common/settings/storage/storage-settings.controller.ts b/wwjcloud/src/common/settings/storage/storage-settings.controller.ts new file mode 100644 index 0000000..79e0e13 --- /dev/null +++ b/wwjcloud/src/common/settings/storage/storage-settings.controller.ts @@ -0,0 +1,32 @@ +import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { StorageSettingsService } from './storage-settings.service'; +import { UpdateStorageSettingsDto, type StorageSettingsVo } from './storage-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/Storage') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('super', 'admin') +@Controller('settings/storage') +export class StorageSettingsController { + constructor(private readonly service: StorageSettingsService) {} + + @Get() + @ApiOperation({ summary: '获取存储设置' }) + async get(): Promise<{ code: number; data: StorageSettingsVo }> { + const data = await this.service.getSettings(); + return { code: 0, data }; + } + + @Put() + @ApiOperation({ summary: '更新存储设置' }) + async update( + @Body() dto: UpdateStorageSettingsDto, + ): Promise<{ code: number; data: StorageSettingsVo }> { + const data = await this.service.updateSettings(dto); + return { code: 0, data }; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/storage/storage-settings.dto.ts b/wwjcloud/src/common/settings/storage/storage-settings.dto.ts new file mode 100644 index 0000000..2e7cf8c --- /dev/null +++ b/wwjcloud/src/common/settings/storage/storage-settings.dto.ts @@ -0,0 +1,54 @@ +import { IsBoolean, IsIn, IsOptional, IsString } from 'class-validator'; + +export class UpdateStorageSettingsDto { + @IsBoolean() + enabled!: boolean; + + @IsIn(['local', 'aliyun', 'tencent', 'qiniu', 's3', 'minio']) + provider!: 'local' | 'aliyun' | 'tencent' | 'qiniu' | 's3' | 'minio'; + + @IsOptional() + @IsString() + accessKeyId?: string; + + @IsOptional() + @IsString() + accessKeySecret?: string; + + @IsOptional() + @IsString() + bucket?: string; + + @IsOptional() + @IsString() + region?: string; + + @IsOptional() + @IsString() + endpoint?: string; + + @IsOptional() + @IsString() + domain?: string; + + @IsOptional() + @IsString() + folder?: string; + + @IsOptional() + @IsBoolean() + isPrivate?: boolean; +} + +export interface StorageSettingsVo { + enabled: boolean; + provider: 'local' | 'aliyun' | 'tencent' | 'qiniu' | 's3' | 'minio'; + accessKeyId?: string; + accessKeySecret?: string; + bucket?: string; + region?: string; + endpoint?: string; + domain?: string; + folder?: string; + isPrivate?: boolean; +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/storage/storage-settings.service.ts b/wwjcloud/src/common/settings/storage/storage-settings.service.ts new file mode 100644 index 0000000..a9ad095 --- /dev/null +++ b/wwjcloud/src/common/settings/storage/storage-settings.service.ts @@ -0,0 +1,51 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { Injectable } from '@nestjs/common'; +import type { StorageSettingsVo } from './storage-settings.dto'; + +const SETTINGS_DIR = path.resolve(process.cwd(), 'config', 'runtime'); +const SETTINGS_FILE = path.resolve(SETTINGS_DIR, 'storage.settings.json'); + +const DEFAULT_SETTINGS: StorageSettingsVo = { + enabled: false, + provider: 'local', + accessKeyId: '', + accessKeySecret: '', + bucket: '', + region: '', + endpoint: '', + domain: '', + folder: '', + isPrivate: false, +}; + +@Injectable() +export class StorageSettingsService { + async getSettings(): Promise { + 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, + ): Promise { + const current = await this.getSettings(); + const next: StorageSettingsVo = { ...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; + } +} \ No newline at end of file diff --git a/wwjcloud/src/common/settings/storage/storage.controller.ts b/wwjcloud/src/common/settings/storage/storage.controller.ts new file mode 100644 index 0000000..8cb1435 --- /dev/null +++ b/wwjcloud/src/common/settings/storage/storage.controller.ts @@ -0,0 +1,9 @@ +import { Controller, Get } from '@nestjs/common'; + +@Controller('storage') +export class StorageController { + @Get('health') + health() { + return { ok: true }; + } +} diff --git a/wwjcloud/src/common/settings/storage/storage.module.ts b/wwjcloud/src/common/settings/storage/storage.module.ts new file mode 100644 index 0000000..10dc3b4 --- /dev/null +++ b/wwjcloud/src/common/settings/storage/storage.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { StorageService } from './storage.service'; +import { StorageController } from './storage.controller'; +import { StorageSettingsService } from './storage-settings.service'; +import { StorageSettingsController } from './storage-settings.controller'; + +@Module({ + providers: [StorageService, StorageSettingsService], + exports: [StorageService, StorageSettingsService], + controllers: [StorageController, StorageSettingsController], +}) +export class StorageModule {} diff --git a/wwjcloud/src/common/settings/storage/storage.service.ts b/wwjcloud/src/common/settings/storage/storage.service.ts new file mode 100644 index 0000000..9b352ca --- /dev/null +++ b/wwjcloud/src/common/settings/storage/storage.service.ts @@ -0,0 +1,12 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class StorageService { + async saveObject( + key: string, + data: Buffer | string, + ): Promise<{ key: string }> { + // mock save + return { key }; + } +} diff --git a/wwjcloud/src/common/settings/upload/upload-settings.controller.ts b/wwjcloud/src/common/settings/upload/upload-settings.controller.ts new file mode 100644 index 0000000..5153e36 --- /dev/null +++ b/wwjcloud/src/common/settings/upload/upload-settings.controller.ts @@ -0,0 +1,35 @@ +import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { UploadSettingsService } from './upload-settings.service'; +import { + UpdateUploadSettingsDto, + type UploadSettingsVo, +} from './upload-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/Upload') +@ApiBearerAuth() +@UseGuards(JwtAuthGuard, RolesGuard) +@Roles('super', 'admin') +@Controller('settings/upload') +export class UploadSettingsController { + constructor(private readonly service: UploadSettingsService) {} + + @Get() + @ApiOperation({ summary: '获取上传设置' }) + async get(): Promise<{ code: number; data: UploadSettingsVo }> { + const data = await this.service.getSettings(); + return { code: 0, data }; + } + + @Put() + @ApiOperation({ summary: '更新上传设置' }) + async update( + @Body() dto: UpdateUploadSettingsDto, + ): Promise<{ code: number; data: UploadSettingsVo }> { + const data = await this.service.updateSettings(dto); + return { code: 0, data }; + } +} diff --git a/wwjcloud/src/common/settings/upload/upload-settings.dto.ts b/wwjcloud/src/common/settings/upload/upload-settings.dto.ts new file mode 100644 index 0000000..3eeb140 --- /dev/null +++ b/wwjcloud/src/common/settings/upload/upload-settings.dto.ts @@ -0,0 +1,25 @@ +import { IsArray, IsIn, IsInt, Max, Min } from 'class-validator'; + +export class UpdateUploadSettingsDto { + @IsInt() + @Min(1) + @Max(50) // 受 fastify-multipart 当前上限约束(main.ts: fileSize=50MB),后续可提升 + maxFileSizeMB!: number; + + @IsInt() + @Min(1) + @Max(20) + maxFiles!: number; + + @IsArray() + @IsIn(['image', 'video', 'audio', 'document', 'archive', 'other'], { + each: true, + }) + allowedTypes!: string[]; +} + +export interface UploadSettingsVo { + maxFileSizeMB: number; + maxFiles: number; + allowedTypes: string[]; +} diff --git a/wwjcloud/src/common/settings/upload/upload-settings.module.ts b/wwjcloud/src/common/settings/upload/upload-settings.module.ts new file mode 100644 index 0000000..4c3f62a --- /dev/null +++ b/wwjcloud/src/common/settings/upload/upload-settings.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { UploadSettingsService } from './upload-settings.service'; +import { UploadSettingsController } from './upload-settings.controller'; + +@Module({ + providers: [UploadSettingsService], + controllers: [UploadSettingsController], + exports: [UploadSettingsService], +}) +export class UploadSettingsModule {} diff --git a/wwjcloud/src/common/settings/upload/upload-settings.service.ts b/wwjcloud/src/common/settings/upload/upload-settings.service.ts new file mode 100644 index 0000000..835e23a --- /dev/null +++ b/wwjcloud/src/common/settings/upload/upload-settings.service.ts @@ -0,0 +1,44 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { Injectable } from '@nestjs/common'; +import type { UploadSettingsVo } from './upload-settings.dto'; + +const SETTINGS_DIR = path.resolve(process.cwd(), 'config', 'runtime'); +const SETTINGS_FILE = path.resolve(SETTINGS_DIR, 'upload.settings.json'); + +const DEFAULT_SETTINGS: UploadSettingsVo = { + maxFileSizeMB: 50, + maxFiles: 10, + allowedTypes: ['image', 'video', 'audio', 'document', 'archive', 'other'], +}; + +@Injectable() +export class UploadSettingsService { + async getSettings(): Promise { + 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, + ): Promise { + const current = await this.getSettings(); + const next: UploadSettingsVo = { ...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; + } +} diff --git a/wwjcloud/src/config/cache/index.ts b/wwjcloud/src/config/cache/index.ts new file mode 100644 index 0000000..be7ed04 --- /dev/null +++ b/wwjcloud/src/config/cache/index.ts @@ -0,0 +1,6 @@ +/** Cache config skeleton */ +export const cacheConfig = () => ({ + host: process.env.REDIS_HOST || 'localhost', + port: Number(process.env.REDIS_PORT || 6379), + password: process.env.REDIS_PASSWORD || '', +}); diff --git a/wwjcloud/src/config/database/index.ts b/wwjcloud/src/config/database/index.ts new file mode 100644 index 0000000..83bee01 --- /dev/null +++ b/wwjcloud/src/config/database/index.ts @@ -0,0 +1,8 @@ +/** Database config skeleton */ +export const databaseConfig = () => ({ + host: process.env.DB_HOST || 'localhost', + port: Number(process.env.DB_PORT || 3306), + username: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_DATABASE || 'wwjcloud', +}); diff --git a/wwjcloud/src/config/env/index.ts b/wwjcloud/src/config/env/index.ts new file mode 100644 index 0000000..fe33af1 --- /dev/null +++ b/wwjcloud/src/config/env/index.ts @@ -0,0 +1,6 @@ +/** + * Environment variables typing or helpers + */ +export const envConfig = () => ({ + nodeEnv: process.env.NODE_ENV || 'development', +}); diff --git a/wwjcloud/src/config/http/index.ts b/wwjcloud/src/config/http/index.ts new file mode 100644 index 0000000..f26872d --- /dev/null +++ b/wwjcloud/src/config/http/index.ts @@ -0,0 +1,5 @@ +/** HTTP client/server config skeleton */ +export const httpConfig = () => ({ + timeout: Number(process.env.HTTP_TIMEOUT || 5000), + retry: Number(process.env.HTTP_RETRY || 2), +}); diff --git a/wwjcloud/src/config/index.ts b/wwjcloud/src/config/index.ts new file mode 100644 index 0000000..644a906 --- /dev/null +++ b/wwjcloud/src/config/index.ts @@ -0,0 +1,28 @@ +export default () => ({ + nodeEnv: process.env.NODE_ENV || 'development', + port: parseInt(process.env.PORT || '3000', 10), + db: { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '3306', 10), + username: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_DATABASE || 'wwjcloud', + }, + redis: { + host: process.env.REDIS_HOST || 'localhost', + port: parseInt(process.env.REDIS_PORT || '6379', 10), + password: process.env.REDIS_PASSWORD || '', + }, + jwt: { + secret: process.env.JWT_SECRET || 'change_me', + expiresIn: process.env.JWT_EXPIRES_IN || '7d', + }, + uploadPath: process.env.UPLOAD_PATH || 'public/upload', + storageProvider: process.env.STORAGE_PROVIDER || 'local', + paymentProvider: process.env.PAYMENT_PROVIDER || 'mock', + logLevel: process.env.LOG_LEVEL || 'info', + throttle: { + ttl: parseInt(process.env.THROTTLE_TTL || '60', 10), + limit: parseInt(process.env.THROTTLE_LIMIT || '100', 10), + }, +}); diff --git a/wwjcloud/src/config/logger/index.ts b/wwjcloud/src/config/logger/index.ts new file mode 100644 index 0000000..3504589 --- /dev/null +++ b/wwjcloud/src/config/logger/index.ts @@ -0,0 +1,4 @@ +/** Logger config skeleton */ +export const loggerConfig = () => ({ + level: process.env.LOG_LEVEL || 'info', +}); diff --git a/wwjcloud/src/config/queue/index.ts b/wwjcloud/src/config/queue/index.ts new file mode 100644 index 0000000..daee6bf --- /dev/null +++ b/wwjcloud/src/config/queue/index.ts @@ -0,0 +1,4 @@ +/** Queue config skeleton */ +export const queueConfig = () => ({ + driver: process.env.QUEUE_DRIVER || 'bull', +}); diff --git a/wwjcloud/src/config/security/index.ts b/wwjcloud/src/config/security/index.ts new file mode 100644 index 0000000..db2efa2 --- /dev/null +++ b/wwjcloud/src/config/security/index.ts @@ -0,0 +1,7 @@ +/** Security config skeleton */ +export const securityConfig = () => ({ + jwt: { + secret: process.env.JWT_SECRET || 'change_me', + expiresIn: process.env.JWT_EXPIRES_IN || '7d', + }, +}); diff --git a/wwjcloud/src/config/third-party/index.ts b/wwjcloud/src/config/third-party/index.ts new file mode 100644 index 0000000..218a41f --- /dev/null +++ b/wwjcloud/src/config/third-party/index.ts @@ -0,0 +1,5 @@ +/** Third-party integrations config skeleton */ +export const thirdPartyConfig = () => ({ + storageProvider: process.env.STORAGE_PROVIDER || 'local', + paymentProvider: process.env.PAYMENT_PROVIDER || 'mock', +}); diff --git a/wwjcloud/src/config/typeorm.config.ts b/wwjcloud/src/config/typeorm.config.ts new file mode 100644 index 0000000..15de0f8 --- /dev/null +++ b/wwjcloud/src/config/typeorm.config.ts @@ -0,0 +1,28 @@ +import * as path from 'path'; +import 'dotenv/config'; +import { DataSource } from 'typeorm'; + +// TypeORM CLI DataSource configuration +// - Used by npm scripts: migration:run / migration:revert / migration:generate +// - Keep synchronize=false, manage schema only via migrations +const AppDataSource = new DataSource({ + type: 'mysql', + host: process.env.DB_HOST || 'localhost', + port: Number(process.env.DB_PORT) || 3306, + username: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_DATABASE || 'wwjcloud', + synchronize: false, + logging: false, + // Use ts-node runtime for dev and compiled js for prod compatibility + entities: [ + path.join(process.cwd(), 'src', '**', '*.entity.ts'), + path.join(process.cwd(), 'dist', '**', '*.entity.js'), + ], + migrations: [ + path.join(process.cwd(), 'src', 'migrations', '*.{ts,js}'), + path.join(process.cwd(), 'dist', 'migrations', '*.js'), + ], +}); + +export default AppDataSource; \ No newline at end of file diff --git a/wwjcloud/src/core/cache/cache.module.ts b/wwjcloud/src/core/cache/cache.module.ts new file mode 100644 index 0000000..11f446f --- /dev/null +++ b/wwjcloud/src/core/cache/cache.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class CacheModule {} diff --git a/wwjcloud/src/core/cache/ports/cache.port.ts b/wwjcloud/src/core/cache/ports/cache.port.ts new file mode 100644 index 0000000..7a438f6 --- /dev/null +++ b/wwjcloud/src/core/cache/ports/cache.port.ts @@ -0,0 +1,5 @@ +export interface CachePort { + get(key: string): Promise; + set(key: string, value: T, ttlSeconds?: number): Promise; + del(key: string): Promise; +} diff --git a/wwjcloud/src/core/config/config.module.ts b/wwjcloud/src/core/config/config.module.ts new file mode 100644 index 0000000..313faaa --- /dev/null +++ b/wwjcloud/src/core/config/config.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class ConfigCoreModule {} diff --git a/wwjcloud/src/core/config/schemas/index.ts b/wwjcloud/src/core/config/schemas/index.ts new file mode 100644 index 0000000..c4b4997 --- /dev/null +++ b/wwjcloud/src/core/config/schemas/index.ts @@ -0,0 +1,2 @@ +// Config schemas placeholder +export {}; diff --git a/wwjcloud/src/core/context/cls.module.ts b/wwjcloud/src/core/context/cls.module.ts new file mode 100644 index 0000000..3a2b10a --- /dev/null +++ b/wwjcloud/src/core/context/cls.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class ClsCoreModule {} diff --git a/wwjcloud/src/core/database/base.entity.ts b/wwjcloud/src/core/database/base.entity.ts new file mode 100644 index 0000000..746b211 --- /dev/null +++ b/wwjcloud/src/core/database/base.entity.ts @@ -0,0 +1,6 @@ +// Base entity skeleton (no ORM dependency) +export class BaseEntity { + id?: string; + createdAt?: Date; + updatedAt?: Date; +} diff --git a/wwjcloud/src/core/database/base.repository.ts b/wwjcloud/src/core/database/base.repository.ts new file mode 100644 index 0000000..8644bf1 --- /dev/null +++ b/wwjcloud/src/core/database/base.repository.ts @@ -0,0 +1,7 @@ +// Base repository abstraction (no ORM dependency) +export abstract class BaseRepository { + abstract findById(id: string): Promise; + abstract create(data: Partial): Promise; + abstract update(id: string, data: Partial): Promise; + abstract delete(id: string): Promise; +} diff --git a/wwjcloud/src/core/database/database.module.ts b/wwjcloud/src/core/database/database.module.ts new file mode 100644 index 0000000..a5cf8a8 --- /dev/null +++ b/wwjcloud/src/core/database/database.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class DatabaseModule {} diff --git a/wwjcloud/src/core/database/transformers/boolean-number.transformer.ts b/wwjcloud/src/core/database/transformers/boolean-number.transformer.ts new file mode 100644 index 0000000..a289ca9 --- /dev/null +++ b/wwjcloud/src/core/database/transformers/boolean-number.transformer.ts @@ -0,0 +1,13 @@ +import type { ValueTransformer } from 'typeorm'; + +// Maps boolean <-> tinyint(1) number (0/1) +export class BooleanNumberTransformer implements ValueTransformer { + to(value?: boolean | null): number | null { + if (value === null || value === undefined) return null; + return value ? 1 : 0; + } + from(value: number | null): boolean | null { + if (value === null || value === undefined) return null; + return Number(value) === 1; + } +} diff --git a/wwjcloud/src/core/database/transformers/index.ts b/wwjcloud/src/core/database/transformers/index.ts new file mode 100644 index 0000000..5741c3c --- /dev/null +++ b/wwjcloud/src/core/database/transformers/index.ts @@ -0,0 +1,3 @@ +export * from './boolean-number.transformer'; +export * from './status-string.transformer'; +export * from './status-number.transformer'; diff --git a/wwjcloud/src/core/database/transformers/status-number.transformer.ts b/wwjcloud/src/core/database/transformers/status-number.transformer.ts new file mode 100644 index 0000000..427c847 --- /dev/null +++ b/wwjcloud/src/core/database/transformers/status-number.transformer.ts @@ -0,0 +1,15 @@ +import type { ValueTransformer } from 'typeorm'; +import { Status } from '../../enums'; + +// Maps tinyint number (0/1/2...) <-> Status enum (0/1) +export class StatusNumberTransformer implements ValueTransformer { + to(value?: Status | null): number | null { + if (value === null || value === undefined) return null; + return Number(value); + } + from(value: number | null): Status | null { + if (value === null || value === undefined) return null; + const n = Number(value); + return n === Status.Enabled ? Status.Enabled : Status.Disabled; + } +} diff --git a/wwjcloud/src/core/database/transformers/status-string.transformer.ts b/wwjcloud/src/core/database/transformers/status-string.transformer.ts new file mode 100644 index 0000000..3ce9508 --- /dev/null +++ b/wwjcloud/src/core/database/transformers/status-string.transformer.ts @@ -0,0 +1,14 @@ +import type { ValueTransformer } from 'typeorm'; +import { Status, StatusNameMap } from '../../enums'; + +// Maps DB enum('enabled'|'disabled') <-> Status (1/0) +export class StatusStringTransformer implements ValueTransformer { + to(value?: Status | null): 'enabled' | 'disabled' | null { + if (value === null || value === undefined) return null; + return StatusNameMap[value]; + } + from(value: 'enabled' | 'disabled' | null): Status | null { + if (value === null || value === undefined) return null; + return value === 'enabled' ? Status.Enabled : Status.Disabled; + } +} diff --git a/wwjcloud/src/core/enums/index.ts b/wwjcloud/src/core/enums/index.ts new file mode 100644 index 0000000..95b58ab --- /dev/null +++ b/wwjcloud/src/core/enums/index.ts @@ -0,0 +1 @@ +export * from './status.enum'; diff --git a/wwjcloud/src/core/enums/status.enum.ts b/wwjcloud/src/core/enums/status.enum.ts new file mode 100644 index 0000000..e47c8d6 --- /dev/null +++ b/wwjcloud/src/core/enums/status.enum.ts @@ -0,0 +1,21 @@ +// Core-level canonical status enum to be shared across layers +export enum Status { + Disabled = 0, + Enabled = 1, +} + +export type StatusName = 'disabled' | 'enabled'; + +export const StatusNameMap: Record = { + [Status.Disabled]: 'disabled', + [Status.Enabled]: 'enabled', +}; + +export function isEnabledStatus( + input: Status | StatusName | number | string | null | undefined, +): boolean { + if (input === null || input === undefined) return false; + if (typeof input === 'number') return Number(input) === Status.Enabled; + if (typeof input === 'string') return input === 'enabled' || input === '1'; + return input === Status.Enabled; +} diff --git a/wwjcloud/src/core/exception/filters/http-exception.filter.ts b/wwjcloud/src/core/exception/filters/http-exception.filter.ts new file mode 100644 index 0000000..eeef2e9 --- /dev/null +++ b/wwjcloud/src/core/exception/filters/http-exception.filter.ts @@ -0,0 +1,30 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, +} from '@nestjs/common'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + const message = + exception instanceof HttpException + ? exception.getResponse() + : 'Internal Server Error'; + + response.status?.(status).json?.({ + statusCode: status, + message, + timestamp: new Date().toISOString(), + }); + } +} diff --git a/wwjcloud/src/core/http/http.module.ts b/wwjcloud/src/core/http/http.module.ts new file mode 100644 index 0000000..23cbbad --- /dev/null +++ b/wwjcloud/src/core/http/http.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class HttpCoreModule {} diff --git a/wwjcloud/src/core/interceptor/logging.interceptor.ts b/wwjcloud/src/core/interceptor/logging.interceptor.ts new file mode 100644 index 0000000..bca9c04 --- /dev/null +++ b/wwjcloud/src/core/interceptor/logging.interceptor.ts @@ -0,0 +1,14 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(_context: ExecutionContext, next: CallHandler): any { + // TODO: add real logging + return next.handle(); + } +} diff --git a/wwjcloud/src/core/interceptor/transform.interceptor.ts b/wwjcloud/src/core/interceptor/transform.interceptor.ts new file mode 100644 index 0000000..aa3219d --- /dev/null +++ b/wwjcloud/src/core/interceptor/transform.interceptor.ts @@ -0,0 +1,14 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; + +@Injectable() +export class TransformInterceptor implements NestInterceptor { + intercept(_context: ExecutionContext, next: CallHandler): any { + // TODO: add response wrapping + return next.handle(); + } +} diff --git a/wwjcloud/src/core/logger/logger.module.ts b/wwjcloud/src/core/logger/logger.module.ts new file mode 100644 index 0000000..73312d3 --- /dev/null +++ b/wwjcloud/src/core/logger/logger.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class LoggerModule {} diff --git a/wwjcloud/src/core/queue/ports/queue.port.ts b/wwjcloud/src/core/queue/ports/queue.port.ts new file mode 100644 index 0000000..08e9a4f --- /dev/null +++ b/wwjcloud/src/core/queue/ports/queue.port.ts @@ -0,0 +1,11 @@ +export interface QueuePort { + enqueue( + queue: string, + payload: T, + opts?: { delay?: number; attempts?: number }, + ): Promise; + process( + queue: string, + handler: (payload: any) => Promise, + ): Promise; +} diff --git a/wwjcloud/src/core/queue/queue.module.ts b/wwjcloud/src/core/queue/queue.module.ts new file mode 100644 index 0000000..c5a9874 --- /dev/null +++ b/wwjcloud/src/core/queue/queue.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class QueueModule {} diff --git a/wwjcloud/src/core/security/guards/index.ts b/wwjcloud/src/core/security/guards/index.ts new file mode 100644 index 0000000..6d04664 --- /dev/null +++ b/wwjcloud/src/core/security/guards/index.ts @@ -0,0 +1,2 @@ +// security guards placeholder +export {}; diff --git a/wwjcloud/src/core/security/security.module.ts b/wwjcloud/src/core/security/security.module.ts new file mode 100644 index 0000000..dc29a1a --- /dev/null +++ b/wwjcloud/src/core/security/security.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class SecurityModule {} diff --git a/wwjcloud/src/core/security/strategies/index.ts b/wwjcloud/src/core/security/strategies/index.ts new file mode 100644 index 0000000..ed220f9 --- /dev/null +++ b/wwjcloud/src/core/security/strategies/index.ts @@ -0,0 +1,2 @@ +// security strategies placeholder +export {}; diff --git a/wwjcloud/src/core/validation/pipes/index.ts b/wwjcloud/src/core/validation/pipes/index.ts new file mode 100644 index 0000000..70cb243 --- /dev/null +++ b/wwjcloud/src/core/validation/pipes/index.ts @@ -0,0 +1,2 @@ +// validation pipes placeholder +export {}; diff --git a/wwjcloud/src/main.ts b/wwjcloud/src/main.ts new file mode 100644 index 0000000..cdaaa18 --- /dev/null +++ b/wwjcloud/src/main.ts @@ -0,0 +1,58 @@ +import 'dotenv/config'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { + FastifyAdapter, + NestFastifyApplication, +} from '@nestjs/platform-fastify'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import multipart from '@fastify/multipart'; + +async function bootstrap() { + const app = await NestFactory.create( + AppModule, + new FastifyAdapter({ logger: false }), + { bufferLogs: true }, + ); + + // 注册 multipart 支持(类型兼容:cast any 规避 fastify 多版本类型冲突) + await (app as any).register(multipart as any, { + limits: { + fieldNameSize: 100, + fieldSize: 1024 * 1024, // 1MB + fields: 10, + fileSize: 1024 * 1024 * 50, // 50MB 单文件 + files: 10, + headerPairs: 2000, + }, + }); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); + + app.enableCors(); + + app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); + + const config = new DocumentBuilder() + .setTitle('WWJCloud API') + .setDescription('WWJCloud 基于 NestJS 的企业级后端 API 文档') + .setVersion('1.0.0') + .addBearerAuth() + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + + const port = + Number(process.env.PORT) || + (process.env.NODE_ENV === 'development' ? 3001 : 3000); + await app.listen({ port, host: '0.0.0.0' }); +} +bootstrap(); diff --git a/wwjcloud/src/migrations/.gitkeep b/wwjcloud/src/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/wwjcloud/src/migrations/1755845112842-InitSchema.ts b/wwjcloud/src/migrations/1755845112842-InitSchema.ts new file mode 100644 index 0000000..c6303e3 --- /dev/null +++ b/wwjcloud/src/migrations/1755845112842-InitSchema.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class InitSchema1755845112842 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Admin 基线表(对应 common/auth/entities/admin.entity.ts) + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS \`wwjauth_admins\` ( + \`id\` bigint unsigned NOT NULL AUTO_INCREMENT, + \`username\` varchar(64) NOT NULL, + \`mobile\` varchar(32) NULL DEFAULT NULL, + \`password_hash\` varchar(255) NOT NULL, + \`nickname\` varchar(128) NULL, + \`avatar\` varchar(255) NULL, + \`login_ip\` varchar(64) NULL, + \`type\` tinyint NOT NULL DEFAULT 1, + \`tenant_id\` int unsigned NOT NULL DEFAULT 0, + \`last_login_at\` datetime NULL, + \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updated_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + \`status\` enum('enabled','disabled') NOT NULL DEFAULT 'enabled', + \`is_founder\` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (\`id\`), + UNIQUE KEY \`uk_admin_username\` (\`username\`), + UNIQUE KEY \`uk_admin_mobile\` (\`mobile\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + + // Member 基线表(对应 common/users/entities/member.entity.ts) + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS \`member\` ( + \`member_id\` int unsigned NOT NULL AUTO_INCREMENT, + \`username\` varchar(255) NOT NULL DEFAULT '', + \`mobile\` varchar(20) NOT NULL DEFAULT '', + \`password\` varchar(255) NOT NULL DEFAULT '', + \`status\` tinyint NOT NULL DEFAULT 1, + \`is_del\` tinyint NOT NULL DEFAULT 0, + \`login_count\` int NOT NULL DEFAULT 0, + \`login_time\` int NOT NULL DEFAULT 0, + \`login_ip\` varchar(255) NOT NULL DEFAULT '', + \`create_time\` int NOT NULL DEFAULT 0, + \`update_time\` int NOT NULL DEFAULT 0, + PRIMARY KEY (\`member_id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // 回滚顺序与创建相反 + await queryRunner.query('DROP TABLE IF EXISTS `member`;'); + await queryRunner.query('DROP TABLE IF EXISTS `wwjauth_admins`;'); + } +} diff --git a/wwjcloud/src/scripts/init-db.ts b/wwjcloud/src/scripts/init-db.ts new file mode 100644 index 0000000..ef481c6 --- /dev/null +++ b/wwjcloud/src/scripts/init-db.ts @@ -0,0 +1,46 @@ +import 'dotenv/config'; +import * as fs from 'fs'; +import * as path from 'path'; +import { createConnection } from 'mysql2/promise'; + +async function main() { + const host = process.env.DB_HOST || '127.0.0.1'; + const port = Number(process.env.DB_PORT || 3306); + const user = process.env.DB_USERNAME || 'root'; + const password = process.env.DB_PASSWORD || ''; + const database = process.env.DB_DATABASE || 'wwjcloud'; + + console.log(`[db:init] Connecting to MySQL ${host}:${port} as ${user}`); + const conn = await createConnection({ + host, + port, + user, + password, + multipleStatements: true, + charset: 'utf8mb4' + }); + + console.log(`[db:init] Ensuring database exists: ${database}`); + await conn.query( + `CREATE DATABASE IF NOT EXISTS \`${database}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`, + ); + await conn.query(`USE \`${database}\``); + + const sqlPath = path.resolve(__dirname, '../../../sql/wwjcloud.sql'); + console.log(`[db:init] Loading SQL from: ${sqlPath}`); + const sqlRaw = fs.readFileSync(sqlPath, 'utf8'); + // 处理 UTF-8 BOM 与首行 SET NAMES,避免 mysql2 解析错误 + let sql = sqlRaw.replace(/^\uFEFF/, ''); + sql = sql.replace(/^\s*SET\s+NAMES\s+utf8mb4\s*;\s*/i, ''); + + console.log('[db:init] Executing SQL... (this may take a while)'); + await conn.query(sql); + + console.log('[db:init] Done.'); + await conn.end(); +} + +main().catch((err) => { + console.error('[db:init] Failed:', err?.message || err); + process.exit(1); +}); \ No newline at end of file diff --git a/wwjcloud/src/vendor/http/axios.adapter.ts b/wwjcloud/src/vendor/http/axios.adapter.ts new file mode 100644 index 0000000..6f5be51 --- /dev/null +++ b/wwjcloud/src/vendor/http/axios.adapter.ts @@ -0,0 +1,6 @@ +export class AxiosAdapter { + async request(config: Record) { + // TODO: implement axios wrapper + return { ...config, status: 200 }; + } +} diff --git a/wwjcloud/src/vendor/index.ts b/wwjcloud/src/vendor/index.ts new file mode 100644 index 0000000..53538f9 --- /dev/null +++ b/wwjcloud/src/vendor/index.ts @@ -0,0 +1 @@ +export { VendorModule } from './vendor.module'; diff --git a/wwjcloud/src/vendor/mailer/nodemailer.adapter.ts b/wwjcloud/src/vendor/mailer/nodemailer.adapter.ts new file mode 100644 index 0000000..9b0e7b6 --- /dev/null +++ b/wwjcloud/src/vendor/mailer/nodemailer.adapter.ts @@ -0,0 +1,6 @@ +export class NodemailerAdapter { + async send(to: string, subject: string, content: string) { + // TODO: implement nodemailer logic + return { to, subject, content, sent: true }; + } +} diff --git a/wwjcloud/src/vendor/payment/mock.adapter.ts b/wwjcloud/src/vendor/payment/mock.adapter.ts new file mode 100644 index 0000000..9b5c1e7 --- /dev/null +++ b/wwjcloud/src/vendor/payment/mock.adapter.ts @@ -0,0 +1,6 @@ +export class MockPaymentAdapter { + async pay(orderId: string, amount: number) { + // TODO: implement mock payment result + return { orderId, amount, status: 'mock_paid' }; + } +} diff --git a/wwjcloud/src/vendor/redis/redis.provider.ts b/wwjcloud/src/vendor/redis/redis.provider.ts new file mode 100644 index 0000000..7ea755f --- /dev/null +++ b/wwjcloud/src/vendor/redis/redis.provider.ts @@ -0,0 +1,6 @@ +export class RedisProvider { + async getClient() { + // TODO: return redis client instance + return {}; + } +} diff --git a/wwjcloud/src/vendor/sms/aliyun-sms.adapter.ts b/wwjcloud/src/vendor/sms/aliyun-sms.adapter.ts new file mode 100644 index 0000000..22fa47e --- /dev/null +++ b/wwjcloud/src/vendor/sms/aliyun-sms.adapter.ts @@ -0,0 +1,6 @@ +export class AliyunSmsAdapter { + async send(to: string, templateId: string, params: Record) { + // TODO: implement aliyun sms logic + return { to, templateId, params, sent: true }; + } +} diff --git a/wwjcloud/src/vendor/storage/local.adapter.ts b/wwjcloud/src/vendor/storage/local.adapter.ts new file mode 100644 index 0000000..39b739a --- /dev/null +++ b/wwjcloud/src/vendor/storage/local.adapter.ts @@ -0,0 +1,6 @@ +export class LocalStorageAdapter { + async save(filePath: string, data: Buffer) { + // TODO: implement local storage logic + return { filePath, size: data.length }; + } +} diff --git a/wwjcloud/src/vendor/vendor.module.ts b/wwjcloud/src/vendor/vendor.module.ts new file mode 100644 index 0000000..89cf760 --- /dev/null +++ b/wwjcloud/src/vendor/vendor.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; + +@Module({ + providers: [], + exports: [], +}) +export class VendorModule {} diff --git a/wwjcloud/test/app.e2e-spec.ts b/wwjcloud/test/app.e2e-spec.ts new file mode 100644 index 0000000..4df6580 --- /dev/null +++ b/wwjcloud/test/app.e2e-spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import * as request from 'supertest'; +import { App } from 'supertest/types'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/wwjcloud/test/jest-e2e.json b/wwjcloud/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/wwjcloud/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/wwjcloud/tsconfig.build.json b/wwjcloud/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/wwjcloud/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/wwjcloud/tsconfig.json b/wwjcloud/tsconfig.json new file mode 100644 index 0000000..57f9635 --- /dev/null +++ b/wwjcloud/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "resolvePackageJsonExports": true, + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "noFallthroughCasesInSwitch": true + } +}