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:
万物街
2025-08-27 11:24:22 +08:00
parent be07b9ffec
commit 1cd5d3bdef
696 changed files with 36708 additions and 16868 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
: '';
}
}
}

View File

@@ -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')
: '';
}
}
}

View File

@@ -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] || '未知';
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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('权限不足');
}
}
}

View File

@@ -7,4 +7,4 @@ export interface User {
export interface RequestWithUser extends Request {
user: User;
}
}

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

View File

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

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

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

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

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

View 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, // 已拉黑
}

View File

@@ -0,0 +1,8 @@
export enum ChannelType {
SMS = 'sms', // 短信渠道
WECHAT = 'wechat', // 微信公众号渠道
WEAPP = 'weapp', // 微信小程序渠道
EMAIL = 'email', // 邮件渠道
SYSTEM = 'system', // 站内系统渠道
PUSH = 'push', // 推送通知渠道
}

View File

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

View File

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

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

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

View File

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

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

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

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

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

View 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": "排序"
}
}

View 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}格式不正确"
}
}

View 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": "钻石会员"
}
}

View 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": "微信配置"
}
}

View 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": "自定义"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 });
// 可以添加其他相关数据的删除逻辑
}
}
}

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

@@ -0,0 +1,7 @@
export enum ChannelType {
H5 = 'h5', // H5端
PC = 'pc', // PC端
APP = 'app', // 移动应用
MINI_PROGRAM = 'mini_program', // 小程序
WECHAT_OFFICIAL = 'wechat_official', // 微信公众号
}

View File

@@ -0,0 +1,7 @@
export enum NotificationType {
SMS = 'sms', // 短信通知
WECHAT = 'wechat', // 微信通知
WEAPP = 'weapp', // 小程序通知
EMAIL = 'email', // 邮件通知
SYSTEM = 'system', // 系统通知
}

View 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', // 发送失败
}

View File

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

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

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View 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', // 已取消
}

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

View File

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

View File

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