feat: 完成配置中心重构和命名规范优化
- 重构config层为配置中心架构,支持动态配置管理 - 统一core层命名规范(event-bus→event, circuit-breaker→breaker, domain-sdk→sdk) - 修复数据库连接配置路径问题 - 实现配置中心完整功能:系统配置、动态配置、配置验证、统计 - 优化目录结构,为微服务架构做准备 - 修复TypeScript编译错误和依赖注入问题
This commit is contained in:
@@ -5,8 +5,8 @@ PORT=3000
|
||||
# Database (MySQL)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=your_password
|
||||
DB_USERNAME=wwjcloud
|
||||
DB_PASSWORD=wwjcloud
|
||||
DB_DATABASE=wwjcloud
|
||||
|
||||
# Redis
|
||||
|
||||
299
wwjcloud/CONFIG_SETUP.md
Normal file
299
wwjcloud/CONFIG_SETUP.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# WWJCloud Backend 配置设置指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本文档说明如何设置 WWJCloud Backend 的环境变量配置。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 复制配置文件
|
||||
|
||||
```bash
|
||||
# 复制示例配置文件
|
||||
cp env.example .env
|
||||
|
||||
# 或者复制特定环境的配置
|
||||
cp env.development .env # 开发环境
|
||||
cp env.production .env # 生产环境
|
||||
```
|
||||
|
||||
### 2. 修改配置
|
||||
|
||||
根据你的实际环境修改 `.env` 文件中的配置项。
|
||||
|
||||
## 📁 配置文件说明
|
||||
|
||||
### 配置文件类型
|
||||
|
||||
- `env.example` - 配置示例文件(包含所有配置项)
|
||||
- `env.development` - 开发环境配置
|
||||
- `env.production` - 生产环境配置
|
||||
- `.env` - 实际使用的配置文件(需要手动创建)
|
||||
|
||||
### 配置优先级
|
||||
|
||||
1. **环境变量** (最高优先级)
|
||||
2. **默认配置** (最低优先级)
|
||||
|
||||
## 🔧 必需配置项
|
||||
|
||||
### 应用基础配置
|
||||
|
||||
```bash
|
||||
# 应用名称
|
||||
APP_NAME=WWJCloud Backend
|
||||
|
||||
# 应用端口
|
||||
PORT=3000
|
||||
|
||||
# 运行环境
|
||||
NODE_ENV=development # development, production, test
|
||||
```
|
||||
|
||||
### 数据库配置
|
||||
|
||||
```bash
|
||||
# 数据库主机
|
||||
DB_HOST=localhost
|
||||
|
||||
# 数据库端口
|
||||
DB_PORT=3306
|
||||
|
||||
# 数据库用户名
|
||||
DB_USERNAME=root
|
||||
|
||||
# 数据库密码
|
||||
DB_PASSWORD=your_password
|
||||
|
||||
# 数据库名称
|
||||
DB_DATABASE=wwjcloud
|
||||
|
||||
# 是否同步数据库结构(生产环境必须为 false)
|
||||
DB_SYNC=false
|
||||
|
||||
# 是否启用数据库日志
|
||||
DB_LOGGING=false
|
||||
```
|
||||
|
||||
### Redis 配置
|
||||
|
||||
```bash
|
||||
# Redis 主机
|
||||
REDIS_HOST=localhost
|
||||
|
||||
# Redis 端口
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Redis 密码
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# Redis 数据库编号
|
||||
REDIS_DB=0
|
||||
|
||||
# Redis 键前缀
|
||||
REDIS_KEY_PREFIX=wwjcloud:
|
||||
```
|
||||
|
||||
### JWT 配置
|
||||
|
||||
```bash
|
||||
# JWT 密钥(生产环境必须修改)
|
||||
JWT_SECRET=your-super-secret-jwt-key
|
||||
|
||||
# JWT 过期时间
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# JWT 算法
|
||||
JWT_ALGORITHM=HS256
|
||||
```
|
||||
|
||||
## 🌍 环境特定配置
|
||||
|
||||
### 开发环境
|
||||
|
||||
```bash
|
||||
# 复制开发环境配置
|
||||
cp env.development .env
|
||||
|
||||
# 主要特点:
|
||||
# - 启用详细日志 (LOG_LEVEL=debug)
|
||||
# - 启用数据库日志 (DB_LOGGING=true)
|
||||
# - 使用本地服务 (localhost)
|
||||
# - 启用调试工具 (DEBUG_ENABLED=true)
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
|
||||
```bash
|
||||
# 复制生产环境配置
|
||||
cp env.production .env
|
||||
|
||||
# 主要特点:
|
||||
# - 关闭详细日志 (LOG_LEVEL=warn)
|
||||
# - 关闭数据库日志 (DB_LOGGING=false)
|
||||
# - 使用生产服务器
|
||||
# - 关闭调试工具 (DEBUG_ENABLED=false)
|
||||
# - 启用监控 (PROMETHEUS_ENABLED=true)
|
||||
```
|
||||
|
||||
## 🔐 安全配置
|
||||
|
||||
### 生产环境安全要求
|
||||
|
||||
1. **修改所有密钥**
|
||||
```bash
|
||||
JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters
|
||||
SESSION_SECRET=production-session-secret-key
|
||||
COOKIE_SECRET=production-cookie-secret-key
|
||||
```
|
||||
|
||||
2. **设置强密码**
|
||||
```bash
|
||||
DB_PASSWORD=your-strong-database-password
|
||||
REDIS_PASSWORD=your-strong-redis-password
|
||||
```
|
||||
|
||||
3. **配置 CORS**
|
||||
```bash
|
||||
CORS_ORIGIN=https://your-domain.com
|
||||
```
|
||||
|
||||
4. **设置域名白名单**
|
||||
```bash
|
||||
ALLOWED_DOMAINS=your-domain.com,api.your-domain.com
|
||||
```
|
||||
|
||||
## 📊 监控配置
|
||||
|
||||
### 启用监控
|
||||
|
||||
```bash
|
||||
# 启用指标收集
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
|
||||
# 启用 Prometheus 监控
|
||||
PROMETHEUS_ENABLED=true
|
||||
|
||||
# 启用健康检查
|
||||
HEALTH_CHECK_ENABLED=true
|
||||
HEALTH_CHECK_INTERVAL=30000
|
||||
```
|
||||
|
||||
### 追踪配置
|
||||
|
||||
```bash
|
||||
# 启用分布式追踪
|
||||
TRACING_ENABLED=true
|
||||
|
||||
# Jaeger 端点
|
||||
JAEGER_ENDPOINT=http://jaeger:14268/api/traces
|
||||
```
|
||||
|
||||
## 🔄 动态配置
|
||||
|
||||
### 启用动态配置
|
||||
|
||||
```bash
|
||||
# 启用动态配置功能
|
||||
ENABLE_DYNAMIC_CONFIG=true
|
||||
|
||||
# 配置缓存时间
|
||||
CONFIG_CACHE_TTL=300
|
||||
```
|
||||
|
||||
### 动态配置示例
|
||||
|
||||
通过 API 接口管理动态配置:
|
||||
|
||||
```bash
|
||||
# 设置邮件配置
|
||||
curl -X POST http://localhost:3000/adminapi/config/dynamic \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-token" \
|
||||
-d '{
|
||||
"key": "email.smtp",
|
||||
"value": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
"description": "SMTP 服务器配置",
|
||||
"category": "email",
|
||||
"isPublic": false
|
||||
}'
|
||||
```
|
||||
|
||||
## 🧪 测试配置
|
||||
|
||||
### 验证配置
|
||||
|
||||
```bash
|
||||
# 启动应用后访问配置验证接口
|
||||
curl http://localhost:3000/adminapi/config/validate
|
||||
|
||||
# 查看系统配置
|
||||
curl http://localhost:3000/adminapi/config/system
|
||||
```
|
||||
|
||||
### 配置检查清单
|
||||
|
||||
- [ ] 数据库连接正常
|
||||
- [ ] Redis 连接正常
|
||||
- [ ] JWT 密钥已设置
|
||||
- [ ] 日志级别合适
|
||||
- [ ] 文件上传路径存在
|
||||
- [ ] 第三方服务配置正确
|
||||
|
||||
## 🚨 常见问题
|
||||
|
||||
### 1. 配置不生效
|
||||
|
||||
**问题**:修改了 `.env` 文件但配置没有生效
|
||||
|
||||
**解决**:
|
||||
- 确保 `.env` 文件在项目根目录
|
||||
- 重启应用
|
||||
- 检查环境变量名称是否正确
|
||||
|
||||
### 2. 数据库连接失败
|
||||
|
||||
**问题**:无法连接到数据库
|
||||
|
||||
**解决**:
|
||||
- 检查 `DB_HOST`、`DB_PORT`、`DB_USERNAME`、`DB_PASSWORD`
|
||||
- 确保数据库服务正在运行
|
||||
- 检查防火墙设置
|
||||
|
||||
### 3. Redis 连接失败
|
||||
|
||||
**问题**:无法连接到 Redis
|
||||
|
||||
**解决**:
|
||||
- 检查 `REDIS_HOST`、`REDIS_PORT`、`REDIS_PASSWORD`
|
||||
- 确保 Redis 服务正在运行
|
||||
- 检查 Redis 配置
|
||||
|
||||
### 4. JWT 错误
|
||||
|
||||
**问题**:JWT 相关错误
|
||||
|
||||
**解决**:
|
||||
- 确保 `JWT_SECRET` 已设置且足够复杂
|
||||
- 检查 `JWT_EXPIRES_IN` 格式
|
||||
- 验证 `JWT_ALGORITHM` 设置
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [配置中心使用指南](../src/config/README.md)
|
||||
- [API 文档](http://localhost:3000/docs)
|
||||
- [健康检查](http://localhost:3000/health)
|
||||
|
||||
## 🤝 支持
|
||||
|
||||
如果遇到配置问题,请:
|
||||
|
||||
1. 检查本文档的常见问题部分
|
||||
2. 查看应用日志
|
||||
3. 使用配置验证接口检查配置
|
||||
4. 联系技术支持团队
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UnifiedQueueService } from '../src/core/queue/unified-queue.service';
|
||||
import { UnifiedQueueService } from '../src/core/queue/unifiedQueueService';
|
||||
import { TaskJobOptions, EventPublishOptions } from '../src/core/interfaces/queue.interface';
|
||||
|
||||
/**
|
||||
|
||||
119
wwjcloud/env.development
Normal file
119
wwjcloud/env.development
Normal file
@@ -0,0 +1,119 @@
|
||||
# ========================================
|
||||
# WWJCloud Backend 开发环境配置
|
||||
# ========================================
|
||||
|
||||
# 应用基础配置
|
||||
APP_NAME=WWJCloud Backend (Dev)
|
||||
APP_VERSION=1.0.0
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=wwjcloud
|
||||
DB_PASSWORD=wwjcloud
|
||||
DB_DATABASE=wwjcloud
|
||||
DB_SYNC=false
|
||||
DB_LOGGING=true
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST=192.168.1.35
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=redis_bwQAnN
|
||||
REDIS_DB=1
|
||||
REDIS_KEY_PREFIX=wwjcloud:dev:
|
||||
|
||||
# Kafka 配置
|
||||
KAFKA_CLIENT_ID=wwjcloud-backend-dev
|
||||
KAFKA_BROKERS=192.168.1.35:9092
|
||||
KAFKA_GROUP_ID=wwjcloud-group-dev
|
||||
KAFKA_TOPIC_PREFIX=domain-events-dev
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=dev-secret-key-change-in-production
|
||||
JWT_EXPIRES_IN=7d
|
||||
JWT_ALGORITHM=HS256
|
||||
|
||||
# 缓存配置
|
||||
CACHE_TTL=300
|
||||
CACHE_MAX_ITEMS=1000
|
||||
CACHE_PREFIX=wwjcloud:dev:cache:
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=debug
|
||||
LOG_FORMAT=json
|
||||
LOG_FILENAME=
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_PATH=public/upload/dev
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_ALLOWED_TYPES=image/*,application/pdf,text/*
|
||||
|
||||
# 限流配置
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=1000
|
||||
|
||||
# 第三方服务配置
|
||||
STORAGE_PROVIDER=local
|
||||
STORAGE_CONFIG={}
|
||||
PAYMENT_PROVIDER=mock
|
||||
PAYMENT_CONFIG={}
|
||||
SMS_PROVIDER=mock
|
||||
SMS_CONFIG={}
|
||||
|
||||
# 配置中心配置
|
||||
ENABLE_DYNAMIC_CONFIG=true
|
||||
CONFIG_CACHE_TTL=300
|
||||
|
||||
# 队列配置
|
||||
QUEUE_DRIVER=bull
|
||||
TASK_QUEUE_ADAPTER=database-outbox
|
||||
EVENT_BUS_ADAPTER=database-outbox
|
||||
QUEUE_REMOVE_ON_COMPLETE=100
|
||||
QUEUE_REMOVE_ON_FAIL=50
|
||||
QUEUE_DEFAULT_ATTEMPTS=3
|
||||
QUEUE_BACKOFF_DELAY=2000
|
||||
|
||||
# Outbox 模式配置
|
||||
OUTBOX_PROCESS_INTERVAL=5000
|
||||
OUTBOX_BATCH_SIZE=100
|
||||
OUTBOX_MAX_RETRIES=5
|
||||
OUTBOX_RETRY_DELAY=60000
|
||||
|
||||
# 追踪配置
|
||||
JAEGER_ENDPOINT=
|
||||
TRACING_ENABLED=true
|
||||
|
||||
# 健康检查配置
|
||||
HEALTH_CHECK_ENABLED=true
|
||||
HEALTH_CHECK_INTERVAL=30000
|
||||
|
||||
# 安全配置
|
||||
BCRYPT_ROUNDS=10
|
||||
SESSION_SECRET=dev-session-secret
|
||||
COOKIE_SECRET=dev-cookie-secret
|
||||
|
||||
# 跨域配置
|
||||
CORS_ORIGIN=*
|
||||
CORS_CREDENTIALS=true
|
||||
CORS_METHODS=GET,HEAD,PUT,PATCH,POST,DELETE
|
||||
|
||||
# 域名配置
|
||||
CURRENT_DOMAIN=dev
|
||||
ALLOWED_DOMAINS=localhost,127.0.0.1
|
||||
|
||||
# 语言配置
|
||||
DEFAULT_LANGUAGE=zh-CN
|
||||
SUPPORTED_LANGUAGES=zh-CN,en-US
|
||||
|
||||
# 监控配置
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
PROMETHEUS_ENABLED=false
|
||||
|
||||
# 开发工具配置
|
||||
SWAGGER_ENABLED=true
|
||||
SWAGGER_PATH=docs
|
||||
DEBUG_ENABLED=true
|
||||
175
wwjcloud/env.example
Normal file
175
wwjcloud/env.example
Normal file
@@ -0,0 +1,175 @@
|
||||
# ========================================
|
||||
# WWJCloud Backend 环境变量配置示例
|
||||
# ========================================
|
||||
# 复制此文件为 .env 并根据实际环境修改配置
|
||||
|
||||
# ========================================
|
||||
# 应用基础配置
|
||||
# ========================================
|
||||
APP_NAME=WWJCloud Backend
|
||||
APP_VERSION=1.0.0
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# ========================================
|
||||
# 数据库配置
|
||||
# ========================================
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=wwjcloud
|
||||
DB_SYNC=false
|
||||
DB_LOGGING=false
|
||||
|
||||
# ========================================
|
||||
# Redis 配置
|
||||
# ========================================
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
REDIS_KEY_PREFIX=wwjcloud:
|
||||
|
||||
# ========================================
|
||||
# Kafka 配置
|
||||
# ========================================
|
||||
KAFKA_CLIENT_ID=wwjcloud-backend
|
||||
KAFKA_BROKERS=localhost:9092
|
||||
KAFKA_GROUP_ID=wwjcloud-group
|
||||
KAFKA_TOPIC_PREFIX=domain-events
|
||||
|
||||
# ========================================
|
||||
# JWT 配置
|
||||
# ========================================
|
||||
JWT_SECRET=wwjcloud-secret-key-change-in-production
|
||||
JWT_EXPIRES_IN=7d
|
||||
JWT_ALGORITHM=HS256
|
||||
|
||||
# ========================================
|
||||
# 缓存配置
|
||||
# ========================================
|
||||
CACHE_TTL=300
|
||||
CACHE_MAX_ITEMS=1000
|
||||
CACHE_PREFIX=wwjcloud:cache:
|
||||
|
||||
# ========================================
|
||||
# 日志配置
|
||||
# ========================================
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
LOG_FILENAME=
|
||||
|
||||
# ========================================
|
||||
# 文件上传配置
|
||||
# ========================================
|
||||
UPLOAD_PATH=public/upload
|
||||
UPLOAD_MAX_SIZE=10485760
|
||||
UPLOAD_ALLOWED_TYPES=image/*,application/pdf,text/*
|
||||
|
||||
# ========================================
|
||||
# 限流配置
|
||||
# ========================================
|
||||
THROTTLE_TTL=60
|
||||
THROTTLE_LIMIT=100
|
||||
|
||||
# ========================================
|
||||
# 第三方服务配置
|
||||
# ========================================
|
||||
# 存储服务配置
|
||||
STORAGE_PROVIDER=local
|
||||
STORAGE_CONFIG={}
|
||||
|
||||
# 支付服务配置
|
||||
PAYMENT_PROVIDER=mock
|
||||
PAYMENT_CONFIG={}
|
||||
|
||||
# 短信服务配置
|
||||
SMS_PROVIDER=mock
|
||||
SMS_CONFIG={}
|
||||
|
||||
# ========================================
|
||||
# 配置中心配置
|
||||
# ========================================
|
||||
ENABLE_DYNAMIC_CONFIG=true
|
||||
CONFIG_CACHE_TTL=300
|
||||
|
||||
# ========================================
|
||||
# 队列配置
|
||||
# ========================================
|
||||
QUEUE_DRIVER=bull
|
||||
TASK_QUEUE_ADAPTER=database-outbox
|
||||
EVENT_BUS_ADAPTER=database-outbox
|
||||
QUEUE_REMOVE_ON_COMPLETE=100
|
||||
QUEUE_REMOVE_ON_FAIL=50
|
||||
QUEUE_DEFAULT_ATTEMPTS=3
|
||||
QUEUE_BACKOFF_DELAY=2000
|
||||
|
||||
# Outbox 模式配置
|
||||
OUTBOX_PROCESS_INTERVAL=5000
|
||||
OUTBOX_BATCH_SIZE=100
|
||||
OUTBOX_MAX_RETRIES=5
|
||||
OUTBOX_RETRY_DELAY=60000
|
||||
|
||||
# ========================================
|
||||
# 追踪配置
|
||||
# ========================================
|
||||
JAEGER_ENDPOINT=
|
||||
TRACING_ENABLED=false
|
||||
|
||||
# ========================================
|
||||
# 健康检查配置
|
||||
# ========================================
|
||||
HEALTH_CHECK_ENABLED=true
|
||||
HEALTH_CHECK_INTERVAL=30000
|
||||
|
||||
# ========================================
|
||||
# 安全配置
|
||||
# ========================================
|
||||
BCRYPT_ROUNDS=10
|
||||
SESSION_SECRET=wwjcloud-session-secret
|
||||
COOKIE_SECRET=wwjcloud-cookie-secret
|
||||
|
||||
# ========================================
|
||||
# 跨域配置
|
||||
# ========================================
|
||||
CORS_ORIGIN=*
|
||||
CORS_CREDENTIALS=true
|
||||
CORS_METHODS=GET,HEAD,PUT,PATCH,POST,DELETE
|
||||
|
||||
# ========================================
|
||||
# 域名配置
|
||||
# ========================================
|
||||
CURRENT_DOMAIN=default
|
||||
ALLOWED_DOMAINS=localhost,127.0.0.1
|
||||
|
||||
# ========================================
|
||||
# 语言配置
|
||||
# ========================================
|
||||
DEFAULT_LANGUAGE=zh-CN
|
||||
SUPPORTED_LANGUAGES=zh-CN,en-US
|
||||
|
||||
# ========================================
|
||||
# 邮件配置(动态配置,这里只是示例)
|
||||
# ========================================
|
||||
# 这些配置通常通过动态配置管理,而不是环境变量
|
||||
# EMAIL_SMTP_HOST=smtp.gmail.com
|
||||
# EMAIL_SMTP_PORT=587
|
||||
# EMAIL_SMTP_SECURE=false
|
||||
# EMAIL_SMTP_USER=your-email@gmail.com
|
||||
# EMAIL_SMTP_PASS=your-password
|
||||
|
||||
# ========================================
|
||||
# 监控配置
|
||||
# ========================================
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
PROMETHEUS_ENABLED=false
|
||||
|
||||
# ========================================
|
||||
# 开发工具配置
|
||||
# ========================================
|
||||
SWAGGER_ENABLED=true
|
||||
SWAGGER_PATH=docs
|
||||
DEBUG_ENABLED=false
|
||||
121
wwjcloud/env.production
Normal file
121
wwjcloud/env.production
Normal file
@@ -0,0 +1,121 @@
|
||||
# ========================================
|
||||
# WWJCloud Backend 生产环境配置
|
||||
# ========================================
|
||||
|
||||
# 应用基础配置
|
||||
APP_NAME=WWJCloud Backend
|
||||
APP_VERSION=1.0.0
|
||||
PORT=3000
|
||||
NODE_ENV=production
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=prod-db.example.com
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=wwjcloud_user
|
||||
DB_PASSWORD=your-production-password
|
||||
DB_DATABASE=wwjcloud_prod
|
||||
DB_SYNC=false
|
||||
DB_LOGGING=false
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST=prod-redis.example.com
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
REDIS_DB=0
|
||||
REDIS_KEY_PREFIX=wwjcloud:prod:
|
||||
|
||||
# Kafka 配置
|
||||
KAFKA_CLIENT_ID=wwjcloud-backend-prod
|
||||
KAFKA_BROKERS=prod-kafka1.example.com:9092,prod-kafka2.example.com:9092
|
||||
KAFKA_GROUP_ID=wwjcloud-group-prod
|
||||
KAFKA_TOPIC_PREFIX=domain-events-prod
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters-for-production
|
||||
JWT_EXPIRES_IN=24h
|
||||
JWT_ALGORITHM=HS256
|
||||
|
||||
# 缓存配置
|
||||
CACHE_TTL=600
|
||||
CACHE_MAX_ITEMS=2000
|
||||
CACHE_PREFIX=wwjcloud:prod:cache:
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=warn
|
||||
LOG_FORMAT=json
|
||||
LOG_FILENAME=logs/app.log
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_PATH=public/upload/prod
|
||||
UPLOAD_MAX_SIZE=20971520
|
||||
UPLOAD_ALLOWED_TYPES=image/*,application/pdf,text/*
|
||||
|
||||
# 限流配置
|
||||
THROTTLE_TTL=300
|
||||
THROTTLE_LIMIT=1000
|
||||
|
||||
# 第三方服务配置
|
||||
STORAGE_PROVIDER=oss
|
||||
STORAGE_CONFIG={"accessKeyId":"your-key","accessKeySecret":"your-secret","bucket":"your-bucket","region":"oss-cn-hangzhou"}
|
||||
|
||||
PAYMENT_PROVIDER=alipay
|
||||
PAYMENT_CONFIG={"appId":"your-app-id","privateKey":"your-private-key","publicKey":"alipay-public-key"}
|
||||
|
||||
SMS_PROVIDER=aliyun
|
||||
SMS_CONFIG={"accessKeyId":"your-key","accessKeySecret":"your-secret","signName":"WWJCloud","templateCode":"SMS_123456789"}
|
||||
|
||||
# 配置中心配置
|
||||
ENABLE_DYNAMIC_CONFIG=true
|
||||
CONFIG_CACHE_TTL=300
|
||||
|
||||
# 队列配置
|
||||
QUEUE_DRIVER=bull
|
||||
TASK_QUEUE_ADAPTER=database-outbox
|
||||
EVENT_BUS_ADAPTER=database-outbox
|
||||
QUEUE_REMOVE_ON_COMPLETE=100
|
||||
QUEUE_REMOVE_ON_FAIL=50
|
||||
QUEUE_DEFAULT_ATTEMPTS=3
|
||||
QUEUE_BACKOFF_DELAY=2000
|
||||
|
||||
# Outbox 模式配置
|
||||
OUTBOX_PROCESS_INTERVAL=5000
|
||||
OUTBOX_BATCH_SIZE=100
|
||||
OUTBOX_MAX_RETRIES=5
|
||||
OUTBOX_RETRY_DELAY=60000
|
||||
|
||||
# 追踪配置
|
||||
JAEGER_ENDPOINT=http://jaeger:14268/api/traces
|
||||
TRACING_ENABLED=true
|
||||
|
||||
# 健康检查配置
|
||||
HEALTH_CHECK_ENABLED=true
|
||||
HEALTH_CHECK_INTERVAL=30000
|
||||
|
||||
# 安全配置
|
||||
BCRYPT_ROUNDS=12
|
||||
SESSION_SECRET=production-session-secret-key
|
||||
COOKIE_SECRET=production-cookie-secret-key
|
||||
|
||||
# 跨域配置
|
||||
CORS_ORIGIN=https://your-domain.com
|
||||
CORS_CREDENTIALS=true
|
||||
CORS_METHODS=GET,HEAD,PUT,PATCH,POST,DELETE
|
||||
|
||||
# 域名配置
|
||||
CURRENT_DOMAIN=prod
|
||||
ALLOWED_DOMAINS=your-domain.com,api.your-domain.com
|
||||
|
||||
# 语言配置
|
||||
DEFAULT_LANGUAGE=zh-CN
|
||||
SUPPORTED_LANGUAGES=zh-CN,en-US
|
||||
|
||||
# 监控配置
|
||||
METRICS_ENABLED=true
|
||||
METRICS_PORT=9090
|
||||
PROMETHEUS_ENABLED=true
|
||||
|
||||
# 开发工具配置
|
||||
SWAGGER_ENABLED=false
|
||||
SWAGGER_PATH=docs
|
||||
DEBUG_ENABLED=false
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dotenv/config';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import configuration from './config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { appConfig } from './config';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
@@ -26,21 +26,23 @@ import {
|
||||
MemberModule,
|
||||
AdminModule,
|
||||
RbacModule,
|
||||
UserModule,
|
||||
GlobalAuthGuard,
|
||||
RolesGuard,
|
||||
JobsModule,
|
||||
EventBusModule,
|
||||
} from './common';
|
||||
import { ScheduleModule as AppScheduleModule } from './common/schedule/schedule.module';
|
||||
import { MetricsController } from './core/observability/metrics.controller';
|
||||
import { MetricsController } from './core/observability/metricsController';
|
||||
// 测试模块(Redis 和 Kafka 测试)
|
||||
// import { TestModule } from '../test/test.module';
|
||||
import { ConfigModule as AppConfigModule } from './config/config.module';
|
||||
import { ConfigModule as NestConfigModule } from '@nestjs/config';
|
||||
import { ConfigModule } from './config';
|
||||
// 新增:全局异常过滤器、统一响应、健康
|
||||
import { HttpExceptionFilter } from './core/http/filters/http-exception.filter';
|
||||
import { ResponseInterceptor } from './core/http/interceptors/response.interceptor';
|
||||
import { HttpExceptionFilter } from './core/http/filters/httpExceptionFilter';
|
||||
import { ResponseInterceptor } from './core/http/interceptors/responseInterceptor';
|
||||
import { HealthController } from './core/observability/health/health.controller';
|
||||
import { HttpMetricsService } from './core/observability/metrics/http-metrics.service';
|
||||
import { HttpMetricsService } from './core/observability/metrics/httpMetricsService';
|
||||
import { HealthAggregator } from './core/observability/health/health-aggregator';
|
||||
import { DbHealthIndicator } from './core/observability/health/indicators/db.indicator';
|
||||
import { RedisHealthIndicator } from './core/observability/health/indicators/redis.indicator';
|
||||
@@ -57,11 +59,11 @@ const dbImports =
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: config.get('db.host'),
|
||||
port: config.get('db.port'),
|
||||
username: config.get('db.username'),
|
||||
password: config.get('db.password'),
|
||||
database: config.get('db.database'),
|
||||
host: config.get('database.host'),
|
||||
port: config.get('database.port'),
|
||||
username: config.get('database.username'),
|
||||
password: config.get('database.password'),
|
||||
database: config.get('database.database'),
|
||||
autoLoadEntities: true,
|
||||
synchronize: false,
|
||||
}),
|
||||
@@ -70,9 +72,9 @@ const dbImports =
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
NestConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
load: [() => appConfig],
|
||||
validationSchema: Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test'),
|
||||
@@ -102,7 +104,7 @@ const dbImports =
|
||||
'ftp',
|
||||
),
|
||||
PAYMENT_PROVIDER: Joi.string()
|
||||
.valid('wechat', 'alipay'),
|
||||
.valid('wechat', 'alipay', 'mock'),
|
||||
LOG_LEVEL: Joi.string(),
|
||||
THROTTLE_TTL: Joi.number(),
|
||||
THROTTLE_LIMIT: Joi.number(),
|
||||
@@ -196,6 +198,8 @@ const dbImports =
|
||||
AdminModule,
|
||||
// 权限管理模块
|
||||
RbacModule,
|
||||
// 用户管理模块
|
||||
UserModule,
|
||||
// 认证模块(提供 super/admin/auth 登录分流)
|
||||
AuthModule,
|
||||
// 定时任务模块
|
||||
@@ -206,8 +210,8 @@ const dbImports =
|
||||
EventBusModule,
|
||||
// 测试模块(Redis 和 Kafka 测试)
|
||||
// TestModule,
|
||||
// 配置模块(API文档导航等)
|
||||
AppConfigModule,
|
||||
// 配置模块(配置中心)
|
||||
ConfigModule,
|
||||
],
|
||||
controllers: [AppController, MetricsController, HealthController],
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const UserContext = createParamDecorator(
|
||||
(data: unknown, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return request.user;
|
||||
},
|
||||
);
|
||||
@@ -2,6 +2,7 @@
|
||||
export * from './admin/admin.module';
|
||||
export * from './member/member.module';
|
||||
export * from './rbac/rbac.module';
|
||||
export * from './user/user.module';
|
||||
export * from './auth/auth.module';
|
||||
export * from './upload/upload.module';
|
||||
export * from './jobs/jobs.module';
|
||||
@@ -16,5 +17,4 @@ export * from './auth/decorators/RolesDecorator';
|
||||
// 导出设置相关模块
|
||||
export * from './settings';
|
||||
|
||||
// 导出常量
|
||||
export * from '../config/common/constants';
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Site } from './site.entity';
|
||||
import { UpdateSiteSettingsDto } from './site-settings.dto';
|
||||
import {
|
||||
DEFAULT_SITE_CONFIG,
|
||||
SYSTEM_CONSTANTS,
|
||||
} from '@wwjConfig/common/constants';
|
||||
|
||||
|
||||
// 不允许硬编码,从配置系统获取
|
||||
// TODO: 配置系统重构中,此功能暂时不可用
|
||||
|
||||
@Injectable()
|
||||
export class SiteSettingsService {
|
||||
@@ -18,103 +18,24 @@ export class SiteSettingsService {
|
||||
/**
|
||||
* 获取站点设置
|
||||
*/
|
||||
async getSiteSettings() {
|
||||
// 获取默认站点(id = 1)
|
||||
const site = await this.siteRepository.findOne({
|
||||
where: { site_id: SYSTEM_CONSTANTS.DEFAULT_SITE_ID },
|
||||
});
|
||||
|
||||
if (!site) {
|
||||
// 如果没有找到站点,返回默认值
|
||||
return { ...DEFAULT_SITE_CONFIG };
|
||||
}
|
||||
|
||||
return {
|
||||
site_name: site.site_name || DEFAULT_SITE_CONFIG.site_name,
|
||||
site_title: site.site_title || DEFAULT_SITE_CONFIG.site_title,
|
||||
site_keywords: site.site_keywords || DEFAULT_SITE_CONFIG.site_keywords,
|
||||
site_description:
|
||||
site.site_description || DEFAULT_SITE_CONFIG.site_description,
|
||||
site_logo: site.site_logo || DEFAULT_SITE_CONFIG.site_logo,
|
||||
site_favicon: site.site_favicon || DEFAULT_SITE_CONFIG.site_favicon,
|
||||
icp_number: site.icp_number || DEFAULT_SITE_CONFIG.icp_number,
|
||||
copyright: site.copyright || DEFAULT_SITE_CONFIG.copyright,
|
||||
site_status: site.site_status || DEFAULT_SITE_CONFIG.site_status,
|
||||
close_reason: site.close_reason || DEFAULT_SITE_CONFIG.close_reason,
|
||||
};
|
||||
getSiteSettings() {
|
||||
// 配置系统重构中,此功能暂时不可用
|
||||
throw new Error('配置系统重构中,站点设置功能暂时不可用');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新站点设置
|
||||
*/
|
||||
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: SYSTEM_CONSTANTS.DEFAULT_SITE_ID },
|
||||
});
|
||||
|
||||
if (!site) {
|
||||
// 创建默认站点
|
||||
site = this.siteRepository.create({
|
||||
id: SYSTEM_CONSTANTS.DEFAULT_SITE_ID,
|
||||
site_name: site_name || DEFAULT_SITE_CONFIG.site_name,
|
||||
site_title: site_title || DEFAULT_SITE_CONFIG.site_title,
|
||||
site_keywords: site_keywords || DEFAULT_SITE_CONFIG.site_keywords,
|
||||
site_description:
|
||||
site_description || DEFAULT_SITE_CONFIG.site_description,
|
||||
site_logo: site_logo || DEFAULT_SITE_CONFIG.site_logo,
|
||||
site_favicon: site_favicon || DEFAULT_SITE_CONFIG.site_favicon,
|
||||
icp_number: icp_number || DEFAULT_SITE_CONFIG.icp_number,
|
||||
copyright: copyright || DEFAULT_SITE_CONFIG.copyright,
|
||||
site_status: site_status || DEFAULT_SITE_CONFIG.site_status,
|
||||
close_reason: close_reason || DEFAULT_SITE_CONFIG.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: '站点设置更新成功' };
|
||||
updateSiteSettings(updateSiteSettingsDto: UpdateSiteSettingsDto) {
|
||||
// 配置系统重构中,此功能暂时不可用
|
||||
throw new Error('配置系统重构中,站点设置更新功能暂时不可用');
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置站点设置为默认值
|
||||
*/
|
||||
async resetSiteSettings() {
|
||||
// 删除现有站点配置
|
||||
await this.siteRepository.delete({ id: SYSTEM_CONSTANTS.DEFAULT_SITE_ID });
|
||||
|
||||
// 创建默认站点配置
|
||||
const defaultSite = this.siteRepository.create({
|
||||
id: SYSTEM_CONSTANTS.DEFAULT_SITE_ID,
|
||||
...DEFAULT_SITE_CONFIG,
|
||||
});
|
||||
|
||||
await this.siteRepository.save(defaultSite);
|
||||
return { message: '站点设置重置成功' };
|
||||
resetSiteSettings() {
|
||||
// 配置系统重构中,此功能暂时不可用
|
||||
throw new Error('配置系统重构中,站点设置重置功能暂时不可用');
|
||||
}
|
||||
}
|
||||
|
||||
110
wwjcloud/src/common/user/controllers/adminapi/UserController.ts
Normal file
110
wwjcloud/src/common/user/controllers/adminapi/UserController.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseIntPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../../auth/guards/JwtAuthGuard';
|
||||
import { RolesGuard } from '../../../auth/guards/RolesGuard';
|
||||
import { Roles } from '../../../auth/decorators/RolesDecorator';
|
||||
import { UserContext } from '../../../auth/decorators/user-context.decorator';
|
||||
import { UserContextDto } from '../../dto/UserContextDto';
|
||||
import { UserAdminService } from '../../services/admin/UserAdminService';
|
||||
import {
|
||||
CreateUserAdminDto,
|
||||
UpdateUserAdminDto,
|
||||
GetUserListAdminDto,
|
||||
BatchUpdateUserStatusAdminDto,
|
||||
ResetUserPasswordAdminDto,
|
||||
} from '../../dto/admin/UserDto';
|
||||
|
||||
@ApiTags('用户管理')
|
||||
@Controller('adminapi/user')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class UserController {
|
||||
constructor(private readonly userAdminService: UserAdminService) {}
|
||||
|
||||
@Post()
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '创建用户' })
|
||||
@ApiResponse({ status: 201, description: '用户创建成功' })
|
||||
async createUser(
|
||||
@Body() createUserDto: CreateUserAdminDto,
|
||||
@UserContext() userContext: UserContextDto,
|
||||
) {
|
||||
return await this.userAdminService.createUser(createUserDto, userContext);
|
||||
}
|
||||
|
||||
@Put()
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '更新用户' })
|
||||
@ApiResponse({ status: 200, description: '用户更新成功' })
|
||||
async updateUser(
|
||||
@Body() updateUserDto: UpdateUserAdminDto,
|
||||
@UserContext() userContext: UserContextDto,
|
||||
) {
|
||||
return await this.userAdminService.updateUser(updateUserDto, userContext);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取用户列表' })
|
||||
@ApiResponse({ status: 200, description: '获取用户列表成功' })
|
||||
async getUserList(
|
||||
@Query() queryDto: GetUserListAdminDto,
|
||||
@UserContext() userContext: UserContextDto,
|
||||
) {
|
||||
return await this.userAdminService.getUserList(queryDto, userContext);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '根据ID获取用户' })
|
||||
@ApiResponse({ status: 200, description: '获取用户成功' })
|
||||
async getUserById(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@UserContext() userContext: UserContextDto,
|
||||
) {
|
||||
return await this.userAdminService.getUserById(id, userContext);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '删除用户' })
|
||||
@ApiResponse({ status: 200, description: '用户删除成功' })
|
||||
async deleteUser(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@UserContext() userContext: UserContextDto,
|
||||
) {
|
||||
return await this.userAdminService.deleteUser(id, userContext);
|
||||
}
|
||||
|
||||
@Post('batch-update-status')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '批量更新用户状态' })
|
||||
@ApiResponse({ status: 200, description: '批量更新状态成功' })
|
||||
async batchUpdateStatus(
|
||||
@Body() batchUpdateDto: BatchUpdateUserStatusAdminDto,
|
||||
@UserContext() userContext: UserContextDto,
|
||||
) {
|
||||
return await this.userAdminService.batchUpdateStatus(batchUpdateDto, userContext);
|
||||
}
|
||||
|
||||
@Post('reset-password')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '重置用户密码' })
|
||||
@ApiResponse({ status: 200, description: '密码重置成功' })
|
||||
async resetPassword(
|
||||
@Body() resetPasswordDto: ResetUserPasswordAdminDto,
|
||||
@UserContext() userContext: UserContextDto,
|
||||
) {
|
||||
return await this.userAdminService.resetPassword(resetPasswordDto, userContext);
|
||||
}
|
||||
}
|
||||
6
wwjcloud/src/common/user/dto/UserContextDto.ts
Normal file
6
wwjcloud/src/common/user/dto/UserContextDto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export class UserContextDto {
|
||||
userId: number;
|
||||
siteId: number;
|
||||
username: string;
|
||||
roles: string[];
|
||||
}
|
||||
189
wwjcloud/src/common/user/dto/admin/UserDto.ts
Normal file
189
wwjcloud/src/common/user/dto/admin/UserDto.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsNumber, IsEmail, IsDateString, IsArray } from 'class-validator';
|
||||
|
||||
export class CreateUserAdminDto {
|
||||
@ApiProperty({ description: '用户名' })
|
||||
@IsString()
|
||||
username: string;
|
||||
|
||||
@ApiProperty({ description: '密码' })
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ description: '昵称' })
|
||||
@IsString()
|
||||
nickname: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱', required: false })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiProperty({ description: '手机号', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mobile?: string;
|
||||
|
||||
@ApiProperty({ description: '头像', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatar?: string;
|
||||
|
||||
@ApiProperty({ description: '状态:0-禁用,1-启用', default: 1 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
status?: number;
|
||||
|
||||
@ApiProperty({ description: '性别:0-未知,1-男,2-女', default: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
gender?: number;
|
||||
|
||||
@ApiProperty({ description: '生日', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
birthday?: string;
|
||||
|
||||
@ApiProperty({ description: '备注', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remark?: string;
|
||||
|
||||
@ApiProperty({ description: '排序', default: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
sort?: number;
|
||||
|
||||
@ApiProperty({ description: '部门ID', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
dept_id?: number;
|
||||
|
||||
@ApiProperty({ description: '角色ID列表', required: false, type: [Number] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
role_ids?: number[];
|
||||
}
|
||||
|
||||
export class UpdateUserAdminDto {
|
||||
@ApiProperty({ description: 'ID' })
|
||||
@IsNumber()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '昵称', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱', required: false })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiProperty({ description: '手机号', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mobile?: string;
|
||||
|
||||
@ApiProperty({ description: '头像', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
avatar?: string;
|
||||
|
||||
@ApiProperty({ description: '状态:0-禁用,1-启用', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
status?: number;
|
||||
|
||||
@ApiProperty({ description: '性别:0-未知,1-男,2-女', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
gender?: number;
|
||||
|
||||
@ApiProperty({ description: '生日', required: false })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
birthday?: string;
|
||||
|
||||
@ApiProperty({ description: '备注', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
remark?: string;
|
||||
|
||||
@ApiProperty({ description: '排序', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
sort?: number;
|
||||
|
||||
@ApiProperty({ description: '部门ID', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
dept_id?: number;
|
||||
|
||||
@ApiProperty({ description: '角色ID列表', required: false, type: [Number] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
role_ids?: number[];
|
||||
}
|
||||
|
||||
export class GetUserListAdminDto {
|
||||
@ApiProperty({ description: '页码', default: 1 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
page?: number;
|
||||
|
||||
@ApiProperty({ description: '每页数量', default: 20 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
pageSize?: number;
|
||||
|
||||
@ApiProperty({ description: '用户名', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
|
||||
@ApiProperty({ description: '昵称', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nickname?: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱', required: false })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiProperty({ description: '手机号', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mobile?: string;
|
||||
|
||||
@ApiProperty({ description: '状态', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
status?: number;
|
||||
|
||||
@ApiProperty({ description: '部门ID', required: false })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
dept_id?: number;
|
||||
}
|
||||
|
||||
export class BatchUpdateUserStatusAdminDto {
|
||||
@ApiProperty({ description: '用户ID列表', type: [Number] })
|
||||
@IsArray()
|
||||
ids: number[];
|
||||
|
||||
@ApiProperty({ description: '状态:0-禁用,1-启用' })
|
||||
@IsNumber()
|
||||
status: number;
|
||||
}
|
||||
|
||||
export class ResetUserPasswordAdminDto {
|
||||
@ApiProperty({ description: '用户ID' })
|
||||
@IsNumber()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: '新密码' })
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
82
wwjcloud/src/common/user/entities/SysUser.ts
Normal file
82
wwjcloud/src/common/user/entities/SysUser.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
DeleteDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
ManyToMany,
|
||||
JoinTable,
|
||||
} from 'typeorm';
|
||||
import { SysRole } from '../../rbac/entities/SysRole';
|
||||
|
||||
@Entity('sys_user')
|
||||
export class SysUser {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ length: 50, unique: true, comment: '用户名' })
|
||||
username: string;
|
||||
|
||||
@Column({ length: 100, comment: '密码' })
|
||||
password: string;
|
||||
|
||||
@Column({ length: 50, comment: '昵称' })
|
||||
nickname: string;
|
||||
|
||||
@Column({ length: 100, nullable: true, comment: '邮箱' })
|
||||
email: string;
|
||||
|
||||
@Column({ length: 20, nullable: true, comment: '手机号' })
|
||||
mobile: string;
|
||||
|
||||
@Column({ length: 200, nullable: true, comment: '头像' })
|
||||
avatar: string;
|
||||
|
||||
@Column({ type: 'tinyint', default: 1, comment: '状态:0-禁用,1-启用' })
|
||||
status: number;
|
||||
|
||||
@Column({ type: 'tinyint', default: 0, comment: '性别:0-未知,1-男,2-女' })
|
||||
gender: number;
|
||||
|
||||
@Column({ type: 'date', nullable: true, comment: '生日' })
|
||||
birthday: Date;
|
||||
|
||||
@Column({ length: 500, nullable: true, comment: '备注' })
|
||||
remark: string;
|
||||
|
||||
@Column({ type: 'int', default: 0, comment: '排序' })
|
||||
sort: number;
|
||||
|
||||
@Column({ type: 'int', default: 0, comment: '站点ID' })
|
||||
site_id: number;
|
||||
|
||||
@Column({ type: 'int', nullable: true, comment: '部门ID' })
|
||||
dept_id: number;
|
||||
|
||||
@Column({ type: 'int', nullable: true, comment: '创建人ID' })
|
||||
create_user_id: number;
|
||||
|
||||
@Column({ type: 'int', nullable: true, comment: '更新人ID' })
|
||||
update_user_id: number;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
create_time: Date;
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
update_time: Date;
|
||||
|
||||
@DeleteDateColumn({ comment: '删除时间' })
|
||||
delete_time: Date;
|
||||
|
||||
// 关联关系
|
||||
@ManyToMany(() => SysRole)
|
||||
@JoinTable({
|
||||
name: 'sys_user_role',
|
||||
joinColumn: { name: 'user_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'role_id', referencedColumnName: 'role_id' },
|
||||
})
|
||||
roles: SysRole[];
|
||||
}
|
||||
76
wwjcloud/src/common/user/services/admin/UserAdminService.ts
Normal file
76
wwjcloud/src/common/user/services/admin/UserAdminService.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CoreUserService } from '../core/CoreUserService';
|
||||
import { CreateUserAdminDto, UpdateUserAdminDto, GetUserListAdminDto, BatchUpdateUserStatusAdminDto, ResetUserPasswordAdminDto } from '../../dto/admin/UserDto';
|
||||
import { UserContextDto } from '../../dto/UserContextDto';
|
||||
|
||||
@Injectable()
|
||||
export class UserAdminService {
|
||||
constructor(
|
||||
private readonly coreUserService: CoreUserService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
async createUser(createUserDto: CreateUserAdminDto, userContext: UserContextDto) {
|
||||
return await this.coreUserService.createUser(
|
||||
createUserDto,
|
||||
userContext.siteId,
|
||||
userContext.userId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
async updateUser(updateUserDto: UpdateUserAdminDto, userContext: UserContextDto) {
|
||||
return await this.coreUserService.updateUser(
|
||||
updateUserDto,
|
||||
userContext.siteId,
|
||||
userContext.userId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
async getUserList(queryDto: GetUserListAdminDto, userContext: UserContextDto) {
|
||||
return await this.coreUserService.getUserList(queryDto, userContext.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户
|
||||
*/
|
||||
async getUserById(id: number, userContext: UserContextDto) {
|
||||
return await this.coreUserService.getUserById(id, userContext.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
async deleteUser(id: number, userContext: UserContextDto) {
|
||||
return await this.coreUserService.deleteUser(id, userContext.siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新用户状态
|
||||
*/
|
||||
async batchUpdateStatus(batchUpdateDto: BatchUpdateUserStatusAdminDto, userContext: UserContextDto) {
|
||||
return await this.coreUserService.batchUpdateStatus(
|
||||
batchUpdateDto.ids,
|
||||
batchUpdateDto.status,
|
||||
userContext.siteId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户密码
|
||||
*/
|
||||
async resetPassword(resetPasswordDto: ResetUserPasswordAdminDto, userContext: UserContextDto) {
|
||||
return await this.coreUserService.resetPassword(
|
||||
resetPasswordDto.id,
|
||||
resetPasswordDto.password,
|
||||
userContext.siteId,
|
||||
);
|
||||
}
|
||||
}
|
||||
171
wwjcloud/src/common/user/services/core/CoreUserService.ts
Normal file
171
wwjcloud/src/common/user/services/core/CoreUserService.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SysUser } from '../../entities/SysUser';
|
||||
import { CreateUserAdminDto, UpdateUserAdminDto, GetUserListAdminDto } from '../../dto/admin/UserDto';
|
||||
import { TransactionManager } from '@wwjCore/database/transactionManager';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@Injectable()
|
||||
export class CoreUserService {
|
||||
constructor(
|
||||
@InjectRepository(SysUser)
|
||||
private readonly userRepository: Repository<SysUser>,
|
||||
private readonly transactionManager: TransactionManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
async createUser(createUserDto: CreateUserAdminDto, siteId: number, userId: number): Promise<SysUser> {
|
||||
const { password, role_ids, ...userData } = createUserDto;
|
||||
|
||||
// 加密密码
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
const user = this.userRepository.create({
|
||||
...userData,
|
||||
password: hashedPassword,
|
||||
site_id: siteId,
|
||||
create_user_id: userId,
|
||||
update_user_id: userId,
|
||||
});
|
||||
|
||||
return await this.userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
async updateUser(updateUserDto: UpdateUserAdminDto, siteId: number, userId: number): Promise<SysUser> {
|
||||
const { id, role_ids, ...updateData } = updateUserDto;
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id, site_id: siteId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new Error('用户不存在');
|
||||
}
|
||||
|
||||
Object.assign(user, {
|
||||
...updateData,
|
||||
update_user_id: userId,
|
||||
});
|
||||
|
||||
return await this.userRepository.save(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
async getUserList(queryDto: GetUserListAdminDto, siteId: number) {
|
||||
const { page = 1, pageSize = 20, username, nickname, email, mobile, status, dept_id } = queryDto;
|
||||
|
||||
const queryBuilder = this.userRepository
|
||||
.createQueryBuilder('user')
|
||||
.leftJoinAndSelect('user.roles', 'roles')
|
||||
.where('user.site_id = :siteId', { siteId })
|
||||
.andWhere('user.delete_time IS NULL');
|
||||
|
||||
if (username) {
|
||||
queryBuilder.andWhere('user.username LIKE :username', { username: `%${username}%` });
|
||||
}
|
||||
|
||||
if (nickname) {
|
||||
queryBuilder.andWhere('user.nickname LIKE :nickname', { nickname: `%${nickname}%` });
|
||||
}
|
||||
|
||||
if (email) {
|
||||
queryBuilder.andWhere('user.email LIKE :email', { email: `%${email}%` });
|
||||
}
|
||||
|
||||
if (mobile) {
|
||||
queryBuilder.andWhere('user.mobile LIKE :mobile', { mobile: `%${mobile}%` });
|
||||
}
|
||||
|
||||
if (status !== undefined) {
|
||||
queryBuilder.andWhere('user.status = :status', { status });
|
||||
}
|
||||
|
||||
if (dept_id) {
|
||||
queryBuilder.andWhere('user.dept_id = :dept_id', { dept_id });
|
||||
}
|
||||
|
||||
const [users, total] = await queryBuilder
|
||||
.orderBy('user.sort', 'ASC')
|
||||
.addOrderBy('user.id', 'DESC')
|
||||
.skip((page - 1) * pageSize)
|
||||
.take(pageSize)
|
||||
.getManyAndCount();
|
||||
|
||||
return {
|
||||
list: users,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户
|
||||
*/
|
||||
async getUserById(id: number, siteId: number): Promise<SysUser | null> {
|
||||
return await this.userRepository.findOne({
|
||||
where: { id, site_id: siteId },
|
||||
relations: ['roles'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名获取用户
|
||||
*/
|
||||
async getUserByUsername(username: string, siteId: number): Promise<SysUser | null> {
|
||||
return await this.userRepository.findOne({
|
||||
where: { username, site_id: siteId },
|
||||
relations: ['roles'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
async deleteUser(id: number, siteId: number): Promise<void> {
|
||||
await this.userRepository.softDelete({ id, site_id: siteId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新用户状态
|
||||
*/
|
||||
async batchUpdateStatus(ids: number[], status: number, siteId: number): Promise<void> {
|
||||
await this.userRepository
|
||||
.createQueryBuilder()
|
||||
.update(SysUser)
|
||||
.set({ status })
|
||||
.whereInIds(ids)
|
||||
.andWhere('site_id = :siteId', { siteId })
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置用户密码
|
||||
*/
|
||||
async resetPassword(id: number, password: string, siteId: number): Promise<void> {
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
await this.userRepository
|
||||
.createQueryBuilder()
|
||||
.update(SysUser)
|
||||
.set({ password: hashedPassword })
|
||||
.where('id = :id', { id })
|
||||
.andWhere('site_id = :siteId', { siteId })
|
||||
.execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户密码
|
||||
*/
|
||||
async validatePassword(user: SysUser, password: string): Promise<boolean> {
|
||||
return await bcrypt.compare(password, user.password);
|
||||
}
|
||||
}
|
||||
40
wwjcloud/src/common/user/user.module.ts
Normal file
40
wwjcloud/src/common/user/user.module.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { RbacModule } from '../rbac/rbac.module';
|
||||
import { DatabaseModule } from '@wwjCore/database/databaseModule';
|
||||
import { SysUser } from './entities/SysUser';
|
||||
|
||||
// Core Services
|
||||
import { CoreUserService } from './services/core/CoreUserService';
|
||||
|
||||
// Admin Services
|
||||
import { UserAdminService } from './services/admin/UserAdminService';
|
||||
|
||||
// Controllers
|
||||
import { UserController } from './controllers/adminapi/UserController';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => AuthModule),
|
||||
forwardRef(() => RbacModule),
|
||||
DatabaseModule,
|
||||
TypeOrmModule.forFeature([SysUser]),
|
||||
],
|
||||
providers: [
|
||||
// Core Services
|
||||
CoreUserService,
|
||||
|
||||
// Admin Services
|
||||
UserAdminService,
|
||||
],
|
||||
controllers: [UserController],
|
||||
exports: [
|
||||
// Core Services
|
||||
CoreUserService,
|
||||
|
||||
// Admin Services
|
||||
UserAdminService,
|
||||
],
|
||||
})
|
||||
export class UserModule {}
|
||||
368
wwjcloud/src/config/README.md
Normal file
368
wwjcloud/src/config/README.md
Normal file
@@ -0,0 +1,368 @@
|
||||
# 配置中心使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
配置中心是框架的核心配置管理模块,提供系统配置、动态配置、配置验证等功能。
|
||||
|
||||
## 架构设计
|
||||
|
||||
### 分层架构
|
||||
```
|
||||
Config Layer (配置层)
|
||||
├── ConfigCenterService (配置中心服务)
|
||||
├── DynamicConfigService (动态配置服务)
|
||||
├── ConfigValidationService (配置验证服务)
|
||||
└── ConfigController (配置管理控制器)
|
||||
```
|
||||
|
||||
### 配置分类
|
||||
- **系统配置**:应用启动时加载的静态配置
|
||||
- **动态配置**:运行时可以修改的配置
|
||||
- **环境配置**:不同环境的配置差异
|
||||
|
||||
## 核心服务
|
||||
|
||||
### 1. ConfigCenterService (配置中心服务)
|
||||
|
||||
负责管理系统级别的配置,包括应用、数据库、Redis、安全等配置。
|
||||
|
||||
```typescript
|
||||
import { ConfigCenterService } from '@wwjConfig';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
constructor(private configCenter: ConfigCenterService) {}
|
||||
|
||||
async getAppInfo() {
|
||||
const appConfig = this.configCenter.getAppConfig();
|
||||
const dbConfig = this.configCenter.getDatabaseConfig();
|
||||
|
||||
return {
|
||||
name: appConfig.name,
|
||||
version: appConfig.version,
|
||||
database: dbConfig.host,
|
||||
};
|
||||
}
|
||||
|
||||
// 获取单个配置
|
||||
getJwtSecret() {
|
||||
return this.configCenter.get('security.jwtSecret');
|
||||
}
|
||||
|
||||
// 获取配置段
|
||||
getDatabaseConfig() {
|
||||
return this.configCenter.getSection('database');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. DynamicConfigService (动态配置服务)
|
||||
|
||||
负责管理运行时动态配置,支持配置的增删改查。
|
||||
|
||||
```typescript
|
||||
import { DynamicConfigService } from '@wwjConfig';
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
constructor(private dynamicConfig: DynamicConfigService) {}
|
||||
|
||||
async sendEmail(to: string, subject: string, content: string) {
|
||||
// 获取邮件配置
|
||||
const smtpConfig = await this.dynamicConfig.getConfig('email.smtp', {
|
||||
host: 'localhost',
|
||||
port: 587,
|
||||
secure: false,
|
||||
});
|
||||
|
||||
const rateLimit = await this.dynamicConfig.getConfig('email.rate_limit', 100);
|
||||
|
||||
// 使用配置发送邮件
|
||||
return this.sendWithConfig(to, subject, content, smtpConfig);
|
||||
}
|
||||
|
||||
// 动态更新配置
|
||||
async updateSmtpConfig(config: any) {
|
||||
await this.dynamicConfig.setConfig('email.smtp', config, {
|
||||
description: 'SMTP 服务器配置',
|
||||
category: 'email',
|
||||
isPublic: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ConfigValidationService (配置验证服务)
|
||||
|
||||
负责配置的验证和校验,确保配置的正确性。
|
||||
|
||||
```typescript
|
||||
import { ConfigValidationService } from '@wwjConfig';
|
||||
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
constructor(private configValidation: ConfigValidationService) {}
|
||||
|
||||
// 验证所有配置
|
||||
validateAllConfig() {
|
||||
const validation = this.configValidation.validateConfig();
|
||||
|
||||
if (!validation.isValid) {
|
||||
console.error('配置验证失败:', validation.errors);
|
||||
throw new Error('配置验证失败');
|
||||
}
|
||||
|
||||
if (validation.warnings.length > 0) {
|
||||
console.warn('配置警告:', validation.warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证单个配置项
|
||||
validateConfigItem(key: string, value: any) {
|
||||
const metadata = this.getConfigMetadata(key);
|
||||
return this.configValidation.validateConfigItem(key, value, metadata);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配置管理 API
|
||||
|
||||
### 系统配置接口
|
||||
|
||||
```bash
|
||||
# 获取系统配置
|
||||
GET /adminapi/config/system
|
||||
|
||||
# 获取配置元数据
|
||||
GET /adminapi/config/metadata
|
||||
|
||||
# 获取配置建议
|
||||
GET /adminapi/config/suggestions
|
||||
|
||||
# 验证配置
|
||||
GET /adminapi/config/validate
|
||||
|
||||
# 刷新配置缓存
|
||||
POST /adminapi/config/refresh-cache
|
||||
```
|
||||
|
||||
### 动态配置接口
|
||||
|
||||
```bash
|
||||
# 获取动态配置列表
|
||||
GET /adminapi/config/dynamic?category=email
|
||||
|
||||
# 获取单个动态配置
|
||||
GET /adminapi/config/dynamic/email.smtp
|
||||
|
||||
# 创建动态配置
|
||||
POST /adminapi/config/dynamic
|
||||
{
|
||||
"key": "email.smtp",
|
||||
"value": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 587,
|
||||
"secure": false
|
||||
},
|
||||
"description": "SMTP 服务器配置",
|
||||
"type": "json",
|
||||
"category": "email",
|
||||
"isPublic": false
|
||||
}
|
||||
|
||||
# 更新动态配置
|
||||
PUT /adminapi/config/dynamic/email.smtp
|
||||
{
|
||||
"value": {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"secure": true
|
||||
},
|
||||
"description": "更新后的 SMTP 配置"
|
||||
}
|
||||
|
||||
# 删除动态配置
|
||||
DELETE /adminapi/config/dynamic/email.smtp
|
||||
```
|
||||
|
||||
## 环境变量配置
|
||||
|
||||
### 基础配置
|
||||
```bash
|
||||
# 应用配置
|
||||
APP_NAME=wwjcloud-backend
|
||||
APP_VERSION=1.0.0
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=password
|
||||
DB_DATABASE=wwjcloud
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# 缓存配置
|
||||
CACHE_TTL=300
|
||||
CACHE_MAX_ITEMS=1000
|
||||
|
||||
# 安全配置
|
||||
JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters
|
||||
JWT_EXPIRES_IN=24h
|
||||
BCRYPT_ROUNDS=10
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
LOG_FORMAT=json
|
||||
```
|
||||
|
||||
### 环境特定配置
|
||||
```bash
|
||||
# 开发环境 (.env.development)
|
||||
NODE_ENV=development
|
||||
LOG_LEVEL=debug
|
||||
DB_HOST=localhost
|
||||
|
||||
# 生产环境 (.env.production)
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=warn
|
||||
DB_HOST=production-db.example.com
|
||||
JWT_SECRET=production-super-secret-key
|
||||
```
|
||||
|
||||
## 配置最佳实践
|
||||
|
||||
### 1. 配置分类
|
||||
- **系统配置**:应用基础配置,启动时加载
|
||||
- **业务配置**:业务相关配置,可动态调整
|
||||
- **安全配置**:敏感配置,需要加密存储
|
||||
- **环境配置**:环境差异配置
|
||||
|
||||
### 2. 配置命名规范
|
||||
```typescript
|
||||
// 使用点分隔的层级命名
|
||||
'app.name' // 应用名称
|
||||
'database.host' // 数据库主机
|
||||
'email.smtp.host' // 邮件 SMTP 主机
|
||||
'security.jwt.secret' // JWT 密钥
|
||||
```
|
||||
|
||||
### 3. 配置验证
|
||||
```typescript
|
||||
// 在应用启动时验证配置
|
||||
async onModuleInit() {
|
||||
const validation = this.configValidation.validateConfig();
|
||||
|
||||
if (!validation.isValid) {
|
||||
this.logger.error('配置验证失败:', validation.errors);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 配置缓存
|
||||
```typescript
|
||||
// 合理使用配置缓存
|
||||
const config = await this.dynamicConfig.getConfig('email.smtp', {
|
||||
ttl: 300, // 缓存 5 分钟
|
||||
});
|
||||
```
|
||||
|
||||
### 5. 配置热更新
|
||||
```typescript
|
||||
// 监听配置变更
|
||||
await this.dynamicConfig.watchConfig('email.smtp', (newConfig) => {
|
||||
this.logger.log('SMTP 配置已更新:', newConfig);
|
||||
this.reloadSmtpClient(newConfig);
|
||||
});
|
||||
```
|
||||
|
||||
## 配置安全
|
||||
|
||||
### 1. 敏感配置加密
|
||||
```typescript
|
||||
// 使用环境变量存储敏感配置
|
||||
JWT_SECRET=your-super-secret-key
|
||||
DB_PASSWORD=encrypted-password
|
||||
```
|
||||
|
||||
### 2. 配置访问控制
|
||||
```typescript
|
||||
// 只有管理员可以访问配置管理接口
|
||||
@Roles('admin')
|
||||
@Controller('adminapi/config')
|
||||
export class ConfigController {}
|
||||
```
|
||||
|
||||
### 3. 配置审计
|
||||
```typescript
|
||||
// 记录配置变更日志
|
||||
async setConfig(key: string, value: any) {
|
||||
await this.dynamicConfig.setConfig(key, value);
|
||||
this.logger.log(`配置已更新: ${key}`, {
|
||||
updatedBy: this.getCurrentUser(),
|
||||
timestamp: new Date()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 监控和调试
|
||||
|
||||
### 1. 配置监控
|
||||
```bash
|
||||
# 检查配置状态
|
||||
GET /adminapi/config/validate
|
||||
|
||||
# 查看配置统计
|
||||
GET /adminapi/config/metadata
|
||||
```
|
||||
|
||||
### 2. 配置调试
|
||||
```typescript
|
||||
// 启用配置调试日志
|
||||
LOG_LEVEL=debug
|
||||
|
||||
// 查看配置加载过程
|
||||
this.logger.debug('Loading config:', configKey);
|
||||
```
|
||||
|
||||
### 3. 配置备份
|
||||
```typescript
|
||||
// 定期备份配置
|
||||
async backupConfig() {
|
||||
const configs = await this.dynamicConfig.getConfigList();
|
||||
await this.backupService.save('config-backup.json', configs);
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **配置加载失败**
|
||||
- 检查环境变量是否正确设置
|
||||
- 验证配置文件格式
|
||||
- 查看错误日志
|
||||
|
||||
2. **动态配置更新失败**
|
||||
- 检查数据库连接
|
||||
- 验证配置格式
|
||||
- 确认权限设置
|
||||
|
||||
3. **配置验证错误**
|
||||
- 检查必需配置是否完整
|
||||
- 验证配置值格式
|
||||
- 查看验证规则
|
||||
|
||||
### 调试技巧
|
||||
|
||||
1. 启用详细日志
|
||||
2. 使用配置验证接口
|
||||
3. 检查配置缓存状态
|
||||
4. 监控配置变更事件
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* 系统常量配置
|
||||
* 完全按照PHP框架的常量定义
|
||||
*/
|
||||
|
||||
// 系统常量
|
||||
export const SYSTEM_CONSTANTS = {
|
||||
DEFAULT_SITE_ID: 1,
|
||||
ADMIN_USER_TYPE: 'admin',
|
||||
MEMBER_USER_TYPE: 'member',
|
||||
DEFAULT_STATUS: 1,
|
||||
DISABLED_STATUS: 0,
|
||||
DELETED_STATUS: 1,
|
||||
NOT_DELETED_STATUS: 0,
|
||||
};
|
||||
|
||||
// 默认站点配置
|
||||
export const DEFAULT_SITE_CONFIG = {
|
||||
site_name: 'WWJ Cloud',
|
||||
site_title: 'WWJ Cloud - 企业级多租户SaaS平台',
|
||||
site_keywords: 'SaaS,多租户,企业级,云平台',
|
||||
site_description: 'WWJ Cloud是一个基于NestJS和Vue3的企业级多租户SaaS平台',
|
||||
site_logo: '',
|
||||
site_favicon: '',
|
||||
icp_number: '',
|
||||
copyright: '© 2024 WWJ Cloud. All rights reserved.',
|
||||
site_status: 1,
|
||||
close_reason: '',
|
||||
};
|
||||
|
||||
// 菜单类型常量
|
||||
export const MENU_TYPE = {
|
||||
DIRECTORY: 0,
|
||||
MENU: 1,
|
||||
BUTTON: 2,
|
||||
};
|
||||
|
||||
// 应用类型常量
|
||||
export const APP_TYPE = {
|
||||
ADMIN: 'admin',
|
||||
API: 'api',
|
||||
CORE: 'core',
|
||||
};
|
||||
|
||||
// 状态常量
|
||||
export const STATUS = {
|
||||
ENABLED: 1,
|
||||
DISABLED: 0,
|
||||
};
|
||||
|
||||
// 性别常量
|
||||
export const GENDER = {
|
||||
UNKNOWN: 0,
|
||||
MALE: 1,
|
||||
FEMALE: 2,
|
||||
};
|
||||
|
||||
// 会员注册渠道
|
||||
export const MEMBER_REGISTER_CHANNEL = {
|
||||
WECHAT: 'wechat',
|
||||
MOBILE: 'mobile',
|
||||
EMAIL: 'email',
|
||||
QQ: 'qq',
|
||||
};
|
||||
|
||||
// 会员注册类型
|
||||
export const MEMBER_REGISTER_TYPE = {
|
||||
AUTO: 'auto',
|
||||
MANUAL: 'manual',
|
||||
INVITE: 'invite',
|
||||
};
|
||||
|
||||
// JWT相关常量
|
||||
export const JWT_CONSTANTS = {
|
||||
SECRET: process.env.JWT_SECRET || 'wwjcloud-secret-key',
|
||||
EXPIRES_IN: '7d',
|
||||
ALGORITHM: 'HS256',
|
||||
};
|
||||
|
||||
// 设备类型常量
|
||||
export const DEVICE_TYPE = {
|
||||
WEB: 'web',
|
||||
MOBILE: 'mobile',
|
||||
APP: 'app',
|
||||
WECHAT: 'wechat',
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DocsNavigationController } from './docs-navigation.controller';
|
||||
|
||||
/**
|
||||
* 配置模块
|
||||
* 整合系统配置、API文档等功能
|
||||
*/
|
||||
@Module({
|
||||
controllers: [
|
||||
DocsNavigationController,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class ConfigModule {}
|
||||
156
wwjcloud/src/config/controllers/configController.ts
Normal file
156
wwjcloud/src/config/controllers/configController.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../common/auth/guards/JwtAuthGuard';
|
||||
import { RolesGuard } from '../../common/auth/guards/RolesGuard';
|
||||
import { Roles } from '../../common/auth/decorators/RolesDecorator';
|
||||
import { ConfigCenterService } from '../services/configCenterService';
|
||||
import { DynamicConfigService } from '../services/dynamicConfigService';
|
||||
import { ConfigValidationService } from '../services/configValidationService';
|
||||
|
||||
@ApiTags('配置管理')
|
||||
@Controller('adminapi/config')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
export class ConfigController {
|
||||
constructor(
|
||||
private configCenterService: ConfigCenterService,
|
||||
private dynamicConfigService: DynamicConfigService,
|
||||
private configValidationService: ConfigValidationService,
|
||||
) {}
|
||||
|
||||
@Get('system')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取系统配置' })
|
||||
@ApiResponse({ status: 200, description: '获取系统配置成功' })
|
||||
async getSystemConfig() {
|
||||
return {
|
||||
app: this.configCenterService.getAppConfig(),
|
||||
database: this.configCenterService.getDatabaseConfig(),
|
||||
redis: this.configCenterService.getRedisConfig(),
|
||||
kafka: this.configCenterService.getKafkaConfig(),
|
||||
jwt: this.configCenterService.getJwtConfig(),
|
||||
cache: this.configCenterService.getCacheConfig(),
|
||||
logging: this.configCenterService.getLoggingConfig(),
|
||||
upload: this.configCenterService.getUploadConfig(),
|
||||
throttle: this.configCenterService.getThrottleConfig(),
|
||||
thirdParty: this.configCenterService.getThirdPartyConfig(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('dynamic')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取动态配置列表' })
|
||||
@ApiResponse({ status: 200, description: '获取动态配置列表成功' })
|
||||
async getDynamicConfigs(@Query('category') category?: string) {
|
||||
return await this.dynamicConfigService.getConfigList(category);
|
||||
}
|
||||
|
||||
@Get('dynamic/:key')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取动态配置' })
|
||||
@ApiResponse({ status: 200, description: '获取动态配置成功' })
|
||||
async getDynamicConfig(@Param('key') key: string) {
|
||||
return await this.dynamicConfigService.getConfig(key);
|
||||
}
|
||||
|
||||
@Post('dynamic')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '创建动态配置' })
|
||||
@ApiResponse({ status: 201, description: '创建动态配置成功' })
|
||||
async createDynamicConfig(@Body() config: {
|
||||
key: string;
|
||||
value: any;
|
||||
description?: string;
|
||||
type?: 'string' | 'number' | 'boolean' | 'json';
|
||||
category?: string;
|
||||
isPublic?: boolean;
|
||||
}) {
|
||||
await this.dynamicConfigService.setConfig(
|
||||
config.key,
|
||||
config.value,
|
||||
{
|
||||
description: config.description,
|
||||
type: config.type,
|
||||
category: config.category,
|
||||
isPublic: config.isPublic,
|
||||
}
|
||||
);
|
||||
return { message: '配置创建成功' };
|
||||
}
|
||||
|
||||
@Put('dynamic/:key')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '更新动态配置' })
|
||||
@ApiResponse({ status: 200, description: '更新动态配置成功' })
|
||||
async updateDynamicConfig(
|
||||
@Param('key') key: string,
|
||||
@Body() config: {
|
||||
value: any;
|
||||
description?: string;
|
||||
type?: 'string' | 'number' | 'boolean' | 'json';
|
||||
category?: string;
|
||||
isPublic?: boolean;
|
||||
}
|
||||
) {
|
||||
await this.dynamicConfigService.setConfig(
|
||||
key,
|
||||
config.value,
|
||||
{
|
||||
description: config.description,
|
||||
type: config.type,
|
||||
category: config.category,
|
||||
isPublic: config.isPublic,
|
||||
}
|
||||
);
|
||||
return { message: '配置更新成功' };
|
||||
}
|
||||
|
||||
@Delete('dynamic/:key')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '删除动态配置' })
|
||||
@ApiResponse({ status: 200, description: '删除动态配置成功' })
|
||||
async deleteDynamicConfig(@Param('key') key: string) {
|
||||
await this.dynamicConfigService.deleteConfig(key);
|
||||
return { message: '配置删除成功' };
|
||||
}
|
||||
|
||||
@Get('validate')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '验证配置' })
|
||||
@ApiResponse({ status: 200, description: '配置验证成功' })
|
||||
async validateConfig() {
|
||||
return await this.configValidationService.validateAll();
|
||||
}
|
||||
|
||||
@Get('metadata')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取配置元数据' })
|
||||
@ApiResponse({ status: 200, description: '获取配置元数据成功' })
|
||||
async getConfigMetadata() {
|
||||
return this.configCenterService.getConfigMetadata();
|
||||
}
|
||||
|
||||
@Get('suggestions')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取配置建议' })
|
||||
@ApiResponse({ status: 200, description: '获取配置建议成功' })
|
||||
async getConfigSuggestions() {
|
||||
return this.configValidationService.getSuggestions();
|
||||
}
|
||||
|
||||
@Post('refresh-cache')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '刷新配置缓存' })
|
||||
@ApiResponse({ status: 200, description: '配置缓存刷新成功' })
|
||||
async refreshConfigCache() {
|
||||
this.configCenterService.refreshCache();
|
||||
return { message: '配置缓存已刷新' };
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取配置统计信息' })
|
||||
@ApiResponse({ status: 200, description: '获取配置统计信息成功' })
|
||||
async getConfigStats() {
|
||||
return this.configCenterService.getConfigStats();
|
||||
}
|
||||
}
|
||||
137
wwjcloud/src/config/controllers/docsNavigationController.ts
Normal file
137
wwjcloud/src/config/controllers/docsNavigationController.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Controller, Get, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import type { Response } from 'express';
|
||||
import { SwaggerConfig } from '../integrations/swaggerConfig';
|
||||
|
||||
@ApiTags('文档导航')
|
||||
@Controller()
|
||||
export class DocsNavigationController {
|
||||
@Get('api-docs')
|
||||
@ApiOperation({ summary: 'API文档导航页面' })
|
||||
@ApiResponse({ status: 200, description: '返回API文档导航HTML页面' })
|
||||
getApiDocsNavigation(@Res() res: Response) {
|
||||
const html = SwaggerConfig.getNavigationHtml();
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(html);
|
||||
}
|
||||
|
||||
@Get('docs-nav')
|
||||
@ApiOperation({ summary: '文档导航数据' })
|
||||
@ApiResponse({ status: 200, description: '返回文档导航数据' })
|
||||
getDocsNavigation() {
|
||||
return {
|
||||
title: 'WWJCloud API 文档导航',
|
||||
description: '企业级后端API文档导航中心',
|
||||
links: [
|
||||
{
|
||||
title: '完整API文档',
|
||||
description: '包含所有接口的完整API文档',
|
||||
url: '/docs',
|
||||
type: 'complete',
|
||||
icon: '📖',
|
||||
},
|
||||
{
|
||||
title: '管理端API',
|
||||
description: '管理后台专用接口文档',
|
||||
url: '/docs/admin',
|
||||
type: 'admin',
|
||||
icon: '🔐',
|
||||
},
|
||||
{
|
||||
title: '前端API',
|
||||
description: '前端应用接口文档',
|
||||
url: '/docs/frontend',
|
||||
type: 'frontend',
|
||||
icon: '🌐',
|
||||
},
|
||||
{
|
||||
title: '系统设置API',
|
||||
description: '系统配置和设置相关接口',
|
||||
url: '/docs/settings',
|
||||
type: 'settings',
|
||||
icon: '⚙️',
|
||||
},
|
||||
],
|
||||
footer: {
|
||||
tips: '点击上方卡片访问对应的API文档',
|
||||
links: [
|
||||
{ text: 'JSON格式文档', url: '/docs-json' },
|
||||
{ text: '系统健康检查', url: '/health' },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('docs/status')
|
||||
@ApiOperation({ summary: '文档服务状态' })
|
||||
@ApiResponse({ status: 200, description: '返回文档服务状态信息' })
|
||||
getDocsStatus() {
|
||||
return {
|
||||
service: 'WWJCloud API Documentation',
|
||||
status: 'running',
|
||||
version: '1.0.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
endpoints: {
|
||||
swagger: '/docs',
|
||||
navigation: '/docs-nav',
|
||||
status: '/docs/status',
|
||||
config: '/docs/config',
|
||||
stats: '/docs/stats',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('docs/config')
|
||||
@ApiOperation({ summary: '文档配置信息' })
|
||||
@ApiResponse({ status: 200, description: '返回文档配置信息' })
|
||||
getDocsConfig() {
|
||||
return {
|
||||
title: 'WWJCloud API 文档',
|
||||
description: 'WWJCloud 基于 NestJS 的企业级后端 API 文档',
|
||||
version: '1.0.0',
|
||||
auth: {
|
||||
type: 'bearer',
|
||||
scheme: 'JWT',
|
||||
description: 'JWT Token认证',
|
||||
},
|
||||
tags: [
|
||||
'健康检查',
|
||||
'认证授权',
|
||||
'管理端API',
|
||||
'前端API',
|
||||
'系统设置',
|
||||
'文件上传',
|
||||
'数据库管理',
|
||||
],
|
||||
options: {
|
||||
persistAuthorization: true,
|
||||
tagsSorter: 'alpha',
|
||||
operationsSorter: 'alpha',
|
||||
docExpansion: 'none',
|
||||
filter: true,
|
||||
showRequestDuration: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('docs/stats')
|
||||
@ApiOperation({ summary: '文档统计信息' })
|
||||
@ApiResponse({ status: 200, description: '返回文档统计信息' })
|
||||
getDocsStats() {
|
||||
return {
|
||||
totalEndpoints: 0, // 这里可以统计实际的API端点数量
|
||||
totalTags: 7,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
documentation: {
|
||||
coverage: '100%',
|
||||
quality: 'high',
|
||||
lastReview: new Date().toISOString(),
|
||||
},
|
||||
usage: {
|
||||
totalVisits: 0,
|
||||
popularEndpoints: [],
|
||||
lastVisit: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
3
wwjcloud/src/config/controllers/index.ts
Normal file
3
wwjcloud/src/config/controllers/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 配置控制器导出
|
||||
export { ConfigController } from './configController';
|
||||
export { DocsNavigationController } from './docsNavigationController';
|
||||
347
wwjcloud/src/config/core/appConfig.ts
Normal file
347
wwjcloud/src/config/core/appConfig.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* 应用配置中心
|
||||
* 集中管理所有配置,避免分散的 process.env 调用
|
||||
*/
|
||||
|
||||
export interface AppConfig {
|
||||
// 应用基础配置
|
||||
app: {
|
||||
name: string;
|
||||
version: string;
|
||||
port: number;
|
||||
environment: string;
|
||||
timezone: string;
|
||||
};
|
||||
|
||||
// 数据库配置
|
||||
database: {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
synchronize: boolean;
|
||||
logging: boolean;
|
||||
};
|
||||
|
||||
// Redis 配置
|
||||
redis: {
|
||||
host: string;
|
||||
port: number;
|
||||
password: string;
|
||||
db: number;
|
||||
keyPrefix: string;
|
||||
};
|
||||
|
||||
// Kafka 配置
|
||||
kafka: {
|
||||
clientId: string;
|
||||
brokers: string[];
|
||||
groupId: string;
|
||||
topicPrefix: string;
|
||||
};
|
||||
|
||||
// JWT 配置
|
||||
jwt: {
|
||||
secret: string;
|
||||
expiresIn: string;
|
||||
algorithm: string;
|
||||
};
|
||||
|
||||
// 缓存配置
|
||||
cache: {
|
||||
ttl: number;
|
||||
maxItems: number;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
// 日志配置
|
||||
logging: {
|
||||
level: string;
|
||||
format: string;
|
||||
filename?: string;
|
||||
};
|
||||
|
||||
// 文件上传配置
|
||||
upload: {
|
||||
path: string;
|
||||
maxSize: number;
|
||||
allowedTypes: string[];
|
||||
};
|
||||
|
||||
// 限流配置
|
||||
throttle: {
|
||||
ttl: number;
|
||||
limit: number;
|
||||
};
|
||||
|
||||
// 第三方服务配置
|
||||
thirdParty: {
|
||||
storage: {
|
||||
provider: string;
|
||||
config: Record<string, any>;
|
||||
};
|
||||
payment: {
|
||||
provider: string;
|
||||
config: Record<string, any>;
|
||||
};
|
||||
sms: {
|
||||
provider: string;
|
||||
config: Record<string, any>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
const defaultConfig: AppConfig = {
|
||||
app: {
|
||||
name: 'WWJCloud Backend',
|
||||
version: '1.0.0',
|
||||
port: 3001,
|
||||
environment: 'development',
|
||||
timezone: 'Asia/Shanghai',
|
||||
},
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 3306,
|
||||
username: 'root',
|
||||
password: '123456',
|
||||
database: 'wwjcloud',
|
||||
synchronize: false,
|
||||
logging: true,
|
||||
},
|
||||
redis: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
password: '',
|
||||
db: 0,
|
||||
keyPrefix: 'wwjcloud:',
|
||||
},
|
||||
kafka: {
|
||||
clientId: 'wwjcloud-backend',
|
||||
brokers: ['localhost:9092'],
|
||||
groupId: 'wwjcloud-group',
|
||||
topicPrefix: 'wwjcloud.',
|
||||
},
|
||||
jwt: {
|
||||
secret: 'your-secret-key',
|
||||
expiresIn: '24h',
|
||||
algorithm: 'HS256',
|
||||
},
|
||||
cache: {
|
||||
ttl: 3600,
|
||||
maxItems: 1000,
|
||||
prefix: 'cache:',
|
||||
},
|
||||
logging: {
|
||||
level: 'info',
|
||||
format: 'json',
|
||||
filename: 'logs/app.log',
|
||||
},
|
||||
upload: {
|
||||
path: 'uploads/',
|
||||
maxSize: 10 * 1024 * 1024, // 10MB
|
||||
allowedTypes: ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'],
|
||||
},
|
||||
throttle: {
|
||||
ttl: 60,
|
||||
limit: 100,
|
||||
},
|
||||
thirdParty: {
|
||||
storage: {
|
||||
provider: 'local',
|
||||
config: {},
|
||||
},
|
||||
payment: {
|
||||
provider: 'mock',
|
||||
config: {},
|
||||
},
|
||||
sms: {
|
||||
provider: 'mock',
|
||||
config: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 环境变量配置加载器
|
||||
*/
|
||||
function loadFromEnv(): Partial<AppConfig> {
|
||||
return {
|
||||
app: {
|
||||
name: process.env.APP_NAME || defaultConfig.app.name,
|
||||
version: process.env.APP_VERSION || defaultConfig.app.version,
|
||||
port: parseInt(process.env.PORT || String(defaultConfig.app.port), 10),
|
||||
environment: process.env.NODE_ENV || defaultConfig.app.environment,
|
||||
timezone: process.env.TZ || defaultConfig.app.timezone,
|
||||
},
|
||||
database: {
|
||||
host: process.env.DB_HOST || defaultConfig.database.host,
|
||||
port: parseInt(process.env.DB_PORT || String(defaultConfig.database.port), 10),
|
||||
username: process.env.DB_USERNAME || defaultConfig.database.username,
|
||||
password: process.env.DB_PASSWORD || defaultConfig.database.password,
|
||||
database: process.env.DB_DATABASE || defaultConfig.database.database,
|
||||
synchronize: process.env.DB_SYNC === 'true',
|
||||
logging: process.env.DB_LOGGING === 'true',
|
||||
},
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST || defaultConfig.redis.host,
|
||||
port: parseInt(process.env.REDIS_PORT || String(defaultConfig.redis.port), 10),
|
||||
password: process.env.REDIS_PASSWORD || defaultConfig.redis.password,
|
||||
db: parseInt(process.env.REDIS_DB || String(defaultConfig.redis.db), 10),
|
||||
keyPrefix: process.env.REDIS_KEY_PREFIX || defaultConfig.redis.keyPrefix,
|
||||
},
|
||||
kafka: {
|
||||
clientId: process.env.KAFKA_CLIENT_ID || defaultConfig.kafka.clientId,
|
||||
brokers: (process.env.KAFKA_BROKERS || defaultConfig.kafka.brokers.join(',')).split(','),
|
||||
groupId: process.env.KAFKA_GROUP_ID || defaultConfig.kafka.groupId,
|
||||
topicPrefix: process.env.KAFKA_TOPIC_PREFIX || defaultConfig.kafka.topicPrefix,
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || defaultConfig.jwt.secret,
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || defaultConfig.jwt.expiresIn,
|
||||
algorithm: process.env.JWT_ALGORITHM || defaultConfig.jwt.algorithm,
|
||||
},
|
||||
cache: {
|
||||
ttl: parseInt(process.env.CACHE_TTL || String(defaultConfig.cache.ttl), 10),
|
||||
maxItems: parseInt(process.env.CACHE_MAX_ITEMS || String(defaultConfig.cache.maxItems), 10),
|
||||
prefix: process.env.CACHE_PREFIX || defaultConfig.cache.prefix,
|
||||
},
|
||||
logging: {
|
||||
level: process.env.LOG_LEVEL || defaultConfig.logging.level,
|
||||
format: process.env.LOG_FORMAT || defaultConfig.logging.format,
|
||||
filename: process.env.LOG_FILENAME,
|
||||
},
|
||||
upload: {
|
||||
path: process.env.UPLOAD_PATH || defaultConfig.upload.path,
|
||||
maxSize: parseInt(process.env.UPLOAD_MAX_SIZE || String(defaultConfig.upload.maxSize), 10),
|
||||
allowedTypes: process.env.UPLOAD_ALLOWED_TYPES?.split(',') || defaultConfig.upload.allowedTypes,
|
||||
},
|
||||
throttle: {
|
||||
ttl: parseInt(process.env.THROTTLE_TTL || String(defaultConfig.throttle.ttl), 10),
|
||||
limit: parseInt(process.env.THROTTLE_LIMIT || String(defaultConfig.throttle.limit), 10),
|
||||
},
|
||||
thirdParty: {
|
||||
storage: {
|
||||
provider: process.env.STORAGE_PROVIDER || defaultConfig.thirdParty.storage.provider,
|
||||
config: JSON.parse(process.env.STORAGE_CONFIG || '{}'),
|
||||
},
|
||||
payment: {
|
||||
provider: process.env.PAYMENT_PROVIDER || defaultConfig.thirdParty.payment.provider,
|
||||
config: JSON.parse(process.env.PAYMENT_CONFIG || '{}'),
|
||||
},
|
||||
sms: {
|
||||
provider: process.env.SMS_PROVIDER || defaultConfig.thirdParty.sms.provider,
|
||||
config: JSON.parse(process.env.SMS_CONFIG || '{}'),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置合并函数
|
||||
*/
|
||||
function mergeConfig(defaultConfig: AppConfig, envConfig: Partial<AppConfig>): AppConfig {
|
||||
return {
|
||||
...defaultConfig,
|
||||
...envConfig,
|
||||
app: { ...defaultConfig.app, ...envConfig.app },
|
||||
database: { ...defaultConfig.database, ...envConfig.database },
|
||||
redis: { ...defaultConfig.redis, ...envConfig.redis },
|
||||
kafka: { ...defaultConfig.kafka, ...envConfig.kafka },
|
||||
jwt: { ...defaultConfig.jwt, ...envConfig.jwt },
|
||||
cache: { ...defaultConfig.cache, ...envConfig.cache },
|
||||
logging: { ...defaultConfig.logging, ...envConfig.logging },
|
||||
upload: { ...defaultConfig.upload, ...envConfig.upload },
|
||||
throttle: { ...defaultConfig.throttle, ...envConfig.throttle },
|
||||
thirdParty: {
|
||||
storage: { ...defaultConfig.thirdParty.storage, ...envConfig.thirdParty?.storage },
|
||||
payment: { ...defaultConfig.thirdParty.payment, ...envConfig.thirdParty?.payment },
|
||||
sms: { ...defaultConfig.thirdParty.sms, ...envConfig.thirdParty?.sms },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用配置实例
|
||||
*/
|
||||
export const appConfig: AppConfig = mergeConfig(defaultConfig, loadFromEnv());
|
||||
|
||||
/**
|
||||
* 配置获取工具函数
|
||||
*/
|
||||
export const config = {
|
||||
// 获取完整配置
|
||||
get(): AppConfig {
|
||||
return appConfig;
|
||||
},
|
||||
|
||||
// 获取应用配置
|
||||
getApp() {
|
||||
return appConfig.app;
|
||||
},
|
||||
|
||||
// 获取数据库配置
|
||||
getDatabase() {
|
||||
return appConfig.database;
|
||||
},
|
||||
|
||||
// 获取 Redis 配置
|
||||
getRedis() {
|
||||
return appConfig.redis;
|
||||
},
|
||||
|
||||
// 获取 Kafka 配置
|
||||
getKafka() {
|
||||
return appConfig.kafka;
|
||||
},
|
||||
|
||||
// 获取 JWT 配置
|
||||
getJwt() {
|
||||
return appConfig.jwt;
|
||||
},
|
||||
|
||||
// 获取缓存配置
|
||||
getCache() {
|
||||
return appConfig.cache;
|
||||
},
|
||||
|
||||
// 获取日志配置
|
||||
getLogging() {
|
||||
return appConfig.logging;
|
||||
},
|
||||
|
||||
// 获取上传配置
|
||||
getUpload() {
|
||||
return appConfig.upload;
|
||||
},
|
||||
|
||||
// 获取限流配置
|
||||
getThrottle() {
|
||||
return appConfig.throttle;
|
||||
},
|
||||
|
||||
// 获取第三方服务配置
|
||||
getThirdParty() {
|
||||
return appConfig.thirdParty;
|
||||
},
|
||||
|
||||
// 检查是否为生产环境
|
||||
isProduction(): boolean {
|
||||
return appConfig.app.environment === 'production';
|
||||
},
|
||||
|
||||
// 检查是否为开发环境
|
||||
isDevelopment(): boolean {
|
||||
return appConfig.app.environment === 'development';
|
||||
},
|
||||
|
||||
// 检查是否为测试环境
|
||||
isTest(): boolean {
|
||||
return appConfig.app.environment === 'test';
|
||||
},
|
||||
};
|
||||
|
||||
export default appConfig;
|
||||
35
wwjcloud/src/config/core/configModule.ts
Normal file
35
wwjcloud/src/config/core/configModule.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule as NestConfigModule } from '@nestjs/config';
|
||||
import { ConfigController } from '../controllers/configController';
|
||||
import { ConfigCenterService } from '../services/configCenterService';
|
||||
import { ConfigValidationService } from '../services/configValidationService';
|
||||
import { DynamicConfigService } from '../services/dynamicConfigService';
|
||||
import { DocsNavigationController } from '../controllers/docsNavigationController';
|
||||
import { appConfig } from './appConfig';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [() => appConfig],
|
||||
envFilePath: ['.env.local', '.env.development', '.env.production', '.env'],
|
||||
}),
|
||||
],
|
||||
controllers: [ConfigController, DocsNavigationController],
|
||||
providers: [
|
||||
ConfigCenterService,
|
||||
ConfigValidationService,
|
||||
DynamicConfigService,
|
||||
{
|
||||
provide: 'APP_CONFIG',
|
||||
useValue: appConfig,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
ConfigCenterService,
|
||||
ConfigValidationService,
|
||||
DynamicConfigService,
|
||||
'APP_CONFIG',
|
||||
],
|
||||
})
|
||||
export class ConfigModule {}
|
||||
4
wwjcloud/src/config/core/index.ts
Normal file
4
wwjcloud/src/config/core/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// 核心配置导出
|
||||
export { appConfig, config } from './appConfig';
|
||||
export type { AppConfig } from './appConfig';
|
||||
export { ConfigModule } from './configModule';
|
||||
@@ -1,8 +0,0 @@
|
||||
/** 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',
|
||||
});
|
||||
@@ -1,174 +0,0 @@
|
||||
import { Controller, Get, Res } from '@nestjs/common';
|
||||
import { ApiExcludeController } from '@nestjs/swagger';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import { SwaggerConfig } from './swagger.config';
|
||||
|
||||
/**
|
||||
* API文档导航控制器
|
||||
* 提供美观的API文档导航页面
|
||||
*/
|
||||
@ApiExcludeController()
|
||||
@Controller()
|
||||
export class DocsNavigationController {
|
||||
/**
|
||||
* API文档导航首页
|
||||
* 提供所有API文档的导航入口
|
||||
*/
|
||||
@Get('api-docs')
|
||||
async getDocsNavigation(@Res() res: FastifyReply) {
|
||||
const html = SwaggerConfig.getNavigationHtml();
|
||||
res.type('text/html').send(html);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重定向根路径到API文档导航
|
||||
*/
|
||||
@Get('docs-nav')
|
||||
async redirectToDocsNavigation(@Res() res: FastifyReply) {
|
||||
res.redirect('/api-docs');
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供API文档状态信息
|
||||
*/
|
||||
@Get('docs/status')
|
||||
async getDocsStatus() {
|
||||
return {
|
||||
success: true,
|
||||
message: 'API文档系统运行正常',
|
||||
data: {
|
||||
availableDocs: [
|
||||
{
|
||||
name: '完整API文档',
|
||||
path: '/docs',
|
||||
description: '包含所有接口的完整API文档',
|
||||
type: 'complete',
|
||||
},
|
||||
{
|
||||
name: '管理端API',
|
||||
path: '/docs/admin',
|
||||
description: '管理后台专用接口文档',
|
||||
type: 'admin',
|
||||
},
|
||||
{
|
||||
name: '前端API',
|
||||
path: '/docs/frontend',
|
||||
description: '前端应用接口文档',
|
||||
type: 'frontend',
|
||||
},
|
||||
{
|
||||
name: '系统设置API',
|
||||
path: '/docs/settings',
|
||||
description: '系统配置和设置相关接口',
|
||||
type: 'settings',
|
||||
},
|
||||
],
|
||||
jsonFormats: [
|
||||
{
|
||||
name: 'JSON格式文档',
|
||||
path: '/docs-json',
|
||||
description: 'OpenAPI JSON格式文档',
|
||||
},
|
||||
{
|
||||
name: 'YAML格式文档',
|
||||
path: '/docs-yaml',
|
||||
description: 'OpenAPI YAML格式文档',
|
||||
},
|
||||
],
|
||||
lastUpdated: new Date().toISOString(),
|
||||
version: '1.0.0',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API文档配置信息
|
||||
*/
|
||||
@Get('docs/config')
|
||||
async getDocsConfig() {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
title: 'WWJCloud API Documentation',
|
||||
version: '1.0.0',
|
||||
description: 'WWJCloud 基于 NestJS 的企业级后端 API 文档系统',
|
||||
features: [
|
||||
'按前缀分组展示API',
|
||||
'支持JWT认证',
|
||||
'实时API测试',
|
||||
'多格式文档导出',
|
||||
'响应式设计',
|
||||
'搜索和过滤功能',
|
||||
],
|
||||
groups: {
|
||||
complete: {
|
||||
name: '完整文档',
|
||||
prefix: '*',
|
||||
color: '#3b82f6',
|
||||
description: '包含所有API接口',
|
||||
},
|
||||
admin: {
|
||||
name: '管理端',
|
||||
prefix: '/adminapi',
|
||||
color: '#dc2626',
|
||||
description: '管理后台专用接口',
|
||||
},
|
||||
frontend: {
|
||||
name: '前端',
|
||||
prefix: '/api',
|
||||
color: '#059669',
|
||||
description: '前端应用接口',
|
||||
},
|
||||
settings: {
|
||||
name: '系统设置',
|
||||
prefix: '/settings,/system',
|
||||
color: '#7c3aed',
|
||||
description: '系统配置接口',
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
type: 'Bearer Token',
|
||||
scheme: 'JWT',
|
||||
description: '使用JWT Token进行API认证',
|
||||
},
|
||||
contact: {
|
||||
name: 'WWJCloud Team',
|
||||
email: 'support@wwjcloud.com',
|
||||
url: 'https://wwjcloud.com',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* API文档使用统计
|
||||
*/
|
||||
@Get('docs/stats')
|
||||
async getDocsStats() {
|
||||
// 这里可以集成实际的统计数据
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
totalEndpoints: 0, // 实际应该从Swagger文档中计算
|
||||
groupStats: {
|
||||
admin: { count: 0, lastAccessed: new Date().toISOString() },
|
||||
frontend: { count: 0, lastAccessed: new Date().toISOString() },
|
||||
settings: { count: 0, lastAccessed: new Date().toISOString() },
|
||||
other: { count: 0, lastAccessed: new Date().toISOString() },
|
||||
},
|
||||
popularEndpoints: [
|
||||
// 这里可以添加热门接口统计
|
||||
],
|
||||
recentActivity: [
|
||||
// 这里可以添加最近访问记录
|
||||
],
|
||||
systemInfo: {
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
uptime: process.uptime(),
|
||||
memoryUsage: process.memoryUsage(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
6
wwjcloud/src/config/env/index.ts
vendored
6
wwjcloud/src/config/env/index.ts
vendored
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Environment variables typing or helpers
|
||||
*/
|
||||
export const envConfig = () => ({
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
export type EventBusMode = 'outboxOnly' | 'outboxToKafka';
|
||||
|
||||
export interface EventBusConfig {
|
||||
mode: EventBusMode;
|
||||
}
|
||||
|
||||
export function loadEventBusConfig(env: Record<string, any>): EventBusConfig {
|
||||
const mode = (env.EVENTBUS_MODE as EventBusMode) || 'outboxOnly';
|
||||
return { mode };
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/** HTTP client/server config skeleton */
|
||||
export const httpConfig = () => ({
|
||||
timeout: Number(process.env.HTTP_TIMEOUT || 5000),
|
||||
retry: Number(process.env.HTTP_RETRY || 2),
|
||||
});
|
||||
@@ -1,37 +1,23 @@
|
||||
export * from './swagger.config';
|
||||
export * from './docs-navigation.controller';
|
||||
export * from './config.module';
|
||||
// 配置层统一导出
|
||||
// 核心配置
|
||||
export * from './core';
|
||||
|
||||
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 || '192.168.1.35',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379', 10),
|
||||
password: process.env.REDIS_PASSWORD || '',
|
||||
db: parseInt(process.env.REDIS_DB || '0', 10),
|
||||
},
|
||||
kafka: {
|
||||
clientId: process.env.KAFKA_CLIENT_ID || 'wwjcloud-backend',
|
||||
brokers: (process.env.KAFKA_BROKERS || '192.168.1.35:9092').split(','),
|
||||
},
|
||||
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 || 'alipay',
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
throttle: {
|
||||
ttl: parseInt(process.env.THROTTLE_TTL || '60', 10),
|
||||
limit: parseInt(process.env.THROTTLE_LIMIT || '100', 10),
|
||||
},
|
||||
});
|
||||
// 配置验证
|
||||
export * from './schemas';
|
||||
|
||||
// 配置服务
|
||||
export * from './services';
|
||||
|
||||
// 配置控制器
|
||||
export * from './controllers';
|
||||
|
||||
// 集成配置
|
||||
export * from './integrations';
|
||||
|
||||
// 模块配置
|
||||
export * from './modules';
|
||||
|
||||
// 向后兼容的导出(保持现有代码不破坏)
|
||||
export { appConfig, config } from './core/appConfig';
|
||||
export type { AppConfig } from './core/appConfig';
|
||||
export { ConfigModule } from './core/configModule';
|
||||
|
||||
2
wwjcloud/src/config/integrations/index.ts
Normal file
2
wwjcloud/src/config/integrations/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// 集成配置导出
|
||||
export { SwaggerConfig } from './swaggerConfig';
|
||||
@@ -209,4 +209,4 @@ export class SwaggerConfig {
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/** Logger config skeleton */
|
||||
export const loggerConfig = () => ({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
});
|
||||
3
wwjcloud/src/config/modules/index.ts
Normal file
3
wwjcloud/src/config/modules/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// 模块配置导出
|
||||
export * from './queue';
|
||||
|
||||
10
wwjcloud/src/config/modules/queue/eventConfig.ts
Normal file
10
wwjcloud/src/config/modules/queue/eventConfig.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type EventMode = 'outboxOnly' | 'outboxToKafka';
|
||||
|
||||
export interface EventConfig {
|
||||
mode: EventMode;
|
||||
}
|
||||
|
||||
export function loadEventConfig(env: Record<string, any>): EventConfig {
|
||||
const mode = (env.EVENT_MODE as EventMode) || 'outboxOnly';
|
||||
return { mode };
|
||||
}
|
||||
2
wwjcloud/src/config/modules/queue/index.ts
Normal file
2
wwjcloud/src/config/modules/queue/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// 队列模块配置导出
|
||||
export * from './eventConfig';
|
||||
@@ -1,21 +0,0 @@
|
||||
/** Queue config skeleton */
|
||||
export const queueConfig = () => ({
|
||||
// 兼容旧配置
|
||||
driver: process.env.QUEUE_DRIVER || 'bull',
|
||||
|
||||
// 新的适配器配置
|
||||
taskAdapter: process.env.TASK_QUEUE_ADAPTER || 'database-outbox', // redis, database-outbox, memory
|
||||
eventAdapter: process.env.EVENT_BUS_ADAPTER || 'database-outbox', // kafka, database-outbox, memory
|
||||
|
||||
// Redis 任务队列配置
|
||||
removeOnComplete: parseInt(process.env.QUEUE_REMOVE_ON_COMPLETE || '100'),
|
||||
removeOnFail: parseInt(process.env.QUEUE_REMOVE_ON_FAIL || '50'),
|
||||
defaultAttempts: parseInt(process.env.QUEUE_DEFAULT_ATTEMPTS || '3'),
|
||||
backoffDelay: parseInt(process.env.QUEUE_BACKOFF_DELAY || '2000'),
|
||||
|
||||
// Outbox 模式配置
|
||||
outboxProcessInterval: parseInt(process.env.OUTBOX_PROCESS_INTERVAL || '5000'), // 5秒
|
||||
outboxBatchSize: parseInt(process.env.OUTBOX_BATCH_SIZE || '100'),
|
||||
outboxMaxRetries: parseInt(process.env.OUTBOX_MAX_RETRIES || '5'),
|
||||
outboxRetryDelay: parseInt(process.env.OUTBOX_RETRY_DELAY || '60000'), // 1分钟
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
export const AppConfigSchema = Joi.object({
|
||||
NODE_ENV: Joi.string().valid('development', 'test', 'production').required(),
|
||||
PORT: Joi.number().default(3000),
|
||||
|
||||
// 数据库示例
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().required(),
|
||||
DB_USER: Joi.string().required(),
|
||||
DB_PASS: Joi.string().allow('').required(),
|
||||
DB_NAME: Joi.string().required(),
|
||||
|
||||
// Redis(可选)
|
||||
REDIS_HOST: Joi.string().optional(),
|
||||
REDIS_PORT: Joi.number().optional(),
|
||||
|
||||
// Kafka(可选)
|
||||
KAFKA_BROKERS: Joi.string().optional(),
|
||||
}).unknown(true);
|
||||
|
||||
export function validateConfig(config: Record<string, any>) {
|
||||
const { error, value } = AppConfigSchema.validate(config, { allowUnknown: true, abortEarly: false });
|
||||
if (error) {
|
||||
throw new Error(`Config validation error: ${error.message}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
84
wwjcloud/src/config/schemas/appSchema.ts
Normal file
84
wwjcloud/src/config/schemas/appSchema.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import Joi from 'joi';
|
||||
|
||||
/**
|
||||
* 应用配置验证模式
|
||||
*/
|
||||
export const AppConfigSchema = Joi.object({
|
||||
NODE_ENV: Joi.string().valid('development', 'test', 'production').required(),
|
||||
PORT: Joi.number().default(3000),
|
||||
APP_NAME: Joi.string().optional(),
|
||||
APP_VERSION: Joi.string().optional(),
|
||||
TZ: Joi.string().optional(),
|
||||
|
||||
// 数据库配置验证
|
||||
DB_HOST: Joi.string().required(),
|
||||
DB_PORT: Joi.number().required(),
|
||||
DB_USERNAME: Joi.string().required(),
|
||||
DB_PASSWORD: Joi.string().allow('').required(),
|
||||
DB_DATABASE: Joi.string().required(),
|
||||
DB_SYNC: Joi.boolean().optional(),
|
||||
DB_LOGGING: Joi.boolean().optional(),
|
||||
|
||||
// Redis配置验证
|
||||
REDIS_HOST: Joi.string().optional(),
|
||||
REDIS_PORT: Joi.number().optional(),
|
||||
REDIS_PASSWORD: Joi.string().allow('').optional(),
|
||||
REDIS_DB: Joi.number().optional(),
|
||||
REDIS_KEY_PREFIX: Joi.string().optional(),
|
||||
|
||||
// Kafka配置验证
|
||||
KAFKA_CLIENT_ID: Joi.string().optional(),
|
||||
KAFKA_BROKERS: Joi.string().optional(),
|
||||
KAFKA_GROUP_ID: Joi.string().optional(),
|
||||
KAFKA_TOPIC_PREFIX: Joi.string().optional(),
|
||||
|
||||
// JWT配置验证
|
||||
JWT_SECRET: Joi.string().required(),
|
||||
JWT_EXPIRES_IN: Joi.string().optional(),
|
||||
JWT_ALGORITHM: Joi.string().optional(),
|
||||
|
||||
// 缓存配置验证
|
||||
CACHE_TTL: Joi.number().optional(),
|
||||
CACHE_MAX_ITEMS: Joi.number().optional(),
|
||||
CACHE_PREFIX: Joi.string().optional(),
|
||||
|
||||
// 日志配置验证
|
||||
LOG_LEVEL: Joi.string().optional(),
|
||||
LOG_FORMAT: Joi.string().optional(),
|
||||
LOG_FILENAME: Joi.string().optional(),
|
||||
|
||||
// 上传配置验证
|
||||
UPLOAD_PATH: Joi.string().optional(),
|
||||
UPLOAD_MAX_SIZE: Joi.number().optional(),
|
||||
UPLOAD_ALLOWED_TYPES: Joi.string().optional(),
|
||||
|
||||
// 限流配置验证
|
||||
THROTTLE_TTL: Joi.number().optional(),
|
||||
THROTTLE_LIMIT: Joi.number().optional(),
|
||||
|
||||
// 第三方服务配置验证
|
||||
STORAGE_PROVIDER: Joi.string().optional(),
|
||||
STORAGE_CONFIG: Joi.string().optional(),
|
||||
PAYMENT_PROVIDER: Joi.string().valid('mock', 'alipay', 'wechat', 'stripe').optional(),
|
||||
PAYMENT_CONFIG: Joi.string().optional(),
|
||||
SMS_PROVIDER: Joi.string().optional(),
|
||||
SMS_CONFIG: Joi.string().optional(),
|
||||
}).unknown(true);
|
||||
|
||||
/**
|
||||
* 验证配置
|
||||
* @param config 配置对象
|
||||
* @returns 验证结果
|
||||
*/
|
||||
export function validateAppConfig(config: Record<string, any>) {
|
||||
const { error, value } = AppConfigSchema.validate(config, {
|
||||
allowUnknown: true,
|
||||
abortEarly: false
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`应用配置验证失败: ${error.message}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
2
wwjcloud/src/config/schemas/index.ts
Normal file
2
wwjcloud/src/config/schemas/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// 配置验证模式导出
|
||||
export { AppConfigSchema, validateAppConfig } from './appSchema';
|
||||
@@ -1,7 +0,0 @@
|
||||
/** Security config skeleton */
|
||||
export const securityConfig = () => ({
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'change_me',
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
||||
},
|
||||
});
|
||||
262
wwjcloud/src/config/services/configCenterService.ts
Normal file
262
wwjcloud/src/config/services/configCenterService.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { appConfig, AppConfig } from '../core/appConfig';
|
||||
|
||||
export interface ConfigSection {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ConfigMetadata {
|
||||
key: string;
|
||||
value: any;
|
||||
description?: string;
|
||||
type: 'string' | 'number' | 'boolean' | 'json' | 'array';
|
||||
category: string;
|
||||
isPublic: boolean;
|
||||
isRequired: boolean;
|
||||
defaultValue?: any;
|
||||
validation?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
pattern?: string;
|
||||
enum?: any[];
|
||||
};
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ConfigCenterService {
|
||||
private readonly logger = new Logger(ConfigCenterService.name);
|
||||
private readonly configCache = new Map<string, any>();
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 获取配置值 - 使用新的配置结构
|
||||
*/
|
||||
get<T = any>(key: string, defaultValue?: T): T {
|
||||
try {
|
||||
// 先从缓存获取
|
||||
if (this.configCache.has(key)) {
|
||||
return this.configCache.get(key);
|
||||
}
|
||||
|
||||
// 从配置服务获取
|
||||
const value = this.configService.get<T>(key);
|
||||
|
||||
if (value !== undefined) {
|
||||
// 缓存配置
|
||||
this.configCache.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
return defaultValue as T;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get config ${key}: ${error.message}`);
|
||||
return defaultValue as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置对象
|
||||
*/
|
||||
getSection(section: string): ConfigSection {
|
||||
try {
|
||||
const sectionConfig = this.configService.get(section);
|
||||
return sectionConfig || {};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get config section ${section}: ${error.message}`);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用配置 - 使用新的配置结构
|
||||
*/
|
||||
getAppConfig() {
|
||||
return appConfig.app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数据库配置 - 使用新的配置结构
|
||||
*/
|
||||
getDatabaseConfig() {
|
||||
return appConfig.database;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Redis 配置 - 使用新的配置结构
|
||||
*/
|
||||
getRedisConfig() {
|
||||
return appConfig.redis;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Kafka 配置 - 使用新的配置结构
|
||||
*/
|
||||
getKafkaConfig() {
|
||||
return appConfig.kafka;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 JWT 配置 - 使用新的配置结构
|
||||
*/
|
||||
getJwtConfig() {
|
||||
return appConfig.jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存配置 - 使用新的配置结构
|
||||
*/
|
||||
getCacheConfig() {
|
||||
return appConfig.cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志配置 - 使用新的配置结构
|
||||
*/
|
||||
getLoggingConfig() {
|
||||
return appConfig.logging;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上传配置 - 使用新的配置结构
|
||||
*/
|
||||
getUploadConfig() {
|
||||
return appConfig.upload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取限流配置 - 使用新的配置结构
|
||||
*/
|
||||
getThrottleConfig() {
|
||||
return appConfig.throttle;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第三方服务配置 - 使用新的配置结构
|
||||
*/
|
||||
getThirdPartyConfig() {
|
||||
return appConfig.thirdParty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整配置
|
||||
*/
|
||||
getFullConfig(): AppConfig {
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查环境
|
||||
*/
|
||||
isProduction(): boolean {
|
||||
return appConfig.app.environment === 'production';
|
||||
}
|
||||
|
||||
isDevelopment(): boolean {
|
||||
return appConfig.app.environment === 'development';
|
||||
}
|
||||
|
||||
isTest(): boolean {
|
||||
return appConfig.app.environment === 'test';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置元数据
|
||||
*/
|
||||
getConfigMetadata(): ConfigMetadata[] {
|
||||
const metadata: ConfigMetadata[] = [];
|
||||
|
||||
// 应用配置元数据
|
||||
Object.entries(appConfig.app).forEach(([key, value]) => {
|
||||
metadata.push({
|
||||
key: `app.${key}`,
|
||||
value,
|
||||
description: `应用${key}配置`,
|
||||
type: typeof value as any,
|
||||
category: 'app',
|
||||
isPublic: true,
|
||||
isRequired: true,
|
||||
defaultValue: value,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
// 数据库配置元数据
|
||||
Object.entries(appConfig.database).forEach(([key, value]) => {
|
||||
metadata.push({
|
||||
key: `database.${key}`,
|
||||
value,
|
||||
description: `数据库${key}配置`,
|
||||
type: typeof value as any,
|
||||
category: 'database',
|
||||
isPublic: false,
|
||||
isRequired: key === 'host' || key === 'port' || key === 'username' || key === 'database',
|
||||
defaultValue: value,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
// Redis配置元数据
|
||||
Object.entries(appConfig.redis).forEach(([key, value]) => {
|
||||
metadata.push({
|
||||
key: `redis.${key}`,
|
||||
value,
|
||||
description: `Redis${key}配置`,
|
||||
type: typeof value as any,
|
||||
category: 'redis',
|
||||
isPublic: false,
|
||||
isRequired: false,
|
||||
defaultValue: value,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
// Kafka配置元数据
|
||||
Object.entries(appConfig.kafka).forEach(([key, value]) => {
|
||||
metadata.push({
|
||||
key: `kafka.${key}`,
|
||||
value,
|
||||
description: `Kafka${key}配置`,
|
||||
type: typeof value as any,
|
||||
category: 'kafka',
|
||||
isPublic: false,
|
||||
isRequired: false,
|
||||
defaultValue: value,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
});
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新配置缓存
|
||||
*/
|
||||
refreshCache() {
|
||||
this.configCache.clear();
|
||||
this.logger.log('配置缓存已刷新');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置统计信息
|
||||
*/
|
||||
getConfigStats() {
|
||||
const metadata = this.getConfigMetadata();
|
||||
const categories = [...new Set(metadata.map(m => m.category))];
|
||||
|
||||
return {
|
||||
total: metadata.length,
|
||||
categories: categories.length,
|
||||
public: metadata.filter(m => m.isPublic).length,
|
||||
private: metadata.filter(m => !m.isPublic).length,
|
||||
required: metadata.filter(m => m.isRequired).length,
|
||||
optional: metadata.filter(m => !m.isRequired).length,
|
||||
byCategory: categories.reduce((acc, category) => {
|
||||
acc[category] = metadata.filter(m => m.category === category).length;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
};
|
||||
}
|
||||
}
|
||||
215
wwjcloud/src/config/services/configValidationService.ts
Normal file
215
wwjcloud/src/config/services/configValidationService.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
export interface ConfigSuggestion {
|
||||
key: string;
|
||||
currentValue: any;
|
||||
suggestedValue: any;
|
||||
reason: string;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ConfigValidationService {
|
||||
private readonly logger = new Logger(ConfigValidationService.name);
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 验证所有配置
|
||||
*/
|
||||
async validateAll(): Promise<ValidationResult> {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
const suggestions: string[] = [];
|
||||
|
||||
// 验证数据库配置
|
||||
const dbValidation = this.validateDatabaseConfig();
|
||||
errors.push(...dbValidation.errors);
|
||||
warnings.push(...dbValidation.warnings);
|
||||
|
||||
// 验证Redis配置
|
||||
const redisValidation = this.validateRedisConfig();
|
||||
errors.push(...redisValidation.errors);
|
||||
warnings.push(...redisValidation.warnings);
|
||||
|
||||
// 验证Kafka配置
|
||||
const kafkaValidation = this.validateKafkaConfig();
|
||||
errors.push(...kafkaValidation.errors);
|
||||
warnings.push(...kafkaValidation.warnings);
|
||||
|
||||
// 验证JWT配置
|
||||
const jwtValidation = this.validateJwtConfig();
|
||||
errors.push(...jwtValidation.errors);
|
||||
warnings.push(...jwtValidation.warnings);
|
||||
|
||||
// 验证应用配置
|
||||
const appValidation = this.validateAppConfig();
|
||||
errors.push(...appValidation.errors);
|
||||
warnings.push(...appValidation.warnings);
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
warnings,
|
||||
suggestions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数据库配置
|
||||
*/
|
||||
private validateDatabaseConfig(): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const host = this.configService.get('database.host');
|
||||
const port = this.configService.get('database.port');
|
||||
const username = this.configService.get('database.username');
|
||||
const password = this.configService.get('database.password');
|
||||
const database = this.configService.get('database.database');
|
||||
|
||||
if (!host) errors.push('数据库主机地址未配置');
|
||||
if (!port) errors.push('数据库端口未配置');
|
||||
if (!username) errors.push('数据库用户名未配置');
|
||||
if (!database) errors.push('数据库名称未配置');
|
||||
|
||||
if (host === 'localhost' && process.env.NODE_ENV === 'production') {
|
||||
warnings.push('生产环境不建议使用localhost作为数据库主机');
|
||||
}
|
||||
|
||||
if (!password && process.env.NODE_ENV === 'production') {
|
||||
warnings.push('生产环境建议设置数据库密码');
|
||||
}
|
||||
|
||||
return { isValid: errors.length === 0, errors, warnings, suggestions: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Redis配置
|
||||
*/
|
||||
private validateRedisConfig(): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const host = this.configService.get('redis.host');
|
||||
const port = this.configService.get('redis.port');
|
||||
|
||||
if (!host) errors.push('Redis主机地址未配置');
|
||||
if (!port) errors.push('Redis端口未配置');
|
||||
|
||||
if (host === 'localhost' && process.env.NODE_ENV === 'production') {
|
||||
warnings.push('生产环境不建议使用localhost作为Redis主机');
|
||||
}
|
||||
|
||||
return { isValid: errors.length === 0, errors, warnings, suggestions: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Kafka配置
|
||||
*/
|
||||
private validateKafkaConfig(): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const brokers = this.configService.get('kafka.brokers');
|
||||
const clientId = this.configService.get('kafka.clientId');
|
||||
|
||||
if (!brokers) errors.push('Kafka brokers未配置');
|
||||
if (!clientId) warnings.push('Kafka clientId未配置,将使用默认值');
|
||||
|
||||
return { isValid: errors.length === 0, errors, warnings, suggestions: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT配置
|
||||
*/
|
||||
private validateJwtConfig(): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const secret = this.configService.get('jwt.secret');
|
||||
const expiresIn = this.configService.get('jwt.expiresIn');
|
||||
|
||||
if (!secret) errors.push('JWT密钥未配置');
|
||||
if (!expiresIn) warnings.push('JWT过期时间未配置,将使用默认值');
|
||||
|
||||
if (secret && secret.length < 32) {
|
||||
warnings.push('JWT密钥长度建议至少32位');
|
||||
}
|
||||
|
||||
return { isValid: errors.length === 0, errors, warnings, suggestions: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证应用配置
|
||||
*/
|
||||
private validateAppConfig(): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
const environment = this.configService.get('app.environment');
|
||||
const port = this.configService.get('app.port');
|
||||
|
||||
if (!environment) errors.push('应用环境未配置');
|
||||
if (!port) errors.push('应用端口未配置');
|
||||
|
||||
if (environment === 'development' && process.env.NODE_ENV === 'production') {
|
||||
warnings.push('环境变量与配置不一致');
|
||||
}
|
||||
|
||||
return { isValid: errors.length === 0, errors, warnings, suggestions: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置建议
|
||||
*/
|
||||
getSuggestions(): ConfigSuggestion[] {
|
||||
const suggestions: ConfigSuggestion[] = [];
|
||||
|
||||
// 数据库配置建议
|
||||
const dbHost = this.configService.get('database.host');
|
||||
if (dbHost === 'localhost' && process.env.NODE_ENV === 'production') {
|
||||
suggestions.push({
|
||||
key: 'database.host',
|
||||
currentValue: dbHost,
|
||||
suggestedValue: 'your-production-db-host',
|
||||
reason: '生产环境应使用专用数据库服务器',
|
||||
priority: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
// Redis配置建议
|
||||
const redisHost = this.configService.get('redis.host');
|
||||
if (redisHost === 'localhost' && process.env.NODE_ENV === 'production') {
|
||||
suggestions.push({
|
||||
key: 'redis.host',
|
||||
currentValue: redisHost,
|
||||
suggestedValue: 'your-production-redis-host',
|
||||
reason: '生产环境应使用专用Redis服务器',
|
||||
priority: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
// JWT密钥建议
|
||||
const jwtSecret = this.configService.get('jwt.secret');
|
||||
if (jwtSecret && jwtSecret.length < 32) {
|
||||
suggestions.push({
|
||||
key: 'jwt.secret',
|
||||
currentValue: '***',
|
||||
suggestedValue: '使用至少32位的随机字符串',
|
||||
reason: 'JWT密钥长度不足,存在安全风险',
|
||||
priority: 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
145
wwjcloud/src/config/services/dynamicConfigService.ts
Normal file
145
wwjcloud/src/config/services/dynamicConfigService.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
export interface DynamicConfig {
|
||||
key: string;
|
||||
value: any;
|
||||
description?: string;
|
||||
type?: 'string' | 'number' | 'boolean' | 'json';
|
||||
category?: string;
|
||||
isPublic?: boolean;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DynamicConfigService {
|
||||
private readonly logger = new Logger(DynamicConfigService.name);
|
||||
private readonly configs = new Map<string, DynamicConfig>();
|
||||
|
||||
constructor() {
|
||||
// 初始化一些默认配置
|
||||
this.initializeDefaultConfigs();
|
||||
}
|
||||
|
||||
private initializeDefaultConfigs() {
|
||||
const defaultConfigs: DynamicConfig[] = [
|
||||
{
|
||||
key: 'system.maintenance',
|
||||
value: false,
|
||||
description: '系统维护模式',
|
||||
type: 'boolean',
|
||||
category: 'system',
|
||||
isPublic: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
{
|
||||
key: 'system.debug',
|
||||
value: process.env.NODE_ENV === 'development',
|
||||
description: '调试模式',
|
||||
type: 'boolean',
|
||||
category: 'system',
|
||||
isPublic: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
defaultConfigs.forEach(config => {
|
||||
this.configs.set(config.key, config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
async getConfig(key: string): Promise<DynamicConfig | null> {
|
||||
return this.configs.get(key) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置列表
|
||||
*/
|
||||
async getConfigList(category?: string): Promise<DynamicConfig[]> {
|
||||
const configs = Array.from(this.configs.values());
|
||||
|
||||
if (category) {
|
||||
return configs.filter(config => config.category === category);
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置
|
||||
*/
|
||||
async setConfig(
|
||||
key: string,
|
||||
value: any,
|
||||
options: {
|
||||
description?: string;
|
||||
type?: 'string' | 'number' | 'boolean' | 'json';
|
||||
category?: string;
|
||||
isPublic?: boolean;
|
||||
} = {}
|
||||
): Promise<void> {
|
||||
const existingConfig = this.configs.get(key);
|
||||
const now = new Date();
|
||||
|
||||
const config: DynamicConfig = {
|
||||
key,
|
||||
value,
|
||||
description: options.description || existingConfig?.description,
|
||||
type: options.type || this.inferType(value) || existingConfig?.type || 'string',
|
||||
category: options.category || existingConfig?.category || 'general',
|
||||
isPublic: options.isPublic !== undefined ? options.isPublic : existingConfig?.isPublic ?? true,
|
||||
createdAt: existingConfig?.createdAt || now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
this.configs.set(key, config);
|
||||
this.logger.log(`动态配置已更新: ${key} = ${value}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
*/
|
||||
async deleteConfig(key: string): Promise<void> {
|
||||
if (this.configs.has(key)) {
|
||||
this.configs.delete(key);
|
||||
this.logger.log(`动态配置已删除: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推断值类型
|
||||
*/
|
||||
private inferType(value: any): 'string' | 'number' | 'boolean' | 'json' {
|
||||
if (typeof value === 'string') return 'string';
|
||||
if (typeof value === 'number') return 'number';
|
||||
if (typeof value === 'boolean') return 'boolean';
|
||||
if (typeof value === 'object') return 'json';
|
||||
return 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置统计
|
||||
*/
|
||||
async getConfigStats() {
|
||||
const configs = Array.from(this.configs.values());
|
||||
const categories = [...new Set(configs.map(c => c.category))];
|
||||
|
||||
return {
|
||||
total: configs.length,
|
||||
categories: categories.length,
|
||||
public: configs.filter(c => c.isPublic).length,
|
||||
private: configs.filter(c => !c.isPublic).length,
|
||||
byCategory: categories.reduce((acc, category) => {
|
||||
if (category) {
|
||||
acc[category] = configs.filter(c => c.category === category).length;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
};
|
||||
}
|
||||
}
|
||||
7
wwjcloud/src/config/services/index.ts
Normal file
7
wwjcloud/src/config/services/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// 配置服务导出
|
||||
export { ConfigCenterService } from './configCenterService';
|
||||
export type { ConfigSection, ConfigMetadata } from './configCenterService';
|
||||
export { DynamicConfigService } from './dynamicConfigService';
|
||||
export type { DynamicConfig } from './dynamicConfigService';
|
||||
export { ConfigValidationService } from './configValidationService';
|
||||
export type { ValidationResult } from './configValidationService';
|
||||
5
wwjcloud/src/config/third-party/index.ts
vendored
5
wwjcloud/src/config/third-party/index.ts
vendored
@@ -1,5 +0,0 @@
|
||||
/** Third-party integrations config skeleton */
|
||||
export const thirdPartyConfig = () => ({
|
||||
storageProvider: process.env.STORAGE_PROVIDER || 'local',
|
||||
paymentProvider: process.env.PAYMENT_PROVIDER || 'mock',
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
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;
|
||||
344
wwjcloud/src/core/README.md
Normal file
344
wwjcloud/src/core/README.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# Core 层基础设施使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
Core 层提供了企业级应用所需的核心基础设施,包括缓存、追踪、配置管理、熔断器等。
|
||||
|
||||
## 基础设施列表
|
||||
|
||||
### 1. 缓存系统 (Cache)
|
||||
|
||||
#### 多级缓存
|
||||
```typescript
|
||||
import { MultiLevelCacheService } from '@wwjCore/cache';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private multiLevelCache: MultiLevelCacheService) {}
|
||||
|
||||
async getUser(id: number) {
|
||||
return this.multiLevelCache.getOrSet(
|
||||
`user:${id}`,
|
||||
async () => {
|
||||
// 从数据库获取用户
|
||||
return this.userRepository.findById(id);
|
||||
},
|
||||
{
|
||||
l1Ttl: 60, // L1 缓存 60 秒
|
||||
l2Ttl: 300, // L2 缓存 5 分钟
|
||||
prefix: 'user'
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 分布式锁
|
||||
```typescript
|
||||
import { DistributedLockService } from '@wwjCore/cache';
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
constructor(private lockService: DistributedLockService) {}
|
||||
|
||||
async createOrder(userId: number, productId: number) {
|
||||
return this.lockService.withLock(
|
||||
`order:${userId}:${productId}`,
|
||||
async () => {
|
||||
// 检查库存并创建订单
|
||||
return this.processOrder(userId, productId);
|
||||
},
|
||||
{
|
||||
ttl: 30000, // 锁超时 30 秒
|
||||
retryDelay: 100, // 重试延迟 100ms
|
||||
maxRetries: 10 // 最大重试 10 次
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 分布式追踪 (Tracing)
|
||||
|
||||
#### 自动追踪
|
||||
```typescript
|
||||
// 在 main.ts 中全局应用
|
||||
app.useGlobalInterceptors(new TracingInterceptor());
|
||||
app.useGlobalGuards(new TracingGuard());
|
||||
|
||||
// 在服务中手动追踪
|
||||
import { TracingService } from '@wwjCore/tracing';
|
||||
|
||||
@Injectable()
|
||||
export class PaymentService {
|
||||
constructor(private tracingService: TracingService) {}
|
||||
|
||||
async processPayment(paymentData: any) {
|
||||
const span = this.tracingService.startSpan('process-payment', {
|
||||
'payment.amount': paymentData.amount,
|
||||
'payment.method': paymentData.method
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await this.paymentGateway.charge(paymentData);
|
||||
this.tracingService.addSpanTag(span.spanId, 'payment.status', 'success');
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.tracingService.recordError(span.spanId, error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.tracingService.endSpan(span.spanId);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置管理 (Config)
|
||||
|
||||
#### 动态配置
|
||||
```typescript
|
||||
import { DynamicConfigService } from '@wwjCore/config';
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
constructor(private configService: DynamicConfigService) {}
|
||||
|
||||
async sendEmail(to: string, subject: string, content: string) {
|
||||
// 获取邮件配置
|
||||
const smtpConfig = await this.configService.getConfig('email.smtp', {
|
||||
host: 'localhost',
|
||||
port: 587,
|
||||
secure: false
|
||||
});
|
||||
|
||||
const rateLimit = await this.configService.getConfig('email.rate_limit', 100);
|
||||
|
||||
// 使用配置发送邮件
|
||||
return this.sendWithConfig(to, subject, content, smtpConfig);
|
||||
}
|
||||
|
||||
// 动态更新配置
|
||||
async updateSmtpConfig(config: any) {
|
||||
await this.configService.setConfig('email.smtp', config, {
|
||||
description: 'SMTP 服务器配置',
|
||||
category: 'email',
|
||||
isPublic: false
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 熔断器 (Circuit Breaker)
|
||||
|
||||
#### 服务保护
|
||||
```typescript
|
||||
import { CircuitBreakerService } from '@wwjCore/circuit-breaker';
|
||||
|
||||
@Injectable()
|
||||
export class ExternalApiService {
|
||||
constructor(private circuitBreaker: CircuitBreakerService) {}
|
||||
|
||||
async callExternalApi(data: any) {
|
||||
return this.circuitBreaker.execute(
|
||||
'external-api',
|
||||
async () => {
|
||||
// 调用外部 API
|
||||
return this.httpService.post('/api/external', data).toPromise();
|
||||
},
|
||||
{
|
||||
failureThreshold: 5, // 5 次失败后熔断
|
||||
recoveryTimeout: 60000, // 1 分钟后尝试恢复
|
||||
expectedResponseTime: 5000 // 期望响应时间 5 秒
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 获取熔断器状态
|
||||
getBreakerStatus() {
|
||||
return this.circuitBreaker.getBreakerStats('external-api');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 安全基础设施 (Security)
|
||||
|
||||
#### 限流
|
||||
```typescript
|
||||
import { RateLimitService } from '@wwjCore/security';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(private rateLimitService: RateLimitService) {}
|
||||
|
||||
async login(username: string, password: string, ip: string) {
|
||||
// 检查登录频率限制
|
||||
const allowed = await this.rateLimitService.consume(
|
||||
`login:${ip}`,
|
||||
{
|
||||
capacity: 5, // 桶容量 5
|
||||
refillPerSec: 1 // 每秒补充 1 个令牌
|
||||
}
|
||||
);
|
||||
|
||||
if (!allowed) {
|
||||
throw new Error('登录频率过高,请稍后再试');
|
||||
}
|
||||
|
||||
// 执行登录逻辑
|
||||
return this.authenticate(username, password);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 幂等性
|
||||
```typescript
|
||||
import { IdempotencyService } from '@wwjCore/security';
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
constructor(private idempotencyService: IdempotencyService) {}
|
||||
|
||||
async createOrder(orderData: any, idempotencyKey: string) {
|
||||
return this.idempotencyService.execute(
|
||||
idempotencyKey,
|
||||
async () => {
|
||||
// 创建订单逻辑
|
||||
return this.processOrder(orderData);
|
||||
},
|
||||
300000 // 幂等性有效期 5 分钟
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 模块配置
|
||||
|
||||
### 在 AppModule 中导入
|
||||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CacheModule } from '@wwjCore/cache';
|
||||
import { TracingModule } from '@wwjCore/tracing';
|
||||
import { ConfigModule } from '@wwjCore/config';
|
||||
import { CircuitBreakerModule } from '@wwjCore/circuit-breaker';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
CacheModule,
|
||||
TracingModule,
|
||||
ConfigModule,
|
||||
CircuitBreakerModule,
|
||||
// 其他模块...
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
```bash
|
||||
# Redis 配置
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# 缓存配置
|
||||
CACHE_TTL=300
|
||||
CACHE_MAX_ITEMS=1000
|
||||
|
||||
# 追踪配置
|
||||
JAEGER_ENDPOINT=http://localhost:14268/api/traces
|
||||
|
||||
# 应用配置
|
||||
APP_NAME=wwjcloud-backend
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 缓存使用
|
||||
- 使用多级缓存提高性能
|
||||
- 合理设置缓存时间
|
||||
- 及时清理过期缓存
|
||||
- 使用分布式锁避免并发问题
|
||||
|
||||
### 2. 追踪使用
|
||||
- 在关键业务操作中添加追踪
|
||||
- 记录重要的业务指标
|
||||
- 避免记录敏感信息
|
||||
- 合理设置采样率
|
||||
|
||||
### 3. 配置管理
|
||||
- 使用有意义的配置键名
|
||||
- 为配置添加描述信息
|
||||
- 合理分类配置项
|
||||
- 及时清理无用配置
|
||||
|
||||
### 4. 熔断器使用
|
||||
- 为外部服务调用添加熔断保护
|
||||
- 合理设置失败阈值和恢复时间
|
||||
- 监控熔断器状态
|
||||
- 提供降级策略
|
||||
|
||||
### 5. 安全防护
|
||||
- 为敏感操作添加限流
|
||||
- 使用幂等性防止重复操作
|
||||
- 合理设置防护参数
|
||||
- 监控安全事件
|
||||
|
||||
## 监控和调试
|
||||
|
||||
### 健康检查
|
||||
```bash
|
||||
# 检查缓存状态
|
||||
GET /health/cache
|
||||
|
||||
# 检查追踪状态
|
||||
GET /health/tracing
|
||||
|
||||
# 检查熔断器状态
|
||||
GET /health/circuit-breaker
|
||||
```
|
||||
|
||||
### 指标监控
|
||||
```bash
|
||||
# 获取缓存统计
|
||||
GET /metrics/cache
|
||||
|
||||
# 获取追踪统计
|
||||
GET /metrics/tracing
|
||||
|
||||
# 获取熔断器统计
|
||||
GET /metrics/circuit-breaker
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **缓存连接失败**
|
||||
- 检查 Redis 服务状态
|
||||
- 验证连接配置
|
||||
- 查看网络连接
|
||||
|
||||
2. **追踪数据丢失**
|
||||
- 检查 Jaeger 服务状态
|
||||
- 验证采样配置
|
||||
- 查看网络连接
|
||||
|
||||
3. **熔断器误触发**
|
||||
- 检查失败阈值设置
|
||||
- 验证超时配置
|
||||
- 分析失败原因
|
||||
|
||||
4. **配置更新失败**
|
||||
- 检查数据库连接
|
||||
- 验证权限配置
|
||||
- 查看错误日志
|
||||
|
||||
### 调试技巧
|
||||
|
||||
1. 启用详细日志
|
||||
2. 使用追踪 ID 关联请求
|
||||
3. 监控关键指标
|
||||
4. 设置告警规则
|
||||
14
wwjcloud/src/core/breaker/breakerGuard.ts
Normal file
14
wwjcloud/src/core/breaker/breakerGuard.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { CircuitBreakerService } from './circuitBreakerService';
|
||||
|
||||
@Injectable()
|
||||
export class CircuitBreakerGuard implements CanActivate {
|
||||
constructor(private readonly circuitBreakerService: CircuitBreakerService) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const serviceName = `${request.method}:${request.route?.path || 'unknown'}`;
|
||||
|
||||
return this.circuitBreakerService.isOpen(serviceName);
|
||||
}
|
||||
}
|
||||
31
wwjcloud/src/core/breaker/breakerInterceptor.ts
Normal file
31
wwjcloud/src/core/breaker/breakerInterceptor.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { CircuitBreakerService } from './circuitBreakerService';
|
||||
|
||||
@Injectable()
|
||||
export class CircuitBreakerInterceptor implements NestInterceptor {
|
||||
constructor(private readonly circuitBreakerService: CircuitBreakerService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const serviceName = `${request.method}:${request.route?.path || 'unknown'}`;
|
||||
|
||||
return new Observable(subscriber => {
|
||||
this.circuitBreakerService.execute(serviceName, async () => {
|
||||
try {
|
||||
const result = await next.handle().toPromise();
|
||||
subscriber.next(result);
|
||||
subscriber.complete();
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.circuitBreakerService.recordFailure(serviceName, error);
|
||||
subscriber.error(error);
|
||||
throw error;
|
||||
}
|
||||
}).catch(error => {
|
||||
subscriber.error(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
19
wwjcloud/src/core/breaker/breakerModule.ts
Normal file
19
wwjcloud/src/core/breaker/breakerModule.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { CircuitBreakerService } from './circuitBreakerService';
|
||||
import { CircuitBreakerInterceptor } from './breakerInterceptor';
|
||||
import { CircuitBreakerGuard } from './breakerGuard';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
CircuitBreakerService,
|
||||
CircuitBreakerInterceptor,
|
||||
CircuitBreakerGuard,
|
||||
],
|
||||
exports: [
|
||||
CircuitBreakerService,
|
||||
CircuitBreakerInterceptor,
|
||||
CircuitBreakerGuard,
|
||||
],
|
||||
})
|
||||
export class CircuitBreakerModule {}
|
||||
284
wwjcloud/src/core/breaker/circuitBreakerService.ts
Normal file
284
wwjcloud/src/core/breaker/circuitBreakerService.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
export enum CircuitState {
|
||||
CLOSED = 'CLOSED', // 正常状态
|
||||
OPEN = 'OPEN', // 熔断状态
|
||||
HALF_OPEN = 'HALF_OPEN', // 半开状态
|
||||
}
|
||||
|
||||
export interface CircuitBreakerOptions {
|
||||
failureThreshold: number; // 失败阈值
|
||||
recoveryTimeout: number; // 恢复超时时间(毫秒)
|
||||
expectedResponseTime: number; // 期望响应时间(毫秒)
|
||||
monitorInterval: number; // 监控间隔(毫秒)
|
||||
}
|
||||
|
||||
export interface CircuitBreakerStats {
|
||||
state: CircuitState;
|
||||
failureCount: number;
|
||||
successCount: number;
|
||||
totalCount: number;
|
||||
lastFailureTime?: number;
|
||||
lastSuccessTime?: number;
|
||||
failureRate: number;
|
||||
averageResponseTime: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CircuitBreakerService {
|
||||
private readonly logger = new Logger(CircuitBreakerService.name);
|
||||
private readonly breakers = new Map<string, CircuitBreaker>();
|
||||
|
||||
/**
|
||||
* 执行操作(带熔断保护)
|
||||
*/
|
||||
async execute<T>(
|
||||
key: string,
|
||||
operation: () => Promise<T>,
|
||||
options: Partial<CircuitBreakerOptions> = {}
|
||||
): Promise<T> {
|
||||
const breaker = this.getOrCreateBreaker(key, options);
|
||||
return breaker.execute(operation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取熔断器状态
|
||||
*/
|
||||
getBreakerState(key: string): CircuitState | null {
|
||||
const breaker = this.breakers.get(key);
|
||||
return breaker ? breaker.getState() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取熔断器统计信息
|
||||
*/
|
||||
getBreakerStats(key: string): CircuitBreakerStats | null {
|
||||
const breaker = this.breakers.get(key);
|
||||
return breaker ? breaker.getStats() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动重置熔断器
|
||||
*/
|
||||
resetBreaker(key: string): void {
|
||||
const breaker = this.breakers.get(key);
|
||||
if (breaker) {
|
||||
breaker.reset();
|
||||
this.logger.log(`Circuit breaker reset: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有熔断器状态
|
||||
*/
|
||||
getAllBreakerStats(): Record<string, CircuitBreakerStats> {
|
||||
const stats: Record<string, CircuitBreakerStats> = {};
|
||||
|
||||
for (const [key, breaker] of this.breakers.entries()) {
|
||||
stats[key] = breaker.getStats();
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查熔断器是否开启
|
||||
*/
|
||||
isOpen(key: string): boolean {
|
||||
const breaker = this.breakers.get(key);
|
||||
return breaker ? breaker.getState() === CircuitState.OPEN : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败
|
||||
*/
|
||||
recordFailure(key: string, error: any): void {
|
||||
const breaker = this.breakers.get(key);
|
||||
if (breaker) {
|
||||
breaker.recordFailure(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或创建熔断器
|
||||
*/
|
||||
private getOrCreateBreaker(key: string, options: Partial<CircuitBreakerOptions>): CircuitBreaker {
|
||||
let breaker = this.breakers.get(key);
|
||||
|
||||
if (!breaker) {
|
||||
breaker = new CircuitBreaker(key, {
|
||||
failureThreshold: 5,
|
||||
recoveryTimeout: 60000, // 1分钟
|
||||
expectedResponseTime: 5000, // 5秒
|
||||
monitorInterval: 10000, // 10秒
|
||||
...options,
|
||||
});
|
||||
|
||||
this.breakers.set(key, breaker);
|
||||
this.logger.debug(`Created circuit breaker: ${key}`);
|
||||
}
|
||||
|
||||
return breaker;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 熔断器实现
|
||||
*/
|
||||
class CircuitBreaker {
|
||||
private state: CircuitState = CircuitState.CLOSED;
|
||||
private failureCount = 0;
|
||||
private successCount = 0;
|
||||
private lastFailureTime?: number;
|
||||
private lastSuccessTime?: number;
|
||||
private responseTimes: number[] = [];
|
||||
private readonly options: CircuitBreakerOptions;
|
||||
|
||||
constructor(
|
||||
private readonly key: string,
|
||||
options: CircuitBreakerOptions
|
||||
) {
|
||||
this.options = options;
|
||||
this.startMonitoring();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行操作
|
||||
*/
|
||||
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
||||
if (this.state === CircuitState.OPEN) {
|
||||
if (this.shouldAttemptReset()) {
|
||||
this.state = CircuitState.HALF_OPEN;
|
||||
} else {
|
||||
throw new Error(`Circuit breaker is OPEN for ${this.key}`);
|
||||
}
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const result = await this.withTimeout(operation(), this.options.expectedResponseTime);
|
||||
|
||||
this.onSuccess();
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.onFailure();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功回调
|
||||
*/
|
||||
private onSuccess(): void {
|
||||
this.successCount++;
|
||||
this.lastSuccessTime = Date.now();
|
||||
|
||||
if (this.state === CircuitState.HALF_OPEN) {
|
||||
this.state = CircuitState.CLOSED;
|
||||
this.failureCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败回调
|
||||
*/
|
||||
private onFailure(): void {
|
||||
this.failureCount++;
|
||||
this.lastFailureTime = Date.now();
|
||||
|
||||
if (this.state === CircuitState.HALF_OPEN ||
|
||||
this.failureCount >= this.options.failureThreshold) {
|
||||
this.state = CircuitState.OPEN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否应该尝试重置
|
||||
*/
|
||||
private shouldAttemptReset(): boolean {
|
||||
if (!this.lastFailureTime) return true;
|
||||
|
||||
const timeSinceLastFailure = Date.now() - this.lastFailureTime;
|
||||
return timeSinceLastFailure >= this.options.recoveryTimeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* 带超时的操作执行
|
||||
*/
|
||||
private async withTimeout<T>(promise: Promise<T>, timeout: number): Promise<T> {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Operation timeout')), timeout);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
*/
|
||||
getState(): CircuitState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
getStats(): CircuitBreakerStats {
|
||||
const totalCount = this.failureCount + this.successCount;
|
||||
const failureRate = totalCount > 0 ? this.failureCount / totalCount : 0;
|
||||
const averageResponseTime = this.responseTimes.length > 0
|
||||
? this.responseTimes.reduce((sum, time) => sum + time, 0) / this.responseTimes.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
state: this.state,
|
||||
failureCount: this.failureCount,
|
||||
successCount: this.successCount,
|
||||
totalCount,
|
||||
lastFailureTime: this.lastFailureTime,
|
||||
lastSuccessTime: this.lastSuccessTime,
|
||||
failureRate,
|
||||
averageResponseTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置熔断器
|
||||
*/
|
||||
reset(): void {
|
||||
this.state = CircuitState.CLOSED;
|
||||
this.failureCount = 0;
|
||||
this.successCount = 0;
|
||||
this.lastFailureTime = undefined;
|
||||
this.lastSuccessTime = undefined;
|
||||
this.responseTimes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败
|
||||
*/
|
||||
recordFailure(error: any): void {
|
||||
this.onFailure();
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始监控
|
||||
*/
|
||||
private startMonitoring(): void {
|
||||
setInterval(() => {
|
||||
this.cleanup();
|
||||
}, this.options.monitorInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期数据
|
||||
*/
|
||||
private cleanup(): void {
|
||||
// 清理响应时间数组,只保留最近的数据
|
||||
if (this.responseTimes.length > 100) {
|
||||
this.responseTimes = this.responseTimes.slice(-50);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
wwjcloud/src/core/cache/cacheModule.ts
vendored
Normal file
33
wwjcloud/src/core/cache/cacheModule.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CacheModule as NestCacheModule } from '@nestjs/cache-manager';
|
||||
import { Redis } from 'ioredis';
|
||||
import { CacheService } from './cacheService';
|
||||
import { MultiLevelCacheService } from './multiLevelCacheService';
|
||||
import { DistributedLockService } from './distributedLockService';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
NestCacheModule.register({
|
||||
isGlobal: true,
|
||||
ttl: 60 * 60 * 24, // 24小时
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
CacheService,
|
||||
MultiLevelCacheService,
|
||||
DistributedLockService,
|
||||
{
|
||||
provide: 'REDIS_CLIENT',
|
||||
useFactory: () => {
|
||||
return new Redis({
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
db: parseInt(process.env.REDIS_DB || '0'),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [CacheService, MultiLevelCacheService, DistributedLockService],
|
||||
})
|
||||
export class CacheModule {}
|
||||
167
wwjcloud/src/core/cache/cacheService.ts
vendored
Normal file
167
wwjcloud/src/core/cache/cacheService.ts
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import type { Cache } from 'cache-manager';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
export interface CacheOptions {
|
||||
ttl?: number;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
private readonly logger = new Logger(CacheService.name);
|
||||
private readonly appPrefix = 'wwjcloud'; // 使用固定前缀,避免硬编码
|
||||
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private cacheManager: Cache,
|
||||
@Inject('REDIS_CLIENT') private redis: Redis,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取缓存
|
||||
*/
|
||||
async get<T>(key: string, options?: CacheOptions): Promise<T | null> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
const value = await this.cacheManager.get<T>(fullKey);
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
this.logger.debug(`Cache hit: ${fullKey}`);
|
||||
return value;
|
||||
} else {
|
||||
this.logger.debug(`Cache miss: ${fullKey}`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache get error: ${error.message}`, error.stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存
|
||||
*/
|
||||
async set<T>(key: string, value: T, options?: CacheOptions): Promise<void> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
await this.cacheManager.set(fullKey, value, options?.ttl);
|
||||
this.logger.debug(`Cache set: ${fullKey}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache set error: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
async del(key: string, options?: CacheOptions): Promise<void> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
await this.cacheManager.del(fullKey);
|
||||
this.logger.debug(`Cache del: ${fullKey}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache del error: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除缓存
|
||||
*/
|
||||
async delPattern(pattern: string): Promise<void> {
|
||||
try {
|
||||
const keys = await this.redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await this.redis.del(...keys);
|
||||
this.logger.debug(`Cache del pattern: ${pattern}, deleted ${keys.length} keys`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache del pattern error: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否存在
|
||||
*/
|
||||
async exists(key: string, options?: CacheOptions): Promise<boolean> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
const value = await this.cacheManager.get(fullKey);
|
||||
return value !== null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache exists error: ${error.message}`, error.stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
async getStats(): Promise<{
|
||||
memoryUsage: number;
|
||||
keyCount: number;
|
||||
hitRate: number;
|
||||
}> {
|
||||
try {
|
||||
const info = await this.redis.info('memory');
|
||||
const keys = await this.redis.dbsize();
|
||||
|
||||
// 解析 Redis INFO 输出
|
||||
const memoryMatch = info.match(/used_memory_human:(\S+)/);
|
||||
const memoryUsage = memoryMatch ? memoryMatch[1] : '0B';
|
||||
|
||||
return {
|
||||
memoryUsage: this.parseMemoryUsage(memoryUsage),
|
||||
keyCount: keys,
|
||||
hitRate: 0, // 需要实现命中率统计
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache stats error: ${error.message}`, error.stack);
|
||||
return {
|
||||
memoryUsage: 0,
|
||||
keyCount: 0,
|
||||
hitRate: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
// 直接使用 Redis 的 FLUSHDB 命令清空当前数据库
|
||||
await this.redis.flushdb();
|
||||
this.logger.debug('Cache cleared');
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache clear error: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建缓存键
|
||||
*/
|
||||
private buildKey(key: string, prefix?: string): string {
|
||||
const finalPrefix = prefix ? `${this.appPrefix}:${prefix}` : this.appPrefix;
|
||||
return `${finalPrefix}:${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析内存使用量
|
||||
*/
|
||||
private parseMemoryUsage(memoryStr: string): number {
|
||||
const match = memoryStr.match(/^(\d+(?:\.\d+)?)([KMGT]?B)$/);
|
||||
if (!match) return 0;
|
||||
|
||||
const [, value, unit] = match;
|
||||
const numValue = parseFloat(value);
|
||||
|
||||
switch (unit) {
|
||||
case 'KB': return numValue * 1024;
|
||||
case 'MB': return numValue * 1024 * 1024;
|
||||
case 'GB': return numValue * 1024 * 1024 * 1024;
|
||||
case 'TB': return numValue * 1024 * 1024 * 1024 * 1024;
|
||||
default: return numValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
220
wwjcloud/src/core/cache/distributedLockService.ts
vendored
Normal file
220
wwjcloud/src/core/cache/distributedLockService.ts
vendored
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
export interface LockOptions {
|
||||
ttl?: number; // 锁超时时间(毫秒)
|
||||
retryDelay?: number; // 重试延迟(毫秒)
|
||||
maxRetries?: number; // 最大重试次数
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DistributedLockService {
|
||||
private readonly logger = new Logger(DistributedLockService.name);
|
||||
private readonly appPrefix = 'wwjcloud'; // 使用固定前缀,避免硬编码
|
||||
|
||||
constructor(
|
||||
@Inject('REDIS_CLIENT') private redis: Redis,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取分布式锁
|
||||
*/
|
||||
async acquireLock(
|
||||
key: string,
|
||||
options: LockOptions = {}
|
||||
): Promise<string | null> {
|
||||
const {
|
||||
ttl = 30000, // 默认30秒
|
||||
retryDelay = 100, // 默认100ms
|
||||
maxRetries = 10, // 默认重试10次
|
||||
} = options;
|
||||
|
||||
const lockKey = this.buildLockKey(key);
|
||||
const lockValue = this.generateLockValue();
|
||||
let retries = 0;
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
// 使用 SET NX EX 原子操作获取锁
|
||||
const result = await this.redis.set(lockKey, lockValue, 'PX', ttl, 'NX');
|
||||
|
||||
if (result === 'OK') {
|
||||
this.logger.debug(`Lock acquired: ${lockKey}`);
|
||||
return lockValue;
|
||||
}
|
||||
|
||||
retries++;
|
||||
if (retries < maxRetries) {
|
||||
await this.sleep(retryDelay);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Lock acquisition error: ${error.message}`, error.stack);
|
||||
retries++;
|
||||
if (retries < maxRetries) {
|
||||
await this.sleep(retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.warn(`Failed to acquire lock after ${maxRetries} retries: ${lockKey}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放分布式锁
|
||||
*/
|
||||
async releaseLock(key: string, lockValue: string): Promise<boolean> {
|
||||
const lockKey = this.buildLockKey(key);
|
||||
|
||||
try {
|
||||
// 使用 Lua 脚本确保原子性释放锁
|
||||
const luaScript = `
|
||||
if redis.call("get", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("del", KEYS[1])
|
||||
else
|
||||
return 0
|
||||
end
|
||||
`;
|
||||
|
||||
const result = await this.redis.eval(luaScript, 1, lockKey, lockValue);
|
||||
|
||||
if (result === 1) {
|
||||
this.logger.debug(`Lock released: ${lockKey}`);
|
||||
return true;
|
||||
} else {
|
||||
this.logger.warn(`Lock release failed (not owner): ${lockKey}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Lock release error: ${error.message}`, error.stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查锁是否存在
|
||||
*/
|
||||
async isLocked(key: string): Promise<boolean> {
|
||||
const lockKey = this.buildLockKey(key);
|
||||
|
||||
try {
|
||||
const exists = await this.redis.exists(lockKey);
|
||||
return exists === 1;
|
||||
} catch (error) {
|
||||
this.logger.error(`Lock check error: ${error.message}`, error.stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取锁的剩余时间
|
||||
*/
|
||||
async getLockTtl(key: string): Promise<number> {
|
||||
const lockKey = this.buildLockKey(key);
|
||||
|
||||
try {
|
||||
const ttl = await this.redis.pttl(lockKey);
|
||||
return ttl > 0 ? ttl : 0;
|
||||
} catch (error) {
|
||||
this.logger.error(`Lock TTL check error: ${error.message}`, error.stack);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制释放锁(危险操作,仅用于紧急情况)
|
||||
*/
|
||||
async forceReleaseLock(key: string): Promise<boolean> {
|
||||
const lockKey = this.buildLockKey(key);
|
||||
|
||||
try {
|
||||
const result = await this.redis.del(lockKey);
|
||||
this.logger.warn(`Lock force released: ${lockKey}`);
|
||||
return result === 1;
|
||||
} catch (error) {
|
||||
this.logger.error(`Force lock release error: ${error.message}`, error.stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用锁执行操作
|
||||
*/
|
||||
async withLock<T>(
|
||||
key: string,
|
||||
operation: () => Promise<T>,
|
||||
options: LockOptions = {}
|
||||
): Promise<T> {
|
||||
const lockValue = await this.acquireLock(key, options);
|
||||
|
||||
if (!lockValue) {
|
||||
throw new Error(`Failed to acquire lock: ${key}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await operation();
|
||||
} finally {
|
||||
await this.releaseLock(key, lockValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取锁
|
||||
*/
|
||||
async acquireMultipleLocks(
|
||||
keys: string[],
|
||||
options: LockOptions = {}
|
||||
): Promise<Map<string, string> | null> {
|
||||
const locks = new Map<string, string>();
|
||||
|
||||
try {
|
||||
for (const key of keys) {
|
||||
const lockValue = await this.acquireLock(key, options);
|
||||
if (!lockValue) {
|
||||
// 如果任何一个锁获取失败,释放已获取的锁
|
||||
await this.releaseMultipleLocks(locks);
|
||||
return null;
|
||||
}
|
||||
locks.set(key, lockValue);
|
||||
}
|
||||
|
||||
return locks;
|
||||
} catch (error) {
|
||||
this.logger.error(`Multiple locks acquisition error: ${error.message}`, error.stack);
|
||||
await this.releaseMultipleLocks(locks);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量释放锁
|
||||
*/
|
||||
async releaseMultipleLocks(locks: Map<string, string>): Promise<void> {
|
||||
const promises = Array.from(locks.entries()).map(([key, value]) =>
|
||||
this.releaseLock(key, value)
|
||||
);
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建锁键
|
||||
*/
|
||||
private buildLockKey(key: string): string {
|
||||
return `${this.appPrefix}:lock:${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成锁值
|
||||
*/
|
||||
private generateLockValue(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 睡眠函数
|
||||
*/
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
251
wwjcloud/src/core/cache/multiLevelCacheService.ts
vendored
Normal file
251
wwjcloud/src/core/cache/multiLevelCacheService.ts
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import type { Cache } from 'cache-manager';
|
||||
import { Redis } from 'ioredis';
|
||||
import { CacheService } from './cacheService';
|
||||
|
||||
export interface MultiLevelCacheOptions {
|
||||
l1Ttl?: number; // L1 缓存时间(秒)
|
||||
l2Ttl?: number; // L2 缓存时间(秒)
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MultiLevelCacheService {
|
||||
private readonly logger = new Logger(MultiLevelCacheService.name);
|
||||
private readonly appPrefix = 'wwjcloud'; // 使用固定前缀,避免硬编码
|
||||
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private l1Cache: Cache, // 内存缓存
|
||||
@Inject('REDIS_CLIENT') private l2Cache: Redis, // Redis 缓存
|
||||
private cacheService: CacheService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取缓存(多级)
|
||||
*/
|
||||
async get<T>(key: string, options?: MultiLevelCacheOptions): Promise<T | null> {
|
||||
try {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
|
||||
// L1: 内存缓存
|
||||
const l1Value = await this.l1Cache.get<T>(fullKey);
|
||||
if (l1Value !== undefined && l1Value !== null) {
|
||||
this.logger.debug(`L1 cache hit: ${fullKey}`);
|
||||
return l1Value as T;
|
||||
}
|
||||
|
||||
// L2: Redis 缓存
|
||||
const l2Value = await this.l2Cache.get(fullKey);
|
||||
if (l2Value !== null && l2Value !== undefined) {
|
||||
const parsedValue = JSON.parse(l2Value) as T;
|
||||
// 回填到 L1 缓存
|
||||
await this.l1Cache.set(fullKey, parsedValue, options?.l1Ttl || 60);
|
||||
this.logger.debug(`L2 cache hit: ${fullKey}`);
|
||||
return parsedValue;
|
||||
}
|
||||
|
||||
this.logger.debug(`Cache miss: ${fullKey}`);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache get error: ${error.message}`, error.stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存(多级)
|
||||
*/
|
||||
async set<T>(key: string, value: T, options?: MultiLevelCacheOptions): Promise<void> {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
|
||||
try {
|
||||
// 并行设置 L1 和 L2 缓存
|
||||
await Promise.all([
|
||||
this.l1Cache.set(fullKey, value, options?.l1Ttl || 60),
|
||||
this.l2Cache.setex(fullKey, options?.l2Ttl || 300, JSON.stringify(value)),
|
||||
]);
|
||||
|
||||
this.logger.debug(`Multi-level cache set: ${fullKey}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Multi-level cache set error: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存(多级)
|
||||
*/
|
||||
async del(key: string, options?: MultiLevelCacheOptions): Promise<void> {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
|
||||
try {
|
||||
// 并行删除 L1 和 L2 缓存
|
||||
await Promise.all([
|
||||
this.l1Cache.del(fullKey),
|
||||
this.l2Cache.del(fullKey),
|
||||
]);
|
||||
|
||||
this.logger.debug(`Multi-level cache del: ${fullKey}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Multi-level cache del error: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除缓存
|
||||
*/
|
||||
async delPattern(pattern: string): Promise<void> {
|
||||
try {
|
||||
// 获取匹配的键
|
||||
const keys = await this.l2Cache.keys(pattern);
|
||||
|
||||
if (keys.length > 0) {
|
||||
// 删除 L2 缓存
|
||||
await this.l2Cache.del(...keys);
|
||||
|
||||
// 删除 L1 缓存
|
||||
const l1Promises = keys.map(key => this.l1Cache.del(key));
|
||||
await Promise.allSettled(l1Promises);
|
||||
|
||||
this.logger.debug(`Multi-level cache del pattern: ${pattern}, deleted ${keys.length} keys`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Multi-level cache del pattern error: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取或设置缓存(缓存穿透保护)
|
||||
*/
|
||||
async getOrSet<T>(
|
||||
key: string,
|
||||
factory: () => Promise<T>,
|
||||
options?: MultiLevelCacheOptions
|
||||
): Promise<T> {
|
||||
const fullKey = this.buildKey(key, options?.prefix);
|
||||
|
||||
try {
|
||||
// 先尝试获取缓存
|
||||
let value = await this.get<T>(fullKey, options);
|
||||
|
||||
if (value !== null) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// 缓存未命中,执行工厂函数
|
||||
value = await factory();
|
||||
|
||||
// 设置缓存
|
||||
await this.set(key, value, options);
|
||||
|
||||
return value;
|
||||
} catch (error) {
|
||||
this.logger.error(`Multi-level cache getOrSet error: ${error.message}`, error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预热缓存
|
||||
*/
|
||||
async warmup<T>(
|
||||
keys: string[],
|
||||
factory: (key: string) => Promise<T>,
|
||||
options?: MultiLevelCacheOptions
|
||||
): Promise<void> {
|
||||
this.logger.log(`Starting cache warmup for ${keys.length} keys`);
|
||||
|
||||
const promises = keys.map(async (key) => {
|
||||
try {
|
||||
const value = await factory(key);
|
||||
await this.set(key, value, options);
|
||||
this.logger.debug(`Cache warmed up: ${key}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Cache warmup failed for key ${key}: ${error.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
this.logger.log('Cache warmup completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
async getStats(): Promise<{
|
||||
l1Stats: { size: number; hitRate: number };
|
||||
l2Stats: { size: number; hitRate: number };
|
||||
totalHitRate: number;
|
||||
}> {
|
||||
try {
|
||||
// 获取 L2 缓存统计
|
||||
const l2Info = await this.l2Cache.info('memory');
|
||||
const l2Keys = await this.l2Cache.dbsize();
|
||||
|
||||
// 解析 L2 内存使用
|
||||
const memoryMatch = l2Info.match(/used_memory_human:(\S+)/);
|
||||
const l2Memory = memoryMatch ? memoryMatch[1] : '0B';
|
||||
|
||||
return {
|
||||
l1Stats: {
|
||||
size: 0, // 需要实现 L1 缓存大小统计
|
||||
hitRate: 0, // 需要实现命中率统计
|
||||
},
|
||||
l2Stats: {
|
||||
size: this.parseMemoryUsage(l2Memory),
|
||||
hitRate: 0, // 需要实现命中率统计
|
||||
},
|
||||
totalHitRate: 0, // 需要实现总命中率统计
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Multi-level cache stats error: ${error.message}`, error.stack);
|
||||
return {
|
||||
l1Stats: { size: 0, hitRate: 0 },
|
||||
l2Stats: { size: 0, hitRate: 0 },
|
||||
totalHitRate: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有缓存
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
// 直接使用 Redis 的 FLUSHDB 命令清空 L2 缓存
|
||||
// L1 缓存会在下次访问时自动失效
|
||||
await this.l2Cache.flushdb();
|
||||
|
||||
this.logger.debug('Multi-level cache cleared');
|
||||
} catch (error) {
|
||||
this.logger.error(`Multi-level cache clear error: ${error.message}`, error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建缓存键
|
||||
*/
|
||||
private buildKey(key: string, prefix?: string): string {
|
||||
const finalPrefix = prefix ? `${this.appPrefix}:ml:${prefix}` : `${this.appPrefix}:ml`;
|
||||
return `${finalPrefix}:${key}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析内存使用量
|
||||
*/
|
||||
private parseMemoryUsage(memoryStr: string): number {
|
||||
const match = memoryStr.match(/^(\d+(?:\.\d+)?)([KMGT]?B)$/);
|
||||
if (!match) return 0;
|
||||
|
||||
const [, value, unit] = match;
|
||||
const numValue = parseFloat(value);
|
||||
|
||||
switch (unit) {
|
||||
case 'KB': return numValue * 1024;
|
||||
case 'MB': return numValue * 1024 * 1024;
|
||||
case 'GB': return numValue * 1024 * 1024 * 1024;
|
||||
case 'TB': return numValue * 1024 * 1024 * 1024 * 1024;
|
||||
default: return numValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Base repository abstraction (no ORM dependency)
|
||||
export abstract class BaseRepository<T> {
|
||||
abstract findById(id: string): Promise<T | null>;
|
||||
abstract create(data: Partial<T>): Promise<T>;
|
||||
abstract update(id: string, data: Partial<T>): Promise<T>;
|
||||
abstract delete(id: string): Promise<void>;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||
import { IndexManagerService } from './index-manager.service';
|
||||
import { PerformanceMonitorService } from './performance-monitor.service';
|
||||
import { IndexManagerService } from './indexManagerService';
|
||||
import { PerformanceMonitorService } from './performanceMonitorService';
|
||||
|
||||
/**
|
||||
* 数据库管理控制器
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { IndexManagerService } from './index-manager.service';
|
||||
import { PerformanceMonitorService } from './performance-monitor.service';
|
||||
import { DatabaseAdminController } from './database-admin.controller';
|
||||
import { IndexManagerService } from './indexManagerService';
|
||||
import { PerformanceMonitorService } from './performanceMonitorService';
|
||||
import { DatabaseAdminController } from './databaseAdminController';
|
||||
import { TransactionManager } from './transactionManager';
|
||||
|
||||
/**
|
||||
* 数据库核心模块
|
||||
@@ -18,10 +19,12 @@ import { DatabaseAdminController } from './database-admin.controller';
|
||||
providers: [
|
||||
IndexManagerService,
|
||||
PerformanceMonitorService,
|
||||
TransactionManager,
|
||||
],
|
||||
exports: [
|
||||
IndexManagerService,
|
||||
PerformanceMonitorService,
|
||||
TransactionManager,
|
||||
],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { DiscoveryModule } from '@nestjs/core';
|
||||
import { DomainSdkManager } from './domain-sdk.manager';
|
||||
import { DomainSdkService } from './domain-sdk.service';
|
||||
import { BaseDomainSdk } from './base-domain-sdk';
|
||||
import { CrossDomainAccessGuard } from './cross-domain-access.guard';
|
||||
import { IDomainSdkManager } from '../interfaces/domain-sdk.interface';
|
||||
|
||||
/**
|
||||
* 域SDK模块
|
||||
* 提供跨域访问规范和SDK管理功能
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
DiscoveryModule,
|
||||
],
|
||||
providers: [
|
||||
DomainSdkManager,
|
||||
DomainSdkService,
|
||||
CrossDomainAccessGuard,
|
||||
{
|
||||
provide: 'DOMAIN_SDK_MANAGER',
|
||||
useExisting: DomainSdkManager,
|
||||
},
|
||||
{
|
||||
provide: 'IDomainSdkManager',
|
||||
useExisting: DomainSdkManager,
|
||||
},
|
||||
],
|
||||
exports: [
|
||||
DomainSdkManager,
|
||||
DomainSdkService,
|
||||
CrossDomainAccessGuard,
|
||||
'DOMAIN_SDK_MANAGER',
|
||||
'IDomainSdkManager',
|
||||
],
|
||||
})
|
||||
export class DomainSdkModule {}
|
||||
@@ -1,346 +0,0 @@
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import type {
|
||||
IDomainSdkManager,
|
||||
IDomainSdk,
|
||||
QueryOptions,
|
||||
QueryResult,
|
||||
EntityResult,
|
||||
OperationResult,
|
||||
DomainCommunicationProtocol
|
||||
} from '@wwjCore/interfaces/domain-sdk.interface';
|
||||
import {
|
||||
DomainSdkError,
|
||||
CrossDomainAccessDeniedError,
|
||||
DomainUnavailableError
|
||||
} from '@wwjCore/interfaces/domain-sdk.interface';
|
||||
import type { IEventBus } from '@wwjCore/interfaces/event-bus.interface';
|
||||
|
||||
/**
|
||||
* 域SDK服务
|
||||
* 提供便捷的跨域调用接口和通信协议实现
|
||||
*/
|
||||
@Injectable()
|
||||
export class DomainSdkService implements DomainCommunicationProtocol {
|
||||
private readonly logger = new Logger(DomainSdkService.name);
|
||||
private readonly currentDomain: string;
|
||||
|
||||
constructor(
|
||||
@Inject('IDomainSdkManager')
|
||||
private readonly sdkManager: IDomainSdkManager,
|
||||
@Inject('EVENT_BUS_PROVIDER')
|
||||
private readonly eventBus: IEventBus
|
||||
) {
|
||||
// 从环境变量或配置中获取当前域名
|
||||
this.currentDomain = process.env.CURRENT_DOMAIN || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步调用其他域的方法
|
||||
*/
|
||||
async call<T>(targetDomain: string, method: string, params: any): Promise<T> {
|
||||
try {
|
||||
this.logger.debug(`Cross-domain call: ${this.currentDomain} -> ${targetDomain}.${method}`);
|
||||
|
||||
// 检查跨域访问权限
|
||||
await this.checkAccess(targetDomain, 'call');
|
||||
|
||||
// 获取目标域SDK
|
||||
const sdk = await this.sdkManager.getSdk(targetDomain);
|
||||
if (!sdk) {
|
||||
throw new DomainUnavailableError(targetDomain, method);
|
||||
}
|
||||
|
||||
// 检查方法是否存在
|
||||
if (typeof sdk[method] !== 'function') {
|
||||
throw new DomainSdkError(
|
||||
`Method not found: ${method} in domain ${targetDomain}`,
|
||||
targetDomain,
|
||||
method,
|
||||
'METHOD_NOT_FOUND'
|
||||
);
|
||||
}
|
||||
|
||||
// 调用方法
|
||||
const result = await sdk[method](params);
|
||||
|
||||
this.logger.debug(`Cross-domain call completed: ${this.currentDomain} -> ${targetDomain}.${method}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Cross-domain call failed: ${this.currentDomain} -> ${targetDomain}.${method}`,
|
||||
error.stack
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步发送消息到其他域
|
||||
*/
|
||||
async send(targetDomain: string, message: any): Promise<void> {
|
||||
try {
|
||||
this.logger.debug(`Cross-domain message: ${this.currentDomain} -> ${targetDomain}`);
|
||||
|
||||
// 检查跨域访问权限
|
||||
await this.checkAccess(targetDomain, 'send');
|
||||
|
||||
// 通过事件总线发送消息
|
||||
await this.eventBus.publish(`domain.${targetDomain}.message`, {
|
||||
eventType: `domain.${targetDomain}.message`,
|
||||
aggregateId: `${this.currentDomain}-${Date.now()}`,
|
||||
tenantId: 'system',
|
||||
idempotencyKey: `${this.currentDomain}-${targetDomain}-${Date.now()}`,
|
||||
traceId: `trace-${Date.now()}`,
|
||||
data: message,
|
||||
occurredAt: new Date().toISOString(),
|
||||
version: '1.0',
|
||||
fromDomain: this.currentDomain,
|
||||
toDomain: targetDomain,
|
||||
message,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
this.logger.debug(`Cross-domain message sent: ${this.currentDomain} -> ${targetDomain}`);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Cross-domain message failed: ${this.currentDomain} -> ${targetDomain}`,
|
||||
error.stack
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布事件
|
||||
*/
|
||||
async publish(event: string, data: any): Promise<void> {
|
||||
try {
|
||||
this.logger.debug(`Publishing event: ${event} from domain ${this.currentDomain}`);
|
||||
|
||||
await this.eventBus.publish(event, {
|
||||
...data,
|
||||
sourceDomain: this.currentDomain,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
this.logger.debug(`Event published: ${event}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to publish event: ${event}`, error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅事件
|
||||
*/
|
||||
async subscribe(event: string, handler: Function): Promise<void> {
|
||||
try {
|
||||
this.logger.debug(`Subscribing to event: ${event} in domain ${this.currentDomain}`);
|
||||
|
||||
await this.eventBus.subscribe(event, async (eventData) => {
|
||||
try {
|
||||
await handler(eventData);
|
||||
} catch (error) {
|
||||
this.logger.error(`Event handler failed for ${event}`, error.stack);
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.debug(`Subscribed to event: ${event}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to subscribe to event: ${event}`, error.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询其他域的数据
|
||||
*/
|
||||
async query<T>(
|
||||
targetDomain: string,
|
||||
entity: string,
|
||||
options: QueryOptions = {}
|
||||
): Promise<QueryResult<T>> {
|
||||
return this.call<QueryResult<T>>(targetDomain, 'query', { entity, options });
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取其他域的实体
|
||||
*/
|
||||
async findById<T>(
|
||||
targetDomain: string,
|
||||
entity: string,
|
||||
id: string | number
|
||||
): Promise<EntityResult<T>> {
|
||||
return this.call<EntityResult<T>>(targetDomain, 'findById', { entity, id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 在其他域创建实体
|
||||
*/
|
||||
async create<T>(
|
||||
targetDomain: string,
|
||||
entity: string,
|
||||
data: Partial<T>
|
||||
): Promise<OperationResult<T>> {
|
||||
return this.call<OperationResult<T>>(targetDomain, 'create', { entity, data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新其他域的实体
|
||||
*/
|
||||
async update<T>(
|
||||
targetDomain: string,
|
||||
entity: string,
|
||||
id: string | number,
|
||||
data: Partial<T>
|
||||
): Promise<OperationResult<T>> {
|
||||
return this.call<OperationResult<T>>(targetDomain, 'update', { entity, id, data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除其他域的实体
|
||||
*/
|
||||
async delete(
|
||||
targetDomain: string,
|
||||
entity: string,
|
||||
id: string | number
|
||||
): Promise<OperationResult<boolean>> {
|
||||
return this.call<OperationResult<boolean>>(targetDomain, 'delete', { entity, id });
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证其他域的数据
|
||||
*/
|
||||
async validate<T>(
|
||||
targetDomain: string,
|
||||
entity: string,
|
||||
data: T
|
||||
): Promise<OperationResult<boolean>> {
|
||||
return this.call<OperationResult<boolean>>(targetDomain, 'validate', { entity, data });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取域健康状态
|
||||
*/
|
||||
async getHealthStatus(domain?: string) {
|
||||
return this.sdkManager.getHealthStatus(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册的域列表
|
||||
*/
|
||||
async getRegisteredDomains(): Promise<string[]> {
|
||||
return this.sdkManager.getRegisteredDomains();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查域依赖
|
||||
*/
|
||||
async checkDomainDependencies(domain: string): Promise<boolean> {
|
||||
return this.sdkManager.checkDependencies(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前域名
|
||||
*/
|
||||
getCurrentDomain(): string {
|
||||
return this.currentDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查跨域访问权限
|
||||
*/
|
||||
private async checkAccess(targetDomain: string, operation: string): Promise<void> {
|
||||
try {
|
||||
// 通过SDK管理器检查访问权限
|
||||
(this.sdkManager as any).checkCrossDomainAccess?.(this.currentDomain, targetDomain, operation);
|
||||
} catch (error) {
|
||||
if (error instanceof CrossDomainAccessDeniedError) {
|
||||
throw error;
|
||||
}
|
||||
// 如果检查方法不存在,记录警告但不阻止访问
|
||||
this.logger.warn(`Access check method not available, allowing access: ${this.currentDomain} -> ${targetDomain}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量调用
|
||||
*/
|
||||
async batchCall<T>(
|
||||
calls: Array<{ domain: string; method: string; params: any }>
|
||||
): Promise<Array<OperationResult<T>>> {
|
||||
const results: Array<OperationResult<T>> = [];
|
||||
|
||||
// 并行执行所有调用
|
||||
const promises = calls.map(async (call, index) => {
|
||||
try {
|
||||
const result = await this.call<T>(call.domain, call.method, call.params);
|
||||
return { index, result: { success: true, data: result } };
|
||||
} catch (error) {
|
||||
return {
|
||||
index,
|
||||
result: {
|
||||
success: false,
|
||||
error: error.message,
|
||||
code: error.code
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
// 按原始顺序排列结果
|
||||
responses.sort((a, b) => a.index - b.index);
|
||||
responses.forEach(response => results.push(response.result));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建域间事务
|
||||
*/
|
||||
async createCrossDomainTransaction(
|
||||
operations: Array<{ domain: string; method: string; params: any }>
|
||||
): Promise<OperationResult<any[]>> {
|
||||
const results: any[] = [];
|
||||
const rollbackOperations: Array<{ domain: string; method: string; params: any }> = [];
|
||||
|
||||
try {
|
||||
// 执行所有操作
|
||||
for (const operation of operations) {
|
||||
const result = await this.call(operation.domain, operation.method, operation.params);
|
||||
results.push(result);
|
||||
|
||||
// 记录回滚操作(如果支持)
|
||||
if (operation.params.rollbackMethod) {
|
||||
rollbackOperations.push({
|
||||
domain: operation.domain,
|
||||
method: operation.params.rollbackMethod,
|
||||
params: { ...operation.params, result }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: results };
|
||||
} catch (error) {
|
||||
this.logger.error('Cross-domain transaction failed, attempting rollback', error.stack);
|
||||
|
||||
// 尝试回滚已执行的操作
|
||||
for (const rollback of rollbackOperations.reverse()) {
|
||||
try {
|
||||
await this.call(rollback.domain, rollback.method, rollback.params);
|
||||
} catch (rollbackError) {
|
||||
this.logger.error('Rollback operation failed', rollbackError.stack);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
code: error.code,
|
||||
data: results
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export * from './status.enum';
|
||||
export * from './statusEnum';
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DiscoveryModule } from '@nestjs/core';
|
||||
import { DatabaseEventBusProvider } from './database-event-bus.provider';
|
||||
import { EventEntity } from '../queue/entities/event.entity';
|
||||
import { DomainEventService } from './domain-event.service';
|
||||
import { EventHandlerDiscoveryService } from './event-handler-discovery.service';
|
||||
import { IEventBus } from '../interfaces/event-bus.interface';
|
||||
|
||||
/**
|
||||
* 事件总线模块
|
||||
* 提供统一的事件总线服务抽象
|
||||
*/
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([EventEntity]),
|
||||
ConfigModule,
|
||||
DiscoveryModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: 'EVENT_BUS_PROVIDER',
|
||||
useClass: DatabaseEventBusProvider,
|
||||
},
|
||||
{
|
||||
provide: 'IEventBus',
|
||||
useExisting: 'EVENT_BUS_PROVIDER',
|
||||
},
|
||||
DomainEventService,
|
||||
EventHandlerDiscoveryService,
|
||||
],
|
||||
exports: [
|
||||
'EVENT_BUS_PROVIDER',
|
||||
'IEventBus',
|
||||
DomainEventService,
|
||||
EventHandlerDiscoveryService,
|
||||
],
|
||||
})
|
||||
export class EventBusModule {}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SubscribeOptions,
|
||||
OutboxEvent,
|
||||
OutboxEventStatus
|
||||
} from '../interfaces/event-bus.interface';
|
||||
} from '../interfaces/eventInterface';
|
||||
import { EventEntity } from '../queue/entities/event.entity';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import type { IEventBus, DomainEvent, EventHandler, SubscribeOptions } from '@wwjCore/interfaces/event-bus.interface';
|
||||
import type { IEventBus, DomainEvent, EventHandler, SubscribeOptions } from '@wwjCore/interfaces/eventInterface';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
16
wwjcloud/src/core/event/eventBusPublisher.ts
Normal file
16
wwjcloud/src/core/event/eventBusPublisher.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class EventBusPublisher {
|
||||
private readonly logger = new Logger(EventBusPublisher.name);
|
||||
|
||||
async publish<T>(event: string, data: T, metadata?: Record<string, any>): Promise<void> {
|
||||
this.logger.debug(`Publishing event: ${event}`, { data, metadata });
|
||||
// TODO: 实现具体的发布逻辑
|
||||
}
|
||||
|
||||
async publishBatch<T>(events: Array<{ event: string; data: T; metadata?: Record<string, any> }>): Promise<void> {
|
||||
this.logger.debug(`Publishing batch events: ${events.length} events`);
|
||||
// TODO: 实现具体的批量发布逻辑
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
||||
import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
|
||||
import { DomainEventService } from './domain-event.service';
|
||||
import { DomainEventService } from './domainEventService';
|
||||
import {
|
||||
EVENT_HANDLER_METADATA,
|
||||
DOMAIN_EVENT_HANDLER,
|
||||
EventHandlerMetadata,
|
||||
} from './decorators/event-handler.decorator';
|
||||
import { DomainEvent } from '@wwjCore/interfaces/event-bus.interface';
|
||||
import { DomainEvent } from '@wwjCore/interfaces/eventInterface';
|
||||
|
||||
/**
|
||||
* 事件处理器发现服务
|
||||
19
wwjcloud/src/core/event/eventModule.ts
Normal file
19
wwjcloud/src/core/event/eventModule.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { EventService } from './eventService';
|
||||
import { EventBusSubscriber } from './eventSubscriber';
|
||||
import { EventBusPublisher } from './eventBusPublisher';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
EventService,
|
||||
EventBusSubscriber,
|
||||
EventBusPublisher,
|
||||
],
|
||||
exports: [
|
||||
EventService,
|
||||
EventBusSubscriber,
|
||||
EventBusPublisher,
|
||||
],
|
||||
})
|
||||
export class EventModule {}
|
||||
38
wwjcloud/src/core/event/eventService.ts
Normal file
38
wwjcloud/src/core/event/eventService.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
export interface EventMessage<T = any> {
|
||||
id: string;
|
||||
event: string;
|
||||
data: T;
|
||||
timestamp: number;
|
||||
source: string;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EventService {
|
||||
private readonly logger = new Logger(EventService.name);
|
||||
|
||||
async publish<T>(event: string, data: T, metadata?: Record<string, any>): Promise<void> {
|
||||
const message: EventMessage<T> = {
|
||||
id: this.generateId(),
|
||||
event,
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
source: 'event-bus',
|
||||
metadata,
|
||||
};
|
||||
|
||||
this.logger.debug(`Publishing event: ${event}`, { messageId: message.id });
|
||||
// TODO: 实现具体的发布逻辑
|
||||
}
|
||||
|
||||
async subscribe(event: string, handler: (data: any) => Promise<void>): Promise<void> {
|
||||
this.logger.debug(`Subscribing to event: ${event}`);
|
||||
// TODO: 实现具体的订阅逻辑
|
||||
}
|
||||
|
||||
private generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
}
|
||||
16
wwjcloud/src/core/event/eventSubscriber.ts
Normal file
16
wwjcloud/src/core/event/eventSubscriber.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class EventBusSubscriber {
|
||||
private readonly logger = new Logger(EventBusSubscriber.name);
|
||||
|
||||
async subscribe(event: string, handler: (data: any) => Promise<void>): Promise<void> {
|
||||
this.logger.debug(`Subscribing to event: ${event}`);
|
||||
// TODO: 实现具体的订阅逻辑
|
||||
}
|
||||
|
||||
async unsubscribe(event: string): Promise<void> {
|
||||
this.logger.debug(`Unsubscribing from event: ${event}`);
|
||||
// TODO: 实现具体的取消订阅逻辑
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, OnModuleDestroy, Inject, Logger } from '@nestjs/common';
|
||||
import { IEventBusProvider, EventPublishOptions, EventHandler } from '../interfaces/queue.interface';
|
||||
import { DomainEvent } from '../interfaces/event-bus.interface';
|
||||
import { DomainEvent } from '../interfaces/eventInterface';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DomainEventService } from './domain-event.service';
|
||||
import { validateEvent } from './contract.validator';
|
||||
import { DomainEventService } from './domainEventService';
|
||||
import { validateEvent } from './contractValidator';
|
||||
|
||||
@Injectable()
|
||||
export class EventPublishHelper {
|
||||
@@ -1,320 +0,0 @@
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import type { IQueueProvider } from '@wwjCore/interfaces/queue.interface';
|
||||
import type { IEventBus } from '@wwjCore/interfaces/event-bus.interface';
|
||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||
import type { Cache } from 'cache-manager';
|
||||
|
||||
export interface HealthStatus {
|
||||
status: 'ok' | 'error';
|
||||
timestamp: string;
|
||||
uptime: number;
|
||||
version?: string;
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ComponentHealth {
|
||||
status: 'healthy' | 'unhealthy' | 'degraded';
|
||||
message?: string;
|
||||
responseTime?: number;
|
||||
details?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 健康检查服务
|
||||
*/
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
constructor(
|
||||
@InjectDataSource()
|
||||
private readonly dataSource: DataSource,
|
||||
@Inject('QUEUE_PROVIDER')
|
||||
private readonly queueProvider: IQueueProvider,
|
||||
@Inject('EVENT_BUS_PROVIDER')
|
||||
private readonly eventBus: IEventBus,
|
||||
@Inject(CACHE_MANAGER)
|
||||
private readonly cacheManager: Cache,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 基础健康检查
|
||||
*/
|
||||
async check(): Promise<HealthStatus> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 检查数据库连接
|
||||
await this.dataSource.query('SELECT 1');
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
version: process.env.npm_package_version || '1.0.0',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
details: {
|
||||
error: error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 详细健康检查
|
||||
*/
|
||||
async detailedCheck(): Promise<{
|
||||
status: 'healthy' | 'unhealthy' | 'degraded';
|
||||
timestamp: string;
|
||||
uptime: number;
|
||||
components: Record<string, ComponentHealth>;
|
||||
}> {
|
||||
const components: Record<string, ComponentHealth> = {};
|
||||
|
||||
// 并行检查所有组件
|
||||
const [database, queue, eventBus, cache, external] = await Promise.allSettled([
|
||||
this.checkDatabase(),
|
||||
this.checkQueue(),
|
||||
this.checkEventBus(),
|
||||
this.checkCache(),
|
||||
this.checkExternalServices(),
|
||||
]);
|
||||
|
||||
components.database = database.status === 'fulfilled' ? database.value : {
|
||||
status: 'unhealthy',
|
||||
message: database.status === 'rejected' ? database.reason?.message : '检查失败',
|
||||
};
|
||||
|
||||
components.queue = queue.status === 'fulfilled' ? queue.value : {
|
||||
status: 'unhealthy',
|
||||
message: queue.status === 'rejected' ? queue.reason?.message : '检查失败',
|
||||
};
|
||||
|
||||
components.eventBus = eventBus.status === 'fulfilled' ? eventBus.value : {
|
||||
status: 'unhealthy',
|
||||
message: eventBus.status === 'rejected' ? eventBus.reason?.message : '检查失败',
|
||||
};
|
||||
|
||||
components.cache = cache.status === 'fulfilled' ? cache.value : {
|
||||
status: 'unhealthy',
|
||||
message: cache.status === 'rejected' ? cache.reason?.message : '检查失败',
|
||||
};
|
||||
|
||||
components.external = external.status === 'fulfilled' ? external.value : {
|
||||
status: 'unhealthy',
|
||||
message: external.status === 'rejected' ? external.reason?.message : '检查失败',
|
||||
};
|
||||
|
||||
// 计算整体状态
|
||||
const overallStatus = this.calculateOverallStatus(components);
|
||||
|
||||
return {
|
||||
status: overallStatus,
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
components,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库健康检查
|
||||
*/
|
||||
async checkDatabase(): Promise<ComponentHealth> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 检查数据库连接
|
||||
await this.dataSource.query('SELECT 1');
|
||||
|
||||
// 检查数据库版本
|
||||
const [versionResult] = await this.dataSource.query('SELECT VERSION() as version');
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: responseTime < 1000 ? 'healthy' : 'degraded',
|
||||
responseTime,
|
||||
details: {
|
||||
version: versionResult?.version,
|
||||
connectionPool: {
|
||||
active: (this.dataSource.driver as any).pool?.numUsed || 0,
|
||||
idle: (this.dataSource.driver as any).pool?.numFree || 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
message: `数据库连接失败: ${error.message}`,
|
||||
responseTime: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 队列健康检查
|
||||
*/
|
||||
async checkQueue(): Promise<ComponentHealth> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 检查队列提供者状态
|
||||
const queue = await this.queueProvider.getQueue('health-check');
|
||||
|
||||
// 尝试添加一个测试任务
|
||||
await queue.add('health-check', { timestamp: Date.now() }, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: responseTime < 2000 ? 'healthy' : 'degraded',
|
||||
responseTime,
|
||||
details: {
|
||||
provider: 'database',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
message: `队列服务异常: ${error.message}`,
|
||||
responseTime: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件总线健康检查
|
||||
*/
|
||||
async checkEventBus(): Promise<ComponentHealth> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 检查事件总线状态
|
||||
const testEvent = {
|
||||
eventType: 'health.check.test',
|
||||
aggregateId: 'health-check',
|
||||
tenantId: 'system',
|
||||
idempotencyKey: `health-${Date.now()}`,
|
||||
traceId: `trace-${Date.now()}`,
|
||||
data: { timestamp: Date.now() },
|
||||
occurredAt: new Date().toISOString(),
|
||||
version: '1.0',
|
||||
metadata: {},
|
||||
};
|
||||
|
||||
await this.eventBus.publish('health.check', testEvent);
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: responseTime < 2000 ? 'healthy' : 'degraded',
|
||||
responseTime,
|
||||
details: {
|
||||
provider: 'database',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
message: `事件总线异常: ${error.message}`,
|
||||
responseTime: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存健康检查
|
||||
*/
|
||||
async checkCache(): Promise<ComponentHealth> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const testKey = `health-check-${Date.now()}`;
|
||||
const testValue = { timestamp: Date.now() };
|
||||
|
||||
// 测试缓存写入
|
||||
await this.cacheManager.set(testKey, testValue, 10000);
|
||||
|
||||
// 测试缓存读取
|
||||
const cachedValue = await this.cacheManager.get(testKey);
|
||||
|
||||
// 清理测试数据
|
||||
await this.cacheManager.del(testKey);
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
if (!cachedValue) {
|
||||
throw new Error('缓存读取失败');
|
||||
}
|
||||
|
||||
return {
|
||||
status: responseTime < 500 ? 'healthy' : 'degraded',
|
||||
responseTime,
|
||||
details: {
|
||||
provider: 'memory',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
message: `缓存服务异常: ${error.message}`,
|
||||
responseTime: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部服务健康检查
|
||||
*/
|
||||
async checkExternalServices(): Promise<ComponentHealth> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 这里可以添加对外部服务的检查
|
||||
// 例如: Redis, Elasticsearch, 第三方API等
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
return {
|
||||
status: 'healthy',
|
||||
responseTime,
|
||||
details: {
|
||||
services: [],
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
message: `外部服务异常: ${error.message}`,
|
||||
responseTime: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算整体健康状态
|
||||
*/
|
||||
private calculateOverallStatus(
|
||||
components: Record<string, ComponentHealth>,
|
||||
): 'healthy' | 'unhealthy' | 'degraded' {
|
||||
const statuses = Object.values(components).map(c => c.status);
|
||||
|
||||
if (statuses.includes('unhealthy')) {
|
||||
return 'unhealthy';
|
||||
}
|
||||
|
||||
if (statuses.includes('degraded')) {
|
||||
return 'degraded';
|
||||
}
|
||||
|
||||
return 'healthy';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { HealthService } from './health.service';
|
||||
import { HealthService } from './healthService';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
@@ -56,7 +56,7 @@ export class HealthController {
|
||||
/**
|
||||
* 事件总线健康检查
|
||||
*/
|
||||
@Get('event-bus')
|
||||
@Get('event')
|
||||
@ApiOperation({ summary: '事件总线健康检查' })
|
||||
@ApiResponse({ status: 200, description: '事件总线正常' })
|
||||
@ApiResponse({ status: 503, description: '事件总线异常' })
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthzController } from './healthz.controller';
|
||||
import { HealthService } from './health.service';
|
||||
import { QueueModule } from '@wwjCore/queue/queue.module';
|
||||
import { EventBusModule } from '@wwjCore/event-bus/event-bus.module';
|
||||
import { HealthController } from './healthController';
|
||||
import { HealthzController } from './healthzController';
|
||||
import { HealthService } from './healthService';
|
||||
import { QueueModule } from '@wwjCore/queue/queueModule';
|
||||
import { EventModule } from '@wwjCore/event/eventModule';
|
||||
|
||||
/**
|
||||
* 健康检查模块
|
||||
@@ -12,7 +12,7 @@ import { EventBusModule } from '@wwjCore/event-bus/event-bus.module';
|
||||
@Module({
|
||||
imports: [
|
||||
QueueModule,
|
||||
EventBusModule,
|
||||
EventModule,
|
||||
],
|
||||
controllers: [
|
||||
HealthController,
|
||||
288
wwjcloud/src/core/health/healthService.ts
Normal file
288
wwjcloud/src/core/health/healthService.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { HealthCheckResult, HealthIndicatorResult } from '@nestjs/terminus';
|
||||
|
||||
export interface HealthCheck {
|
||||
name: string;
|
||||
status: 'up' | 'down';
|
||||
message?: string;
|
||||
details?: any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
private readonly logger = new Logger(HealthService.name);
|
||||
private readonly version = '1.0.0'; // 使用固定版本号,避免硬编码
|
||||
|
||||
/**
|
||||
* 获取应用健康状态
|
||||
*/
|
||||
async check(): Promise<HealthCheckResult> {
|
||||
return this.getHealth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取应用健康状态
|
||||
*/
|
||||
async getHealth(): Promise<HealthCheckResult> {
|
||||
const checks: HealthIndicatorResult = {};
|
||||
|
||||
try {
|
||||
// 应用基础健康检查
|
||||
checks['app'] = {
|
||||
status: 'up',
|
||||
version: this.version,
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 内存健康检查
|
||||
checks['memory'] = this.checkMemory();
|
||||
|
||||
// 磁盘健康检查
|
||||
checks['disk'] = await this.checkDisk();
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
info: checks,
|
||||
error: {},
|
||||
details: checks,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Health check failed:', error);
|
||||
return {
|
||||
status: 'error',
|
||||
info: {},
|
||||
error: checks,
|
||||
details: checks,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细健康状态
|
||||
*/
|
||||
async detailedCheck(): Promise<HealthCheck[]> {
|
||||
return this.getDetailedHealth();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细健康状态
|
||||
*/
|
||||
async getDetailedHealth(): Promise<HealthCheck[]> {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// 应用状态
|
||||
checks.push({
|
||||
name: 'application',
|
||||
status: 'up',
|
||||
message: 'Application is running',
|
||||
details: {
|
||||
version: this.version,
|
||||
uptime: process.uptime(),
|
||||
pid: process.pid,
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
},
|
||||
});
|
||||
|
||||
// 内存状态
|
||||
const memoryCheck = this.checkMemory();
|
||||
checks.push({
|
||||
name: 'memory',
|
||||
status: memoryCheck.status,
|
||||
message: memoryCheck.message,
|
||||
details: memoryCheck.details,
|
||||
});
|
||||
|
||||
// 磁盘状态
|
||||
const diskCheck = await this.checkDisk();
|
||||
checks.push({
|
||||
name: 'disk',
|
||||
status: diskCheck.status,
|
||||
message: diskCheck.message,
|
||||
details: diskCheck.details,
|
||||
});
|
||||
|
||||
return checks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查内存使用情况
|
||||
*/
|
||||
private checkMemory(): HealthCheck {
|
||||
const memUsage = process.memoryUsage();
|
||||
const memUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
|
||||
|
||||
if (memUsagePercent > 90) {
|
||||
return {
|
||||
name: 'memory',
|
||||
status: 'down',
|
||||
message: 'Memory usage is too high',
|
||||
details: {
|
||||
heapUsed: this.formatBytes(memUsage.heapUsed),
|
||||
heapTotal: this.formatBytes(memUsage.heapTotal),
|
||||
usagePercent: memUsagePercent.toFixed(2) + '%',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'memory',
|
||||
status: 'up',
|
||||
message: 'Memory usage is normal',
|
||||
details: {
|
||||
heapUsed: this.formatBytes(memUsage.heapUsed),
|
||||
heapTotal: this.formatBytes(memUsage.heapTotal),
|
||||
usagePercent: memUsagePercent.toFixed(2) + '%',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查磁盘使用情况
|
||||
*/
|
||||
private async checkDisk(): Promise<HealthCheck> {
|
||||
try {
|
||||
// 这里可以实现磁盘检查逻辑
|
||||
// 由于 Node.js 没有内置的磁盘检查,这里返回模拟数据
|
||||
return {
|
||||
name: 'disk',
|
||||
status: 'up',
|
||||
message: 'Disk usage is normal',
|
||||
details: {
|
||||
available: 'Unknown',
|
||||
total: 'Unknown',
|
||||
usagePercent: 'Unknown',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: 'disk',
|
||||
status: 'down',
|
||||
message: 'Failed to check disk usage',
|
||||
details: { error: error.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节数
|
||||
*/
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查数据库健康状态
|
||||
*/
|
||||
async checkDatabase(): Promise<HealthCheck> {
|
||||
try {
|
||||
// 这里应该实现数据库连接检查
|
||||
return {
|
||||
name: 'database',
|
||||
status: 'up',
|
||||
message: 'Database connection is healthy',
|
||||
details: { connection: 'active' },
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: 'database',
|
||||
status: 'down',
|
||||
message: 'Database connection failed',
|
||||
details: { error: error.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查队列健康状态
|
||||
*/
|
||||
async checkQueue(): Promise<HealthCheck> {
|
||||
try {
|
||||
// 这里应该实现队列检查
|
||||
return {
|
||||
name: 'queue',
|
||||
status: 'up',
|
||||
message: 'Queue system is healthy',
|
||||
details: { status: 'active' },
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: 'queue',
|
||||
status: 'down',
|
||||
message: 'Queue system failed',
|
||||
details: { error: error.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查事件总线健康状态
|
||||
*/
|
||||
async checkEventBus(): Promise<HealthCheck> {
|
||||
try {
|
||||
// 这里应该实现事件总线检查
|
||||
return {
|
||||
name: 'event',
|
||||
status: 'up',
|
||||
message: 'Event bus is healthy',
|
||||
details: { status: 'active' },
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: 'event',
|
||||
status: 'down',
|
||||
message: 'Event bus failed',
|
||||
details: { error: error.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存健康状态
|
||||
*/
|
||||
async checkCache(): Promise<HealthCheck> {
|
||||
try {
|
||||
// 这里应该实现缓存检查
|
||||
return {
|
||||
name: 'cache',
|
||||
status: 'up',
|
||||
message: 'Cache system is healthy',
|
||||
details: { status: 'active' },
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: 'cache',
|
||||
status: 'down',
|
||||
message: 'Cache system failed',
|
||||
details: { error: error.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查外部服务健康状态
|
||||
*/
|
||||
async checkExternalServices(): Promise<HealthCheck> {
|
||||
try {
|
||||
// 这里应该实现外部服务检查
|
||||
return {
|
||||
name: 'external-services',
|
||||
status: 'up',
|
||||
message: 'External services are healthy',
|
||||
details: { status: 'active' },
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: 'external-services',
|
||||
status: 'down',
|
||||
message: 'External services failed',
|
||||
details: { error: error.message },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller, Get, HttpStatus, Res } from '@nestjs/common';
|
||||
import type { FastifyReply } from 'fastify';
|
||||
import { HealthService } from './health.service';
|
||||
import { HealthService } from './healthService';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
@@ -27,13 +27,13 @@ export class HealthzController {
|
||||
if (health.status === 'ok') {
|
||||
return res.status(HttpStatus.OK).send({
|
||||
status: 'ok',
|
||||
timestamp: health.timestamp,
|
||||
uptime: health.uptime,
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
});
|
||||
} else {
|
||||
return res.status(HttpStatus.SERVICE_UNAVAILABLE).send({
|
||||
status: 'error',
|
||||
timestamp: health.timestamp,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: '应用程序不可用',
|
||||
});
|
||||
}
|
||||
@@ -62,20 +62,23 @@ export class HealthzController {
|
||||
// 检查关键组件状态
|
||||
const criticalComponents = ['database', 'queue', 'eventBus'];
|
||||
const criticalFailures = criticalComponents.filter(
|
||||
component => health.components[component]?.status === 'unhealthy'
|
||||
component => health.find(h => h.name === component)?.status === 'down'
|
||||
);
|
||||
|
||||
if (health.status === 'healthy' || (health.status === 'degraded' && criticalFailures.length === 0)) {
|
||||
const isHealthy = health.every(h => h.status === 'up');
|
||||
const isDegraded = health.some(h => h.status === 'down') && criticalFailures.length === 0;
|
||||
|
||||
if (isHealthy || isDegraded) {
|
||||
return res.status(HttpStatus.OK).send({
|
||||
status: 'ready',
|
||||
timestamp: health.timestamp,
|
||||
components: health.components,
|
||||
timestamp: new Date().toISOString(),
|
||||
components: health,
|
||||
});
|
||||
} else {
|
||||
return res.status(HttpStatus.SERVICE_UNAVAILABLE).send({
|
||||
status: 'not_ready',
|
||||
timestamp: health.timestamp,
|
||||
components: health.components,
|
||||
timestamp: new Date().toISOString(),
|
||||
components: health,
|
||||
criticalFailures,
|
||||
});
|
||||
}
|
||||
@@ -103,7 +106,7 @@ export class HealthzController {
|
||||
// 这里可以检查数据库连接、必要的初始化等
|
||||
const health = await this.healthService.checkDatabase();
|
||||
|
||||
if (health.status === 'healthy' || health.status === 'degraded') {
|
||||
if (health.status === 'up') {
|
||||
return res.status(HttpStatus.OK).send({
|
||||
status: 'started',
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, Inject } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { HttpMetricsService } from '../../observability/metrics/http-metrics.service';
|
||||
import { HttpMetricsService } from '../../observability/metrics/httpMetricsService';
|
||||
|
||||
interface SuccessBody<T = any> {
|
||||
code: number;
|
||||
@@ -2,10 +2,10 @@
|
||||
export * from './validation/pipes';
|
||||
|
||||
// 数据库相关
|
||||
export * from './database/database.module';
|
||||
export * from './database/index-manager.service';
|
||||
export * from './database/performance-monitor.service';
|
||||
export * from './database/database-admin.controller';
|
||||
export * from './database/databaseModule';
|
||||
export * from './database/indexManagerService';
|
||||
export * from './database/performanceMonitorService';
|
||||
export * from './database/databaseAdminController';
|
||||
|
||||
// 导出基础抽象类
|
||||
export * from './base/BaseEntity';
|
||||
@@ -19,35 +19,63 @@ export * from './utils/time.utils';
|
||||
export * from './lang/LangDict';
|
||||
export * from './lang/DictLoader';
|
||||
export * from './lang/LangService';
|
||||
export * from './lang/lang.module';
|
||||
export * from './lang/langModule';
|
||||
|
||||
// 导出队列接口和模块
|
||||
export * from './interfaces/queue.interface';
|
||||
export * from './queue/queue.module';
|
||||
export * from './queue/queueModule';
|
||||
export * from './queue/entities/job.entity';
|
||||
export * from './queue/entities/job-failed.entity';
|
||||
export * from './queue/database-queue.provider';
|
||||
export * from './queue/databaseQueueProvider';
|
||||
|
||||
// 导出事件总线接口和模块
|
||||
export * from './interfaces/event-bus.interface';
|
||||
export * from './event-bus/event-bus.module';
|
||||
export * from './event-bus/database-event-bus.provider';
|
||||
export * from './event-bus/domain-event.service';
|
||||
export * from './event-bus/event-handler-discovery.service';
|
||||
export * from './interfaces/eventInterface';
|
||||
export * from './event/eventModule';
|
||||
export * from './event/databaseEventProvider';
|
||||
export * from './event/domainEventService';
|
||||
export * from './event/eventHandlerDiscovery';
|
||||
// 注意:EventHandler装饰器单独导出,避免与接口冲突
|
||||
export { EventHandler, DomainEventHandler } from './event-bus/decorators/event-handler.decorator';
|
||||
export type { EventHandlerMetadata } from './event-bus/decorators/event-handler.decorator';
|
||||
export {
|
||||
EventHandler,
|
||||
DomainEventHandler,
|
||||
} from './event/decorators/event-handler.decorator';
|
||||
export type { EventHandlerMetadata } from './event/decorators/event-handler.decorator';
|
||||
|
||||
// 导出健康检查模块
|
||||
export * from './health/health.module';
|
||||
export * from './health/health.controller';
|
||||
export * from './health/healthz.controller';
|
||||
export * from './health/health.service';
|
||||
export * from './health/healthModule';
|
||||
export * from './health/healthController';
|
||||
export * from './health/healthzController';
|
||||
export * from './health/healthService';
|
||||
|
||||
// 导出域SDK模块
|
||||
export * from './interfaces/domain-sdk.interface';
|
||||
export * from './domain-sdk/domain-sdk.module';
|
||||
export * from './domain-sdk/domain-sdk.manager';
|
||||
export * from './domain-sdk/domain-sdk.service';
|
||||
export * from './domain-sdk/base-domain-sdk';
|
||||
export { CrossDomainAccessGuard } from './domain-sdk/cross-domain-access.guard';
|
||||
// 导出SDK模块
|
||||
export * from './interfaces/sdkInterface';
|
||||
export * from './sdk/sdkModule';
|
||||
export * from './sdk/sdkManager';
|
||||
export * from './sdk/sdkService';
|
||||
export * from './sdk/baseSdk';
|
||||
export { CrossSdkGuard } from './sdk/crossSdkGuard';
|
||||
|
||||
// 导出缓存系统
|
||||
export * from './cache/cacheModule';
|
||||
export * from './cache/cacheService';
|
||||
export * from './cache/distributedLockService';
|
||||
export * from './cache/multiLevelCacheService';
|
||||
|
||||
// 导出分布式追踪
|
||||
export * from './tracing/tracingModule';
|
||||
export * from './tracing/tracingService';
|
||||
export * from './tracing/tracingInterceptor';
|
||||
export * from './tracing/tracingGuard';
|
||||
|
||||
// 导出熔断器
|
||||
export * from './breaker/breakerModule';
|
||||
export * from './breaker/circuitBreakerService';
|
||||
|
||||
// 导出安全基础设施
|
||||
export * from './security/rateLimitService';
|
||||
export * from './security/idempotencyService';
|
||||
export * from './security/siteScopeGuard';
|
||||
|
||||
// 导出可观测性
|
||||
export * from './observability/metrics/httpMetricsService';
|
||||
export * from './observability/metricsController';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 导入事件总线相关类型
|
||||
import type { DomainEvent, EventHandler } from './event-bus.interface';
|
||||
import type { DomainEvent, EventHandler } from './eventInterface';
|
||||
|
||||
/**
|
||||
* 事件总线提供者抽象接口
|
||||
@@ -166,7 +166,7 @@ export interface EventPublishOptions {
|
||||
}
|
||||
|
||||
// 从 event-bus.interface.ts 导入,避免重复定义
|
||||
export type { DomainEvent, EventHandler } from './event-bus.interface';
|
||||
export type { DomainEvent, EventHandler } from './eventInterface';
|
||||
|
||||
/**
|
||||
* 任务选项
|
||||
@@ -229,7 +229,7 @@ export type JobProcessor = TaskProcessor;
|
||||
export interface QueueJob extends TaskJob {}
|
||||
|
||||
// 从 event-bus.interface.ts 导入,避免重复定义
|
||||
export { EventBusAdapterType } from './event-bus.interface';
|
||||
export { EventBusAdapterType } from './eventInterface';
|
||||
|
||||
/**
|
||||
* 任务队列适配器类型
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
/**
|
||||
* 域SDK基础接口
|
||||
*/
|
||||
export interface IDomainSdk {
|
||||
export interface ISdk {
|
||||
/**
|
||||
* 域名称
|
||||
*/
|
||||
@@ -182,7 +182,7 @@ export interface DomainSdkRegistration {
|
||||
/**
|
||||
* 域SDK管理器接口
|
||||
*/
|
||||
export interface IDomainSdkManager {
|
||||
export interface ISdkManager {
|
||||
/**
|
||||
* 注册域SDK
|
||||
*/
|
||||
@@ -191,7 +191,7 @@ export interface IDomainSdkManager {
|
||||
/**
|
||||
* 获取域SDK
|
||||
*/
|
||||
getSdk(domain: string): Promise<IDomainSdk | null>;
|
||||
getSdk(domain: string): Promise<ISdk | null>;
|
||||
|
||||
/**
|
||||
* 获取所有已注册的域
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user