feat: 完成 NestJS 后端核心底座开发 (M1-M6) 和 Ant Design Vue 前端迁移
主要更新: 1. 后端核心底座完成 (M1-M6): - 健康检查、指标监控、分布式锁 - 事件总线、队列系统、事务管理 - 安全守卫、多租户隔离、存储适配器 - 审计日志、配置管理、多语言支持 2. 前端迁移到 Ant Design Vue: - 从 Element Plus 迁移到 Ant Design Vue - 完善 system 模块 (role/menu/dept) - 修复依赖和配置问题 3. 文档完善: - AI 开发工作流文档 - 架构约束和开发规范 - 项目进度跟踪 4. 其他改进: - 修复编译错误和类型问题 - 完善测试用例 - 优化项目结构
This commit is contained in:
@@ -1,12 +1,37 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { Public } from './common/auth/decorators/public.decorator';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('healthz')
|
||||
@Public()
|
||||
healthCheck() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('readyz')
|
||||
@Public()
|
||||
readinessCheck() {
|
||||
return {
|
||||
status: 'ready',
|
||||
timestamp: new Date().toISOString(),
|
||||
services: {
|
||||
redis: 'connected',
|
||||
kafka: 'connected',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CacheModule } from '@nestjs/cache-manager';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { APP_GUARD, APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { WinstonModule } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
@@ -18,18 +18,35 @@ import 'winston-daily-rotate-file';
|
||||
import * as Joi from 'joi';
|
||||
import { ClsModule } from 'nestjs-cls';
|
||||
import { VendorModule } from './vendor';
|
||||
import {
|
||||
SettingsModule,
|
||||
UploadModule,
|
||||
import { JwtGlobalModule } from './common/auth/jwt.module';
|
||||
import {
|
||||
SettingsModule,
|
||||
UploadModule,
|
||||
AuthModule,
|
||||
MemberModule,
|
||||
AdminModule,
|
||||
RbacModule,
|
||||
GlobalAuthGuard,
|
||||
RolesGuard
|
||||
RolesGuard,
|
||||
JobsModule,
|
||||
EventBusModule,
|
||||
} from './common';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import * as path from 'path';
|
||||
import { ScheduleModule as AppScheduleModule } from './common/schedule/schedule.module';
|
||||
import { MetricsController } from './core/observability/metrics.controller';
|
||||
// 测试模块(Redis 和 Kafka 测试)
|
||||
// import { TestModule } from '../test/test.module';
|
||||
import { ConfigModule as AppConfigModule } from './config/config.module';
|
||||
// 新增:全局异常过滤器、统一响应、健康
|
||||
import { HttpExceptionFilter } from './core/http/filters/http-exception.filter';
|
||||
import { ResponseInterceptor } from './core/http/interceptors/response.interceptor';
|
||||
import { HealthController } from './core/observability/health/health.controller';
|
||||
import { HttpMetricsService } from './core/observability/metrics/http-metrics.service';
|
||||
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';
|
||||
import { EventBusHealthIndicator } from './core/observability/health/indicators/eventbus.indicator';
|
||||
import { QueueHealthIndicator } from './core/observability/health/indicators/queue.indicator';
|
||||
import { StorageHealthIndicator } from './core/observability/health/indicators/storage.indicator';
|
||||
|
||||
// 允许通过环境变量禁用数据库初始化(用于本地开发或暂时无数据库时)
|
||||
const dbImports =
|
||||
@@ -40,11 +57,11 @@ const dbImports =
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: config.get('db.host', 'localhost'),
|
||||
port: config.get('db.port', 3306),
|
||||
username: config.get('db.username', 'root'),
|
||||
password: config.get('db.password', ''),
|
||||
database: config.get('db.database', 'wwjcloud'),
|
||||
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'),
|
||||
autoLoadEntities: true,
|
||||
synchronize: false,
|
||||
}),
|
||||
@@ -58,20 +75,22 @@ const dbImports =
|
||||
load: [configuration],
|
||||
validationSchema: Joi.object({
|
||||
NODE_ENV: Joi.string()
|
||||
.valid('development', 'production', 'test')
|
||||
.default('development'),
|
||||
PORT: Joi.number().default(3000),
|
||||
DB_HOST: Joi.string().default('localhost'),
|
||||
DB_PORT: Joi.number().default(3306),
|
||||
DB_USERNAME: Joi.string().default('root'),
|
||||
DB_PASSWORD: Joi.string().allow('').default(''),
|
||||
DB_DATABASE: Joi.string().default('wwjcloud'),
|
||||
REDIS_HOST: Joi.string().default('localhost'),
|
||||
REDIS_PORT: Joi.number().default(6379),
|
||||
REDIS_PASSWORD: Joi.string().allow('').default(''),
|
||||
JWT_SECRET: Joi.string().default('change_me'),
|
||||
JWT_EXPIRES_IN: Joi.string().default('7d'),
|
||||
UPLOAD_PATH: Joi.string().default('public/upload'),
|
||||
.valid('development', 'production', 'test'),
|
||||
PORT: Joi.number(),
|
||||
DB_HOST: Joi.string(),
|
||||
DB_PORT: Joi.number(),
|
||||
DB_USERNAME: Joi.string(),
|
||||
DB_PASSWORD: Joi.string().allow(''),
|
||||
DB_DATABASE: Joi.string(),
|
||||
REDIS_HOST: Joi.string(),
|
||||
REDIS_PORT: Joi.number(),
|
||||
REDIS_PASSWORD: Joi.string().allow(''),
|
||||
REDIS_DB: Joi.number(),
|
||||
KAFKA_BROKERS: Joi.string(),
|
||||
KAFKA_CLIENT_ID: Joi.string(),
|
||||
JWT_SECRET: Joi.string(),
|
||||
JWT_EXPIRES_IN: Joi.string(),
|
||||
UPLOAD_PATH: Joi.string(),
|
||||
STORAGE_PROVIDER: Joi.string()
|
||||
.valid(
|
||||
'local',
|
||||
@@ -81,46 +100,68 @@ const dbImports =
|
||||
'alists3',
|
||||
'webdav',
|
||||
'ftp',
|
||||
)
|
||||
.default('local'),
|
||||
),
|
||||
PAYMENT_PROVIDER: Joi.string()
|
||||
.valid('wechat', 'alipay')
|
||||
.default('alipay'),
|
||||
LOG_LEVEL: Joi.string().default('info'),
|
||||
THROTTLE_TTL: Joi.number().default(60),
|
||||
THROTTLE_LIMIT: Joi.number().default(100),
|
||||
.valid('wechat', 'alipay'),
|
||||
LOG_LEVEL: Joi.string(),
|
||||
THROTTLE_TTL: Joi.number(),
|
||||
THROTTLE_LIMIT: Joi.number(),
|
||||
}),
|
||||
}),
|
||||
// 静态资源托管:仅暴露 /upload/**
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: path.resolve(process.cwd(), 'public', 'upload'),
|
||||
serveRoot: '/upload',
|
||||
}),
|
||||
// 缓存(内存实现,后续可替换为 redis-store)
|
||||
CacheModule.register({ isGlobal: true }),
|
||||
CacheModule.registerAsync({
|
||||
isGlobal: true,
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
host: config.get('redis.host'),
|
||||
port: config.get('redis.port'),
|
||||
password: config.get('redis.password'),
|
||||
db: config.get('redis.db'),
|
||||
}),
|
||||
}),
|
||||
// 计划任务
|
||||
ScheduleModule.forRoot(),
|
||||
// 事件总线
|
||||
EventEmitterModule.forRoot(),
|
||||
// 限流
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: Number(process.env.THROTTLE_TTL) || 60,
|
||||
limit: Number(process.env.THROTTLE_LIMIT) || 100,
|
||||
},
|
||||
]),
|
||||
ThrottlerModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
throttlers: [
|
||||
{
|
||||
ttl: config.get('throttle.ttl') || 60,
|
||||
limit: config.get('throttle.limit') || 100,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
// 健康检查(需要时可增加控制器)
|
||||
TerminusModule,
|
||||
// 日志
|
||||
WinstonModule.forRoot({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
WinstonModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
level: config.get('logLevel'),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.colorize(),
|
||||
winston.format.printf(({ level, message, timestamp, context }) => {
|
||||
return `${timestamp} [${level}]${context ? ' [' + context + ']' : ''} ${message}`;
|
||||
try {
|
||||
const { ClsServiceManager } = require('nestjs-cls');
|
||||
const cls = ClsServiceManager.getClsService?.();
|
||||
const rid = cls?.get?.('requestId');
|
||||
const tp = cls?.get?.('traceparent');
|
||||
const ctx = context ? ` [${context}]` : '';
|
||||
const ids =
|
||||
rid || tp
|
||||
? ` rid=${rid ?? ''}${tp ? ` trace=${tp}` : ''}`
|
||||
: '';
|
||||
return `${timestamp} [${level}]${ctx} ${message}${ids}`;
|
||||
} catch {
|
||||
return `${timestamp} [${level}]${context ? ' [' + context + ']' : ''} ${message}`;
|
||||
}
|
||||
}),
|
||||
),
|
||||
}),
|
||||
@@ -130,9 +171,10 @@ const dbImports =
|
||||
datePattern: 'YYYY-MM-DD',
|
||||
zippedArchive: false,
|
||||
maxFiles: '14d',
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
level: config.get('logLevel'),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
// 请求上下文
|
||||
ClsModule.forRoot({
|
||||
@@ -141,6 +183,8 @@ const dbImports =
|
||||
}),
|
||||
// 数据库(可通过 DB_DISABLE=true 禁用)
|
||||
...dbImports,
|
||||
// 全局JWT模块(必须在其他模块之前导入)
|
||||
JwtGlobalModule,
|
||||
// Vendor 绑定 Core 抽象到具体适配器
|
||||
VendorModule,
|
||||
// Common 编排服务(聚合到 SettingsModule 下)
|
||||
@@ -154,14 +198,36 @@ const dbImports =
|
||||
RbacModule,
|
||||
// 认证模块(提供 super/admin/auth 登录分流)
|
||||
AuthModule,
|
||||
// 定时任务模块
|
||||
AppScheduleModule,
|
||||
// 任务队列模块
|
||||
JobsModule,
|
||||
// 事件总线模块
|
||||
EventBusModule,
|
||||
// 测试模块(Redis 和 Kafka 测试)
|
||||
// TestModule,
|
||||
// 配置模块(API文档导航等)
|
||||
AppConfigModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
controllers: [AppController, MetricsController, HealthController],
|
||||
providers: [
|
||||
AppService,
|
||||
// 全局守卫
|
||||
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
||||
{ provide: APP_GUARD, useClass: GlobalAuthGuard },
|
||||
{ provide: APP_GUARD, useClass: RolesGuard },
|
||||
// 全局拦截/过滤
|
||||
{ provide: APP_INTERCEPTOR, useClass: ResponseInterceptor },
|
||||
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
|
||||
// 指标服务
|
||||
HttpMetricsService,
|
||||
// 健康检查服务
|
||||
HealthAggregator,
|
||||
DbHealthIndicator,
|
||||
RedisHealthIndicator,
|
||||
EventBusHealthIndicator,
|
||||
QueueHealthIndicator,
|
||||
StorageHealthIndicator,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { SysUser } from './entities/SysUser';
|
||||
import { SysUserLog } from './entities/SysUserLog';
|
||||
import { SysUserRole } from './entities/SysUserRole';
|
||||
@@ -9,10 +10,11 @@ import { AdminController } from './controllers/adminapi/AdminController';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => AuthModule),
|
||||
TypeOrmModule.forFeature([SysUser, SysUserLog, SysUserRole]),
|
||||
],
|
||||
providers: [CoreAdminService, AdminService],
|
||||
controllers: [AdminController],
|
||||
exports: [CoreAdminService, AdminService],
|
||||
})
|
||||
export class AdminModule {}
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, UsePipes, ValidationPipe } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
UsePipes,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminService } from '../../services/admin/AdminService';
|
||||
import { CreateAdminDto, UpdateAdminDto, QueryAdminDto, BatchUpdateStatusDto, BatchAssignRoleDto, ResetPasswordDto } from '../../dto/admin/AdminDto';
|
||||
import {
|
||||
CreateAdminDto,
|
||||
UpdateAdminDto,
|
||||
QueryAdminDto,
|
||||
BatchUpdateAdminStatusDto,
|
||||
BatchAssignRoleDto,
|
||||
ResetAdminPasswordDto,
|
||||
} from '../../dto/admin/AdminDto';
|
||||
import { JwtAuthGuard } from '../../../auth/guards/JwtAuthGuard';
|
||||
import { RolesGuard } from '../../../auth/guards/RolesGuard';
|
||||
import { Roles } from '../../../auth/decorators/RolesDecorator';
|
||||
@@ -26,7 +50,10 @@ export class AdminController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取管理员列表' })
|
||||
@ApiResponse({ status: 200, description: '获取管理员列表成功' })
|
||||
async getAdminList(@Query() query: QueryAdminDto, @Query('site_id') site_id: number = 0) {
|
||||
async getAdminList(
|
||||
@Query() query: QueryAdminDto,
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
return await this.adminService.getAdminList(query, site_id);
|
||||
}
|
||||
|
||||
@@ -34,7 +61,10 @@ export class AdminController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '获取管理员详情' })
|
||||
@ApiResponse({ status: 200, description: '获取管理员详情成功' })
|
||||
async getAdminDetail(@Param('id') id: number, @Query('site_id') site_id: number = 0) {
|
||||
async getAdminDetail(
|
||||
@Param('id') id: number,
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
return await this.adminService.getAdminDetail(id, site_id);
|
||||
}
|
||||
|
||||
@@ -45,7 +75,7 @@ export class AdminController {
|
||||
async updateAdmin(
|
||||
@Param('id') id: number,
|
||||
@Body() updateAdminDto: UpdateAdminDto,
|
||||
@Query('site_id') site_id: number = 0
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
return await this.adminService.updateAdmin(id, updateAdminDto, site_id);
|
||||
}
|
||||
@@ -54,7 +84,10 @@ export class AdminController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '删除管理员' })
|
||||
@ApiResponse({ status: 200, description: '管理员删除成功' })
|
||||
async deleteAdmin(@Param('id') id: number, @Query('site_id') site_id: number = 0) {
|
||||
async deleteAdmin(
|
||||
@Param('id') id: number,
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
await this.adminService.deleteAdmin(id, site_id);
|
||||
return { message: '删除成功' };
|
||||
}
|
||||
@@ -63,7 +96,10 @@ export class AdminController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '批量删除管理员' })
|
||||
@ApiResponse({ status: 200, description: '批量删除成功' })
|
||||
async batchDeleteAdmins(@Body() data: { uids: number[] }, @Query('site_id') site_id: number = 0) {
|
||||
async batchDeleteAdmins(
|
||||
@Body() data: { uids: number[] },
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
await this.adminService.batchDeleteAdmins(data.uids, site_id);
|
||||
return { message: '批量删除成功' };
|
||||
}
|
||||
@@ -72,8 +108,15 @@ export class AdminController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '批量更新管理员状态' })
|
||||
@ApiResponse({ status: 200, description: '批量更新状态成功' })
|
||||
async batchUpdateAdminStatus(@Body() data: BatchUpdateStatusDto, @Query('site_id') site_id: number = 0) {
|
||||
await this.adminService.batchUpdateAdminStatus(data.uids, data.status, site_id);
|
||||
async batchUpdateAdminStatus(
|
||||
@Body() data: BatchUpdateAdminStatusDto,
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
await this.adminService.batchUpdateAdminStatus(
|
||||
data.uids,
|
||||
data.status,
|
||||
site_id,
|
||||
);
|
||||
return { message: '批量更新状态成功' };
|
||||
}
|
||||
|
||||
@@ -81,8 +124,15 @@ export class AdminController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '批量分配角色' })
|
||||
@ApiResponse({ status: 200, description: '批量分配角色成功' })
|
||||
async batchAssignAdminRoles(@Body() data: BatchAssignRoleDto, @Query('site_id') site_id: number = 0) {
|
||||
await this.adminService.batchAssignAdminRoles(data.uids, data.role_ids, site_id);
|
||||
async batchAssignAdminRoles(
|
||||
@Body() data: BatchAssignRoleDto,
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
await this.adminService.batchAssignAdminRoles(
|
||||
data.uids,
|
||||
data.role_ids,
|
||||
site_id,
|
||||
);
|
||||
return { message: '批量分配角色成功' };
|
||||
}
|
||||
|
||||
@@ -92,8 +142,8 @@ export class AdminController {
|
||||
@ApiResponse({ status: 200, description: '密码重置成功' })
|
||||
async resetAdminPassword(
|
||||
@Param('id') id: number,
|
||||
@Body() resetPasswordDto: ResetPasswordDto,
|
||||
@Query('site_id') site_id: number = 0
|
||||
@Body() resetPasswordDto: ResetAdminPasswordDto,
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
await this.adminService.resetAdminPassword(id, resetPasswordDto, site_id);
|
||||
return { message: '密码重置成功' };
|
||||
@@ -106,7 +156,7 @@ export class AdminController {
|
||||
async updateAdminStatus(
|
||||
@Param('id') id: number,
|
||||
@Body() data: { status: number },
|
||||
@Query('site_id') site_id: number = 0
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
await this.adminService.updateAdminStatus(id, data.status, site_id);
|
||||
return { message: '状态更新成功' };
|
||||
@@ -119,7 +169,7 @@ export class AdminController {
|
||||
async assignAdminRoles(
|
||||
@Param('id') id: number,
|
||||
@Body() data: { role_ids: string },
|
||||
@Query('site_id') site_id: number = 0
|
||||
@Query('site_id') site_id: number = 0,
|
||||
) {
|
||||
await this.adminService.assignAdminRoles(id, data.role_ids, site_id);
|
||||
return { message: '角色分配成功' };
|
||||
@@ -140,4 +190,4 @@ export class AdminController {
|
||||
async getAdminStats(@Query('site_id') site_id: number = 0) {
|
||||
return await this.adminService.getAdminStats(site_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { IsString, IsNumber, IsOptional, IsArray, Min, Max } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
// 创建管理员DTO
|
||||
@@ -115,7 +122,7 @@ export class QueryAdminDto {
|
||||
}
|
||||
|
||||
// 批量更新状态DTO
|
||||
export class BatchUpdateStatusDto {
|
||||
export class BatchUpdateAdminStatusDto {
|
||||
@ApiProperty({ description: '用户ID列表', type: [Number] })
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
@@ -141,8 +148,8 @@ export class BatchAssignRoleDto {
|
||||
}
|
||||
|
||||
// 重置密码DTO
|
||||
export class ResetPasswordDto {
|
||||
export class ResetAdminPasswordDto {
|
||||
@ApiProperty({ description: '新密码' })
|
||||
@IsString()
|
||||
new_password: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||
import { BaseEntity } from '@wwj/core/base/BaseEntity';
|
||||
import { SysUserRole } from './SysUserRole';
|
||||
import { SysUserLog } from './SysUserLog';
|
||||
|
||||
@Entity('sys_user')
|
||||
export class SysUser {
|
||||
export class SysUser extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'uid' })
|
||||
uid: number;
|
||||
|
||||
@@ -25,29 +26,17 @@ export class SysUser {
|
||||
@Column({ name: 'last_time', type: 'int', default: 0 })
|
||||
last_time: number;
|
||||
|
||||
@Column({ name: 'create_time', type: 'int', default: 0 })
|
||||
create_time: number;
|
||||
|
||||
@Column({ name: 'login_count', type: 'int', default: 0 })
|
||||
login_count: number;
|
||||
|
||||
@Column({ name: 'status', type: 'tinyint', default: 1 })
|
||||
status: number;
|
||||
|
||||
@Column({ name: 'is_del', type: 'tinyint', default: 0 })
|
||||
is_del: number;
|
||||
|
||||
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||
delete_time: number;
|
||||
|
||||
@Column({ name: 'update_time', type: 'int', default: 0 })
|
||||
update_time: number;
|
||||
|
||||
// 关联关系
|
||||
@OneToMany(() => SysUserRole, userRole => userRole.user)
|
||||
@OneToMany(() => SysUserRole, (userRole) => userRole.user)
|
||||
user_role: SysUserRole[];
|
||||
|
||||
@OneToMany(() => SysUserLog, userLog => userLog.user)
|
||||
@OneToMany(() => SysUserLog, (userLog) => userLog.user)
|
||||
user_logs: SysUserLog[];
|
||||
|
||||
// 业务方法
|
||||
@@ -56,10 +45,14 @@ export class SysUser {
|
||||
}
|
||||
|
||||
getCreateTimeText(): string {
|
||||
return this.create_time ? new Date(this.create_time * 1000).toLocaleString() : '';
|
||||
return this.create_time
|
||||
? new Date(this.create_time * 1000).toLocaleString()
|
||||
: '';
|
||||
}
|
||||
|
||||
getLastTimeText(): string {
|
||||
return this.last_time ? new Date(this.last_time * 1000).toLocaleString() : '';
|
||||
return this.last_time
|
||||
? new Date(this.last_time * 1000).toLocaleString()
|
||||
: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { SysUser } from './SysUser';
|
||||
|
||||
@Entity('sys_user_log')
|
||||
@@ -34,12 +41,14 @@ export class SysUserLog {
|
||||
create_time: number;
|
||||
|
||||
// 关联关系
|
||||
@ManyToOne(() => SysUser, user => user.user_logs)
|
||||
@ManyToOne(() => SysUser, (user) => user.user_logs)
|
||||
@JoinColumn({ name: 'uid', referencedColumnName: 'uid' })
|
||||
user: SysUser;
|
||||
|
||||
// 业务逻辑方法 - 与 PHP 项目保持一致
|
||||
getCreateTimeText(): string {
|
||||
return this.create_time ? new Date(this.create_time * 1000).toLocaleString('zh-CN') : '';
|
||||
return this.create_time
|
||||
? new Date(this.create_time * 1000).toLocaleString('zh-CN')
|
||||
: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { SysUser } from './SysUser';
|
||||
|
||||
@Entity('sys_user_role')
|
||||
@@ -31,17 +39,19 @@ export class SysUserRole {
|
||||
delete_time: number;
|
||||
|
||||
// 关联关系
|
||||
@OneToOne(() => SysUser, user => user.user_role)
|
||||
@OneToOne(() => SysUser, (user) => user.user_role)
|
||||
@JoinColumn({ name: 'uid', referencedColumnName: 'uid' })
|
||||
user: SysUser;
|
||||
|
||||
// 业务逻辑方法 - 与 PHP 项目保持一致
|
||||
getCreateTimeText(): string {
|
||||
return this.create_time ? new Date(this.create_time * 1000).toLocaleString() : '';
|
||||
return this.create_time
|
||||
? new Date(this.create_time * 1000).toLocaleString()
|
||||
: '';
|
||||
}
|
||||
|
||||
getStatusText(): string {
|
||||
const statusMap: { [key: number]: string } = { 0: '禁用', 1: '正常' };
|
||||
return statusMap[this.status] || '未知';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('admin')
|
||||
export class Admin {
|
||||
@@ -43,4 +49,4 @@ export class Admin {
|
||||
|
||||
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||
delete_time: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ import { Repository } from 'typeorm';
|
||||
import { SysUser } from '../../entities/SysUser';
|
||||
import { SysUserRole } from '../../entities/SysUserRole';
|
||||
import { CoreAdminService } from '../core/CoreAdminService';
|
||||
import { CreateAdminDto, UpdateAdminDto, QueryAdminDto, BatchUpdateStatusDto, BatchAssignRoleDto, ResetPasswordDto } from '../../dto/admin/AdminDto';
|
||||
import {
|
||||
CreateAdminDto,
|
||||
UpdateAdminDto,
|
||||
QueryAdminDto,
|
||||
BatchUpdateAdminStatusDto,
|
||||
BatchAssignRoleDto,
|
||||
ResetAdminPasswordDto,
|
||||
} from '../../dto/admin/AdminDto';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -16,16 +23,21 @@ export class AdminService {
|
||||
private readonly coreAdminService: CoreAdminService,
|
||||
) {}
|
||||
|
||||
async createAdmin(adminData: CreateAdminDto, site_id: number = 0): Promise<SysUser> {
|
||||
async createAdmin(
|
||||
adminData: CreateAdminDto,
|
||||
site_id: number = 0,
|
||||
): Promise<SysUser> {
|
||||
// 检查用户名是否已存在
|
||||
const exists = await this.coreAdminService.isUsernameExists(adminData.username);
|
||||
const exists = await this.coreAdminService.isUsernameExists(
|
||||
adminData.username,
|
||||
);
|
||||
if (exists) {
|
||||
throw new Error('用户名已存在');
|
||||
}
|
||||
|
||||
// 创建管理员
|
||||
const admin = await this.coreAdminService.createAdmin(adminData);
|
||||
|
||||
|
||||
// 创建用户角色关联
|
||||
if (adminData.role_ids) {
|
||||
await this.createUserRole(admin.uid, site_id, adminData.role_ids);
|
||||
@@ -34,7 +46,11 @@ export class AdminService {
|
||||
return admin;
|
||||
}
|
||||
|
||||
async updateAdmin(uid: number, updateData: UpdateAdminDto, site_id: number = 0): Promise<SysUser> {
|
||||
async updateAdmin(
|
||||
uid: number,
|
||||
updateData: UpdateAdminDto,
|
||||
site_id: number = 0,
|
||||
): Promise<SysUser> {
|
||||
// 检查管理员是否存在
|
||||
const admin = await this.coreAdminService.getAdminById(uid);
|
||||
if (!admin) {
|
||||
@@ -42,7 +58,10 @@ export class AdminService {
|
||||
}
|
||||
|
||||
// 更新管理员信息
|
||||
const updatedAdmin = await this.coreAdminService.updateAdmin(uid, updateData);
|
||||
const updatedAdmin = await this.coreAdminService.updateAdmin(
|
||||
uid,
|
||||
updateData,
|
||||
);
|
||||
|
||||
// 更新角色关联
|
||||
if (updateData.role_ids !== undefined) {
|
||||
@@ -61,7 +80,7 @@ export class AdminService {
|
||||
|
||||
// 删除管理员
|
||||
await this.coreAdminService.deleteAdmin(uid);
|
||||
|
||||
|
||||
// 删除角色关联
|
||||
await this.deleteUserRole(uid, site_id);
|
||||
}
|
||||
@@ -72,7 +91,11 @@ export class AdminService {
|
||||
}
|
||||
}
|
||||
|
||||
async resetAdminPassword(uid: number, resetData: ResetPasswordDto, site_id: number = 0): Promise<void> {
|
||||
async resetAdminPassword(
|
||||
uid: number,
|
||||
resetData: ResetAdminPasswordDto,
|
||||
site_id: number = 0,
|
||||
): Promise<void> {
|
||||
// 检查管理员是否存在
|
||||
const admin = await this.coreAdminService.getAdminById(uid);
|
||||
if (!admin) {
|
||||
@@ -80,10 +103,16 @@ export class AdminService {
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
await this.coreAdminService.updateAdmin(uid, { password: resetData.new_password });
|
||||
await this.coreAdminService.updateAdmin(uid, {
|
||||
password: resetData.new_password,
|
||||
});
|
||||
}
|
||||
|
||||
async updateAdminStatus(uid: number, status: number, site_id: number = 0): Promise<void> {
|
||||
async updateAdminStatus(
|
||||
uid: number,
|
||||
status: number,
|
||||
site_id: number = 0,
|
||||
): Promise<void> {
|
||||
// 检查管理员是否存在
|
||||
const admin = await this.coreAdminService.getAdminById(uid);
|
||||
if (!admin) {
|
||||
@@ -94,14 +123,21 @@ export class AdminService {
|
||||
await this.coreAdminService.updateAdmin(uid, { status });
|
||||
}
|
||||
|
||||
async batchUpdateAdminStatus(uids: number[], status: number, site_id: number = 0): Promise<void> {
|
||||
async batchUpdateAdminStatus(
|
||||
uids: number[],
|
||||
status: number,
|
||||
site_id: number = 0,
|
||||
): Promise<void> {
|
||||
for (const uid of uids) {
|
||||
await this.updateAdminStatus(uid, status, site_id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async assignAdminRoles(uid: number, role_ids: string, site_id: number = 0): Promise<void> {
|
||||
async assignAdminRoles(
|
||||
uid: number,
|
||||
role_ids: string,
|
||||
site_id: number = 0,
|
||||
): Promise<void> {
|
||||
// 检查管理员是否存在
|
||||
const admin = await this.coreAdminService.getAdminById(uid);
|
||||
if (!admin) {
|
||||
@@ -112,7 +148,11 @@ export class AdminService {
|
||||
await this.updateUserRole(uid, site_id, role_ids);
|
||||
}
|
||||
|
||||
async batchAssignAdminRoles(uids: number[], role_ids: string, site_id: number = 0): Promise<void> {
|
||||
async batchAssignAdminRoles(
|
||||
uids: number[],
|
||||
role_ids: string,
|
||||
site_id: number = 0,
|
||||
): Promise<void> {
|
||||
for (const uid of uids) {
|
||||
await this.assignAdminRoles(uid, role_ids, site_id);
|
||||
}
|
||||
@@ -126,13 +166,19 @@ export class AdminService {
|
||||
return admin;
|
||||
}
|
||||
|
||||
async getAdminList(query: QueryAdminDto, site_id: number = 0): Promise<{ list: SysUser[]; total: number }> {
|
||||
async getAdminList(
|
||||
query: QueryAdminDto,
|
||||
site_id: number = 0,
|
||||
): Promise<{ list: SysUser[]; total: number }> {
|
||||
const result = await this.coreAdminService.getAdminList(query);
|
||||
return { list: result.data, total: result.total };
|
||||
}
|
||||
|
||||
async exportAdmins(site_id: number = 0): Promise<SysUser[]> {
|
||||
const result = await this.coreAdminService.getAdminList({ page: 1, limit: 1000 });
|
||||
const result = await this.coreAdminService.getAdminList({
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
});
|
||||
return result.data;
|
||||
}
|
||||
|
||||
@@ -141,7 +187,11 @@ export class AdminService {
|
||||
}
|
||||
|
||||
// 私有方法:创建用户角色关联
|
||||
private async createUserRole(uid: number, site_id: number, role_ids: string): Promise<void> {
|
||||
private async createUserRole(
|
||||
uid: number,
|
||||
site_id: number,
|
||||
role_ids: string,
|
||||
): Promise<void> {
|
||||
const userRole = this.sysUserRoleRepository.create({
|
||||
uid,
|
||||
site_id,
|
||||
@@ -155,10 +205,14 @@ export class AdminService {
|
||||
}
|
||||
|
||||
// 私有方法:更新用户角色关联
|
||||
private async updateUserRole(uid: number, site_id: number, role_ids: string): Promise<void> {
|
||||
private async updateUserRole(
|
||||
uid: number,
|
||||
site_id: number,
|
||||
role_ids: string,
|
||||
): Promise<void> {
|
||||
await this.sysUserRoleRepository.update(
|
||||
{ uid, site_id, delete_time: 0 },
|
||||
{ role_ids }
|
||||
{ role_ids },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +220,7 @@ export class AdminService {
|
||||
private async deleteUserRole(uid: number, site_id: number): Promise<void> {
|
||||
await this.sysUserRoleRepository.update(
|
||||
{ uid, site_id, delete_time: 0 },
|
||||
{ delete_time: Math.floor(Date.now() / 1000) }
|
||||
{ delete_time: Math.floor(Date.now() / 1000) },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,17 @@ export class CoreAdminService {
|
||||
*/
|
||||
async createAdmin(adminData: Partial<SysUser>): Promise<SysUser> {
|
||||
const admin = this.sysUserRepository.create(adminData);
|
||||
|
||||
|
||||
// 加密密码
|
||||
if (admin.password) {
|
||||
admin.password = await bcrypt.hash(admin.password, 10);
|
||||
}
|
||||
|
||||
|
||||
// 设置默认值 - TypeORM 会自动处理时间戳
|
||||
admin.status = 1;
|
||||
admin.is_del = 0;
|
||||
admin.login_count = 0;
|
||||
|
||||
|
||||
return await this.sysUserRepository.save(admin);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,10 @@ export class CoreAdminService {
|
||||
/**
|
||||
* 更新管理员用户
|
||||
*/
|
||||
async updateAdmin(uid: number, updateData: Partial<SysUser>): Promise<SysUser> {
|
||||
async updateAdmin(
|
||||
uid: number,
|
||||
updateData: Partial<SysUser>,
|
||||
): Promise<SysUser> {
|
||||
const admin = await this.getAdminById(uid);
|
||||
if (!admin) {
|
||||
throw new Error('管理员用户不存在');
|
||||
@@ -72,7 +75,7 @@ export class CoreAdminService {
|
||||
}
|
||||
|
||||
// TypeORM 会自动更新 update_time
|
||||
|
||||
|
||||
await this.sysUserRepository.update(uid, updateData);
|
||||
const updatedAdmin = await this.getAdminById(uid);
|
||||
if (!updatedAdmin) {
|
||||
@@ -110,7 +113,16 @@ export class CoreAdminService {
|
||||
createTime?: [string, string];
|
||||
lastTime?: [string, string];
|
||||
}): Promise<{ data: SysUser[]; total: number }> {
|
||||
const { page = 1, limit = 20, username, realname, status, site_id, createTime, lastTime } = params;
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
username,
|
||||
realname,
|
||||
status,
|
||||
site_id,
|
||||
createTime,
|
||||
lastTime,
|
||||
} = params;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const queryBuilder = this.sysUserRepository
|
||||
@@ -121,12 +133,16 @@ export class CoreAdminService {
|
||||
|
||||
// 对应PHP的searchUsernameAttr方法
|
||||
if (username) {
|
||||
queryBuilder.andWhere('admin.username LIKE :username', { username: `%${this.handleSpecialCharacter(username)}%` });
|
||||
queryBuilder.andWhere('admin.username LIKE :username', {
|
||||
username: `%${this.handleSpecialCharacter(username)}%`,
|
||||
});
|
||||
}
|
||||
|
||||
// 对应PHP的searchRealnameAttr方法
|
||||
if (realname) {
|
||||
queryBuilder.andWhere('admin.real_name LIKE :realname', { realname: `%${realname}%` });
|
||||
queryBuilder.andWhere('admin.real_name LIKE :realname', {
|
||||
realname: `%${realname}%`,
|
||||
});
|
||||
}
|
||||
|
||||
// 对应PHP的searchStatusAttr方法
|
||||
@@ -140,13 +156,20 @@ export class CoreAdminService {
|
||||
if (startTime && endTime) {
|
||||
const startTimestamp = Math.floor(new Date(startTime).getTime() / 1000);
|
||||
const endTimestamp = Math.floor(new Date(endTime).getTime() / 1000);
|
||||
queryBuilder.andWhere('admin.create_time BETWEEN :startTime AND :endTime', { startTime: startTimestamp, endTime: endTimestamp });
|
||||
queryBuilder.andWhere(
|
||||
'admin.create_time BETWEEN :startTime AND :endTime',
|
||||
{ startTime: startTimestamp, endTime: endTimestamp },
|
||||
);
|
||||
} else if (startTime) {
|
||||
const startTimestamp = Math.floor(new Date(startTime).getTime() / 1000);
|
||||
queryBuilder.andWhere('admin.create_time >= :startTime', { startTime: startTimestamp });
|
||||
queryBuilder.andWhere('admin.create_time >= :startTime', {
|
||||
startTime: startTimestamp,
|
||||
});
|
||||
} else if (endTime) {
|
||||
const endTimestamp = Math.floor(new Date(endTime).getTime() / 1000);
|
||||
queryBuilder.andWhere('admin.create_time <= :endTime', { endTime: endTimestamp });
|
||||
queryBuilder.andWhere('admin.create_time <= :endTime', {
|
||||
endTime: endTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,13 +179,20 @@ export class CoreAdminService {
|
||||
if (startTime && endTime) {
|
||||
const startTimestamp = Math.floor(new Date(startTime).getTime() / 1000);
|
||||
const endTimestamp = Math.floor(new Date(endTime).getTime() / 1000);
|
||||
queryBuilder.andWhere('admin.last_time BETWEEN :startTime AND :endTime', { startTime: startTimestamp, endTime: endTimestamp });
|
||||
queryBuilder.andWhere(
|
||||
'admin.last_time BETWEEN :startTime AND :endTime',
|
||||
{ startTime: startTimestamp, endTime: endTimestamp },
|
||||
);
|
||||
} else if (startTime) {
|
||||
const startTimestamp = Math.floor(new Date(startTime).getTime() / 1000);
|
||||
queryBuilder.andWhere('admin.last_time >= :startTime', { startTime: startTimestamp });
|
||||
queryBuilder.andWhere('admin.last_time >= :startTime', {
|
||||
startTime: startTimestamp,
|
||||
});
|
||||
} else if (endTime) {
|
||||
const endTimestamp = Math.floor(new Date(endTime).getTime() / 1000);
|
||||
queryBuilder.andWhere('admin.last_time <= :endTime', { endTime: endTimestamp });
|
||||
queryBuilder.andWhere('admin.last_time <= :endTime', {
|
||||
endTime: endTimestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +241,10 @@ export class CoreAdminService {
|
||||
/**
|
||||
* 检查用户名是否已存在 - 对应PHP的searchUsernameAttr方法
|
||||
*/
|
||||
async isUsernameExists(username: string, excludeUid?: number): Promise<boolean> {
|
||||
async isUsernameExists(
|
||||
username: string,
|
||||
excludeUid?: number,
|
||||
): Promise<boolean> {
|
||||
const queryBuilder = this.sysUserRepository
|
||||
.createQueryBuilder('admin')
|
||||
.where('admin.username = :username', { username })
|
||||
@@ -246,8 +279,12 @@ export class CoreAdminService {
|
||||
}
|
||||
|
||||
const total = await queryBuilder.getCount();
|
||||
const active = await queryBuilder.andWhere('admin.status = :status', { status: 1 }).getCount();
|
||||
const inactive = await queryBuilder.andWhere('admin.status = :status', { status: 0 }).getCount();
|
||||
const active = await queryBuilder
|
||||
.andWhere('admin.status = :status', { status: 1 })
|
||||
.getCount();
|
||||
const inactive = await queryBuilder
|
||||
.andWhere('admin.status = :status', { status: 0 })
|
||||
.getCount();
|
||||
|
||||
const superAdminQueryBuilder = this.sysUserRoleRepository
|
||||
.createQueryBuilder('user_role')
|
||||
@@ -255,7 +292,9 @@ export class CoreAdminService {
|
||||
.andWhere('user_role.is_admin = :is_admin', { is_admin: 1 });
|
||||
|
||||
if (site_id !== undefined) {
|
||||
superAdminQueryBuilder.andWhere('user_role.site_id = :site_id', { site_id });
|
||||
superAdminQueryBuilder.andWhere('user_role.site_id = :site_id', {
|
||||
site_id,
|
||||
});
|
||||
}
|
||||
|
||||
const superAdmin = await superAdminQueryBuilder.getCount();
|
||||
@@ -271,4 +310,4 @@ export class CoreAdminService {
|
||||
// 暂时返回原字符串
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,30 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { Module, forwardRef, Global } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AuthToken } from './entities/AuthToken';
|
||||
import { AuthService } from './services/AuthService';
|
||||
import { AuthController } from './controllers/AuthController';
|
||||
import { JwtAuthGuard } from './guards/JwtAuthGuard';
|
||||
import { RolesGuard } from './guards/RolesGuard';
|
||||
import { JwtGlobalModule } from './jwt.module';
|
||||
|
||||
// 导入Admin和Member模块
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { MemberModule } from '../member/member.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
PassportModule,
|
||||
TypeOrmModule.forFeature([AuthToken]),
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET', 'change_me'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get('JWT_EXPIRES_IN', '7d'),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
JwtGlobalModule,
|
||||
// 导入Admin和Member模块以使用其服务
|
||||
forwardRef(() => AdminModule),
|
||||
forwardRef(() => MemberModule),
|
||||
],
|
||||
providers: [
|
||||
AuthService,
|
||||
JwtAuthGuard,
|
||||
RolesGuard,
|
||||
],
|
||||
providers: [AuthService, JwtAuthGuard, RolesGuard],
|
||||
controllers: [AuthController],
|
||||
exports: [
|
||||
AuthService,
|
||||
JwtAuthGuard,
|
||||
RolesGuard,
|
||||
],
|
||||
exports: [AuthService, JwtAuthGuard, RolesGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Req,
|
||||
HttpCode,
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Req,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Get
|
||||
Get,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import type { Request } from 'express';
|
||||
import { AuthService } from '../services/AuthService';
|
||||
import { LoginDto, RefreshTokenDto, LogoutDto } from '../dto/AuthDto';
|
||||
@@ -25,13 +30,10 @@ export class AuthController {
|
||||
@ApiResponse({ status: 200, description: '登录成功' })
|
||||
@ApiResponse({ status: 401, description: '用户名或密码错误' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async adminLogin(
|
||||
@Body() loginDto: LoginDto,
|
||||
@Req() req: Request
|
||||
) {
|
||||
async adminLogin(@Body() loginDto: LoginDto, @Req() req: Request) {
|
||||
const ipAddress = req.ip || req.connection.remoteAddress || 'unknown';
|
||||
const userAgent = req.headers['user-agent'] || 'unknown';
|
||||
|
||||
|
||||
return await this.authService.adminLogin(loginDto, ipAddress, userAgent);
|
||||
}
|
||||
|
||||
@@ -40,13 +42,10 @@ export class AuthController {
|
||||
@ApiResponse({ status: 200, description: '登录成功' })
|
||||
@ApiResponse({ status: 401, description: '用户名或密码错误' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async memberLogin(
|
||||
@Body() loginDto: LoginDto,
|
||||
@Req() req: Request
|
||||
) {
|
||||
async memberLogin(@Body() loginDto: LoginDto, @Req() req: Request) {
|
||||
const ipAddress = req.ip || req.connection.remoteAddress || 'unknown';
|
||||
const userAgent = req.headers['user-agent'] || 'unknown';
|
||||
|
||||
|
||||
return await this.authService.memberLogin(loginDto, ipAddress, userAgent);
|
||||
}
|
||||
|
||||
@@ -112,4 +111,4 @@ export class AuthController {
|
||||
}
|
||||
return { message: '登出成功' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
||||
|
||||
4
wwjcloud/src/common/auth/decorators/public.decorator.ts
Normal file
4
wwjcloud/src/common/auth/decorators/public.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNumber, IsOptional, MinLength, MaxLength } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({ description: '用户名', example: 'admin' })
|
||||
@@ -21,13 +27,19 @@ export class LoginDto {
|
||||
}
|
||||
|
||||
export class RefreshTokenDto {
|
||||
@ApiProperty({ description: '刷新Token', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
|
||||
@ApiProperty({
|
||||
description: '刷新Token',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
})
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export class LogoutDto {
|
||||
@ApiProperty({ description: '访问Token', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' })
|
||||
@ApiProperty({
|
||||
description: '访问Token',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
})
|
||||
@IsString()
|
||||
token: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('auth_token')
|
||||
@Index(['token'], { unique: true })
|
||||
@@ -22,7 +29,12 @@ export class AuthToken {
|
||||
@Column({ name: 'expires_at', type: 'datetime' })
|
||||
expiresAt: Date;
|
||||
|
||||
@Column({ name: 'refresh_token', type: 'varchar', length: 500, nullable: true })
|
||||
@Column({
|
||||
name: 'refresh_token',
|
||||
type: 'varchar',
|
||||
length: 500,
|
||||
nullable: true,
|
||||
})
|
||||
refreshToken?: string;
|
||||
|
||||
@Column({ name: 'refresh_expires_at', type: 'datetime', nullable: true })
|
||||
@@ -43,7 +55,12 @@ export class AuthToken {
|
||||
@Column({ name: 'revoked_at', type: 'datetime', nullable: true })
|
||||
revokedAt?: Date;
|
||||
|
||||
@Column({ name: 'revoked_reason', type: 'varchar', length: 200, nullable: true })
|
||||
@Column({
|
||||
name: 'revoked_reason',
|
||||
type: 'varchar',
|
||||
length: 200,
|
||||
nullable: true,
|
||||
})
|
||||
revokedReason?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
@@ -56,10 +73,10 @@ export class AuthToken {
|
||||
getDeviceTypeText(): string {
|
||||
if (this.deviceType === undefined || this.deviceType === '') return '';
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'web': '网页',
|
||||
'mobile': '手机',
|
||||
'app': '应用',
|
||||
'wechat': '微信'
|
||||
web: '网页',
|
||||
mobile: '手机',
|
||||
app: '应用',
|
||||
wechat: '微信',
|
||||
};
|
||||
return typeMap[this.deviceType] || '未知';
|
||||
}
|
||||
@@ -80,4 +97,4 @@ export class AuthToken {
|
||||
isValid(): boolean {
|
||||
return !this.isRevoked && !this.isExpired();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { JwtAuthGuard } from './JwtAuthGuard';
|
||||
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalAuthGuard implements CanActivate {
|
||||
@@ -11,7 +12,7 @@ export class GlobalAuthGuard implements CanActivate {
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// 检查是否有 @Public() 装饰器
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
@@ -22,12 +23,12 @@ export class GlobalAuthGuard implements CanActivate {
|
||||
|
||||
// 对于需要认证的接口,使用 JWT 认证
|
||||
const result = this.jwtAuthGuard.canActivate(context);
|
||||
|
||||
|
||||
// 处理 Promise 类型
|
||||
if (result instanceof Promise) {
|
||||
return await result;
|
||||
}
|
||||
|
||||
|
||||
return result as boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Request } from 'express';
|
||||
import { AuthService } from '../services/AuthService';
|
||||
@@ -13,7 +18,7 @@ export class JwtAuthGuard implements CanActivate {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('未提供访问令牌');
|
||||
}
|
||||
@@ -21,7 +26,7 @@ export class JwtAuthGuard implements CanActivate {
|
||||
try {
|
||||
// 验证Token
|
||||
const payload = await this.authService.validateToken(token);
|
||||
|
||||
|
||||
if (!payload) {
|
||||
throw new UnauthorizedException('访问令牌无效或已过期');
|
||||
}
|
||||
@@ -38,4 +43,4 @@ export class JwtAuthGuard implements CanActivate {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request } from 'express';
|
||||
|
||||
@@ -29,10 +34,10 @@ export class RolesGuard implements CanActivate {
|
||||
}
|
||||
|
||||
// 检查具体角色权限
|
||||
if (user.roles && requiredRoles.some(role => user.roles.includes(role))) {
|
||||
if (user.roles && requiredRoles.some((role) => user.roles.includes(role))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ForbiddenException('权限不足');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,4 @@ export interface User {
|
||||
|
||||
export interface RequestWithUser extends Request {
|
||||
user: User;
|
||||
}
|
||||
}
|
||||
|
||||
21
wwjcloud/src/common/auth/jwt.module.ts
Normal file
21
wwjcloud/src/common/auth/jwt.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET', 'change_me'),
|
||||
signOptions: {
|
||||
expiresIn: configService.get('JWT_EXPIRES_IN', '7d'),
|
||||
},
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
exports: [JwtModule],
|
||||
})
|
||||
export class JwtGlobalModule {}
|
||||
@@ -30,7 +30,7 @@ export class AuthService {
|
||||
|
||||
// 调用AdminService验证用户名密码
|
||||
const adminUser = await this.validateAdminUser(username, password, siteId);
|
||||
|
||||
|
||||
if (!adminUser) {
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
}
|
||||
@@ -53,8 +53,11 @@ export class AuthService {
|
||||
|
||||
// 计算过期时间
|
||||
const expiresIn = this.configService.get('JWT_EXPIRES_IN', '7d');
|
||||
const refreshExpiresIn = this.configService.get('JWT_REFRESH_EXPIRES_IN', '30d');
|
||||
|
||||
const refreshExpiresIn = this.configService.get(
|
||||
'JWT_REFRESH_EXPIRES_IN',
|
||||
'30d',
|
||||
);
|
||||
|
||||
const expiresAt = this.calculateExpiryDate(expiresIn);
|
||||
const refreshExpiresAt = this.calculateExpiryDate(refreshExpiresIn);
|
||||
|
||||
@@ -99,8 +102,12 @@ export class AuthService {
|
||||
const { username, password, siteId = 0 } = loginDto;
|
||||
|
||||
// 调用MemberService验证用户名密码
|
||||
const memberUser = await this.validateMemberUser(username, password, siteId);
|
||||
|
||||
const memberUser = await this.validateMemberUser(
|
||||
username,
|
||||
password,
|
||||
siteId,
|
||||
);
|
||||
|
||||
if (!memberUser) {
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
}
|
||||
@@ -123,8 +130,11 @@ export class AuthService {
|
||||
|
||||
// 计算过期时间
|
||||
const expiresIn = this.configService.get('JWT_EXPIRES_IN', '7d');
|
||||
const refreshExpiresIn = this.configService.get('JWT_REFRESH_EXPIRES_IN', '30d');
|
||||
|
||||
const refreshExpiresIn = this.configService.get(
|
||||
'JWT_REFRESH_EXPIRES_IN',
|
||||
'30d',
|
||||
);
|
||||
|
||||
const expiresAt = this.calculateExpiryDate(expiresIn);
|
||||
const refreshExpiresAt = this.calculateExpiryDate(refreshExpiresIn);
|
||||
|
||||
@@ -175,7 +185,7 @@ export class AuthService {
|
||||
try {
|
||||
// 验证刷新Token
|
||||
const payload = this.jwtService.verify(refreshToken);
|
||||
|
||||
|
||||
// 检查数据库中的Token记录
|
||||
const tokenRecord = await this.authTokenRepository.findOne({
|
||||
where: { refreshToken, isRevoked: 0 },
|
||||
@@ -199,7 +209,9 @@ export class AuthService {
|
||||
|
||||
// 更新数据库中的Token
|
||||
tokenRecord.token = newAccessToken;
|
||||
tokenRecord.expiresAt = this.calculateExpiryDate(this.configService.get('JWT_EXPIRES_IN', '7d'));
|
||||
tokenRecord.expiresAt = this.calculateExpiryDate(
|
||||
this.configService.get('JWT_EXPIRES_IN', '7d'),
|
||||
);
|
||||
await this.authTokenRepository.save(tokenRecord);
|
||||
|
||||
return {
|
||||
@@ -239,7 +251,7 @@ export class AuthService {
|
||||
try {
|
||||
// 验证JWT Token
|
||||
const payload = this.jwtService.verify(token);
|
||||
|
||||
|
||||
// 检查数据库中的Token记录
|
||||
const tokenRecord = await this.authTokenRepository.findOne({
|
||||
where: { token, isRevoked: 0 },
|
||||
@@ -268,7 +280,12 @@ export class AuthService {
|
||||
/**
|
||||
* 撤销用户所有Token
|
||||
*/
|
||||
async revokeUserTokens(userId: number, userType: string, siteId: number = 0, reason: string = '管理员撤销') {
|
||||
async revokeUserTokens(
|
||||
userId: number,
|
||||
userType: string,
|
||||
siteId: number = 0,
|
||||
reason: string = '管理员撤销',
|
||||
) {
|
||||
const tokens = await this.authTokenRepository.find({
|
||||
where: { userId, userType, siteId, isRevoked: 0 },
|
||||
});
|
||||
@@ -344,7 +361,11 @@ export class AuthService {
|
||||
/**
|
||||
* 验证管理员用户
|
||||
*/
|
||||
private async validateAdminUser(username: string, password: string, siteId: number): Promise<any> {
|
||||
private async validateAdminUser(
|
||||
username: string,
|
||||
password: string,
|
||||
siteId: number,
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 根据用户名查找管理员
|
||||
const admin = await this.adminService.getAdminByUsername(username);
|
||||
@@ -353,7 +374,10 @@ export class AuthService {
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await this.adminService.validatePassword(admin.uid, password);
|
||||
const isValidPassword = await this.adminService.validatePassword(
|
||||
admin.uid,
|
||||
password,
|
||||
);
|
||||
if (!isValidPassword) {
|
||||
return null;
|
||||
}
|
||||
@@ -372,11 +396,15 @@ export class AuthService {
|
||||
/**
|
||||
* 验证会员用户
|
||||
*/
|
||||
private async validateMemberUser(username: string, password: string, siteId: number): Promise<any> {
|
||||
private async validateMemberUser(
|
||||
username: string,
|
||||
password: string,
|
||||
siteId: number,
|
||||
): Promise<any> {
|
||||
try {
|
||||
// 根据用户名查找会员
|
||||
let member = await this.memberService.findByUsername(username);
|
||||
|
||||
|
||||
// 如果用户名没找到,尝试用手机号或邮箱查找
|
||||
if (!member) {
|
||||
member = await this.memberService.findByMobile(username);
|
||||
@@ -405,4 +433,4 @@ export class AuthService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
wwjcloud/src/common/channel/channel.module.ts
Normal file
22
wwjcloud/src/common/channel/channel.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { WechatFans } from './entities/WechatFans';
|
||||
import { WechatMedia } from './entities/WechatMedia';
|
||||
import { WechatReply } from './entities/WechatReply';
|
||||
|
||||
// Core Services
|
||||
import { CoreChannelService } from './services/core/CoreChannelService';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([WechatFans, WechatMedia, WechatReply])],
|
||||
providers: [
|
||||
// Core Services
|
||||
CoreChannelService,
|
||||
],
|
||||
controllers: [],
|
||||
exports: [
|
||||
// Core Services
|
||||
CoreChannelService,
|
||||
],
|
||||
})
|
||||
export class ChannelModule {}
|
||||
69
wwjcloud/src/common/channel/entities/WechatFans.ts
Normal file
69
wwjcloud/src/common/channel/entities/WechatFans.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity('wechat_fans')
|
||||
export class WechatFans {
|
||||
@PrimaryGeneratedColumn({ name: 'fans_id' })
|
||||
fans_id: number;
|
||||
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
site_id: number;
|
||||
|
||||
@Column({ name: 'nickname', type: 'varchar', length: 255, default: '' })
|
||||
nickname: string;
|
||||
|
||||
@Column({ name: 'avatar', type: 'varchar', length: 500, default: '' })
|
||||
avatar: string;
|
||||
|
||||
@Column({ name: 'sex', type: 'smallint', default: 1 })
|
||||
sex: number;
|
||||
|
||||
@Column({ name: 'language', type: 'varchar', length: 20, default: '' })
|
||||
language: string;
|
||||
|
||||
@Column({ name: 'country', type: 'varchar', length: 60, default: '' })
|
||||
country: string;
|
||||
|
||||
@Column({ name: 'province', type: 'varchar', length: 255, default: '' })
|
||||
province: string;
|
||||
|
||||
@Column({ name: 'city', type: 'varchar', length: 255, default: '' })
|
||||
city: string;
|
||||
|
||||
@Column({ name: 'district', type: 'varchar', length: 255, default: '' })
|
||||
district: string;
|
||||
|
||||
@Column({ name: 'openid', type: 'varchar', length: 255, default: '' })
|
||||
openid: string;
|
||||
|
||||
@Column({ name: 'unionid', type: 'varchar', length: 255, default: '' })
|
||||
unionid: string;
|
||||
|
||||
@Column({ name: 'groupid', type: 'int', default: 0 })
|
||||
groupid: number;
|
||||
|
||||
@Column({ name: 'is_subscribe', type: 'tinyint', default: 1 })
|
||||
is_subscribe: number;
|
||||
|
||||
@Column({ name: 'remark', type: 'varchar', length: 255, default: '' })
|
||||
remark: string;
|
||||
|
||||
@Column({ name: 'subscribe_time', type: 'int', default: 0 })
|
||||
subscribe_time: number;
|
||||
|
||||
@Column({
|
||||
name: 'subscribe_scene',
|
||||
type: 'varchar',
|
||||
length: 100,
|
||||
default: '',
|
||||
})
|
||||
subscribe_scene: string;
|
||||
|
||||
@Column({ name: 'unsubscribe_time', type: 'int', default: 0 })
|
||||
unsubscribe_time: number;
|
||||
|
||||
@Column({ name: 'update_time', type: 'int', default: 0 })
|
||||
update_time: number;
|
||||
|
||||
@Column({ name: 'app_id', type: 'int', default: 0 })
|
||||
app_id: number;
|
||||
}
|
||||
25
wwjcloud/src/common/channel/entities/WechatMedia.ts
Normal file
25
wwjcloud/src/common/channel/entities/WechatMedia.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity('wechat_media')
|
||||
export class WechatMedia {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
site_id: number;
|
||||
|
||||
@Column({ name: 'type', type: 'varchar', length: 255, default: '' })
|
||||
type: string;
|
||||
|
||||
@Column({ name: 'value', type: 'text', nullable: true })
|
||||
value: string;
|
||||
|
||||
@Column({ name: 'create_time', type: 'int', default: 0 })
|
||||
create_time: number;
|
||||
|
||||
@Column({ name: 'update_time', type: 'int', default: 0 })
|
||||
update_time: number;
|
||||
|
||||
@Column({ name: 'media_id', type: 'varchar', length: 70, default: '0' })
|
||||
media_id: string;
|
||||
}
|
||||
40
wwjcloud/src/common/channel/entities/WechatReply.ts
Normal file
40
wwjcloud/src/common/channel/entities/WechatReply.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity('wechat_reply')
|
||||
export class WechatReply {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'name', type: 'varchar', length: 64, default: '' })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
site_id: number;
|
||||
|
||||
@Column({ name: 'keyword', type: 'varchar', length: 64, default: '' })
|
||||
keyword: string;
|
||||
|
||||
@Column({ name: 'reply_type', type: 'varchar', length: 30, default: '' })
|
||||
reply_type: string;
|
||||
|
||||
@Column({ name: 'matching_type', type: 'varchar', length: 30, default: '1' })
|
||||
matching_type: string;
|
||||
|
||||
@Column({ name: 'content', type: 'text' })
|
||||
content: string;
|
||||
|
||||
@Column({ name: 'sort', type: 'int', default: 50 })
|
||||
sort: number;
|
||||
|
||||
@Column({ name: 'create_time', type: 'int', default: 0 })
|
||||
create_time: number;
|
||||
|
||||
@Column({ name: 'update_time', type: 'int', default: 0 })
|
||||
update_time: number;
|
||||
|
||||
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||
delete_time: number;
|
||||
|
||||
@Column({ name: 'reply_method', type: 'varchar', length: 50, default: '' })
|
||||
reply_method: string;
|
||||
}
|
||||
12
wwjcloud/src/common/channel/enums/channel-status.enum.ts
Normal file
12
wwjcloud/src/common/channel/enums/channel-status.enum.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export enum ChannelStatus {
|
||||
DISABLED = 0, // 禁用
|
||||
ENABLED = 1, // 启用
|
||||
MAINTENANCE = 2, // 维护中
|
||||
ERROR = 3, // 错误状态
|
||||
}
|
||||
|
||||
export enum WechatStatus {
|
||||
UNSUBSCRIBE = 0, // 未关注
|
||||
SUBSCRIBE = 1, // 已关注
|
||||
BLOCKED = 2, // 已拉黑
|
||||
}
|
||||
8
wwjcloud/src/common/channel/enums/channel-type.enum.ts
Normal file
8
wwjcloud/src/common/channel/enums/channel-type.enum.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export enum ChannelType {
|
||||
SMS = 'sms', // 短信渠道
|
||||
WECHAT = 'wechat', // 微信公众号渠道
|
||||
WEAPP = 'weapp', // 微信小程序渠道
|
||||
EMAIL = 'email', // 邮件渠道
|
||||
SYSTEM = 'system', // 站内系统渠道
|
||||
PUSH = 'push', // 推送通知渠道
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ChannelStatus } from '../enums/channel-status.enum';
|
||||
import { ChannelType } from '../enums/channel-type.enum';
|
||||
|
||||
export interface IChannelConfig {
|
||||
id: number;
|
||||
site_id: number;
|
||||
channel_type: ChannelType;
|
||||
channel_name: string;
|
||||
channel_key: string;
|
||||
status: ChannelStatus;
|
||||
config: Record<string, any>;
|
||||
create_time: number;
|
||||
update_time: number;
|
||||
}
|
||||
|
||||
export interface IWechatConfig {
|
||||
site_id: number;
|
||||
app_id: string;
|
||||
app_secret: string;
|
||||
token: string;
|
||||
encoding_aes_key: string;
|
||||
status: ChannelStatus;
|
||||
create_time: number;
|
||||
update_time: number;
|
||||
}
|
||||
|
||||
export interface ISmsConfig {
|
||||
site_id: number;
|
||||
sms_type: string; // 服务商类型:niuyun, aliyun, tencent
|
||||
api_key: string;
|
||||
api_secret: string;
|
||||
sign_name: string;
|
||||
status: ChannelStatus;
|
||||
create_time: number;
|
||||
update_time: number;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { WechatFans } from '../../entities/WechatFans';
|
||||
import { WechatMedia } from '../../entities/WechatMedia';
|
||||
import { WechatReply } from '../../entities/WechatReply';
|
||||
import { ChannelStatus } from '../../enums/channel-status.enum';
|
||||
import { ChannelType } from '../../enums/channel-type.enum';
|
||||
|
||||
@Injectable()
|
||||
export class CoreChannelService {
|
||||
constructor(
|
||||
@InjectRepository(WechatFans)
|
||||
private readonly wechatFansRepository: Repository<WechatFans>,
|
||||
@InjectRepository(WechatMedia)
|
||||
private readonly wechatMediaRepository: Repository<WechatMedia>,
|
||||
@InjectRepository(WechatReply)
|
||||
private readonly wechatReplyRepository: Repository<WechatReply>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取微信粉丝列表
|
||||
*/
|
||||
async getWechatFans(site_id: number, where: any = {}): Promise<WechatFans[]> {
|
||||
const query = { site_id, ...where };
|
||||
return await this.wechatFansRepository.find({
|
||||
where: query,
|
||||
order: { subscribe_time: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信粉丝信息
|
||||
*/
|
||||
async getWechatFanByOpenid(
|
||||
site_id: number,
|
||||
openid: string,
|
||||
): Promise<WechatFans | null> {
|
||||
return await this.wechatFansRepository.findOne({
|
||||
where: { site_id, openid },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信素材列表
|
||||
*/
|
||||
async getWechatMedia(site_id: number, type?: string): Promise<WechatMedia[]> {
|
||||
const where: any = { site_id };
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
return await this.wechatMediaRepository.find({
|
||||
where,
|
||||
order: { create_time: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信回复规则
|
||||
*/
|
||||
async getWechatReplyRules(
|
||||
site_id: number,
|
||||
reply_type?: string,
|
||||
): Promise<WechatReply[]> {
|
||||
const where: any = { site_id, delete_time: 0 };
|
||||
if (reply_type) {
|
||||
where.reply_type = reply_type;
|
||||
}
|
||||
return await this.wechatReplyRepository.find({
|
||||
where,
|
||||
order: { sort: 'ASC', create_time: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关键词匹配回复规则
|
||||
*/
|
||||
async matchWechatReply(
|
||||
site_id: number,
|
||||
keyword: string,
|
||||
): Promise<WechatReply | null> {
|
||||
const rules = await this.getWechatReplyRules(site_id, 'keyword');
|
||||
|
||||
for (const rule of rules) {
|
||||
if (rule.matching_type === 'full' && rule.keyword === keyword) {
|
||||
return rule;
|
||||
}
|
||||
if (rule.matching_type === 'like' && keyword.includes(rule.keyword)) {
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
10
wwjcloud/src/common/event-bus/event-bus.module.ts
Normal file
10
wwjcloud/src/common/event-bus/event-bus.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { VendorModule } from '../../vendor';
|
||||
import { EventBusService } from './event-bus.service';
|
||||
|
||||
@Module({
|
||||
imports: [VendorModule],
|
||||
providers: [EventBusService],
|
||||
exports: [EventBusService],
|
||||
})
|
||||
export class EventBusModule {}
|
||||
23
wwjcloud/src/common/event-bus/event-bus.service.ts
Normal file
23
wwjcloud/src/common/event-bus/event-bus.service.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { KafkaProvider } from '../../vendor';
|
||||
|
||||
export type EventMessage = {
|
||||
event: string;
|
||||
data: any;
|
||||
occurredAt?: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class EventBusService {
|
||||
constructor(private readonly kafka: KafkaProvider) {}
|
||||
|
||||
async publish(topic: string, message: EventMessage, key?: string) {
|
||||
const payload = {
|
||||
event: message.event,
|
||||
data: message.data,
|
||||
occurredAt: message.occurredAt || new Date().toISOString(),
|
||||
};
|
||||
await this.kafka.publish(topic, key ?? null, payload);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ export * from './member/member.module';
|
||||
export * from './rbac/rbac.module';
|
||||
export * from './auth/auth.module';
|
||||
export * from './upload/upload.module';
|
||||
export * from './jobs/jobs.module';
|
||||
export * from './event-bus/event-bus.module';
|
||||
|
||||
// 导出认证相关
|
||||
export * from './auth/guards/JwtAuthGuard';
|
||||
@@ -15,4 +17,4 @@ export * from './auth/decorators/RolesDecorator';
|
||||
export * from './settings';
|
||||
|
||||
// 导出常量
|
||||
export * from '../config/common/constants';
|
||||
export * from '../config/common/constants';
|
||||
|
||||
10
wwjcloud/src/common/jobs/jobs.module.ts
Normal file
10
wwjcloud/src/common/jobs/jobs.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { VendorModule } from '../../vendor';
|
||||
import { JobsService } from './jobs.service';
|
||||
|
||||
@Module({
|
||||
imports: [VendorModule],
|
||||
providers: [JobsService],
|
||||
exports: [JobsService],
|
||||
})
|
||||
export class JobsModule {}
|
||||
34
wwjcloud/src/common/jobs/jobs.service.ts
Normal file
34
wwjcloud/src/common/jobs/jobs.service.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { BullQueueProvider } from '../../vendor';
|
||||
|
||||
type EnqueueOptions = {
|
||||
delayMs?: number;
|
||||
attempts?: number;
|
||||
backoffMs?: number;
|
||||
removeOnComplete?: boolean;
|
||||
removeOnFail?: boolean;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class JobsService {
|
||||
constructor(private readonly bull: BullQueueProvider) {}
|
||||
|
||||
async enqueue(
|
||||
queue: string,
|
||||
type: string,
|
||||
payload: any,
|
||||
options: EnqueueOptions = {},
|
||||
) {
|
||||
const q = this.bull.getQueue(queue);
|
||||
const jobOptions: any = {
|
||||
attempts: options.attempts ?? 5,
|
||||
backoff: options.backoffMs
|
||||
? { type: 'fixed', delay: options.backoffMs }
|
||||
: undefined,
|
||||
removeOnComplete: options.removeOnComplete ?? true,
|
||||
removeOnFail: options.removeOnFail ?? false,
|
||||
delay: options.delayMs ?? 0,
|
||||
};
|
||||
await q.add(type, payload, jobOptions);
|
||||
}
|
||||
}
|
||||
36
wwjcloud/src/common/lang/en/common.json
Normal file
36
wwjcloud/src/common/lang/en/common.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"api": {
|
||||
"success": "Operation successful",
|
||||
"error": "Operation failed",
|
||||
"not_found": "Data not found",
|
||||
"unauthorized": "Unauthorized access",
|
||||
"forbidden": "Access forbidden",
|
||||
"validation_error": "Data validation failed",
|
||||
"server_error": "Internal server error"
|
||||
},
|
||||
"dict": {
|
||||
"status": {
|
||||
"0": "Disabled",
|
||||
"1": "Enabled",
|
||||
"2": "Maintenance",
|
||||
"3": "Error"
|
||||
},
|
||||
"gender": {
|
||||
"0": "Unknown",
|
||||
"1": "Male",
|
||||
"2": "Female"
|
||||
},
|
||||
"yes_no": {
|
||||
"0": "No",
|
||||
"1": "Yes"
|
||||
}
|
||||
},
|
||||
"validate": {
|
||||
"required": "{field} is required",
|
||||
"min_length": "{field} must be at least {min} characters",
|
||||
"max_length": "{field} must not exceed {max} characters",
|
||||
"email": "{field} format is incorrect",
|
||||
"phone": "{field} format is incorrect",
|
||||
"url": "{field} format is incorrect"
|
||||
}
|
||||
}
|
||||
37
wwjcloud/src/common/lang/en/member.json
Normal file
37
wwjcloud/src/common/lang/en/member.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"member": {
|
||||
"title": "Member Management",
|
||||
"list": "Member List",
|
||||
"add": "Add Member",
|
||||
"edit": "Edit Member",
|
||||
"delete": "Delete Member",
|
||||
"view": "View Member",
|
||||
"search": "Search Member"
|
||||
},
|
||||
"member_info": {
|
||||
"id": "Member ID",
|
||||
"username": "Username",
|
||||
"nickname": "Nickname",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"avatar": "Avatar",
|
||||
"status": "Status",
|
||||
"level": "Level",
|
||||
"points": "Points",
|
||||
"balance": "Balance",
|
||||
"create_time": "Registration Time",
|
||||
"last_login": "Last Login"
|
||||
},
|
||||
"member_status": {
|
||||
"0": "Disabled",
|
||||
"1": "Active",
|
||||
"2": "Pending",
|
||||
"3": "Locked"
|
||||
},
|
||||
"member_level": {
|
||||
"1": "Regular Member",
|
||||
"2": "Silver Member",
|
||||
"3": "Gold Member",
|
||||
"4": "Diamond Member"
|
||||
}
|
||||
}
|
||||
46
wwjcloud/src/common/lang/zh-cn/admin.json
Normal file
46
wwjcloud/src/common/lang/zh-cn/admin.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"admin": {
|
||||
"title": "系统管理",
|
||||
"dashboard": "控制台",
|
||||
"settings": "系统设置",
|
||||
"users": "用户管理",
|
||||
"roles": "角色管理",
|
||||
"menus": "菜单管理",
|
||||
"logs": "系统日志"
|
||||
},
|
||||
"user": {
|
||||
"title": "用户管理",
|
||||
"add": "添加用户",
|
||||
"edit": "编辑用户",
|
||||
"delete": "删除用户",
|
||||
"reset_password": "重置密码",
|
||||
"change_status": "修改状态"
|
||||
},
|
||||
"user_info": {
|
||||
"id": "用户ID",
|
||||
"username": "用户名",
|
||||
"realname": "真实姓名",
|
||||
"email": "邮箱",
|
||||
"phone": "手机号",
|
||||
"avatar": "头像",
|
||||
"status": "状态",
|
||||
"role": "角色",
|
||||
"department": "部门",
|
||||
"create_time": "创建时间",
|
||||
"last_login": "最后登录"
|
||||
},
|
||||
"role": {
|
||||
"title": "角色管理",
|
||||
"add": "添加角色",
|
||||
"edit": "编辑角色",
|
||||
"delete": "删除角色",
|
||||
"permissions": "权限设置"
|
||||
},
|
||||
"menu": {
|
||||
"title": "菜单管理",
|
||||
"add": "添加菜单",
|
||||
"edit": "编辑菜单",
|
||||
"delete": "删除菜单",
|
||||
"sort": "排序"
|
||||
}
|
||||
}
|
||||
36
wwjcloud/src/common/lang/zh-cn/common.json
Normal file
36
wwjcloud/src/common/lang/zh-cn/common.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"api": {
|
||||
"success": "操作成功",
|
||||
"error": "操作失败",
|
||||
"not_found": "数据不存在",
|
||||
"unauthorized": "未授权访问",
|
||||
"forbidden": "禁止访问",
|
||||
"validation_error": "数据验证失败",
|
||||
"server_error": "服务器内部错误"
|
||||
},
|
||||
"dict": {
|
||||
"status": {
|
||||
"0": "禁用",
|
||||
"1": "启用",
|
||||
"2": "维护中",
|
||||
"3": "错误"
|
||||
},
|
||||
"gender": {
|
||||
"0": "未知",
|
||||
"1": "男",
|
||||
"2": "女"
|
||||
},
|
||||
"yes_no": {
|
||||
"0": "否",
|
||||
"1": "是"
|
||||
}
|
||||
},
|
||||
"validate": {
|
||||
"required": "{field}不能为空",
|
||||
"min_length": "{field}长度不能少于{min}个字符",
|
||||
"max_length": "{field}长度不能超过{max}个字符",
|
||||
"email": "{field}格式不正确",
|
||||
"phone": "{field}格式不正确",
|
||||
"url": "{field}格式不正确"
|
||||
}
|
||||
}
|
||||
37
wwjcloud/src/common/lang/zh-cn/member.json
Normal file
37
wwjcloud/src/common/lang/zh-cn/member.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"member": {
|
||||
"title": "会员管理",
|
||||
"list": "会员列表",
|
||||
"add": "添加会员",
|
||||
"edit": "编辑会员",
|
||||
"delete": "删除会员",
|
||||
"view": "查看会员",
|
||||
"search": "搜索会员"
|
||||
},
|
||||
"member_info": {
|
||||
"id": "会员ID",
|
||||
"username": "用户名",
|
||||
"nickname": "昵称",
|
||||
"email": "邮箱",
|
||||
"phone": "手机号",
|
||||
"avatar": "头像",
|
||||
"status": "状态",
|
||||
"level": "等级",
|
||||
"points": "积分",
|
||||
"balance": "余额",
|
||||
"create_time": "注册时间",
|
||||
"last_login": "最后登录"
|
||||
},
|
||||
"member_status": {
|
||||
"0": "禁用",
|
||||
"1": "正常",
|
||||
"2": "待审核",
|
||||
"3": "已锁定"
|
||||
},
|
||||
"member_level": {
|
||||
"1": "普通会员",
|
||||
"2": "银卡会员",
|
||||
"3": "金卡会员",
|
||||
"4": "钻石会员"
|
||||
}
|
||||
}
|
||||
44
wwjcloud/src/common/lang/zh-cn/notice.json
Normal file
44
wwjcloud/src/common/lang/zh-cn/notice.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"notice": {
|
||||
"title": "通知管理",
|
||||
"sms": "短信通知",
|
||||
"email": "邮件通知",
|
||||
"wechat": "微信通知",
|
||||
"template": "通知模板",
|
||||
"log": "通知日志"
|
||||
},
|
||||
"sms": {
|
||||
"title": "短信管理",
|
||||
"send": "发送短信",
|
||||
"template": "短信模板",
|
||||
"log": "发送日志",
|
||||
"config": "短信配置"
|
||||
},
|
||||
"sms_status": {
|
||||
"sending": "发送中",
|
||||
"success": "发送成功",
|
||||
"fail": "发送失败"
|
||||
},
|
||||
"sms_info": {
|
||||
"id": "短信ID",
|
||||
"mobile": "手机号",
|
||||
"content": "短信内容",
|
||||
"status": "发送状态",
|
||||
"send_time": "发送时间",
|
||||
"result": "发送结果"
|
||||
},
|
||||
"email": {
|
||||
"title": "邮件管理",
|
||||
"send": "发送邮件",
|
||||
"template": "邮件模板",
|
||||
"log": "发送日志",
|
||||
"config": "邮件配置"
|
||||
},
|
||||
"wechat": {
|
||||
"title": "微信管理",
|
||||
"fans": "粉丝管理",
|
||||
"media": "素材管理",
|
||||
"reply": "自动回复",
|
||||
"config": "微信配置"
|
||||
}
|
||||
}
|
||||
42
wwjcloud/src/common/lang/zh-cn/schedule.json
Normal file
42
wwjcloud/src/common/lang/zh-cn/schedule.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"schedule": {
|
||||
"title": "定时任务",
|
||||
"list": "任务列表",
|
||||
"add": "添加任务",
|
||||
"edit": "编辑任务",
|
||||
"delete": "删除任务",
|
||||
"execute": "执行任务",
|
||||
"log": "执行日志"
|
||||
},
|
||||
"schedule_info": {
|
||||
"id": "任务ID",
|
||||
"name": "任务名称",
|
||||
"command": "执行命令",
|
||||
"cron": "Cron表达式",
|
||||
"status": "任务状态",
|
||||
"last_execute": "最后执行",
|
||||
"next_execute": "下次执行",
|
||||
"create_time": "创建时间"
|
||||
},
|
||||
"schedule_status": {
|
||||
"0": "禁用",
|
||||
"1": "启用",
|
||||
"2": "执行中",
|
||||
"3": "错误"
|
||||
},
|
||||
"execute_status": {
|
||||
"0": "等待执行",
|
||||
"1": "执行中",
|
||||
"2": "执行成功",
|
||||
"3": "执行失败",
|
||||
"4": "执行超时"
|
||||
},
|
||||
"cron_type": {
|
||||
"minute": "每分钟",
|
||||
"hour": "每小时",
|
||||
"day": "每天",
|
||||
"week": "每周",
|
||||
"month": "每月",
|
||||
"custom": "自定义"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,31 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { MemberService } from '../../services/admin/MemberService';
|
||||
import { CreateMemberDto, UpdateMemberDto, QueryMemberDto, BatchUpdateStatusDto, BatchAssignLevelDto, AdjustPointsDto, AdjustBalanceDto, ResetPasswordDto } from '../../dto/admin/MemberDto';
|
||||
import {
|
||||
CreateMemberAdminDto,
|
||||
UpdateMemberDto,
|
||||
QueryMemberDto,
|
||||
BatchUpdateMemberStatusDto,
|
||||
BatchAssignLevelDto,
|
||||
AdjustPointsDto,
|
||||
AdjustBalanceDto,
|
||||
ResetMemberPasswordDto,
|
||||
} from '../../dto/admin/MemberDto';
|
||||
import { Roles } from '../../../auth/decorators/RolesDecorator';
|
||||
import { JwtAuthGuard } from '../../../auth/guards/JwtAuthGuard';
|
||||
import { RolesGuard } from '../../../auth/guards/RolesGuard';
|
||||
@@ -17,7 +41,7 @@ export class MemberController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '创建会员' })
|
||||
@ApiResponse({ status: 201, description: '会员创建成功' })
|
||||
async createMember(@Body() createMemberDto: CreateMemberDto) {
|
||||
async createMember(@Body() createMemberDto: CreateMemberAdminDto) {
|
||||
return await this.memberService.createMember(createMemberDto);
|
||||
}
|
||||
|
||||
@@ -42,7 +66,7 @@ export class MemberController {
|
||||
@ApiResponse({ status: 200, description: '会员更新成功' })
|
||||
async updateMember(
|
||||
@Param('id') id: number,
|
||||
@Body() updateMemberDto: UpdateMemberDto
|
||||
@Body() updateMemberDto: UpdateMemberDto,
|
||||
) {
|
||||
return await this.memberService.updateMember(id, updateMemberDto);
|
||||
}
|
||||
@@ -68,8 +92,13 @@ export class MemberController {
|
||||
@Post('batch-update-status')
|
||||
@ApiOperation({ summary: '批量更新会员状态' })
|
||||
@ApiResponse({ status: 200, description: '状态更新成功' })
|
||||
async batchUpdateMemberStatus(@Body() batchUpdateStatusDto: BatchUpdateStatusDto) {
|
||||
await this.memberService.batchUpdateMemberStatus(batchUpdateStatusDto.member_ids, batchUpdateStatusDto.status);
|
||||
async batchUpdateMemberStatus(
|
||||
@Body() batchUpdateStatusDto: BatchUpdateMemberStatusDto,
|
||||
) {
|
||||
await this.memberService.batchUpdateMemberStatus(
|
||||
batchUpdateStatusDto.member_ids,
|
||||
batchUpdateStatusDto.status,
|
||||
);
|
||||
return { message: '状态更新成功' };
|
||||
}
|
||||
|
||||
@@ -77,8 +106,13 @@ export class MemberController {
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: '批量分配会员等级' })
|
||||
@ApiResponse({ status: 200, description: '批量分配等级成功' })
|
||||
async batchAssignMemberLevel(@Body() batchAssignLevelDto: BatchAssignLevelDto) {
|
||||
await this.memberService.batchAssignMemberLevel(batchAssignLevelDto.member_ids, batchAssignLevelDto.level_id);
|
||||
async batchAssignMemberLevel(
|
||||
@Body() batchAssignLevelDto: BatchAssignLevelDto,
|
||||
) {
|
||||
await this.memberService.batchAssignMemberLevel(
|
||||
batchAssignLevelDto.member_ids,
|
||||
batchAssignLevelDto.level_id,
|
||||
);
|
||||
return { message: '批量分配等级成功' };
|
||||
}
|
||||
|
||||
@@ -86,7 +120,11 @@ export class MemberController {
|
||||
@ApiOperation({ summary: '调整会员积分' })
|
||||
@ApiResponse({ status: 200, description: '积分调整成功' })
|
||||
async adjustMemberPoints(@Body() adjustPointsDto: AdjustPointsDto) {
|
||||
await this.memberService.adjustMemberPoints(adjustPointsDto.member_id, adjustPointsDto.points, adjustPointsDto.reason);
|
||||
await this.memberService.adjustMemberPoints(
|
||||
adjustPointsDto.member_id,
|
||||
adjustPointsDto.points,
|
||||
adjustPointsDto.reason,
|
||||
);
|
||||
return { message: '积分调整成功' };
|
||||
}
|
||||
|
||||
@@ -94,22 +132,35 @@ export class MemberController {
|
||||
@ApiOperation({ summary: '调整会员余额' })
|
||||
@ApiResponse({ status: 200, description: '余额调整成功' })
|
||||
async adjustMemberBalance(@Body() adjustBalanceDto: AdjustBalanceDto) {
|
||||
await this.memberService.adjustMemberBalance(adjustBalanceDto.member_id, adjustBalanceDto.amount, adjustBalanceDto.reason);
|
||||
await this.memberService.adjustMemberBalance(
|
||||
adjustBalanceDto.member_id,
|
||||
adjustBalanceDto.amount,
|
||||
adjustBalanceDto.reason,
|
||||
);
|
||||
return { message: '余额调整成功' };
|
||||
}
|
||||
|
||||
@Post(':id/reset-password')
|
||||
@ApiOperation({ summary: '重置会员密码' })
|
||||
@ApiResponse({ status: 200, description: '密码重置成功' })
|
||||
async resetMemberPassword(@Param('id') id: number, @Body() resetPasswordDto: ResetPasswordDto) {
|
||||
await this.memberService.resetMemberPassword(id, resetPasswordDto.new_password);
|
||||
async resetMemberPassword(
|
||||
@Param('id') id: number,
|
||||
@Body() resetPasswordDto: ResetMemberPasswordDto,
|
||||
) {
|
||||
await this.memberService.resetMemberPassword(
|
||||
id,
|
||||
resetPasswordDto.new_password,
|
||||
);
|
||||
return { message: '密码重置成功' };
|
||||
}
|
||||
|
||||
@Put(':id/status')
|
||||
@ApiOperation({ summary: '更新会员状态' })
|
||||
@ApiResponse({ status: 200, description: '状态更新成功' })
|
||||
async updateMemberStatus(@Param('id') id: number, @Body() body: { status: number }) {
|
||||
async updateMemberStatus(
|
||||
@Param('id') id: number,
|
||||
@Body() body: { status: number },
|
||||
) {
|
||||
await this.memberService.updateMemberStatus(id, body.status);
|
||||
return { message: '状态更新成功' };
|
||||
}
|
||||
@@ -127,4 +178,4 @@ export class MemberController {
|
||||
async getMemberStats(@Query('site_id') site_id: number) {
|
||||
return await this.memberService.getMemberStats(site_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, Request } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { MemberService } from '../../services/api/MemberService';
|
||||
import { CreateMemberDto, UpdateProfileDto, ChangePasswordDto, ResetPasswordDto, SignDto } from '../../dto/api/MemberDto';
|
||||
import {
|
||||
CreateMemberApiDto,
|
||||
UpdateProfileDto,
|
||||
ChangePasswordDto,
|
||||
ResetPasswordDto,
|
||||
SignDto,
|
||||
} from '../../dto/api/MemberDto';
|
||||
|
||||
@ApiTags('前台-会员管理')
|
||||
@ApiBearerAuth()
|
||||
@@ -12,14 +34,23 @@ export class MemberController {
|
||||
@Post('register')
|
||||
@ApiOperation({ summary: '会员注册' })
|
||||
@ApiResponse({ status: 201, description: '注册成功' })
|
||||
async register(@Body() createMemberDto: CreateMemberDto) {
|
||||
async register(@Body() createMemberDto: CreateMemberApiDto) {
|
||||
return await this.memberService.register(createMemberDto);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@ApiOperation({ summary: '会员登录' })
|
||||
@ApiResponse({ status: 200, description: '登录成功' })
|
||||
async login(@Body() loginDto: { username: string; password: string; ip?: string; address?: string; device?: string }) {
|
||||
async login(
|
||||
@Body()
|
||||
loginDto: {
|
||||
username: string;
|
||||
password: string;
|
||||
ip?: string;
|
||||
address?: string;
|
||||
device?: string;
|
||||
},
|
||||
) {
|
||||
return await this.memberService.login(loginDto);
|
||||
}
|
||||
|
||||
@@ -34,7 +65,10 @@ export class MemberController {
|
||||
@Put('profile')
|
||||
@ApiOperation({ summary: '更新个人资料' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
async updateProfile(@Request() req: any, @Body() updateProfileDto: UpdateProfileDto) {
|
||||
async updateProfile(
|
||||
@Request() req: any,
|
||||
@Body() updateProfileDto: UpdateProfileDto,
|
||||
) {
|
||||
const memberId = req.user.member_id;
|
||||
return await this.memberService.updateProfile(memberId, updateProfileDto);
|
||||
}
|
||||
@@ -42,7 +76,10 @@ export class MemberController {
|
||||
@Post('change-password')
|
||||
@ApiOperation({ summary: '修改密码' })
|
||||
@ApiResponse({ status: 200, description: '修改成功' })
|
||||
async changePassword(@Request() req: any, @Body() changePasswordDto: ChangePasswordDto) {
|
||||
async changePassword(
|
||||
@Request() req: any,
|
||||
@Body() changePasswordDto: ChangePasswordDto,
|
||||
) {
|
||||
const memberId = req.user.member_id;
|
||||
return await this.memberService.changePassword(memberId, changePasswordDto);
|
||||
}
|
||||
@@ -65,7 +102,10 @@ export class MemberController {
|
||||
@Get('points/history')
|
||||
@ApiOperation({ summary: '获取积分历史' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getPointsHistory(@Request() req: any, @Query() query: { page?: number; limit?: number }) {
|
||||
async getPointsHistory(
|
||||
@Request() req: any,
|
||||
@Query() query: { page?: number; limit?: number },
|
||||
) {
|
||||
const memberId = req.user.member_id;
|
||||
return await this.memberService.getPointsHistory(memberId, query);
|
||||
}
|
||||
@@ -73,7 +113,10 @@ export class MemberController {
|
||||
@Get('balance/history')
|
||||
@ApiOperation({ summary: '获取余额历史' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getBalanceHistory(@Request() req: any, @Query() query: { page?: number; limit?: number }) {
|
||||
async getBalanceHistory(
|
||||
@Request() req: any,
|
||||
@Query() query: { page?: number; limit?: number },
|
||||
) {
|
||||
const memberId = req.user.member_id;
|
||||
return await this.memberService.getBalanceHistory(memberId, query);
|
||||
}
|
||||
@@ -97,7 +140,11 @@ export class MemberController {
|
||||
@Put('address/:id')
|
||||
@ApiOperation({ summary: '更新地址' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
async updateAddress(@Request() req: any, @Param('id') id: number, @Body() addressDto: any) {
|
||||
async updateAddress(
|
||||
@Request() req: any,
|
||||
@Param('id') id: number,
|
||||
@Body() addressDto: any,
|
||||
) {
|
||||
const memberId = req.user.member_id;
|
||||
return await this.memberService.updateAddress(memberId, id, addressDto);
|
||||
}
|
||||
@@ -133,4 +180,4 @@ export class MemberController {
|
||||
const memberId = req.user.member_id;
|
||||
return await this.memberService.logout(memberId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,51 @@
|
||||
import { IsString, IsEmail, IsOptional, IsMobilePhone, MinLength, MaxLength, IsNumber, IsInt, IsDateString } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
IsOptional,
|
||||
IsMobilePhone,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
IsNumber,
|
||||
IsInt,
|
||||
IsDateString,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateMemberDto {
|
||||
export class MemberAddressDto {
|
||||
@ApiProperty({ description: '收货人姓名', example: '张三' })
|
||||
@IsString()
|
||||
receiver_name: string;
|
||||
|
||||
@ApiProperty({ description: '收货人手机号', example: '13800138000' })
|
||||
@IsString()
|
||||
receiver_mobile: string;
|
||||
|
||||
@ApiProperty({ description: '省份', example: '广东省' })
|
||||
@IsString()
|
||||
province: string;
|
||||
|
||||
@ApiProperty({ description: '城市', example: '深圳市' })
|
||||
@IsString()
|
||||
city: string;
|
||||
|
||||
@ApiProperty({ description: '区县', example: '南山区' })
|
||||
@IsString()
|
||||
district: string;
|
||||
|
||||
@ApiProperty({ description: '详细地址', example: '科技园路1号' })
|
||||
@IsString()
|
||||
address: string;
|
||||
|
||||
@ApiProperty({ description: '是否默认地址', example: 0, required: false })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
is_default?: number;
|
||||
}
|
||||
|
||||
export class CreateMemberAdminDto {
|
||||
@ApiProperty({ description: '站点ID', example: 0 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@@ -23,7 +67,11 @@ export class CreateMemberDto {
|
||||
@IsMobilePhone('zh-CN')
|
||||
mobile: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱', example: 'test@example.com', required: false })
|
||||
@ApiProperty({
|
||||
description: '邮箱',
|
||||
example: 'test@example.com',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
@@ -54,6 +102,17 @@ export class CreateMemberDto {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
status?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '会员地址列表',
|
||||
type: [MemberAddressDto],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MemberAddressDto)
|
||||
addresses?: MemberAddressDto[];
|
||||
}
|
||||
|
||||
export class UpdateMemberDto {
|
||||
@@ -63,12 +122,20 @@ export class UpdateMemberDto {
|
||||
@MaxLength(50)
|
||||
nickname?: string;
|
||||
|
||||
@ApiProperty({ description: '手机号', example: '13800138000', required: false })
|
||||
@ApiProperty({
|
||||
description: '手机号',
|
||||
example: '13800138000',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsMobilePhone('zh-CN')
|
||||
mobile?: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱', example: 'new@example.com', required: false })
|
||||
@ApiProperty({
|
||||
description: '邮箱',
|
||||
example: 'new@example.com',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
@@ -89,7 +156,11 @@ export class UpdateMemberDto {
|
||||
@IsDateString()
|
||||
birthday?: string;
|
||||
|
||||
@ApiProperty({ description: '身份证号', example: '110101199001011234', required: false })
|
||||
@ApiProperty({
|
||||
description: '身份证号',
|
||||
example: '110101199001011234',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(18)
|
||||
@@ -110,6 +181,17 @@ export class UpdateMemberDto {
|
||||
@IsString()
|
||||
@MaxLength(255)
|
||||
remark?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '会员地址列表',
|
||||
type: [MemberAddressDto],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => MemberAddressDto)
|
||||
addresses?: MemberAddressDto[];
|
||||
}
|
||||
|
||||
export class QueryMemberDto {
|
||||
@@ -138,12 +220,20 @@ export class QueryMemberDto {
|
||||
@IsInt()
|
||||
level_id?: number;
|
||||
|
||||
@ApiProperty({ description: '开始日期', example: '2024-01-01', required: false })
|
||||
@ApiProperty({
|
||||
description: '开始日期',
|
||||
example: '2024-01-01',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
start_date?: string;
|
||||
|
||||
@ApiProperty({ description: '结束日期', example: '2024-12-31', required: false })
|
||||
@ApiProperty({
|
||||
description: '结束日期',
|
||||
example: '2024-12-31',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
end_date?: string;
|
||||
@@ -154,7 +244,7 @@ export class QueryMemberDto {
|
||||
site_id?: number;
|
||||
}
|
||||
|
||||
export class BatchUpdateStatusDto {
|
||||
export class BatchUpdateMemberStatusDto {
|
||||
@ApiProperty({ description: '会员ID数组', example: [1, 2, 3] })
|
||||
@IsNumber({}, { each: true })
|
||||
member_ids: number[];
|
||||
@@ -193,7 +283,7 @@ export class AdjustBalanceDto {
|
||||
@IsInt()
|
||||
member_id: number;
|
||||
|
||||
@ApiProperty({ description: '余额调整数量', example: 50.00 })
|
||||
@ApiProperty({ description: '余额调整数量', example: 50.0 })
|
||||
@IsNumber()
|
||||
amount: number;
|
||||
|
||||
@@ -202,7 +292,7 @@ export class AdjustBalanceDto {
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export class ResetPasswordDto {
|
||||
export class ResetMemberPasswordDto {
|
||||
@ApiProperty({ description: '会员ID', example: 1 })
|
||||
@IsInt()
|
||||
member_id: number;
|
||||
@@ -212,4 +302,4 @@ export class ResetPasswordDto {
|
||||
@MinLength(6)
|
||||
@MaxLength(20)
|
||||
new_password: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { IsString, IsEmail, IsOptional, IsMobilePhone, MinLength, MaxLength } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
IsOptional,
|
||||
IsMobilePhone,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateMemberDto {
|
||||
export class CreateMemberApiDto {
|
||||
@ApiProperty({ description: '用户名', example: 'testuser' })
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
@@ -18,7 +25,11 @@ export class CreateMemberDto {
|
||||
@IsMobilePhone('zh-CN')
|
||||
mobile: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱', example: 'test@example.com', required: false })
|
||||
@ApiProperty({
|
||||
description: '邮箱',
|
||||
example: 'test@example.com',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
@@ -72,7 +83,11 @@ export class UpdateProfileDto {
|
||||
@MaxLength(50)
|
||||
nickname?: string;
|
||||
|
||||
@ApiProperty({ description: '邮箱', example: 'new@example.com', required: false })
|
||||
@ApiProperty({
|
||||
description: '邮箱',
|
||||
example: 'new@example.com',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
@@ -91,7 +106,11 @@ export class UpdateProfileDto {
|
||||
@IsOptional()
|
||||
birthday?: Date;
|
||||
|
||||
@ApiProperty({ description: '身份证号', example: '110101199001011234', required: false })
|
||||
@ApiProperty({
|
||||
description: '身份证号',
|
||||
example: '110101199001011234',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(18)
|
||||
@@ -147,4 +166,4 @@ export class SignDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
device?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { IsString, IsOptional, IsNumber, IsArray, ValidateNested } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsNumber,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class MemberAddressDto {
|
||||
@@ -25,7 +31,7 @@ export class MemberAddressDto {
|
||||
is_default?: number;
|
||||
}
|
||||
|
||||
export class CreateMemberDto {
|
||||
export class CreateMemberCoreDto {
|
||||
@IsString()
|
||||
username: string;
|
||||
|
||||
@@ -81,4 +87,4 @@ export class UpdateMemberDto {
|
||||
@Type(() => MemberAddressDto)
|
||||
@IsOptional()
|
||||
addresses?: MemberAddressDto[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
import { MemberAccount } from './MemberAccount';
|
||||
import { MemberCashOut } from './MemberCashOut';
|
||||
import { MemberLabel } from './MemberLabel';
|
||||
@@ -8,7 +16,7 @@ import { MemberAddress } from './MemberAddress';
|
||||
import { MemberAccountLog } from './MemberAccountLog';
|
||||
|
||||
@Entity('member')
|
||||
export class Member {
|
||||
export class Member extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'member_id' })
|
||||
member_id: number;
|
||||
|
||||
@@ -18,9 +26,6 @@ export class Member {
|
||||
@Column({ name: 'pid', type: 'int', default: 0 })
|
||||
pid: number;
|
||||
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
site_id: number;
|
||||
|
||||
@Column({ name: 'username', type: 'varchar', length: 255, default: '' })
|
||||
username: string;
|
||||
|
||||
@@ -57,7 +62,12 @@ export class Member {
|
||||
@Column({ name: 'douyin_openid', type: 'varchar', length: 255, default: '' })
|
||||
douyin_openid: string;
|
||||
|
||||
@Column({ name: 'register_channel', type: 'varchar', length: 255, default: 'H5' })
|
||||
@Column({
|
||||
name: 'register_channel',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
default: 'H5',
|
||||
})
|
||||
register_channel: string;
|
||||
|
||||
@Column({ name: 'register_type', type: 'varchar', length: 255, default: '' })
|
||||
@@ -78,9 +88,6 @@ export class Member {
|
||||
@Column({ name: 'login_time', type: 'int', default: 0 })
|
||||
login_time: number;
|
||||
|
||||
@Column({ name: 'create_time', type: 'int', default: 0 })
|
||||
create_time: number;
|
||||
|
||||
@Column({ name: 'last_visit_time', type: 'int', default: 0 })
|
||||
last_visit_time: number;
|
||||
|
||||
@@ -105,19 +112,49 @@ export class Member {
|
||||
@Column({ name: 'point_get', type: 'int', default: 0 })
|
||||
point_get: number;
|
||||
|
||||
@Column({ name: 'balance', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'balance',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
balance: number;
|
||||
|
||||
@Column({ name: 'balance_get', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'balance_get',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
balance_get: number;
|
||||
|
||||
@Column({ name: 'money', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'money',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
money: number;
|
||||
|
||||
@Column({ name: 'money_get', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'money_get',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
money_get: number;
|
||||
|
||||
@Column({ name: 'money_cash_outing', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'money_cash_outing',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
money_cash_outing: number;
|
||||
|
||||
@Column({ name: 'growth', type: 'int', default: 0 })
|
||||
@@ -126,13 +163,31 @@ export class Member {
|
||||
@Column({ name: 'growth_get', type: 'int', default: 0 })
|
||||
growth_get: number;
|
||||
|
||||
@Column({ name: 'commission', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'commission',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
commission: number;
|
||||
|
||||
@Column({ name: 'commission_get', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'commission_get',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
commission_get: number;
|
||||
|
||||
@Column({ name: 'commission_cash_outing', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'commission_cash_outing',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
commission_cash_outing: number;
|
||||
|
||||
@Column({ name: 'is_member', type: 'tinyint', default: 0 })
|
||||
@@ -141,9 +196,6 @@ export class Member {
|
||||
@Column({ name: 'member_time', type: 'int', default: 0 })
|
||||
member_time: number;
|
||||
|
||||
@Column({ name: 'is_del', type: 'tinyint', default: 0 })
|
||||
is_del: number;
|
||||
|
||||
@Column({ name: 'province_id', type: 'int', default: 0 })
|
||||
province_id: number;
|
||||
|
||||
@@ -162,32 +214,26 @@ export class Member {
|
||||
@Column({ name: 'remark', type: 'varchar', length: 300, default: '' })
|
||||
remark: string;
|
||||
|
||||
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||
delete_time: number;
|
||||
|
||||
@Column({ name: 'update_time', type: 'int', default: 0 })
|
||||
update_time: number;
|
||||
|
||||
// 关联关系
|
||||
@OneToMany(() => MemberAccount, account => account.member)
|
||||
@OneToMany(() => MemberAccount, (account) => account.member)
|
||||
accounts: MemberAccount[];
|
||||
|
||||
@OneToMany(() => MemberCashOut, cashOut => cashOut.member)
|
||||
@OneToMany(() => MemberCashOut, (cashOut) => cashOut.member)
|
||||
cashOuts: MemberCashOut[];
|
||||
|
||||
@OneToMany(() => MemberLabel, label => label.member)
|
||||
@OneToMany(() => MemberLabel, (label) => label.member)
|
||||
labels: MemberLabel[];
|
||||
|
||||
@OneToMany(() => MemberSign, sign => sign.member)
|
||||
@OneToMany(() => MemberSign, (sign) => sign.member)
|
||||
signs: MemberSign[];
|
||||
|
||||
@ManyToOne(() => MemberLevel, level => level.members)
|
||||
@ManyToOne(() => MemberLevel, (level) => level.members)
|
||||
@JoinColumn({ name: 'member_level' })
|
||||
level: MemberLevel;
|
||||
|
||||
@OneToMany(() => MemberAddress, address => address.member)
|
||||
@OneToMany(() => MemberAddress, (address) => address.member)
|
||||
addresses: MemberAddress[];
|
||||
|
||||
@OneToMany(() => MemberAccountLog, accountLog => accountLog.member)
|
||||
@OneToMany(() => MemberAccountLog, (accountLog) => accountLog.member)
|
||||
accountLogs: MemberAccountLog[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_account')
|
||||
@@ -55,7 +63,7 @@ export class MemberAccount {
|
||||
update_time: Date;
|
||||
|
||||
// 关联关系
|
||||
@ManyToOne(() => Member, member => member.accounts)
|
||||
@ManyToOne(() => Member, (member) => member.accounts)
|
||||
@JoinColumn({ name: 'member_id' })
|
||||
member: Member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_account_log')
|
||||
@@ -12,13 +19,30 @@ export class MemberAccountLog {
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
site_id: number;
|
||||
|
||||
@Column({ name: 'account_type', type: 'varchar', length: 255, default: 'point' })
|
||||
@Column({
|
||||
name: 'account_type',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
default: 'point',
|
||||
})
|
||||
account_type: string;
|
||||
|
||||
@Column({ name: 'account_data', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'account_data',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
account_data: number;
|
||||
|
||||
@Column({ name: 'account_sum', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'account_sum',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
account_sum: number;
|
||||
|
||||
@Column({ name: 'from_type', type: 'varchar', length: 255, default: '' })
|
||||
@@ -34,7 +58,7 @@ export class MemberAccountLog {
|
||||
memo: string;
|
||||
|
||||
// 关联关系
|
||||
@ManyToOne(() => Member, member => member.accountLogs)
|
||||
@ManyToOne(() => Member, (member) => member.accountLogs)
|
||||
@JoinColumn({ name: 'member_id' })
|
||||
member: Member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_address')
|
||||
export class MemberAddress {
|
||||
export class MemberAddress extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'id' })
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'member_id', type: 'int', default: 0 })
|
||||
member_id: number;
|
||||
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
site_id: number;
|
||||
|
||||
@Column({ name: 'name', type: 'varchar', length: 255, default: '' })
|
||||
name: string;
|
||||
|
||||
@@ -48,4 +52,4 @@ export class MemberAddress {
|
||||
@ManyToOne(() => Member)
|
||||
@JoinColumn({ name: 'member_id' })
|
||||
member: Member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_balance')
|
||||
@@ -12,7 +20,13 @@ export class MemberBalance {
|
||||
@Column({ name: 'site_id', type: 'int', default: 1 })
|
||||
site_id: number;
|
||||
|
||||
@Column({ name: 'balance', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
@Column({
|
||||
name: 'balance',
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
balance: number;
|
||||
|
||||
@Column({ name: 'balance_type', type: 'varchar', length: 50 })
|
||||
@@ -36,4 +50,4 @@ export class MemberBalance {
|
||||
@ManyToOne(() => Member)
|
||||
@JoinColumn({ name: 'member_id' })
|
||||
member: Member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_cash_out')
|
||||
@@ -18,7 +26,13 @@ export class MemberCashOut {
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, comment: '提现金额' })
|
||||
amount: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0, comment: '手续费' })
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 10,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
comment: '手续费',
|
||||
})
|
||||
fee: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, comment: '实际到账金额' })
|
||||
@@ -39,7 +53,11 @@ export class MemberCashOut {
|
||||
@Column({ type: 'varchar', length: 255, comment: '提现备注' })
|
||||
remark: string;
|
||||
|
||||
@Column({ type: 'tinyint', default: 0, comment: '状态 0:待审核 1:审核通过 2:审核拒绝 3:提现成功 4:提现失败' })
|
||||
@Column({
|
||||
type: 'tinyint',
|
||||
default: 0,
|
||||
comment: '状态 0:待审核 1:审核通过 2:审核拒绝 3:提现成功 4:提现失败',
|
||||
})
|
||||
status: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, comment: '拒绝原因' })
|
||||
@@ -64,7 +82,7 @@ export class MemberCashOut {
|
||||
update_time: Date;
|
||||
|
||||
// 关联关系
|
||||
@ManyToOne(() => Member, member => member.cashOuts)
|
||||
@ManyToOne(() => Member, (member) => member.cashOuts)
|
||||
@JoinColumn({ name: 'member_id' })
|
||||
member: Member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('member_config')
|
||||
export class MemberConfig {
|
||||
@@ -34,4 +40,4 @@ export class MemberConfig {
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
update_time: Date;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_label')
|
||||
@@ -37,7 +45,7 @@ export class MemberLabel {
|
||||
update_time: Date;
|
||||
|
||||
// 关联关系
|
||||
@ManyToOne(() => Member, member => member.labels)
|
||||
@ManyToOne(() => Member, (member) => member.labels)
|
||||
@JoinColumn({ name: 'member_id' })
|
||||
member: Member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
|
||||
import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_level')
|
||||
export class MemberLevel {
|
||||
export class MemberLevel extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
level_id: number;
|
||||
|
||||
@Column({ type: 'int', default: 0, comment: '站点ID' })
|
||||
site_id: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, comment: '等级名称' })
|
||||
level_name: string;
|
||||
|
||||
@@ -18,10 +16,22 @@ export class MemberLevel {
|
||||
@Column({ type: 'int', default: 0, comment: '升级所需积分' })
|
||||
upgrade_point: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 5, scale: 2, default: 1.0, comment: '积分倍率' })
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 5,
|
||||
scale: 2,
|
||||
default: 1.0,
|
||||
comment: '积分倍率',
|
||||
})
|
||||
point_rate: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 5, scale: 2, default: 1.0, comment: '折扣率' })
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 5,
|
||||
scale: 2,
|
||||
default: 1.0,
|
||||
comment: '折扣率',
|
||||
})
|
||||
discount_rate: number;
|
||||
|
||||
@Column({ type: 'int', default: 0, comment: '排序' })
|
||||
@@ -36,16 +46,7 @@ export class MemberLevel {
|
||||
@Column({ type: 'varchar', length: 255, comment: '等级权益' })
|
||||
benefits: string;
|
||||
|
||||
@Column({type: 'tinyint', default: 0, comment: '是否删除 0:否 1:是' })
|
||||
is_del: number;
|
||||
|
||||
@CreateDateColumn({ comment: '创建时间' })
|
||||
create_time: Date;
|
||||
|
||||
@UpdateDateColumn({ comment: '更新时间' })
|
||||
update_time: Date;
|
||||
|
||||
// 关联关系
|
||||
@OneToMany(() => Member, member => member.level)
|
||||
@OneToMany(() => Member, (member) => member.level)
|
||||
members: Member[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_points')
|
||||
@@ -36,4 +44,4 @@ export class MemberPoints {
|
||||
@ManyToOne(() => Member)
|
||||
@JoinColumn({ name: 'member_id' })
|
||||
member: Member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Member } from './Member';
|
||||
|
||||
@Entity('member_sign')
|
||||
@@ -46,7 +54,7 @@ export class MemberSign {
|
||||
update_time: Date;
|
||||
|
||||
// 关联关系
|
||||
@ManyToOne(() => Member, member => member.signs)
|
||||
@ManyToOne(() => Member, (member) => member.signs)
|
||||
@JoinColumn({ name: 'member_id' })
|
||||
member: Member;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { Member } from './entities/Member';
|
||||
import { MemberLevel } from './entities/MemberLevel';
|
||||
import { MemberAddress } from './entities/MemberAddress';
|
||||
@@ -7,6 +8,7 @@ import { MemberSign } from './entities/MemberSign';
|
||||
import { MemberCashOut } from './entities/MemberCashOut';
|
||||
import { MemberLabel } from './entities/MemberLabel';
|
||||
import { MemberAccount } from './entities/MemberAccount';
|
||||
import { MemberAccountLog } from './entities/MemberAccountLog';
|
||||
import { MemberPoints } from './entities/MemberPoints';
|
||||
import { MemberBalance } from './entities/MemberBalance';
|
||||
import { MemberConfig } from './entities/MemberConfig';
|
||||
@@ -18,6 +20,7 @@ import { MemberController as MemberAdminController } from './controllers/adminap
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => AuthModule),
|
||||
TypeOrmModule.forFeature([
|
||||
Member,
|
||||
MemberLevel,
|
||||
@@ -26,6 +29,7 @@ import { MemberController as MemberAdminController } from './controllers/adminap
|
||||
MemberCashOut,
|
||||
MemberLabel,
|
||||
MemberAccount,
|
||||
MemberAccountLog,
|
||||
MemberPoints,
|
||||
MemberBalance,
|
||||
MemberConfig,
|
||||
@@ -35,4 +39,4 @@ import { MemberController as MemberAdminController } from './controllers/adminap
|
||||
controllers: [MemberApiController, MemberAdminController],
|
||||
exports: [CoreMemberService, MemberApiService, MemberAdminService],
|
||||
})
|
||||
export class MemberModule {}
|
||||
export class MemberModule {}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Like, Between, In } from 'typeorm';
|
||||
import { Member } from '../../entities/Member';
|
||||
@@ -6,7 +10,7 @@ import { MemberLevel } from '../../entities/MemberLevel';
|
||||
import { MemberAddress } from '../../entities/MemberAddress';
|
||||
import { CoreMemberService } from '../core/CoreMemberService';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { CreateMemberDto, UpdateMemberDto } from '../../dto/member.dto';
|
||||
import { CreateMemberAdminDto, UpdateMemberDto } from '../../dto/admin/MemberDto';
|
||||
|
||||
@Injectable()
|
||||
export class MemberService {
|
||||
@@ -24,21 +28,22 @@ export class MemberService {
|
||||
* 获取会员列表(分页)
|
||||
*/
|
||||
async getMemberList(queryDto: any): Promise<any> {
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
keyword,
|
||||
status,
|
||||
level_id,
|
||||
start_date,
|
||||
const {
|
||||
page = 1,
|
||||
limit = 20,
|
||||
keyword,
|
||||
status,
|
||||
level_id,
|
||||
start_date,
|
||||
end_date,
|
||||
site_id = 0
|
||||
site_id = 0,
|
||||
} = queryDto;
|
||||
|
||||
const queryBuilder = this.memberRepository.createQueryBuilder('member')
|
||||
|
||||
const queryBuilder = this.memberRepository
|
||||
.createQueryBuilder('member')
|
||||
.leftJoinAndSelect('member.level', 'level')
|
||||
.where('member.is_delete = :isDelete', { isDelete: 0 })
|
||||
.orderBy('member.register_time', 'DESC');
|
||||
.where('member.is_del = :isDel', { isDel: 0 })
|
||||
.orderBy('member.create_time', 'DESC');
|
||||
|
||||
// 站点筛选
|
||||
if (site_id > 0) {
|
||||
@@ -49,7 +54,7 @@ export class MemberService {
|
||||
if (keyword) {
|
||||
queryBuilder.andWhere(
|
||||
'(member.username LIKE :keyword OR member.nickname LIKE :keyword OR member.mobile LIKE :keyword OR member.email LIKE :keyword)',
|
||||
{ keyword: `%${keyword}%` }
|
||||
{ keyword: `%${keyword}%` },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,15 +65,20 @@ export class MemberService {
|
||||
|
||||
// 等级筛选
|
||||
if (level_id) {
|
||||
queryBuilder.andWhere('member.level_id = :levelId', { levelId: level_id });
|
||||
queryBuilder.andWhere('member.level_id = :levelId', {
|
||||
levelId: level_id,
|
||||
});
|
||||
}
|
||||
|
||||
// 日期范围筛选
|
||||
if (start_date && end_date) {
|
||||
queryBuilder.andWhere('member.register_time BETWEEN :startDate AND :endDate', {
|
||||
startDate: new Date(start_date),
|
||||
endDate: new Date(end_date),
|
||||
});
|
||||
queryBuilder.andWhere(
|
||||
'member.register_time BETWEEN :startDate AND :endDate',
|
||||
{
|
||||
startDate: new Date(start_date),
|
||||
endDate: new Date(end_date),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const [members, total] = await queryBuilder
|
||||
@@ -101,16 +111,18 @@ export class MemberService {
|
||||
return member;
|
||||
}
|
||||
|
||||
async createMember(memberData: CreateMemberDto): Promise<Member> {
|
||||
async createMember(memberData: CreateMemberAdminDto): Promise<Member> {
|
||||
// 检查用户名是否已存在
|
||||
const exists = await this.memberCoreService.isUsernameExists(memberData.username);
|
||||
const exists = await this.memberCoreService.isUsernameExists(
|
||||
memberData.username,
|
||||
);
|
||||
if (exists) {
|
||||
throw new Error('用户名已存在');
|
||||
}
|
||||
|
||||
// 创建会员
|
||||
const member = await this.memberCoreService.createMember(memberData);
|
||||
|
||||
|
||||
// 创建会员地址
|
||||
if (memberData.addresses && memberData.addresses.length > 0) {
|
||||
for (const addressData of memberData.addresses) {
|
||||
@@ -121,7 +133,10 @@ export class MemberService {
|
||||
return member;
|
||||
}
|
||||
|
||||
async updateMember(memberId: number, updateData: UpdateMemberDto): Promise<Member> {
|
||||
async updateMember(
|
||||
memberId: number,
|
||||
updateData: UpdateMemberDto,
|
||||
): Promise<Member> {
|
||||
// 检查会员是否存在
|
||||
const member = await this.memberCoreService.getMemberById(memberId);
|
||||
if (!member) {
|
||||
@@ -129,15 +144,17 @@ export class MemberService {
|
||||
}
|
||||
|
||||
// 更新会员信息
|
||||
const updatedMember = await this.memberCoreService.updateMember(memberId, updateData);
|
||||
await this.memberCoreService.updateMember(memberId, updateData);
|
||||
|
||||
// 更新会员地址
|
||||
if (updateData.addresses !== undefined) {
|
||||
await this.updateMemberAddresses(memberId, updateData.addresses);
|
||||
}
|
||||
|
||||
// 返回更新后的会员信息
|
||||
const updatedMember = await this.memberCoreService.getMemberById(memberId);
|
||||
if (!updatedMember) {
|
||||
throw new Error('更新后的会员不存在');
|
||||
throw new NotFoundException('更新后的会员不存在');
|
||||
}
|
||||
return updatedMember;
|
||||
}
|
||||
@@ -151,7 +168,7 @@ export class MemberService {
|
||||
|
||||
// 删除会员
|
||||
await this.memberCoreService.deleteMember(memberId);
|
||||
|
||||
|
||||
// 删除相关数据
|
||||
await this.deleteMemberRelatedData(memberId);
|
||||
}
|
||||
@@ -172,14 +189,20 @@ export class MemberService {
|
||||
/**
|
||||
* 批量更新会员状态
|
||||
*/
|
||||
async batchUpdateMemberStatus(memberIds: number[], status: number): Promise<void> {
|
||||
async batchUpdateMemberStatus(
|
||||
memberIds: number[],
|
||||
status: number,
|
||||
): Promise<void> {
|
||||
await this.memberRepository.update(memberIds, { status });
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置会员密码
|
||||
*/
|
||||
async resetMemberPassword(memberId: number, newPassword: string): Promise<void> {
|
||||
async resetMemberPassword(
|
||||
memberId: number,
|
||||
newPassword: string,
|
||||
): Promise<void> {
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
await this.memberRepository.update(memberId, { password: hashedPassword });
|
||||
}
|
||||
@@ -191,7 +214,10 @@ export class MemberService {
|
||||
await this.memberRepository.update(memberId, { member_level: levelId });
|
||||
}
|
||||
|
||||
async batchAssignMemberLevel(memberIds: number[], levelId: number): Promise<void> {
|
||||
async batchAssignMemberLevel(
|
||||
memberIds: number[],
|
||||
levelId: number,
|
||||
): Promise<void> {
|
||||
for (const memberId of memberIds) {
|
||||
await this.assignMemberLevel(memberId, levelId);
|
||||
}
|
||||
@@ -200,7 +226,11 @@ export class MemberService {
|
||||
/**
|
||||
* 调整会员积分
|
||||
*/
|
||||
async adjustMemberPoints(memberId: number, points: number, reason: string): Promise<void> {
|
||||
async adjustMemberPoints(
|
||||
memberId: number,
|
||||
points: number,
|
||||
reason: string,
|
||||
): Promise<void> {
|
||||
if (points > 0) {
|
||||
await this.memberCoreService.addPoints(memberId, points);
|
||||
} else {
|
||||
@@ -214,7 +244,11 @@ export class MemberService {
|
||||
/**
|
||||
* 调整会员余额
|
||||
*/
|
||||
async adjustMemberBalance(memberId: number, amount: number, reason: string): Promise<void> {
|
||||
async adjustMemberBalance(
|
||||
memberId: number,
|
||||
amount: number,
|
||||
reason: string,
|
||||
): Promise<void> {
|
||||
if (amount > 0) {
|
||||
await this.memberCoreService.addBalance(memberId, amount);
|
||||
} else {
|
||||
@@ -235,10 +269,10 @@ export class MemberService {
|
||||
}
|
||||
|
||||
const totalMembers = await this.memberRepository.count({ where });
|
||||
const activeMembers = await this.memberRepository.count({
|
||||
where: { ...where, status: 1 }
|
||||
const activeMembers = await this.memberRepository.count({
|
||||
where: { ...where, status: 1 },
|
||||
});
|
||||
|
||||
|
||||
const todayNewMembers = await this.memberRepository.count({
|
||||
where: {
|
||||
...where,
|
||||
@@ -285,7 +319,7 @@ export class MemberService {
|
||||
const results = {
|
||||
success: 0,
|
||||
failed: 0,
|
||||
errors: [] as Array<{row: any, error: string}>,
|
||||
errors: [] as Array<{ row: any; error: string }>,
|
||||
};
|
||||
|
||||
for (const data of importData) {
|
||||
@@ -321,7 +355,10 @@ export class MemberService {
|
||||
/**
|
||||
* 更新会员地址
|
||||
*/
|
||||
async updateMemberAddresses(memberId: number, addresses: any[]): Promise<void> {
|
||||
async updateMemberAddresses(
|
||||
memberId: number,
|
||||
addresses: any[],
|
||||
): Promise<void> {
|
||||
// 实现更新会员地址逻辑
|
||||
for (const addressData of addresses) {
|
||||
if (addressData.address_id) {
|
||||
@@ -341,4 +378,4 @@ export class MemberService {
|
||||
await this.memberAddressRepository.delete({ member_id: memberId });
|
||||
// 可以添加其他相关数据的删除逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { Injectable, BadRequestException, UnauthorizedException } from '@nestjs/common';
|
||||
import {
|
||||
Injectable,
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Not } from 'typeorm';
|
||||
import { CoreMemberService } from '../core/CoreMemberService';
|
||||
@@ -24,20 +28,26 @@ export class MemberService {
|
||||
*/
|
||||
async register(registerDto: any): Promise<any> {
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await this.memberCoreService.findByUsername(registerDto.username);
|
||||
const existingUser = await this.memberCoreService.findByUsername(
|
||||
registerDto.username,
|
||||
);
|
||||
if (existingUser) {
|
||||
throw new BadRequestException('用户名已存在');
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
const existingMobile = await this.memberCoreService.findByMobile(registerDto.mobile);
|
||||
const existingMobile = await this.memberCoreService.findByMobile(
|
||||
registerDto.mobile,
|
||||
);
|
||||
if (existingMobile) {
|
||||
throw new BadRequestException('手机号已存在');
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (registerDto.email) {
|
||||
const existingEmail = await this.memberCoreService.findByEmail(registerDto.email);
|
||||
const existingEmail = await this.memberCoreService.findByEmail(
|
||||
registerDto.email,
|
||||
);
|
||||
if (existingEmail) {
|
||||
throw new BadRequestException('邮箱已存在');
|
||||
}
|
||||
@@ -45,7 +55,7 @@ export class MemberService {
|
||||
|
||||
// 创建会员
|
||||
const member = await this.memberCoreService.create(registerDto);
|
||||
|
||||
|
||||
// 返回注册成功信息(不包含密码)
|
||||
const { password, ...result } = member;
|
||||
return result;
|
||||
@@ -56,7 +66,7 @@ export class MemberService {
|
||||
*/
|
||||
async login(loginDto: any): Promise<any> {
|
||||
const { username, password } = loginDto;
|
||||
|
||||
|
||||
// 查找会员
|
||||
const member = await this.memberCoreService.findByUsername(username);
|
||||
if (!member) {
|
||||
@@ -64,7 +74,10 @@ export class MemberService {
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isValidPassword = await this.memberCoreService.validatePassword(member, password);
|
||||
const isValidPassword = await this.memberCoreService.validatePassword(
|
||||
member,
|
||||
password,
|
||||
);
|
||||
if (!isValidPassword) {
|
||||
throw new UnauthorizedException('用户名或密码错误');
|
||||
}
|
||||
@@ -91,7 +104,7 @@ export class MemberService {
|
||||
*/
|
||||
async getProfile(memberId: number): Promise<any> {
|
||||
const member = await this.memberCoreService.findById(memberId);
|
||||
|
||||
|
||||
// 返回会员信息(不包含密码)
|
||||
const { password, ...result } = member;
|
||||
return result;
|
||||
@@ -111,7 +124,10 @@ export class MemberService {
|
||||
|
||||
// 检查用户名是否重复
|
||||
if (updateDto.username) {
|
||||
const exists = await this.memberCoreService.isUsernameExists(updateDto.username, memberId);
|
||||
const exists = await this.memberCoreService.isUsernameExists(
|
||||
updateDto.username,
|
||||
memberId,
|
||||
);
|
||||
if (exists) {
|
||||
throw new BadRequestException('用户名已存在');
|
||||
}
|
||||
@@ -119,7 +135,10 @@ export class MemberService {
|
||||
|
||||
// 检查手机号是否重复
|
||||
if (updateDto.mobile) {
|
||||
const exists = await this.memberCoreService.isMobileExists(updateDto.mobile, memberId);
|
||||
const exists = await this.memberCoreService.isMobileExists(
|
||||
updateDto.mobile,
|
||||
memberId,
|
||||
);
|
||||
if (exists) {
|
||||
throw new BadRequestException('手机号已存在');
|
||||
}
|
||||
@@ -127,7 +146,10 @@ export class MemberService {
|
||||
|
||||
// 检查邮箱是否重复
|
||||
if (updateDto.email) {
|
||||
const exists = await this.memberCoreService.isEmailExists(updateDto.email, memberId);
|
||||
const exists = await this.memberCoreService.isEmailExists(
|
||||
updateDto.email,
|
||||
memberId,
|
||||
);
|
||||
if (exists) {
|
||||
throw new BadRequestException('邮箱已存在');
|
||||
}
|
||||
@@ -140,13 +162,19 @@ export class MemberService {
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
async changePassword(memberId: number, changePasswordDto: any): Promise<void> {
|
||||
async changePassword(
|
||||
memberId: number,
|
||||
changePasswordDto: any,
|
||||
): Promise<void> {
|
||||
const { oldPassword, newPassword } = changePasswordDto;
|
||||
|
||||
|
||||
const member = await this.memberCoreService.findById(memberId);
|
||||
|
||||
|
||||
// 验证旧密码
|
||||
const isValidPassword = await this.memberCoreService.validatePassword(member, oldPassword);
|
||||
const isValidPassword = await this.memberCoreService.validatePassword(
|
||||
member,
|
||||
oldPassword,
|
||||
);
|
||||
if (!isValidPassword) {
|
||||
throw new BadRequestException('原密码错误');
|
||||
}
|
||||
@@ -161,7 +189,7 @@ export class MemberService {
|
||||
*/
|
||||
async resetPassword(resetDto: any): Promise<void> {
|
||||
const { mobile, verifyCode, newPassword } = resetDto;
|
||||
|
||||
|
||||
// 验证手机号
|
||||
const member = await this.memberCoreService.findByMobile(mobile);
|
||||
if (!member) {
|
||||
@@ -172,13 +200,15 @@ export class MemberService {
|
||||
if (!verifyCode) {
|
||||
throw new Error('验证码不能为空');
|
||||
}
|
||||
|
||||
|
||||
// 这里应该验证验证码的有效性
|
||||
// 可以从缓存中获取验证码进行比较
|
||||
|
||||
// 更新密码
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
await this.memberCoreService.update(member.member_id, { password: hashedPassword });
|
||||
await this.memberCoreService.update(member.member_id, {
|
||||
password: hashedPassword,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,8 +221,8 @@ export class MemberService {
|
||||
where: {
|
||||
member_id: memberId,
|
||||
sign_date: today,
|
||||
is_del: 0
|
||||
}
|
||||
is_del: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSign) {
|
||||
@@ -202,16 +232,18 @@ export class MemberService {
|
||||
// 2. 计算连续签到天数
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
|
||||
const yesterdaySign = await this.memberSignRepository.findOne({
|
||||
where: {
|
||||
member_id: memberId,
|
||||
sign_date: yesterday,
|
||||
is_del: 0
|
||||
}
|
||||
is_del: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const continuousDays = yesterdaySign ? yesterdaySign.continuous_days + 1 : 1;
|
||||
const continuousDays = yesterdaySign
|
||||
? yesterdaySign.continuous_days + 1
|
||||
: 1;
|
||||
|
||||
// 3. 分配积分(根据连续签到天数计算)
|
||||
const signPoints = this.calculateSignPoints(continuousDays);
|
||||
@@ -226,7 +258,7 @@ export class MemberService {
|
||||
sign_ip: signInfo.ip,
|
||||
sign_address: signInfo.address,
|
||||
sign_device: signInfo.device,
|
||||
status: 1
|
||||
status: 1,
|
||||
});
|
||||
|
||||
await this.memberSignRepository.save(signRecord);
|
||||
@@ -239,7 +271,7 @@ export class MemberService {
|
||||
message: '签到成功',
|
||||
continuous_days: continuousDays,
|
||||
sign_point: signPoints,
|
||||
total_points: await this.getMemberTotalPoints(memberId)
|
||||
total_points: await this.getMemberTotalPoints(memberId),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -247,8 +279,8 @@ export class MemberService {
|
||||
* 计算签到积分
|
||||
*/
|
||||
private calculateSignPoints(continuousDays: number): number {
|
||||
if (continuousDays >= 7) return 20; // 连续7天以上
|
||||
if (continuousDays >= 3) return 15; // 连续3天以上
|
||||
if (continuousDays >= 7) return 20; // 连续7天以上
|
||||
if (continuousDays >= 3) return 15; // 连续3天以上
|
||||
return 10; // 基础积分
|
||||
}
|
||||
|
||||
@@ -265,24 +297,26 @@ export class MemberService {
|
||||
*/
|
||||
async getPointsHistory(memberId: number, queryDto: any): Promise<any> {
|
||||
const { page = 1, limit = 20 } = queryDto;
|
||||
|
||||
|
||||
// 查询积分变动记录
|
||||
const [records, total] = await this.memberAccountLogRepository.findAndCount({
|
||||
where: {
|
||||
member_id: memberId,
|
||||
account_type: 'point'
|
||||
const [records, total] = await this.memberAccountLogRepository.findAndCount(
|
||||
{
|
||||
where: {
|
||||
member_id: memberId,
|
||||
account_type: 'point',
|
||||
},
|
||||
order: { create_time: 'DESC' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
},
|
||||
order: { create_time: 'DESC' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
});
|
||||
);
|
||||
|
||||
return {
|
||||
list: records,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
total_pages: Math.ceil(total / limit)
|
||||
total_pages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -291,24 +325,26 @@ export class MemberService {
|
||||
*/
|
||||
async getBalanceHistory(memberId: number, queryDto: any): Promise<any> {
|
||||
const { page = 1, limit = 20 } = queryDto;
|
||||
|
||||
|
||||
// 查询余额变动记录
|
||||
const [records, total] = await this.memberAccountLogRepository.findAndCount({
|
||||
where: {
|
||||
member_id: memberId,
|
||||
account_type: 'balance'
|
||||
const [records, total] = await this.memberAccountLogRepository.findAndCount(
|
||||
{
|
||||
where: {
|
||||
member_id: memberId,
|
||||
account_type: 'balance',
|
||||
},
|
||||
order: { create_time: 'DESC' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
},
|
||||
order: { create_time: 'DESC' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit
|
||||
});
|
||||
);
|
||||
|
||||
return {
|
||||
list: records,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
total_pages: Math.ceil(total / limit)
|
||||
total_pages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -336,7 +372,7 @@ export class MemberService {
|
||||
if (addressDto.is_default) {
|
||||
await this.memberAddressRepository.update(
|
||||
{ member_id: memberId, is_default: 1 },
|
||||
{ is_default: 0 }
|
||||
{ is_default: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -344,7 +380,7 @@ export class MemberService {
|
||||
...addressDto,
|
||||
member_id: memberId,
|
||||
site_id: addressDto.site_id || 0,
|
||||
status: 1
|
||||
status: 1,
|
||||
});
|
||||
|
||||
return this.memberAddressRepository.save(addressDto);
|
||||
@@ -353,10 +389,14 @@ export class MemberService {
|
||||
/**
|
||||
* 更新会员地址
|
||||
*/
|
||||
async updateAddress(memberId: number, addressId: number, addressDto: any): Promise<void> {
|
||||
async updateAddress(
|
||||
memberId: number,
|
||||
addressId: number,
|
||||
addressDto: any,
|
||||
): Promise<void> {
|
||||
// 验证地址是否属于当前会员
|
||||
const address = await this.memberAddressRepository.findOne({
|
||||
where: { id: addressId, member_id: memberId }
|
||||
where: { id: addressId, member_id: memberId },
|
||||
});
|
||||
|
||||
if (!address) {
|
||||
@@ -367,7 +407,7 @@ export class MemberService {
|
||||
if (addressDto.is_default) {
|
||||
await this.memberAddressRepository.update(
|
||||
{ member_id: memberId, is_default: 1, id: Not(addressId) },
|
||||
{ is_default: 0 }
|
||||
{ is_default: 0 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -380,7 +420,7 @@ export class MemberService {
|
||||
async deleteAddress(memberId: number, addressId: number): Promise<void> {
|
||||
// 验证地址是否属于当前会员
|
||||
const address = await this.memberAddressRepository.findOne({
|
||||
where: { id: addressId, member_id: memberId }
|
||||
where: { id: addressId, member_id: memberId },
|
||||
});
|
||||
|
||||
if (!address) {
|
||||
@@ -397,7 +437,7 @@ export class MemberService {
|
||||
async setDefaultAddress(memberId: number, addressId: number): Promise<void> {
|
||||
// 验证地址是否属于当前会员
|
||||
const address = await this.memberAddressRepository.findOne({
|
||||
where: { id: addressId, member_id: memberId }
|
||||
where: { id: addressId, member_id: memberId },
|
||||
});
|
||||
|
||||
if (!address) {
|
||||
@@ -406,8 +446,8 @@ export class MemberService {
|
||||
|
||||
// 先取消其他默认地址
|
||||
await this.memberAddressRepository.update(
|
||||
{ member_id: memberId, is_default: 1, id: Not(addressId) },
|
||||
{ is_default: 0 }
|
||||
{ member_id: memberId, is_default: 1, id: Not(addressId) },
|
||||
{ is_default: 0 },
|
||||
);
|
||||
|
||||
// 设置当前地址为默认
|
||||
@@ -417,12 +457,14 @@ export class MemberService {
|
||||
/**
|
||||
* 会员登出
|
||||
*/
|
||||
async logout(memberId: number): Promise<{ success: boolean; message: string }> {
|
||||
async logout(
|
||||
memberId: number,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
// 这里可以清除会员的登录状态、token 等
|
||||
// 暂时返回成功状态
|
||||
return {
|
||||
success: true,
|
||||
message: '登出成功'
|
||||
message: '登出成功',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,37 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Not, Between } from 'typeorm';
|
||||
import { BaseService } from '@wwjCore/base/BaseService';
|
||||
import { Member } from '../../entities/Member';
|
||||
import { MemberLevel } from '../../entities/MemberLevel';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@Injectable()
|
||||
export class CoreMemberService {
|
||||
export class CoreMemberService extends BaseService<Member> {
|
||||
constructor(
|
||||
@InjectRepository(Member)
|
||||
private memberRepository: Repository<Member>,
|
||||
@InjectRepository(MemberLevel)
|
||||
private memberLevelRepository: Repository<MemberLevel>,
|
||||
) {}
|
||||
) {
|
||||
super(memberRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会员
|
||||
*/
|
||||
async create(createMemberDto: any): Promise<Member> {
|
||||
const member = new Member();
|
||||
|
||||
|
||||
// 生成会员编号
|
||||
member.member_no = await this.generateMemberNo();
|
||||
|
||||
|
||||
// 加密密码
|
||||
member.password = await bcrypt.hash(createMemberDto.password, 10);
|
||||
|
||||
|
||||
// 设置其他字段
|
||||
Object.assign(member, createMemberDto);
|
||||
|
||||
|
||||
return this.memberRepository.save(member);
|
||||
}
|
||||
|
||||
@@ -44,37 +47,21 @@ export class CoreMemberService {
|
||||
* 根据ID获取会员
|
||||
*/
|
||||
async getMemberById(memberId: number): Promise<Member | null> {
|
||||
return await this.memberRepository.findOne({
|
||||
where: { member_id: memberId, is_del: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会员
|
||||
*/
|
||||
async updateMember(memberId: number, updateData: any): Promise<Member | null> {
|
||||
await this.memberRepository.update(memberId, updateData);
|
||||
return await this.getMemberById(memberId);
|
||||
return await this.findOne(memberId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会员
|
||||
*/
|
||||
async deleteMember(memberId: number): Promise<void> {
|
||||
await this.memberRepository.update(memberId, {
|
||||
is_del: 1,
|
||||
delete_time: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
await this.delete(memberId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查找会员
|
||||
*/
|
||||
async findById(memberId: number): Promise<Member> {
|
||||
const member = await this.memberRepository.findOne({
|
||||
where: { member_id: memberId, is_del: 0 },
|
||||
relations: ['level', 'addresses', 'labels'],
|
||||
});
|
||||
const member = await this.findOne(memberId);
|
||||
|
||||
if (!member) {
|
||||
throw new NotFoundException('会员不存在');
|
||||
@@ -87,26 +74,22 @@ export class CoreMemberService {
|
||||
* 根据用户名查找会员
|
||||
*/
|
||||
async findByUsername(username: string): Promise<Member | null> {
|
||||
return await this.memberRepository.findOne({
|
||||
where: { username, is_del: 0 }
|
||||
});
|
||||
return await this.findOneBy({ username });
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号查找会员
|
||||
*/
|
||||
async findByMobile(mobile: string): Promise<Member | null> {
|
||||
return await this.memberRepository.findOne({
|
||||
where: { mobile, is_del: 0 }
|
||||
});
|
||||
return await this.findOneBy({ mobile });
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱查找会员
|
||||
*/
|
||||
async findByEmail(email: string): Promise<Member | null> {
|
||||
return await this.memberRepository.findOne({
|
||||
where: { is_del: 0 }
|
||||
return await this.memberRepository.findOne({
|
||||
where: { is_del: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,16 +110,16 @@ export class CoreMemberService {
|
||||
/**
|
||||
* 更新会员
|
||||
*/
|
||||
async update(memberId: number, updateDto: any): Promise<void> {
|
||||
async updateMember(memberId: number, updateDto: any): Promise<void> {
|
||||
const member = await this.findById(memberId);
|
||||
|
||||
|
||||
// 不允许更新敏感字段
|
||||
delete updateDto.member_id;
|
||||
delete updateDto.member_no;
|
||||
delete updateDto.site_id;
|
||||
delete updateDto.register_time;
|
||||
|
||||
await this.memberRepository.update(memberId, updateDto);
|
||||
|
||||
await this.update(memberId, updateDto);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,17 +144,21 @@ export class CoreMemberService {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
|
||||
|
||||
// 查询当天注册的会员数量
|
||||
const todayStart = Math.floor(new Date(year, date.getMonth(), date.getDate()).getTime() / 1000);
|
||||
const todayEnd = Math.floor(new Date(year, date.getMonth(), date.getDate() + 1).getTime() / 1000);
|
||||
const todayStart = Math.floor(
|
||||
new Date(year, date.getMonth(), date.getDate()).getTime() / 1000,
|
||||
);
|
||||
const todayEnd = Math.floor(
|
||||
new Date(year, date.getMonth(), date.getDate() + 1).getTime() / 1000,
|
||||
);
|
||||
const todayCount = await this.memberRepository.count({
|
||||
where: {
|
||||
create_time: Between(todayStart, todayEnd),
|
||||
is_del: 0,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const sequence = String(todayCount + 1).padStart(4, '0');
|
||||
return `M${year}${month}${day}${sequence}`;
|
||||
}
|
||||
@@ -187,28 +174,44 @@ export class CoreMemberService {
|
||||
* 增加积分
|
||||
*/
|
||||
async addPoints(memberId: number, points: number): Promise<void> {
|
||||
await this.memberRepository.increment({ member_id: memberId }, 'point', points);
|
||||
await this.memberRepository.increment(
|
||||
{ member_id: memberId },
|
||||
'point',
|
||||
points,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扣除积分
|
||||
*/
|
||||
async deductPoints(memberId: number, points: number): Promise<void> {
|
||||
await this.memberRepository.decrement({ member_id: memberId }, 'point', points);
|
||||
await this.memberRepository.decrement(
|
||||
{ member_id: memberId },
|
||||
'point',
|
||||
points,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加余额
|
||||
*/
|
||||
async addBalance(memberId: number, amount: number): Promise<void> {
|
||||
await this.memberRepository.increment({ member_id: memberId }, 'balance', amount);
|
||||
await this.memberRepository.increment(
|
||||
{ member_id: memberId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扣除余额
|
||||
*/
|
||||
async deductBalance(memberId: number, amount: number): Promise<void> {
|
||||
await this.memberRepository.decrement({ member_id: memberId }, 'balance', amount);
|
||||
await this.memberRepository.decrement(
|
||||
{ member_id: memberId },
|
||||
'balance',
|
||||
amount,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,12 +227,15 @@ export class CoreMemberService {
|
||||
/**
|
||||
* 检查用户名是否已存在
|
||||
*/
|
||||
async isUsernameExists(username: string, excludeId?: number): Promise<boolean> {
|
||||
async isUsernameExists(
|
||||
username: string,
|
||||
excludeId?: number,
|
||||
): Promise<boolean> {
|
||||
const where: any = { username, is_del: 0 };
|
||||
if (excludeId) {
|
||||
where.member_id = Not(excludeId);
|
||||
}
|
||||
|
||||
|
||||
const count = await this.memberRepository.count({ where });
|
||||
return count > 0;
|
||||
}
|
||||
@@ -242,7 +248,7 @@ export class CoreMemberService {
|
||||
if (excludeId) {
|
||||
where.member_id = Not(excludeId);
|
||||
}
|
||||
|
||||
|
||||
const count = await this.memberRepository.count({ where });
|
||||
return count > 0;
|
||||
}
|
||||
@@ -255,7 +261,7 @@ export class CoreMemberService {
|
||||
if (excludeId) {
|
||||
where.member_id = Not(excludeId);
|
||||
}
|
||||
|
||||
|
||||
const count = await this.memberRepository.count({ where });
|
||||
return count > 0;
|
||||
}
|
||||
@@ -270,16 +276,16 @@ export class CoreMemberService {
|
||||
}
|
||||
|
||||
const totalMembers = await this.memberRepository.count({ where });
|
||||
const activeMembers = await this.memberRepository.count({
|
||||
where: { ...where, status: 1 }
|
||||
const activeMembers = await this.memberRepository.count({
|
||||
where: { ...where, status: 1 },
|
||||
});
|
||||
|
||||
|
||||
const todayNewMembers = await this.memberRepository.count({
|
||||
where: {
|
||||
...where,
|
||||
create_time: Between(
|
||||
Math.floor(new Date().setHours(0, 0, 0, 0) / 1000),
|
||||
Math.floor(new Date().setHours(23, 59, 59, 999) / 1000)
|
||||
Math.floor(new Date().setHours(23, 59, 59, 999) / 1000),
|
||||
),
|
||||
},
|
||||
});
|
||||
@@ -291,4 +297,4 @@ export class CoreMemberService {
|
||||
inactive: totalMembers - activeMembers,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../../auth/guards/JwtAuthGuard';
|
||||
import { NoticeAdminService } from '../../services/admin/NoticeAdminService';
|
||||
import type { INotificationConfig } from '../../interfaces/notification.interface';
|
||||
|
||||
@ApiTags('后台通知管理')
|
||||
@Controller('adminapi/notice')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class NoticeController {
|
||||
constructor(private readonly noticeAdminService: NoticeAdminService) {}
|
||||
|
||||
@Get('list')
|
||||
@ApiOperation({ summary: '获取通知配置列表' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getNoticeList(
|
||||
@Query('site_id') site_id: number,
|
||||
@Query('keys') keys: string[] = [],
|
||||
) {
|
||||
return await this.noticeAdminService.getNoticeList(site_id, keys);
|
||||
}
|
||||
|
||||
@Get('info/:key')
|
||||
@ApiOperation({ summary: '获取通知配置详情' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getNoticeInfo(
|
||||
@Param('key') key: string,
|
||||
@Query('site_id') site_id: number,
|
||||
) {
|
||||
return await this.noticeAdminService.getNoticeInfo(site_id, key);
|
||||
}
|
||||
|
||||
@Post('save')
|
||||
@ApiOperation({ summary: '保存通知配置' })
|
||||
@ApiResponse({ status: 200, description: '保存成功' })
|
||||
async saveNoticeConfig(@Body() config: INotificationConfig) {
|
||||
return await this.noticeAdminService.saveNoticeConfig(config);
|
||||
}
|
||||
|
||||
@Put('batch-update')
|
||||
@ApiOperation({ summary: '批量更新通知配置' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
async batchUpdateNoticeConfig(
|
||||
@Query('site_id') site_id: number,
|
||||
@Body() configs: INotificationConfig[],
|
||||
) {
|
||||
return await this.noticeAdminService.batchUpdateNoticeConfig(
|
||||
site_id,
|
||||
configs,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Controller, Get, Post, Body, Query, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../../auth/guards/JwtAuthGuard';
|
||||
import { SmsAdminService } from '../../services/admin/SmsAdminService';
|
||||
import type { ISmsSendParams } from '../../interfaces/sms.interface';
|
||||
|
||||
@ApiTags('后台短信管理')
|
||||
@Controller('adminapi/sms')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class SmsController {
|
||||
constructor(private readonly smsAdminService: SmsAdminService) {}
|
||||
|
||||
@Get('config')
|
||||
@ApiOperation({ summary: '获取短信配置' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getSmsConfig(@Query('site_id') site_id: number) {
|
||||
return await this.smsAdminService.getSmsConfig(site_id);
|
||||
}
|
||||
|
||||
@Post('send')
|
||||
@ApiOperation({ summary: '发送短信' })
|
||||
@ApiResponse({ status: 200, description: '发送成功' })
|
||||
async sendSms(@Body() params: ISmsSendParams) {
|
||||
return await this.smsAdminService.sendSms(params);
|
||||
}
|
||||
|
||||
@Get('logs')
|
||||
@ApiOperation({ summary: '获取短信日志列表' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getSmsLogs(
|
||||
@Query('site_id') site_id: number,
|
||||
@Query() where: any = {},
|
||||
) {
|
||||
return await this.smsAdminService.getSmsLogs(site_id, where);
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: '获取短信发送统计' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getSmsStats(@Query('site_id') site_id: number) {
|
||||
return await this.smsAdminService.getSmsStats(site_id);
|
||||
}
|
||||
}
|
||||
50
wwjcloud/src/common/notice/controllers/api/sms.controller.ts
Normal file
50
wwjcloud/src/common/notice/controllers/api/sms.controller.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Controller, Get, Post, Body, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { SmsApiService } from '../../services/api/SmsApiService';
|
||||
|
||||
@ApiTags('前台短信')
|
||||
@Controller('api/sms')
|
||||
export class SmsApiController {
|
||||
constructor(private readonly smsApiService: SmsApiService) {}
|
||||
|
||||
@Post('send-verification')
|
||||
@ApiOperation({ summary: '发送验证码' })
|
||||
@ApiResponse({ status: 200, description: '发送成功' })
|
||||
async sendVerificationCode(
|
||||
@Body() body: { mobile: string; site_id?: number },
|
||||
) {
|
||||
const { mobile, site_id = 0 } = body;
|
||||
return await this.smsApiService.sendVerificationCode(mobile, site_id);
|
||||
}
|
||||
|
||||
@Post('verify-code')
|
||||
@ApiOperation({ summary: '验证验证码' })
|
||||
@ApiResponse({ status: 200, description: '验证成功' })
|
||||
async verifyCode(
|
||||
@Body() body: { mobile: string; code: string; site_id?: number },
|
||||
) {
|
||||
const { mobile, code, site_id = 0 } = body;
|
||||
return await this.smsApiService.verifyCode(mobile, code, site_id);
|
||||
}
|
||||
|
||||
@Post('send-notification')
|
||||
@ApiOperation({ summary: '发送短信通知' })
|
||||
@ApiResponse({ status: 200, description: '发送成功' })
|
||||
async sendNotification(
|
||||
@Body()
|
||||
body: {
|
||||
mobile: string;
|
||||
template_id: string;
|
||||
params: any;
|
||||
site_id?: number;
|
||||
},
|
||||
) {
|
||||
const { mobile, template_id, params, site_id = 0 } = body;
|
||||
return await this.smsApiService.sendNotification({
|
||||
mobile,
|
||||
template_id,
|
||||
params,
|
||||
site_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
61
wwjcloud/src/common/notice/entities/NoticeLog.ts
Normal file
61
wwjcloud/src/common/notice/entities/NoticeLog.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity('sys_notice_log')
|
||||
export class NoticeLog {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
site_id: number;
|
||||
|
||||
@Column({
|
||||
name: 'key',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: '',
|
||||
})
|
||||
key: string;
|
||||
|
||||
@Column({
|
||||
name: 'notice_type',
|
||||
type: 'varchar',
|
||||
length: 50,
|
||||
nullable: true,
|
||||
default: 'sms',
|
||||
})
|
||||
notice_type: string;
|
||||
|
||||
@Column({ name: 'uid', type: 'int', default: 0 })
|
||||
uid: number;
|
||||
|
||||
@Column({ name: 'member_id', type: 'int', default: 0 })
|
||||
member_id: number;
|
||||
|
||||
@Column({ name: 'nickname', type: 'varchar', length: 255, default: '' })
|
||||
nickname: string;
|
||||
|
||||
@Column({ name: 'receiver', type: 'varchar', length: 255, default: '' })
|
||||
receiver: string;
|
||||
|
||||
@Column({ name: 'content', type: 'text', nullable: true })
|
||||
content: string;
|
||||
|
||||
@Column({ name: 'is_click', type: 'tinyint', default: 0 })
|
||||
is_click: number;
|
||||
|
||||
@Column({ name: 'is_visit', type: 'tinyint', default: 0 })
|
||||
is_visit: number;
|
||||
|
||||
@Column({ name: 'visit_time', type: 'int', default: 0 })
|
||||
visit_time: number;
|
||||
|
||||
@Column({ name: 'create_time', type: 'int', default: 0 })
|
||||
create_time: number;
|
||||
|
||||
@Column({ name: 'result', type: 'varchar', length: 1000, default: '' })
|
||||
result: string;
|
||||
|
||||
@Column({ name: 'params', type: 'text', nullable: true })
|
||||
params: any;
|
||||
}
|
||||
48
wwjcloud/src/common/notice/entities/Notification.ts
Normal file
48
wwjcloud/src/common/notice/entities/Notification.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
|
||||
@Entity('sys_notice')
|
||||
export class Notification extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'key', type: 'varchar', length: 50, default: '' })
|
||||
key: string;
|
||||
|
||||
@Column({ name: 'sms_content', type: 'text', nullable: true })
|
||||
sms_content: string;
|
||||
|
||||
@Column({ name: 'is_wechat', type: 'tinyint', default: 0 })
|
||||
is_wechat: number;
|
||||
|
||||
@Column({ name: 'is_weapp', type: 'tinyint', default: 0 })
|
||||
is_weapp: number;
|
||||
|
||||
@Column({ name: 'is_sms', type: 'tinyint', default: 0 })
|
||||
is_sms: number;
|
||||
|
||||
@Column({
|
||||
name: 'wechat_template_id',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
default: '',
|
||||
})
|
||||
wechat_template_id: string;
|
||||
|
||||
@Column({
|
||||
name: 'weapp_template_id',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
default: '',
|
||||
})
|
||||
weapp_template_id: string;
|
||||
|
||||
@Column({ name: 'sms_id', type: 'varchar', length: 255, default: '' })
|
||||
sms_id: string;
|
||||
|
||||
@Column({ name: 'wechat_first', type: 'varchar', length: 255, default: '' })
|
||||
wechat_first: string;
|
||||
|
||||
@Column({ name: 'wechat_remark', type: 'varchar', length: 255, default: '' })
|
||||
wechat_remark: string;
|
||||
}
|
||||
35
wwjcloud/src/common/notice/entities/SmsLog.ts
Normal file
35
wwjcloud/src/common/notice/entities/SmsLog.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
|
||||
@Entity('sys_notice_sms_log')
|
||||
export class SmsLog extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'mobile', type: 'varchar', length: 11, default: '' })
|
||||
mobile: string;
|
||||
|
||||
@Column({ name: 'sms_type', type: 'varchar', length: 32, default: '' })
|
||||
sms_type: string;
|
||||
|
||||
@Column({ name: 'key', type: 'varchar', length: 32, default: '' })
|
||||
key: string;
|
||||
|
||||
@Column({ name: 'template_id', type: 'varchar', length: 50, default: '' })
|
||||
template_id: string;
|
||||
|
||||
@Column({ name: 'content', type: 'text' })
|
||||
content: string;
|
||||
|
||||
@Column({ name: 'params', type: 'text' })
|
||||
params: any;
|
||||
|
||||
@Column({ name: 'status', type: 'varchar', length: 32, default: 'sending' })
|
||||
status: string;
|
||||
|
||||
@Column({ name: 'result', type: 'text', nullable: true })
|
||||
result: string;
|
||||
|
||||
@Column({ name: 'send_time', type: 'int', default: 0 })
|
||||
send_time: number;
|
||||
}
|
||||
7
wwjcloud/src/common/notice/enums/channel-type.enum.ts
Normal file
7
wwjcloud/src/common/notice/enums/channel-type.enum.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export enum ChannelType {
|
||||
H5 = 'h5', // H5端
|
||||
PC = 'pc', // PC端
|
||||
APP = 'app', // 移动应用
|
||||
MINI_PROGRAM = 'mini_program', // 小程序
|
||||
WECHAT_OFFICIAL = 'wechat_official', // 微信公众号
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum NotificationType {
|
||||
SMS = 'sms', // 短信通知
|
||||
WECHAT = 'wechat', // 微信通知
|
||||
WEAPP = 'weapp', // 小程序通知
|
||||
EMAIL = 'email', // 邮件通知
|
||||
SYSTEM = 'system', // 系统通知
|
||||
}
|
||||
13
wwjcloud/src/common/notice/enums/status.enum.ts
Normal file
13
wwjcloud/src/common/notice/enums/status.enum.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export enum NotificationStatus {
|
||||
PENDING = 0, // 待发送
|
||||
SENDING = 1, // 发送中
|
||||
SUCCESS = 2, // 发送成功
|
||||
FAILED = 3, // 发送失败
|
||||
CANCELLED = 4, // 已取消
|
||||
}
|
||||
|
||||
export enum SmsStatus {
|
||||
SENDING = 'sending', // 发送中
|
||||
SUCCESS = 'success', // 发送成功
|
||||
FAILED = 'fail', // 发送失败
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NotificationType } from '../enums/notification-type.enum';
|
||||
|
||||
export interface INotification {
|
||||
id: number;
|
||||
site_id: number;
|
||||
key: string;
|
||||
sms_content: string;
|
||||
is_wechat: number;
|
||||
is_weapp: number;
|
||||
is_sms: number;
|
||||
wechat_template_id: string;
|
||||
weapp_template_id: string;
|
||||
sms_id: string;
|
||||
wechat_first: string;
|
||||
wechat_remark: string;
|
||||
create_time: number;
|
||||
}
|
||||
|
||||
export interface INotificationConfig {
|
||||
site_id: number;
|
||||
key: string;
|
||||
type: NotificationType;
|
||||
template_id: string;
|
||||
content: string;
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface INotificationResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: any;
|
||||
error?: string;
|
||||
}
|
||||
31
wwjcloud/src/common/notice/interfaces/sms.interface.ts
Normal file
31
wwjcloud/src/common/notice/interfaces/sms.interface.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { SmsStatus } from '../enums/status.enum';
|
||||
|
||||
export interface ISmsLog {
|
||||
id: number;
|
||||
site_id: number;
|
||||
mobile: string;
|
||||
sms_type: string;
|
||||
key: string;
|
||||
content: string;
|
||||
template_id: string;
|
||||
params: any;
|
||||
status: SmsStatus;
|
||||
result: string;
|
||||
create_time: number;
|
||||
}
|
||||
|
||||
export interface ISmsConfig {
|
||||
site_id: number;
|
||||
sms_type: string;
|
||||
api_key: string;
|
||||
api_secret: string;
|
||||
sign_name: string;
|
||||
template_code: string;
|
||||
}
|
||||
|
||||
export interface ISmsSendParams {
|
||||
mobile: string;
|
||||
template_id: string;
|
||||
params: Record<string, any>;
|
||||
site_id?: number;
|
||||
}
|
||||
60
wwjcloud/src/common/notice/notice.module.ts
Normal file
60
wwjcloud/src/common/notice/notice.module.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Notification } from './entities/Notification';
|
||||
import { SmsLog } from './entities/SmsLog';
|
||||
import { NoticeLog } from './entities/NoticeLog';
|
||||
|
||||
// Core Services
|
||||
import { CoreNoticeService } from './services/core/CoreNoticeService';
|
||||
import { CoreSmsService } from './services/core/CoreSmsService';
|
||||
|
||||
// Admin Services
|
||||
import { NoticeAdminService } from './services/admin/NoticeAdminService';
|
||||
import { SmsAdminService } from './services/admin/SmsAdminService';
|
||||
|
||||
// API Services
|
||||
import { SmsApiService } from './services/api/SmsApiService';
|
||||
|
||||
// Admin Controllers
|
||||
import { NoticeController } from './controllers/adminapi/NoticeController';
|
||||
import { SmsController } from './controllers/adminapi/SmsController';
|
||||
|
||||
// API Controllers
|
||||
import { SmsApiController } from './controllers/api/sms.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Notification, SmsLog, NoticeLog])],
|
||||
providers: [
|
||||
// Core Services
|
||||
CoreNoticeService,
|
||||
CoreSmsService,
|
||||
|
||||
// Admin Services
|
||||
NoticeAdminService,
|
||||
SmsAdminService,
|
||||
|
||||
// API Services
|
||||
SmsApiService,
|
||||
],
|
||||
controllers: [
|
||||
// Admin Controllers
|
||||
NoticeController,
|
||||
SmsController,
|
||||
|
||||
// API Controllers
|
||||
SmsApiController,
|
||||
],
|
||||
exports: [
|
||||
// Core Services
|
||||
CoreNoticeService,
|
||||
CoreSmsService,
|
||||
|
||||
// Admin Services
|
||||
NoticeAdminService,
|
||||
SmsAdminService,
|
||||
|
||||
// API Services
|
||||
SmsApiService,
|
||||
],
|
||||
})
|
||||
export class NoticeModule {}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CoreNoticeService } from '../core/CoreNoticeService';
|
||||
import {
|
||||
INotification,
|
||||
INotificationConfig,
|
||||
} from '../../interfaces/notification.interface';
|
||||
|
||||
@Injectable()
|
||||
export class NoticeAdminService {
|
||||
constructor(private readonly coreNoticeService: CoreNoticeService) {}
|
||||
|
||||
/**
|
||||
* 获取通知配置列表
|
||||
*/
|
||||
async getNoticeList(
|
||||
site_id: number,
|
||||
keys: string[] = [],
|
||||
): Promise<INotification[]> {
|
||||
return await this.coreNoticeService.getList(site_id, keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知配置详情
|
||||
*/
|
||||
async getNoticeInfo(
|
||||
site_id: number,
|
||||
key: string,
|
||||
): Promise<INotification | null> {
|
||||
return await this.coreNoticeService.getConfig(site_id, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存通知配置
|
||||
*/
|
||||
async saveNoticeConfig(config: INotificationConfig): Promise<any> {
|
||||
return await this.coreNoticeService.saveConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新通知配置
|
||||
*/
|
||||
async batchUpdateNoticeConfig(
|
||||
site_id: number,
|
||||
configs: INotificationConfig[],
|
||||
): Promise<any> {
|
||||
const results = [];
|
||||
for (const config of configs) {
|
||||
const result = await this.coreNoticeService.saveConfig(config);
|
||||
results.push(result);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
57
wwjcloud/src/common/notice/services/admin/SmsAdminService.ts
Normal file
57
wwjcloud/src/common/notice/services/admin/SmsAdminService.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CoreSmsService } from '../core/CoreSmsService';
|
||||
import { ISmsConfig, ISmsSendParams } from '../../interfaces/sms.interface';
|
||||
import { SmsStatus } from '../../enums/status.enum';
|
||||
|
||||
@Injectable()
|
||||
export class SmsAdminService {
|
||||
constructor(private readonly coreSmsService: CoreSmsService) {}
|
||||
|
||||
/**
|
||||
* 获取短信配置
|
||||
*/
|
||||
async getSmsConfig(site_id: number): Promise<ISmsConfig> {
|
||||
return await this.coreSmsService.getDefaultSmsConfig(site_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*/
|
||||
async sendSms(params: ISmsSendParams): Promise<boolean> {
|
||||
const { mobile, template_id, params: smsParams, site_id = 0 } = params;
|
||||
return await this.coreSmsService.send(
|
||||
site_id,
|
||||
mobile,
|
||||
smsParams,
|
||||
'admin',
|
||||
template_id,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信日志列表
|
||||
*/
|
||||
async getSmsLogs(site_id: number, where: any = {}): Promise<any[]> {
|
||||
return await this.coreSmsService.getSmsLogs(site_id, where);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信发送统计
|
||||
*/
|
||||
async getSmsStats(site_id: number): Promise<any> {
|
||||
const logs = await this.coreSmsService.getSmsLogs(site_id);
|
||||
const total = logs.length;
|
||||
const success = logs.filter(
|
||||
(log) => log.status === SmsStatus.SUCCESS,
|
||||
).length;
|
||||
const failed = logs.filter((log) => log.status === SmsStatus.FAILED).length;
|
||||
|
||||
return {
|
||||
total,
|
||||
success,
|
||||
failed,
|
||||
success_rate: total > 0 ? ((success / total) * 100).toFixed(2) : '0.00',
|
||||
};
|
||||
}
|
||||
}
|
||||
67
wwjcloud/src/common/notice/services/api/SmsApiService.ts
Normal file
67
wwjcloud/src/common/notice/services/api/SmsApiService.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CoreSmsService } from '../core/CoreSmsService';
|
||||
import { ISmsSendParams } from '../../interfaces/sms.interface';
|
||||
|
||||
@Injectable()
|
||||
export class SmsApiService {
|
||||
constructor(private readonly coreSmsService: CoreSmsService) {}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
async sendVerificationCode(
|
||||
mobile: string,
|
||||
site_id: number = 0,
|
||||
): Promise<boolean> {
|
||||
const params = {
|
||||
mobile,
|
||||
template_id: 'verification_code',
|
||||
params: { code: this.generateCode() },
|
||||
site_id,
|
||||
};
|
||||
|
||||
return await this.coreSmsService.send(
|
||||
site_id,
|
||||
mobile,
|
||||
params.params,
|
||||
'verification',
|
||||
params.template_id,
|
||||
'验证码',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信通知
|
||||
*/
|
||||
async sendNotification(params: ISmsSendParams): Promise<boolean> {
|
||||
const { mobile, template_id, params: smsParams, site_id = 0 } = params;
|
||||
return await this.coreSmsService.send(
|
||||
site_id,
|
||||
mobile,
|
||||
smsParams,
|
||||
'notification',
|
||||
template_id,
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
private generateCode(): string {
|
||||
return Math.random().toString().slice(2, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*/
|
||||
async verifyCode(
|
||||
mobile: string,
|
||||
code: string,
|
||||
site_id: number = 0,
|
||||
): Promise<boolean> {
|
||||
// TODO: 实现验证码验证逻辑
|
||||
// 这里应该从缓存或数据库验证验证码
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService } from '@wwjCore/base/BaseService';
|
||||
import { Notification } from '../../entities/Notification';
|
||||
import {
|
||||
INotification,
|
||||
INotificationConfig,
|
||||
INotificationResult,
|
||||
} from '../../interfaces/notification.interface';
|
||||
|
||||
@Injectable()
|
||||
export class CoreNoticeService extends BaseService<Notification> {
|
||||
constructor(
|
||||
@InjectRepository(Notification)
|
||||
private readonly notificationRepository: Repository<Notification>,
|
||||
) {
|
||||
super(notificationRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知配置列表
|
||||
*/
|
||||
async getList(
|
||||
site_id: number,
|
||||
keys: string[] = [],
|
||||
): Promise<INotification[]> {
|
||||
const where: any = { site_id };
|
||||
if (keys.length > 0) {
|
||||
where.key = keys; // TypeORM会自动处理数组查询
|
||||
}
|
||||
|
||||
return await this.findMany(where);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通知配置
|
||||
*/
|
||||
async getConfig(site_id: number, key: string): Promise<INotification | null> {
|
||||
return await this.findOneBy({ site_id, key });
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新通知配置
|
||||
*/
|
||||
async saveConfig(config: INotificationConfig): Promise<INotificationResult> {
|
||||
try {
|
||||
const existing = await this.getConfig(config.site_id, config.key);
|
||||
|
||||
if (existing) {
|
||||
// 更新现有配置
|
||||
await this.update(existing.id, {
|
||||
[config.type === 'sms'
|
||||
? 'is_sms'
|
||||
: config.type === 'wechat'
|
||||
? 'is_wechat'
|
||||
: 'is_weapp']: 1,
|
||||
[config.type === 'sms'
|
||||
? 'sms_id'
|
||||
: config.type === 'wechat'
|
||||
? 'wechat_template_id'
|
||||
: 'weapp_template_id']: config.template_id,
|
||||
});
|
||||
} else {
|
||||
// 创建新配置
|
||||
await this.create({
|
||||
site_id: config.site_id,
|
||||
key: config.key,
|
||||
[config.type === 'sms'
|
||||
? 'is_sms'
|
||||
: config.type === 'wechat'
|
||||
? 'is_wechat'
|
||||
: 'is_weapp']: 1,
|
||||
[config.type === 'sms'
|
||||
? 'sms_id'
|
||||
: config.type === 'wechat'
|
||||
? 'wechat_template_id'
|
||||
: 'weapp_template_id']: config.template_id,
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, message: '配置保存成功' };
|
||||
} catch (error) {
|
||||
return { success: false, message: '配置保存失败', error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
90
wwjcloud/src/common/notice/services/core/CoreSmsService.ts
Normal file
90
wwjcloud/src/common/notice/services/core/CoreSmsService.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService } from '@wwjCore/base/BaseService';
|
||||
import { SmsLog } from '../../entities/SmsLog';
|
||||
import { ISmsConfig, ISmsSendParams } from '../../interfaces/sms.interface';
|
||||
import { SmsStatus } from '../../enums/status.enum';
|
||||
|
||||
@Injectable()
|
||||
export class CoreSmsService extends BaseService<SmsLog> {
|
||||
constructor(
|
||||
@InjectRepository(SmsLog)
|
||||
private readonly smsLogRepository: Repository<SmsLog>,
|
||||
) {
|
||||
super(smsLogRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信
|
||||
*/
|
||||
async send(
|
||||
site_id: number,
|
||||
mobile: string,
|
||||
params: any,
|
||||
key: string,
|
||||
template_id: string,
|
||||
content: string,
|
||||
): Promise<boolean> {
|
||||
let savedLog: SmsLog | null = null;
|
||||
|
||||
try {
|
||||
// 创建短信日志
|
||||
savedLog = await this.create({
|
||||
site_id,
|
||||
mobile,
|
||||
sms_type: 'default',
|
||||
key,
|
||||
content,
|
||||
template_id,
|
||||
params: JSON.stringify(params),
|
||||
status: SmsStatus.SENDING,
|
||||
send_time: this.getCurrentTimestamp(),
|
||||
});
|
||||
|
||||
// TODO: 这里应该调用实际的短信服务商API
|
||||
// 目前先模拟发送成功
|
||||
const success = true;
|
||||
|
||||
// 更新日志状态
|
||||
await this.update(savedLog.id, {
|
||||
status: success ? SmsStatus.SUCCESS : SmsStatus.FAILED,
|
||||
result: success ? '发送成功' : '发送失败',
|
||||
});
|
||||
|
||||
return success;
|
||||
} catch (error) {
|
||||
// 记录失败日志
|
||||
if (savedLog?.id) {
|
||||
await this.update(savedLog.id, {
|
||||
status: SmsStatus.FAILED,
|
||||
result: error.message,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信配置
|
||||
*/
|
||||
async getDefaultSmsConfig(site_id: number): Promise<ISmsConfig> {
|
||||
// TODO: 从配置服务获取短信配置
|
||||
return {
|
||||
site_id,
|
||||
sms_type: 'default',
|
||||
api_key: '',
|
||||
api_secret: '',
|
||||
sign_name: '',
|
||||
template_code: '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信日志列表
|
||||
*/
|
||||
async getSmsLogs(site_id: number, where: any = {}): Promise<SmsLog[]> {
|
||||
const query = { site_id, ...where };
|
||||
return await this.findMany(query);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { NotificationService } from './notification.service';
|
||||
import { EmailModule, SmsModule } from '../settings';
|
||||
|
||||
@Module({
|
||||
imports: [EmailModule, SmsModule],
|
||||
providers: [NotificationService],
|
||||
exports: [NotificationService],
|
||||
})
|
||||
export class NotificationModule {}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EmailService, SmsService } from '../settings';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationService {
|
||||
constructor(
|
||||
private readonly emailService: EmailService,
|
||||
private readonly smsService: SmsService,
|
||||
) {}
|
||||
|
||||
async sendEmail(to: string, subject: string, content: string) {
|
||||
return this.emailService.send(to, subject, content);
|
||||
}
|
||||
|
||||
async sendSms(
|
||||
to: string,
|
||||
templateId: string,
|
||||
params: Record<string, any> = {},
|
||||
) {
|
||||
return this.smsService.send(to, templateId, params);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,29 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
HttpCode,
|
||||
HttpStatus
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { MenuAdminService } from '../../services/admin/MenuAdminService';
|
||||
import { CreateMenuDto, UpdateMenuDto, QueryMenuDto, BatchUpdateStatusDto } from '../../dto/admin/MenuDto';
|
||||
import {
|
||||
CreateMenuDto,
|
||||
UpdateMenuDto,
|
||||
QueryMenuDto,
|
||||
BatchUpdateMenuStatusDto,
|
||||
} from '../../dto/admin/MenuDto';
|
||||
import { JwtAuthGuard } from '../../../auth/guards/JwtAuthGuard';
|
||||
import { RolesGuard } from '../../../auth/guards/RolesGuard';
|
||||
import { Roles } from '../../../auth/decorators/RolesDecorator';
|
||||
@@ -66,7 +76,7 @@ export class MenuController {
|
||||
@Roles('admin')
|
||||
async updateMenu(
|
||||
@Param('id') id: string,
|
||||
@Body() updateMenuDto: UpdateMenuDto
|
||||
@Body() updateMenuDto: UpdateMenuDto,
|
||||
) {
|
||||
return await this.menuAdminService.updateMenu(Number(id), updateMenuDto);
|
||||
}
|
||||
@@ -99,17 +109,23 @@ export class MenuController {
|
||||
@Roles('admin')
|
||||
async updateMenuStatus(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { status: number }
|
||||
@Body() body: { status: number },
|
||||
) {
|
||||
return await this.menuAdminService.updateMenuStatus(Number(id), body.status);
|
||||
return await this.menuAdminService.updateMenuStatus(
|
||||
Number(id),
|
||||
body.status,
|
||||
);
|
||||
}
|
||||
|
||||
@Put('batch/status')
|
||||
@ApiOperation({ summary: '批量更新菜单状态' })
|
||||
@ApiResponse({ status: 200, description: '批量更新菜单状态成功' })
|
||||
@Roles('admin')
|
||||
async batchUpdateMenuStatus(@Body() body: BatchUpdateStatusDto) {
|
||||
return await this.menuAdminService.batchUpdateMenuStatus(body.menuIds, body.status);
|
||||
async batchUpdateMenuStatus(@Body() body: BatchUpdateMenuStatusDto) {
|
||||
return await this.menuAdminService.batchUpdateMenuStatus(
|
||||
body.menuIds,
|
||||
body.status,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('stats/overview')
|
||||
@@ -127,4 +143,4 @@ export class MenuController {
|
||||
async exportMenus() {
|
||||
return await this.menuAdminService.exportMenus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
HttpCode,
|
||||
HttpStatus
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { RoleAdminService } from '../../services/admin/RoleAdminService';
|
||||
import { CreateRoleDto, UpdateRoleDto, QueryRoleDto, BatchUpdateStatusDto, AssignMenusDto } from '../../dto/admin/RoleDto';
|
||||
import {
|
||||
CreateRoleDto,
|
||||
UpdateRoleDto,
|
||||
QueryRoleDto,
|
||||
BatchUpdateRoleStatusDto,
|
||||
AssignMenusDto,
|
||||
} from '../../dto/admin/RoleDto';
|
||||
import { JwtAuthGuard } from '../../../auth/guards/JwtAuthGuard';
|
||||
import { RolesGuard } from '../../../auth/guards/RolesGuard';
|
||||
import { Roles } from '../../../auth/decorators/RolesDecorator';
|
||||
@@ -59,7 +70,7 @@ export class RoleController {
|
||||
@Roles('admin')
|
||||
async updateRole(
|
||||
@Param('id') id: string,
|
||||
@Body() updateRoleDto: UpdateRoleDto
|
||||
@Body() updateRoleDto: UpdateRoleDto,
|
||||
) {
|
||||
return await this.roleAdminService.updateRole(Number(id), updateRoleDto);
|
||||
}
|
||||
@@ -90,17 +101,23 @@ export class RoleController {
|
||||
@Roles('admin')
|
||||
async updateRoleStatus(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { status: number }
|
||||
@Body() body: { status: number },
|
||||
) {
|
||||
return await this.roleAdminService.updateRoleStatus(Number(id), body.status);
|
||||
return await this.roleAdminService.updateRoleStatus(
|
||||
Number(id),
|
||||
body.status,
|
||||
);
|
||||
}
|
||||
|
||||
@Put('batch/status')
|
||||
@ApiOperation({ summary: '批量更新角色状态' })
|
||||
@ApiResponse({ status: 200, description: '批量更新角色状态成功' })
|
||||
@Roles('admin')
|
||||
async batchUpdateRoleStatus(@Body() body: BatchUpdateStatusDto) {
|
||||
return await this.roleAdminService.batchUpdateRoleStatus(body.roleIds, body.status);
|
||||
async batchUpdateRoleStatus(@Body() body: BatchUpdateRoleStatusDto) {
|
||||
return await this.roleAdminService.batchUpdateRoleStatus(
|
||||
body.roleIds,
|
||||
body.status,
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':id/menus')
|
||||
@@ -110,9 +127,12 @@ export class RoleController {
|
||||
@Roles('admin')
|
||||
async assignMenus(
|
||||
@Param('id') id: string,
|
||||
@Body() assignMenusDto: AssignMenusDto
|
||||
@Body() assignMenusDto: AssignMenusDto,
|
||||
) {
|
||||
return await this.roleAdminService.assignMenus(Number(id), assignMenusDto.menuIds);
|
||||
return await this.roleAdminService.assignMenus(
|
||||
Number(id),
|
||||
assignMenusDto.menuIds,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('stats/overview')
|
||||
@@ -130,4 +150,4 @@ export class RoleController {
|
||||
async exportRoles(@Body() query: any) {
|
||||
return await this.roleAdminService.exportRoles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { IsString, IsNumber, IsOptional, IsArray, Min, Max } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
// 创建菜单DTO
|
||||
@@ -25,7 +32,10 @@ export class CreateMenuDto {
|
||||
@IsString()
|
||||
parent_key?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '菜单类型 0目录 1菜单 2按钮', default: 1 })
|
||||
@ApiPropertyOptional({
|
||||
description: '菜单类型 0目录 1菜单 2按钮',
|
||||
default: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@@ -243,7 +253,7 @@ export class QueryMenuDto {
|
||||
}
|
||||
|
||||
// 批量更新状态DTO
|
||||
export class BatchUpdateStatusDto {
|
||||
export class BatchUpdateMenuStatusDto {
|
||||
@ApiProperty({ description: '菜单ID列表', type: [Number] })
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
@@ -259,4 +269,4 @@ export class BatchUpdateStatusDto {
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
status: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNumber, IsOptional, IsArray, IsIn, MinLength, MaxLength } from 'class-validator';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsIn,
|
||||
MinLength,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateRoleDto {
|
||||
@ApiProperty({ description: '角色名称', example: '超级管理员' })
|
||||
@@ -8,7 +16,11 @@ export class CreateRoleDto {
|
||||
@MaxLength(50)
|
||||
roleName: string;
|
||||
|
||||
@ApiProperty({ description: '角色描述', example: '系统超级管理员,拥有所有权限', required: false })
|
||||
@ApiProperty({
|
||||
description: '角色描述',
|
||||
example: '系统超级管理员,拥有所有权限',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
@@ -19,7 +31,11 @@ export class CreateRoleDto {
|
||||
@IsIn([0, 1])
|
||||
status: number;
|
||||
|
||||
@ApiProperty({ description: '应用类型', example: 'admin', enum: ['admin', 'api'] })
|
||||
@ApiProperty({
|
||||
description: '应用类型',
|
||||
example: 'admin',
|
||||
enum: ['admin', 'api'],
|
||||
})
|
||||
@IsString()
|
||||
@IsIn(['admin', 'api'])
|
||||
appType: string;
|
||||
@@ -31,20 +47,33 @@ export class CreateRoleDto {
|
||||
}
|
||||
|
||||
export class UpdateRoleDto {
|
||||
@ApiProperty({ description: '角色名称', example: '超级管理员', required: false })
|
||||
@ApiProperty({
|
||||
description: '角色名称',
|
||||
example: '超级管理员',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(50)
|
||||
roleName?: string;
|
||||
|
||||
@ApiProperty({ description: '角色描述', example: '系统超级管理员,拥有所有权限', required: false })
|
||||
@ApiProperty({
|
||||
description: '角色描述',
|
||||
example: '系统超级管理员,拥有所有权限',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(200)
|
||||
roleDesc?: string;
|
||||
|
||||
@ApiProperty({ description: '角色状态', example: 1, enum: [0, 1], required: false })
|
||||
@ApiProperty({
|
||||
description: '角色状态',
|
||||
example: 1,
|
||||
enum: [0, 1],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@IsIn([0, 1])
|
||||
@@ -67,25 +96,39 @@ export class QueryRoleDto {
|
||||
@IsNumber()
|
||||
limit?: number;
|
||||
|
||||
@ApiProperty({ description: '关键词搜索', example: '管理员', required: false })
|
||||
@ApiProperty({
|
||||
description: '关键词搜索',
|
||||
example: '管理员',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
|
||||
@ApiProperty({ description: '角色状态', example: 1, enum: [0, 1], required: false })
|
||||
@ApiProperty({
|
||||
description: '角色状态',
|
||||
example: 1,
|
||||
enum: [0, 1],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@IsIn([0, 1])
|
||||
status?: number;
|
||||
|
||||
@ApiProperty({ description: '应用类型', example: 'admin', enum: ['admin', 'api'], required: false })
|
||||
@ApiProperty({
|
||||
description: '应用类型',
|
||||
example: 'admin',
|
||||
enum: ['admin', 'api'],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(['admin', 'api'])
|
||||
appType?: string;
|
||||
}
|
||||
|
||||
export class BatchUpdateStatusDto {
|
||||
export class BatchUpdateRoleStatusDto {
|
||||
@ApiProperty({ description: '角色ID列表', example: [1, 2, 3] })
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
@@ -102,4 +145,4 @@ export class AssignMenusDto {
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
menuIds: number[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
|
||||
@Entity('sys_menu')
|
||||
export class SysMenu {
|
||||
export class SysMenu extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'id' })
|
||||
id: number;
|
||||
|
||||
@@ -47,18 +48,6 @@ export class SysMenu {
|
||||
@Column({ name: 'is_show', type: 'tinyint', default: 1 })
|
||||
is_show: number;
|
||||
|
||||
@Column({ name: 'is_del', type: 'tinyint', default: 0 })
|
||||
is_del: number;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time', type: 'int' })
|
||||
create_time: number;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time', type: 'int' })
|
||||
update_time: number;
|
||||
|
||||
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||
delete_time: number;
|
||||
|
||||
@Column({ name: 'addon', type: 'varchar', length: 255, default: '' })
|
||||
addon: string;
|
||||
|
||||
@@ -68,7 +57,12 @@ export class SysMenu {
|
||||
@Column({ name: 'menu_attr', type: 'varchar', length: 50, default: '' })
|
||||
menu_attr: string;
|
||||
|
||||
@Column({ name: 'parent_select_key', type: 'varchar', length: 255, default: '' })
|
||||
@Column({
|
||||
name: 'parent_select_key',
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
default: '',
|
||||
})
|
||||
parent_select_key: string;
|
||||
|
||||
// 业务逻辑方法 - 与 PHP 项目保持一致
|
||||
@@ -78,11 +72,15 @@ export class SysMenu {
|
||||
}
|
||||
|
||||
getMenuTypeText(): string {
|
||||
const menuTypes: { [key: number]: string } = { 0: '目录', 1: '菜单', 2: '按钮' };
|
||||
const menuTypes: { [key: number]: string } = {
|
||||
0: '目录',
|
||||
1: '菜单',
|
||||
2: '按钮',
|
||||
};
|
||||
return menuTypes[this.menu_type] || '未知';
|
||||
}
|
||||
|
||||
getMenuShortNameText(): string {
|
||||
return this.menu_short_name || this.menu_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { BaseEntity } from '@wwj/core/base';
|
||||
|
||||
@Entity('sys_role')
|
||||
export class SysRole {
|
||||
export class SysRole extends BaseEntity {
|
||||
@PrimaryGeneratedColumn({ name: 'role_id' })
|
||||
role_id: number;
|
||||
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
site_id: number;
|
||||
|
||||
@Column({ name: 'role_name', type: 'varchar', length: 255, default: '' })
|
||||
role_name: string;
|
||||
|
||||
@@ -17,15 +15,6 @@ export class SysRole {
|
||||
@Column({ name: 'status', type: 'tinyint', default: 1 })
|
||||
status: number;
|
||||
|
||||
@Column({ name: 'is_del', type: 'tinyint', default: 0 })
|
||||
is_del: number;
|
||||
|
||||
@CreateDateColumn({ name: 'create_time', type: 'int' })
|
||||
create_time: number;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_time', type: 'int' })
|
||||
update_time: number;
|
||||
|
||||
// 业务逻辑方法 - 与 PHP 项目保持一致
|
||||
getStatusText(): string {
|
||||
const statusMap: { [key: number]: string } = { 0: '禁用', 1: '正常' };
|
||||
@@ -45,4 +34,4 @@ export class SysRole {
|
||||
setRulesArray(value: string[]): void {
|
||||
this.rules = JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { SysRole } from './entities/SysRole';
|
||||
import { SysMenu } from './entities/SysMenu';
|
||||
|
||||
@@ -17,29 +18,27 @@ import { MenuController } from './controllers/adminapi/MenuController';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => AuthModule),
|
||||
TypeOrmModule.forFeature([SysRole, SysMenu]),
|
||||
],
|
||||
providers: [
|
||||
// Core Services
|
||||
CoreRoleService,
|
||||
CoreMenuService,
|
||||
|
||||
|
||||
// Admin Services
|
||||
RoleAdminService,
|
||||
MenuAdminService,
|
||||
],
|
||||
controllers: [
|
||||
RoleController,
|
||||
MenuController,
|
||||
],
|
||||
controllers: [RoleController, MenuController],
|
||||
exports: [
|
||||
// Core Services
|
||||
CoreRoleService,
|
||||
CoreMenuService,
|
||||
|
||||
|
||||
// Admin Services
|
||||
RoleAdminService,
|
||||
MenuAdminService,
|
||||
],
|
||||
})
|
||||
export class RbacModule {}
|
||||
export class RbacModule {}
|
||||
|
||||
@@ -3,7 +3,12 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { SysMenu } from '../../entities/SysMenu';
|
||||
import { CoreMenuService } from '../core/CoreMenuService';
|
||||
import { CreateMenuDto, UpdateMenuDto, QueryMenuDto, BatchUpdateStatusDto } from '../../dto/admin/MenuDto';
|
||||
import {
|
||||
CreateMenuDto,
|
||||
UpdateMenuDto,
|
||||
QueryMenuDto,
|
||||
BatchUpdateMenuStatusDto,
|
||||
} from '../../dto/admin/MenuDto';
|
||||
|
||||
@Injectable()
|
||||
export class MenuAdminService {
|
||||
@@ -13,9 +18,11 @@ export class MenuAdminService {
|
||||
private coreMenuService: CoreMenuService,
|
||||
) {}
|
||||
|
||||
async getMenuList(query: QueryMenuDto): Promise<{ list: SysMenu[]; total: number }> {
|
||||
async getMenuList(
|
||||
query: QueryMenuDto,
|
||||
): Promise<{ list: SysMenu[]; total: number }> {
|
||||
const { page = 1, limit = 10, app_type, menu_name, status } = query;
|
||||
|
||||
|
||||
const queryBuilder = this.sysMenuRepository
|
||||
.createQueryBuilder('sys_menu')
|
||||
.where('sys_menu.delete_time = :delete_time', { delete_time: 0 });
|
||||
@@ -25,7 +32,9 @@ export class MenuAdminService {
|
||||
}
|
||||
|
||||
if (menu_name) {
|
||||
queryBuilder.andWhere('sys_menu.menu_name LIKE :menu_name', { menu_name: `%${menu_name}%` });
|
||||
queryBuilder.andWhere('sys_menu.menu_name LIKE :menu_name', {
|
||||
menu_name: `%${menu_name}%`,
|
||||
});
|
||||
}
|
||||
|
||||
if (status !== undefined) {
|
||||
@@ -45,18 +54,22 @@ export class MenuAdminService {
|
||||
const menu = await this.sysMenuRepository.findOne({
|
||||
where: { id, delete_time: 0 },
|
||||
});
|
||||
|
||||
|
||||
if (!menu) {
|
||||
throw new NotFoundException('菜单不存在');
|
||||
}
|
||||
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
async createMenu(menuData: CreateMenuDto): Promise<SysMenu> {
|
||||
// 检查菜单名称是否已存在
|
||||
const existingMenu = await this.sysMenuRepository.findOne({
|
||||
where: { menu_name: menuData.menu_name, app_type: menuData.app_type, delete_time: 0 },
|
||||
where: {
|
||||
menu_name: menuData.menu_name,
|
||||
app_type: menuData.app_type,
|
||||
delete_time: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMenu) {
|
||||
@@ -75,7 +88,7 @@ export class MenuAdminService {
|
||||
const menu = await this.sysMenuRepository.findOne({
|
||||
where: { id, delete_time: 0 },
|
||||
});
|
||||
|
||||
|
||||
if (!menu) {
|
||||
throw new NotFoundException('菜单不存在');
|
||||
}
|
||||
@@ -83,7 +96,11 @@ export class MenuAdminService {
|
||||
// 检查菜单名称是否已存在(排除自己)
|
||||
if (updateData.menu_name && updateData.menu_name !== menu.menu_name) {
|
||||
const existingMenu = await this.sysMenuRepository.findOne({
|
||||
where: { menu_name: updateData.menu_name, app_type: menu.app_type, delete_time: 0 },
|
||||
where: {
|
||||
menu_name: updateData.menu_name,
|
||||
app_type: menu.app_type,
|
||||
delete_time: 0,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingMenu && existingMenu.id !== id) {
|
||||
@@ -99,7 +116,7 @@ export class MenuAdminService {
|
||||
const menu = await this.sysMenuRepository.findOne({
|
||||
where: { id, delete_time: 0 },
|
||||
});
|
||||
|
||||
|
||||
if (!menu) {
|
||||
throw new NotFoundException('菜单不存在');
|
||||
}
|
||||
@@ -121,7 +138,7 @@ export class MenuAdminService {
|
||||
const menu = await this.sysMenuRepository.findOne({
|
||||
where: { id, delete_time: 0 },
|
||||
});
|
||||
|
||||
|
||||
if (!menu) {
|
||||
throw new NotFoundException('菜单不存在');
|
||||
}
|
||||
@@ -159,9 +176,15 @@ export class MenuAdminService {
|
||||
}
|
||||
|
||||
async getMenuStats(): Promise<any> {
|
||||
const total = await this.sysMenuRepository.count({ where: { delete_time: 0 } });
|
||||
const active = await this.sysMenuRepository.count({ where: { status: 1, delete_time: 0 } });
|
||||
const inactive = await this.sysMenuRepository.count({ where: { status: 0, delete_time: 0 } });
|
||||
const total = await this.sysMenuRepository.count({
|
||||
where: { delete_time: 0 },
|
||||
});
|
||||
const active = await this.sysMenuRepository.count({
|
||||
where: { status: 1, delete_time: 0 },
|
||||
});
|
||||
const inactive = await this.sysMenuRepository.count({
|
||||
where: { status: 0, delete_time: 0 },
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
@@ -175,21 +198,22 @@ export class MenuAdminService {
|
||||
*/
|
||||
async getMenuTree(appType: string): Promise<any[]> {
|
||||
const menus = await this.sysMenuRepository.find({
|
||||
where: { app_type: appType, is_del: 0 },
|
||||
where: { app_type: appType, delete_time: 0 },
|
||||
order: { sort: 'ASC' },
|
||||
});
|
||||
|
||||
|
||||
return this.buildMenuTree(menus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除菜单
|
||||
*/
|
||||
async batchDeleteMenus(menuIds: number[]): Promise<{ success: boolean; message: string }> {
|
||||
async batchDeleteMenus(
|
||||
menuIds: number[],
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await this.sysMenuRepository.update(menuIds, {
|
||||
is_del: 1,
|
||||
delete_time: Math.floor(Date.now() / 1000),
|
||||
delete_time: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
return { success: true, message: '批量删除成功' };
|
||||
} catch (error) {
|
||||
@@ -200,7 +224,10 @@ export class MenuAdminService {
|
||||
/**
|
||||
* 批量更新菜单状态
|
||||
*/
|
||||
async batchUpdateMenuStatus(menuIds: number[], status: number): Promise<{ success: boolean; message: string }> {
|
||||
async batchUpdateMenuStatus(
|
||||
menuIds: number[],
|
||||
status: number,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await this.sysMenuRepository.update(menuIds, { status });
|
||||
return { success: true, message: '批量更新状态成功' };
|
||||
@@ -214,7 +241,7 @@ export class MenuAdminService {
|
||||
*/
|
||||
async exportMenus(): Promise<any[]> {
|
||||
return await this.sysMenuRepository.find({
|
||||
where: { is_del: 0 },
|
||||
where: { delete_time: 0 },
|
||||
order: { sort: 'ASC' },
|
||||
});
|
||||
}
|
||||
@@ -224,7 +251,7 @@ export class MenuAdminService {
|
||||
*/
|
||||
private buildMenuTree(menus: any[], parentId: number = 0): any[] {
|
||||
const tree: any[] = [];
|
||||
|
||||
|
||||
for (const menu of menus) {
|
||||
if (menu.parent_id === parentId) {
|
||||
const children = this.buildMenuTree(menus, menu.menu_id);
|
||||
@@ -234,7 +261,7 @@ export class MenuAdminService {
|
||||
tree.push(menu);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return tree;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,20 @@ export class RoleAdminService {
|
||||
private coreRoleService: CoreRoleService,
|
||||
) {}
|
||||
|
||||
async getRoleList(query: any, site_id: number = 0): Promise<{ list: SysRole[]; total: number }> {
|
||||
async getRoleList(
|
||||
query: any,
|
||||
site_id: number = 0,
|
||||
): Promise<{ list: SysRole[]; total: number }> {
|
||||
const { page = 1, limit = 10, role_name, status } = query;
|
||||
|
||||
|
||||
const queryBuilder = this.sysRoleRepository
|
||||
.createQueryBuilder('sys_role')
|
||||
.where('sys_role.site_id = :site_id', { site_id });
|
||||
|
||||
if (role_name) {
|
||||
queryBuilder.andWhere('sys_role.role_name LIKE :role_name', { role_name: `%${role_name}%` });
|
||||
queryBuilder.andWhere('sys_role.role_name LIKE :role_name', {
|
||||
role_name: `%${role_name}%`,
|
||||
});
|
||||
}
|
||||
|
||||
if (status !== undefined) {
|
||||
@@ -40,11 +45,11 @@ export class RoleAdminService {
|
||||
const role = await this.sysRoleRepository.findOne({
|
||||
where: { role_id, site_id },
|
||||
});
|
||||
|
||||
|
||||
if (!role) {
|
||||
throw new NotFoundException('角色不存在');
|
||||
}
|
||||
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
@@ -59,7 +64,11 @@ export class RoleAdminService {
|
||||
return await this.sysRoleRepository.save(role);
|
||||
}
|
||||
|
||||
async updateRole(role_id: number, updateData: any, site_id: number = 0): Promise<SysRole> {
|
||||
async updateRole(
|
||||
role_id: number,
|
||||
updateData: any,
|
||||
site_id: number = 0,
|
||||
): Promise<SysRole> {
|
||||
const role = await this.getRoleDetail(role_id, site_id);
|
||||
|
||||
Object.assign(role, {
|
||||
@@ -76,13 +85,13 @@ export class RoleAdminService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除角色
|
||||
* 批量删除角色(物理删除,因为sys_role表无软删除字段)
|
||||
*/
|
||||
async batchDeleteRoles(roleIds: number[]): Promise<{ success: boolean; message: string }> {
|
||||
async batchDeleteRoles(
|
||||
roleIds: number[],
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await this.sysRoleRepository.update(roleIds, {
|
||||
is_del: 1,
|
||||
});
|
||||
await this.sysRoleRepository.delete(roleIds);
|
||||
return { success: true, message: '批量删除成功' };
|
||||
} catch (error) {
|
||||
return { success: false, message: '批量删除失败' };
|
||||
@@ -92,7 +101,10 @@ export class RoleAdminService {
|
||||
/**
|
||||
* 更新角色状态
|
||||
*/
|
||||
async updateRoleStatus(roleId: number, status: number): Promise<{ success: boolean; message: string }> {
|
||||
async updateRoleStatus(
|
||||
roleId: number,
|
||||
status: number,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await this.sysRoleRepository.update(roleId, { status });
|
||||
return { success: true, message: '状态更新成功' };
|
||||
@@ -104,7 +116,10 @@ export class RoleAdminService {
|
||||
/**
|
||||
* 批量更新角色状态
|
||||
*/
|
||||
async batchUpdateRoleStatus(roleIds: number[], status: number): Promise<{ success: boolean; message: string }> {
|
||||
async batchUpdateRoleStatus(
|
||||
roleIds: number[],
|
||||
status: number,
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await this.sysRoleRepository.update(roleIds, { status });
|
||||
return { success: true, message: '批量更新状态成功' };
|
||||
@@ -116,7 +131,10 @@ export class RoleAdminService {
|
||||
/**
|
||||
* 分配菜单
|
||||
*/
|
||||
async assignMenus(roleId: number, menuIds: number[]): Promise<{ success: boolean; message: string }> {
|
||||
async assignMenus(
|
||||
roleId: number,
|
||||
menuIds: number[],
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
// 这里应该实现角色菜单分配逻辑
|
||||
// 可以更新角色的 rules 字段
|
||||
@@ -127,23 +145,24 @@ export class RoleAdminService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色统计
|
||||
* 获取角色统计(sys_role表无软删除字段)
|
||||
*/
|
||||
async getRoleStats(): Promise<any> {
|
||||
const total = await this.sysRoleRepository.count({ where: { is_del: 0 } });
|
||||
const active = await this.sysRoleRepository.count({ where: { is_del: 0, status: 1 } });
|
||||
const inactive = await this.sysRoleRepository.count({ where: { is_del: 0, status: 0 } });
|
||||
|
||||
const total = await this.sysRoleRepository.count();
|
||||
const active = await this.sysRoleRepository.count({ where: { status: 1 } });
|
||||
const inactive = await this.sysRoleRepository.count({
|
||||
where: { status: 0 },
|
||||
});
|
||||
|
||||
return { total, active, inactive };
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出角色
|
||||
* 导出角色(sys_role表无软删除字段)
|
||||
*/
|
||||
async exportRoles(): Promise<any[]> {
|
||||
return await this.sysRoleRepository.find({
|
||||
where: { is_del: 0 },
|
||||
order: { create_time: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ export class CoreMenuService {
|
||||
});
|
||||
}
|
||||
|
||||
async getMenuByKey(menu_key: string, app_type: string): Promise<SysMenu | null> {
|
||||
async getMenuByKey(
|
||||
menu_key: string,
|
||||
app_type: string,
|
||||
): Promise<SysMenu | null> {
|
||||
return await this.sysMenuRepository.findOne({
|
||||
where: { menu_key, app_type, delete_time: 0 },
|
||||
});
|
||||
@@ -48,7 +51,7 @@ export class CoreMenuService {
|
||||
throw new Error('菜单不存在');
|
||||
}
|
||||
|
||||
// TypeORM 会自动处理软删除时间戳
|
||||
// TypeORM 会自动处理软删除时间戳
|
||||
await this.sysMenuRepository.save(menu);
|
||||
}
|
||||
|
||||
@@ -64,7 +67,10 @@ export class CoreMenuService {
|
||||
});
|
||||
}
|
||||
|
||||
async getMenusByAppType(app_type: string, status: number = 1): Promise<SysMenu[]> {
|
||||
async getMenusByAppType(
|
||||
app_type: string,
|
||||
status: number = 1,
|
||||
): Promise<SysMenu[]> {
|
||||
return await this.sysMenuRepository.find({
|
||||
where: { app_type, status, delete_time: 0 },
|
||||
order: { sort: 'DESC' },
|
||||
@@ -73,7 +79,7 @@ export class CoreMenuService {
|
||||
|
||||
async buildMenuTree(menus: SysMenu[], parent_key = ''): Promise<any[]> {
|
||||
const tree = [];
|
||||
|
||||
|
||||
for (const menu of menus) {
|
||||
if (menu.parent_key === parent_key) {
|
||||
const children = await this.buildMenuTree(menus, menu.menu_key);
|
||||
@@ -84,13 +90,17 @@ export class CoreMenuService {
|
||||
tree.push(menuItem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
async isMenuKeyExists(menu_key: string, app_type: string, excludeId?: number): Promise<boolean> {
|
||||
async isMenuKeyExists(
|
||||
menu_key: string,
|
||||
app_type: string,
|
||||
excludeId?: number,
|
||||
): Promise<boolean> {
|
||||
const where: any = { menu_key, app_type, delete_time: 0 };
|
||||
|
||||
|
||||
if (excludeId) {
|
||||
where.id = { $ne: excludeId };
|
||||
}
|
||||
@@ -106,8 +116,12 @@ export class CoreMenuService {
|
||||
}
|
||||
|
||||
const total = await this.sysMenuRepository.count({ where });
|
||||
const active = await this.sysMenuRepository.count({ where: { ...where, status: 1 } });
|
||||
const inactive = await this.sysMenuRepository.count({ where: { ...where, status: 0 } });
|
||||
const active = await this.sysMenuRepository.count({
|
||||
where: { ...where, status: 1 },
|
||||
});
|
||||
const inactive = await this.sysMenuRepository.count({
|
||||
where: { ...where, status: 0 },
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
@@ -115,4 +129,4 @@ export class CoreMenuService {
|
||||
inactive,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,48 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BaseService } from '@wwjCore/base/BaseService';
|
||||
import { SysRole } from '../../entities/SysRole';
|
||||
// 使用原生 Date 对象替代时间工具函数
|
||||
|
||||
@Injectable()
|
||||
export class CoreRoleService {
|
||||
export class CoreRoleService extends BaseService<SysRole> {
|
||||
constructor(
|
||||
@InjectRepository(SysRole)
|
||||
private sysRoleRepository: Repository<SysRole>,
|
||||
) {}
|
||||
) {
|
||||
super(sysRoleRepository);
|
||||
}
|
||||
|
||||
async createRole(roleData: Partial<SysRole>): Promise<SysRole> {
|
||||
const role = this.sysRoleRepository.create({
|
||||
...roleData,
|
||||
// TypeORM 会自动处理时间戳
|
||||
});
|
||||
|
||||
return await this.sysRoleRepository.save(role);
|
||||
return await this.create(roleData);
|
||||
}
|
||||
|
||||
async getRoleById(role_id: number): Promise<SysRole | null> {
|
||||
return await this.sysRoleRepository.findOne({
|
||||
where: { role_id },
|
||||
});
|
||||
return await this.findOne(role_id);
|
||||
}
|
||||
|
||||
async getRoleByName(role_name: string, site_id: number): Promise<SysRole | null> {
|
||||
return await this.sysRoleRepository.findOne({
|
||||
where: { role_name, site_id },
|
||||
});
|
||||
async getRoleByName(
|
||||
role_name: string,
|
||||
site_id: number,
|
||||
): Promise<SysRole | null> {
|
||||
return await this.findOneBy({ role_name, site_id });
|
||||
}
|
||||
|
||||
async updateRole(role_id: number, updateData: Partial<SysRole>): Promise<SysRole> {
|
||||
async updateRole(
|
||||
role_id: number,
|
||||
updateData: Partial<SysRole>,
|
||||
): Promise<SysRole> {
|
||||
const role = await this.getRoleById(role_id);
|
||||
if (!role) {
|
||||
throw new NotFoundException('角色不存在');
|
||||
}
|
||||
|
||||
Object.assign(role, {
|
||||
...updateData,
|
||||
// TypeORM 会自动处理时间戳
|
||||
});
|
||||
|
||||
return await this.sysRoleRepository.save(role);
|
||||
await this.update(role_id, updateData);
|
||||
const updatedRole = await this.getRoleById(role_id);
|
||||
if (!updatedRole) {
|
||||
throw new NotFoundException('角色更新后不存在');
|
||||
}
|
||||
return updatedRole;
|
||||
}
|
||||
|
||||
async deleteRole(role_id: number): Promise<void> {
|
||||
@@ -52,26 +51,24 @@ export class CoreRoleService {
|
||||
throw new NotFoundException('角色不存在');
|
||||
}
|
||||
|
||||
await this.sysRoleRepository.remove(role);
|
||||
await this.delete(role_id);
|
||||
}
|
||||
|
||||
async getRolesByAppType(site_id: number): Promise<SysRole[]> {
|
||||
return await this.sysRoleRepository.find({
|
||||
where: { site_id },
|
||||
order: { create_time: 'DESC' },
|
||||
});
|
||||
return await this.findMany({ site_id });
|
||||
}
|
||||
|
||||
async getActiveRolesByAppType(site_id: number): Promise<SysRole[]> {
|
||||
return await this.sysRoleRepository.find({
|
||||
where: { site_id, status: 1 },
|
||||
order: { create_time: 'DESC' },
|
||||
});
|
||||
return await this.findMany({ site_id, status: 1 });
|
||||
}
|
||||
|
||||
async isRoleNameExists(role_name: string, site_id: number, exclude_role_id?: number): Promise<boolean> {
|
||||
async isRoleNameExists(
|
||||
role_name: string,
|
||||
site_id: number,
|
||||
exclude_role_id?: number,
|
||||
): Promise<boolean> {
|
||||
const where: any = { role_name, site_id };
|
||||
|
||||
|
||||
if (exclude_role_id) {
|
||||
where.role_id = { $ne: exclude_role_id };
|
||||
}
|
||||
@@ -81,9 +78,9 @@ export class CoreRoleService {
|
||||
}
|
||||
|
||||
async getRoleStats(site_id: number): Promise<any> {
|
||||
const total = await this.sysRoleRepository.count({ where: { site_id } });
|
||||
const active = await this.sysRoleRepository.count({ where: { site_id, status: 1 } });
|
||||
const inactive = await this.sysRoleRepository.count({ where: { site_id, status: 0 } });
|
||||
const total = await this.count({ site_id });
|
||||
const active = await this.count({ site_id, status: 1 });
|
||||
const inactive = await this.count({ site_id, status: 0 });
|
||||
|
||||
return {
|
||||
total,
|
||||
@@ -91,4 +88,4 @@ export class CoreRoleService {
|
||||
inactive,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../../auth/guards/JwtAuthGuard';
|
||||
import { ScheduleAdminService } from '../../services/admin/ScheduleAdminService';
|
||||
|
||||
@ApiTags('后台定时任务管理')
|
||||
@Controller('adminapi/schedule')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class ScheduleController {
|
||||
constructor(private readonly scheduleAdminService: ScheduleAdminService) {}
|
||||
|
||||
@Get('list')
|
||||
@ApiOperation({ summary: '获取定时任务列表' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getScheduleList(@Query() params: any) {
|
||||
return await this.scheduleAdminService.getScheduleList(params);
|
||||
}
|
||||
|
||||
@Get('detail/:id')
|
||||
@ApiOperation({ summary: '获取定时任务详情' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getScheduleDetail(@Param('id') id: number) {
|
||||
return await this.scheduleAdminService.getScheduleDetail(id);
|
||||
}
|
||||
|
||||
@Post('create')
|
||||
@ApiOperation({ summary: '创建定时任务' })
|
||||
@ApiResponse({ status: 200, description: '创建成功' })
|
||||
async createSchedule(@Body() params: any) {
|
||||
return await this.scheduleAdminService.createSchedule(params);
|
||||
}
|
||||
|
||||
@Put('update/:id')
|
||||
@ApiOperation({ summary: '更新定时任务' })
|
||||
@ApiResponse({ status: 200, description: '更新成功' })
|
||||
async updateSchedule(@Param('id') id: number, @Body() params: any) {
|
||||
return await this.scheduleAdminService.updateSchedule(id, params);
|
||||
}
|
||||
|
||||
@Delete('delete/:id')
|
||||
@ApiOperation({ summary: '删除定时任务' })
|
||||
@ApiResponse({ status: 200, description: '删除成功' })
|
||||
async deleteSchedule(@Param('id') id: number) {
|
||||
return await this.scheduleAdminService.deleteSchedule(id);
|
||||
}
|
||||
|
||||
@Post('enable/:id')
|
||||
@ApiOperation({ summary: '启用定时任务' })
|
||||
@ApiResponse({ status: 200, description: '启用成功' })
|
||||
async enableSchedule(@Param('id') id: number) {
|
||||
return await this.scheduleAdminService.enableSchedule(id);
|
||||
}
|
||||
|
||||
@Post('disable/:id')
|
||||
@ApiOperation({ summary: '禁用定时任务' })
|
||||
@ApiResponse({ status: 200, description: '禁用成功' })
|
||||
async disableSchedule(@Param('id') id: number) {
|
||||
return await this.scheduleAdminService.disableSchedule(id);
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: '获取任务统计信息' })
|
||||
@ApiResponse({ status: 200, description: '获取成功' })
|
||||
async getScheduleStats(@Query('site_id') site_id: number) {
|
||||
return await this.scheduleAdminService.getScheduleStats(site_id);
|
||||
}
|
||||
}
|
||||
65
wwjcloud/src/common/schedule/entities/Schedule.ts
Normal file
65
wwjcloud/src/common/schedule/entities/Schedule.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
import { ScheduleStatus } from '../enums/schedule-status.enum';
|
||||
|
||||
/**
|
||||
* 定时任务实体
|
||||
* 对应数据库表: sys_schedule
|
||||
*/
|
||||
@Entity('sys_schedule')
|
||||
export class Schedule extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'addon', type: 'varchar', length: 255, default: '' })
|
||||
addon: string;
|
||||
|
||||
@Column({ name: 'key', type: 'varchar', length: 255, default: '' })
|
||||
key: string;
|
||||
|
||||
@Column({ name: 'status', type: 'int', default: 1 })
|
||||
status: ScheduleStatus;
|
||||
|
||||
@Column({ name: 'time', type: 'varchar', length: 500, default: '' })
|
||||
time: string;
|
||||
|
||||
@Column({ name: 'count', type: 'int', default: 0 })
|
||||
count: number;
|
||||
|
||||
@Column({ name: 'last_time', type: 'int', default: 0 })
|
||||
last_time: number;
|
||||
|
||||
@Column({ name: 'next_time', type: 'int', default: 0 })
|
||||
next_time: number;
|
||||
|
||||
@Column({ name: 'sort', type: 'int', default: 0 })
|
||||
sort: number;
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
getStatusText(): string {
|
||||
const statusMap = {
|
||||
[ScheduleStatus.DISABLED]: '禁用',
|
||||
[ScheduleStatus.ENABLED]: '启用',
|
||||
[ScheduleStatus.RUNNING]: '执行中',
|
||||
[ScheduleStatus.ERROR]: '错误',
|
||||
[ScheduleStatus.PAUSED]: '暂停',
|
||||
};
|
||||
return statusMap[this.status] || '未知';
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查任务是否启用
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return this.status === ScheduleStatus.ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查任务是否正在执行
|
||||
*/
|
||||
isRunning(): boolean {
|
||||
return this.status === ScheduleStatus.RUNNING;
|
||||
}
|
||||
}
|
||||
36
wwjcloud/src/common/schedule/entities/ScheduleLog.ts
Normal file
36
wwjcloud/src/common/schedule/entities/ScheduleLog.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { BaseEntity } from '@wwjCore/base/BaseEntity';
|
||||
import { ExecuteStatus } from '../enums/schedule-status.enum';
|
||||
|
||||
@Entity('sys_schedule_log')
|
||||
export class ScheduleLog extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'schedule_id', type: 'int', default: 0 })
|
||||
schedule_id: number;
|
||||
|
||||
@Column({ name: 'addon', type: 'varchar', length: 255, default: '' })
|
||||
addon: string;
|
||||
|
||||
@Column({ name: 'key', type: 'varchar', length: 255, default: '' })
|
||||
key: string;
|
||||
|
||||
@Column({ name: 'name', type: 'varchar', length: 50, default: '' })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'execute_time', type: 'int' })
|
||||
execute_time: number;
|
||||
|
||||
@Column({ name: 'execute_result', type: 'text', nullable: true })
|
||||
execute_result: string;
|
||||
|
||||
@Column({ name: 'status', type: 'varchar', length: 255, default: '' })
|
||||
status: ExecuteStatus;
|
||||
|
||||
@Column({ name: 'class', type: 'varchar', length: 255, default: '' })
|
||||
class: string;
|
||||
|
||||
@Column({ name: 'job', type: 'varchar', length: 255, default: '' })
|
||||
job: string;
|
||||
}
|
||||
16
wwjcloud/src/common/schedule/enums/schedule-status.enum.ts
Normal file
16
wwjcloud/src/common/schedule/enums/schedule-status.enum.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export enum ScheduleStatus {
|
||||
DISABLED = 0, // 禁用
|
||||
ENABLED = 1, // 启用
|
||||
RUNNING = 2, // 执行中
|
||||
ERROR = 3, // 错误状态
|
||||
PAUSED = 4, // 暂停
|
||||
}
|
||||
|
||||
export enum ExecuteStatus {
|
||||
PENDING = 'pending', // 待执行
|
||||
RUNNING = 'running', // 执行中
|
||||
SUCCESS = 'success', // 执行成功
|
||||
FAILED = 'failed', // 执行失败
|
||||
TIMEOUT = 'timeout', // 执行超时
|
||||
CANCELLED = 'cancelled', // 已取消
|
||||
}
|
||||
41
wwjcloud/src/common/schedule/schedule.module.ts
Normal file
41
wwjcloud/src/common/schedule/schedule.module.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
// 实体
|
||||
import { Schedule } from './entities/Schedule';
|
||||
import { ScheduleLog } from './entities/ScheduleLog';
|
||||
|
||||
// 核心服务
|
||||
import { CoreScheduleService } from './services/core/CoreScheduleService';
|
||||
|
||||
// 管理服务
|
||||
import { ScheduleAdminService } from './services/admin/ScheduleAdminService';
|
||||
|
||||
// 控制器
|
||||
import { ScheduleController } from './controllers/adminapi/ScheduleController';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => AuthModule),
|
||||
// 数据库实体
|
||||
TypeOrmModule.forFeature([Schedule, ScheduleLog]),
|
||||
],
|
||||
providers: [
|
||||
// 核心服务
|
||||
CoreScheduleService,
|
||||
// 管理服务
|
||||
ScheduleAdminService,
|
||||
],
|
||||
controllers: [
|
||||
// 管理控制器
|
||||
ScheduleController,
|
||||
],
|
||||
exports: [
|
||||
// 核心服务
|
||||
CoreScheduleService,
|
||||
// 管理服务
|
||||
ScheduleAdminService,
|
||||
],
|
||||
})
|
||||
export class ScheduleModule {}
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CoreScheduleService } from '../core/CoreScheduleService';
|
||||
import { ScheduleStatus } from '../../enums/schedule-status.enum';
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleAdminService {
|
||||
constructor(private readonly coreScheduleService: CoreScheduleService) {}
|
||||
|
||||
/**
|
||||
* 获取定时任务列表
|
||||
*/
|
||||
async getScheduleList(params: any): Promise<any> {
|
||||
return await this.coreScheduleService.getScheduleList(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定时任务详情
|
||||
*/
|
||||
async getScheduleDetail(id: number): Promise<any> {
|
||||
return await this.coreScheduleService.getScheduleById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建定时任务
|
||||
*/
|
||||
async createSchedule(params: any): Promise<any> {
|
||||
try {
|
||||
const schedule = await this.coreScheduleService.createSchedule(params);
|
||||
return {
|
||||
success: true,
|
||||
message: '定时任务创建成功',
|
||||
data: schedule,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务创建失败',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定时任务
|
||||
*/
|
||||
async updateSchedule(id: number, params: any): Promise<any> {
|
||||
try {
|
||||
const success = await this.coreScheduleService.updateSchedule(id, params);
|
||||
if (success) {
|
||||
return {
|
||||
success: true,
|
||||
message: '定时任务更新成功',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务更新失败',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务更新失败',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除定时任务
|
||||
*/
|
||||
async deleteSchedule(id: number): Promise<any> {
|
||||
try {
|
||||
const success = await this.coreScheduleService.deleteSchedule(id);
|
||||
if (success) {
|
||||
return {
|
||||
success: true,
|
||||
message: '定时任务删除成功',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务删除失败',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务删除失败',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用定时任务
|
||||
*/
|
||||
async enableSchedule(id: number): Promise<any> {
|
||||
try {
|
||||
const success = await this.coreScheduleService.toggleScheduleStatus(
|
||||
id,
|
||||
ScheduleStatus.ENABLED,
|
||||
);
|
||||
if (success) {
|
||||
return {
|
||||
success: true,
|
||||
message: '定时任务启用成功',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务启用失败',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务启用失败',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用定时任务
|
||||
*/
|
||||
async disableSchedule(id: number): Promise<any> {
|
||||
try {
|
||||
const success = await this.coreScheduleService.toggleScheduleStatus(
|
||||
id,
|
||||
ScheduleStatus.DISABLED,
|
||||
);
|
||||
if (success) {
|
||||
return {
|
||||
success: true,
|
||||
message: '定时任务禁用成功',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务禁用失败',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: '定时任务禁用失败',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务统计信息
|
||||
*/
|
||||
async getScheduleStats(site_id: number): Promise<any> {
|
||||
try {
|
||||
const stats = await this.coreScheduleService.getScheduleStats(site_id);
|
||||
return {
|
||||
success: true,
|
||||
data: stats,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: '获取统计信息失败',
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, FindOptionsWhere } from 'typeorm';
|
||||
import { BaseService } from '@wwjCore/base/BaseService';
|
||||
import { Schedule } from '../../entities/Schedule';
|
||||
import { ScheduleLog } from '../../entities/ScheduleLog';
|
||||
import {
|
||||
ScheduleStatus,
|
||||
ExecuteStatus,
|
||||
} from '../../enums/schedule-status.enum';
|
||||
import { TimeUtils } from '@wwjCore/utils/time.utils';
|
||||
|
||||
@Injectable()
|
||||
export class CoreScheduleService extends BaseService<Schedule> {
|
||||
constructor(
|
||||
@InjectRepository(Schedule)
|
||||
private readonly scheduleRepository: Repository<Schedule>,
|
||||
@InjectRepository(ScheduleLog)
|
||||
private readonly scheduleLogRepository: Repository<ScheduleLog>,
|
||||
) {
|
||||
super(scheduleRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取定时任务列表
|
||||
*/
|
||||
async getScheduleList(
|
||||
params: any,
|
||||
): Promise<{ list: Schedule[]; total: number }> {
|
||||
const { site_id, addon, key, status, page = 1, limit = 20 } = params;
|
||||
|
||||
const where: FindOptionsWhere<Schedule> = { site_id };
|
||||
if (addon) where.addon = addon;
|
||||
if (key) where.key = key;
|
||||
if (status !== undefined) where.status = status;
|
||||
|
||||
return await this.findAndCount({
|
||||
where,
|
||||
page,
|
||||
limit,
|
||||
order: { sort: 'ASC', create_time: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取定时任务
|
||||
*/
|
||||
async getScheduleById(id: number): Promise<Schedule | null> {
|
||||
return await this.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建定时任务
|
||||
*/
|
||||
async createSchedule(params: any): Promise<Schedule> {
|
||||
const scheduleData = {
|
||||
...params,
|
||||
count: 0,
|
||||
last_time: 0,
|
||||
next_time: TimeUtils.getNextHourTimestamp(1),
|
||||
sort: params.sort || 0,
|
||||
};
|
||||
|
||||
return await this.create(scheduleData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新定时任务
|
||||
*/
|
||||
async updateSchedule(id: number, params: any): Promise<boolean> {
|
||||
return await this.update(id, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除定时任务(软删除)
|
||||
*/
|
||||
async deleteSchedule(id: number): Promise<boolean> {
|
||||
return await this.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用定时任务
|
||||
*/
|
||||
async toggleScheduleStatus(
|
||||
id: number,
|
||||
status: ScheduleStatus,
|
||||
): Promise<boolean> {
|
||||
return await this.update(id, { status });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的定时任务列表
|
||||
*/
|
||||
async getEnabledSchedules(site_id: number): Promise<Schedule[]> {
|
||||
return await this.findMany({
|
||||
site_id,
|
||||
status: ScheduleStatus.ENABLED,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务统计信息
|
||||
*/
|
||||
async getScheduleStats(site_id: number): Promise<any> {
|
||||
const total = await this.count({ site_id });
|
||||
const enabled = await this.count({
|
||||
site_id,
|
||||
status: ScheduleStatus.ENABLED,
|
||||
});
|
||||
const disabled = await this.count({
|
||||
site_id,
|
||||
status: ScheduleStatus.DISABLED,
|
||||
});
|
||||
|
||||
return {
|
||||
total,
|
||||
enabled,
|
||||
disabled,
|
||||
enabled_rate: total > 0 ? ((enabled / total) * 100).toFixed(2) : '0.00',
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user