feat: 初始化 WWJ Cloud 企业级框架项目
- 后端:基于 NestJS 的分层架构设计 - 前端:基于 VbenAdmin + Element Plus 的管理系统 - 支持 SaaS + 独立版双架构模式 - 完整的用户权限管理系统 - 系统设置、文件上传、通知等核心功能 - 多租户支持和插件化扩展架构
This commit is contained in:
112
wwjcloud/src/common/member/dto/create-member.dto.ts
Normal file
112
wwjcloud/src/common/member/dto/create-member.dto.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional, IsInt, IsEmail, IsIn, Length, IsPhoneNumber } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class CreateMemberDto {
|
||||
@ApiPropertyOptional({ description: '会员编码' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
memberNo?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '推广会员ID' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
pid?: number;
|
||||
|
||||
@ApiProperty({ description: '站点ID' })
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
siteId: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '会员用户名' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(1, 255)
|
||||
username?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '手机号' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(11, 11)
|
||||
mobile?: string;
|
||||
|
||||
@ApiProperty({ description: '会员密码' })
|
||||
@IsString()
|
||||
@Length(6, 255)
|
||||
password: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '会员昵称' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(1, 255)
|
||||
nickname?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '会员头像' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
headimg?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '会员等级' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
memberLevel?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '会员标签' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
memberLabel?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '微信用户openid' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
wxOpenid?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '微信小程序openid' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
weappOpenid?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '微信unionid' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
wxUnionid?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '支付宝账户id' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
aliOpenid?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '抖音小程序openid' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
douyinOpenid?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '注册类型' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
regType?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '生日' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
birthday?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '性别:1男 2女 0保密' })
|
||||
@IsOptional()
|
||||
@IsIn([0, 1, 2])
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
sex?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '邮箱' })
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态:1正常 0禁用', default: 1 })
|
||||
@IsOptional()
|
||||
@IsIn([0, 1])
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
status?: number;
|
||||
}
|
||||
3
wwjcloud/src/common/member/dto/index.ts
Normal file
3
wwjcloud/src/common/member/dto/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { CreateMemberDto } from './create-member.dto';
|
||||
export { UpdateMemberDto } from './update-member.dto';
|
||||
export { QueryMemberDto } from './query-member.dto';
|
||||
63
wwjcloud/src/common/member/dto/query-member.dto.ts
Normal file
63
wwjcloud/src/common/member/dto/query-member.dto.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsOptional, IsString, IsInt, IsIn } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class QueryMemberDto {
|
||||
@ApiPropertyOptional({ description: '页码', default: 1 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value) || 1)
|
||||
page?: number = 1;
|
||||
|
||||
@ApiPropertyOptional({ description: '每页数量', default: 10 })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value) || 10)
|
||||
limit?: number = 10;
|
||||
|
||||
@ApiPropertyOptional({ description: '关键词搜索(用户名/昵称/手机号)' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
keyword?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '站点ID' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
siteId?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '会员等级' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
memberLevel?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '性别:1男 2女 0保密' })
|
||||
@IsOptional()
|
||||
@IsIn([0, 1, 2])
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
sex?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '状态:1正常 0禁用' })
|
||||
@IsOptional()
|
||||
@IsIn([0, 1])
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
status?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '注册类型' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
regType?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '开始时间(时间戳)' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
startTime?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '结束时间(时间戳)' })
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
endTime?: number;
|
||||
}
|
||||
4
wwjcloud/src/common/member/dto/update-member.dto.ts
Normal file
4
wwjcloud/src/common/member/dto/update-member.dto.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateMemberDto } from './create-member.dto';
|
||||
|
||||
export class UpdateMemberDto extends PartialType(CreateMemberDto) {}
|
||||
113
wwjcloud/src/common/member/entities/member.entity.ts
Normal file
113
wwjcloud/src/common/member/entities/member.entity.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
@Entity('member')
|
||||
export class Member {
|
||||
@ApiProperty({ description: '会员ID' })
|
||||
@PrimaryGeneratedColumn({ name: 'member_id', type: 'int', unsigned: true })
|
||||
memberId: number;
|
||||
|
||||
@ApiProperty({ description: '会员编码' })
|
||||
@Column({ name: 'member_no', type: 'varchar', length: 255, default: '' })
|
||||
memberNo: string;
|
||||
|
||||
@ApiProperty({ description: '推广会员ID' })
|
||||
@Column({ name: 'pid', type: 'int', default: 0 })
|
||||
pid: number;
|
||||
|
||||
@ApiProperty({ description: '站点ID' })
|
||||
@Column({ name: 'site_id', type: 'int', default: 0 })
|
||||
siteId: number;
|
||||
|
||||
@ApiProperty({ description: '会员用户名' })
|
||||
@Column({ name: 'username', type: 'varchar', length: 255, default: '' })
|
||||
username: string;
|
||||
|
||||
@ApiProperty({ description: '手机号' })
|
||||
@Column({ name: 'mobile', type: 'varchar', length: 20, default: '' })
|
||||
mobile: string;
|
||||
|
||||
@ApiProperty({ description: '会员密码' })
|
||||
@Column({ name: 'password', type: 'varchar', length: 255, default: '' })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ description: '会员昵称' })
|
||||
@Column({ name: 'nickname', type: 'varchar', length: 255, default: '' })
|
||||
nickname: string;
|
||||
|
||||
@ApiProperty({ description: '会员头像' })
|
||||
@Column({ name: 'headimg', type: 'varchar', length: 1000, default: '' })
|
||||
headimg: string;
|
||||
|
||||
@ApiProperty({ description: '会员等级' })
|
||||
@Column({ name: 'member_level', type: 'int', default: 0 })
|
||||
memberLevel: number;
|
||||
|
||||
@ApiProperty({ description: '会员标签' })
|
||||
@Column({ name: 'member_label', type: 'varchar', length: 255, default: '' })
|
||||
memberLabel: string;
|
||||
|
||||
@ApiProperty({ description: '微信用户openid' })
|
||||
@Column({ name: 'wx_openid', type: 'varchar', length: 255, default: '' })
|
||||
wxOpenid: string;
|
||||
|
||||
@ApiProperty({ description: '微信小程序openid' })
|
||||
@Column({ name: 'weapp_openid', type: 'varchar', length: 255, default: '' })
|
||||
weappOpenid: string;
|
||||
|
||||
@ApiProperty({ description: '微信unionid' })
|
||||
@Column({ name: 'wx_unionid', type: 'varchar', length: 255, default: '' })
|
||||
wxUnionid: string;
|
||||
|
||||
@ApiProperty({ description: '支付宝账户id' })
|
||||
@Column({ name: 'ali_openid', type: 'varchar', length: 255, default: '' })
|
||||
aliOpenid: string;
|
||||
|
||||
@ApiProperty({ description: '抖音小程序openid' })
|
||||
@Column({ name: 'douyin_openid', type: 'varchar', length: 255, default: '' })
|
||||
douyinOpenid: string;
|
||||
|
||||
@ApiProperty({ description: '注册时间' })
|
||||
@Column({ name: 'reg_time', type: 'int', default: 0 })
|
||||
regTime: number;
|
||||
|
||||
@ApiProperty({ description: '注册类型' })
|
||||
@Column({ name: 'reg_type', type: 'varchar', length: 255, default: '' })
|
||||
regType: string;
|
||||
|
||||
@ApiProperty({ description: '生日' })
|
||||
@Column({ name: 'birthday', type: 'varchar', length: 255, default: '' })
|
||||
birthday: string;
|
||||
|
||||
@ApiProperty({ description: '性别:1男 2女 0保密' })
|
||||
@Column({ name: 'sex', type: 'tinyint', default: 0 })
|
||||
sex: number;
|
||||
|
||||
@ApiProperty({ description: '邮箱' })
|
||||
@Column({ name: 'email', type: 'varchar', length: 255, default: '' })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ description: '状态:1正常 0禁用' })
|
||||
@Column({ name: 'status', type: 'tinyint', default: 1 })
|
||||
status: number;
|
||||
|
||||
@ApiProperty({ description: '最后登录时间' })
|
||||
@Column({ name: 'last_visit_time', type: 'int', default: 0 })
|
||||
lastVisitTime: number;
|
||||
|
||||
@ApiProperty({ description: '最后登录IP' })
|
||||
@Column({ name: 'last_visit_ip', type: 'varchar', length: 255, default: '' })
|
||||
lastVisitIp: string;
|
||||
|
||||
@ApiProperty({ description: '删除时间' })
|
||||
@Column({ name: 'delete_time', type: 'int', default: 0 })
|
||||
deleteTime: number;
|
||||
|
||||
@ApiProperty({ description: '创建时间' })
|
||||
@CreateDateColumn({ name: 'create_time', type: 'int' })
|
||||
createTime: number;
|
||||
|
||||
@ApiProperty({ description: '更新时间' })
|
||||
@UpdateDateColumn({ name: 'update_time', type: 'int' })
|
||||
updateTime: number;
|
||||
}
|
||||
5
wwjcloud/src/common/member/index.ts
Normal file
5
wwjcloud/src/common/member/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export { MemberModule } from './member.module';
|
||||
export { MemberService } from './member.service';
|
||||
export { MemberController } from './member.controller';
|
||||
export { Member } from './entities/member.entity';
|
||||
export * from './dto';
|
||||
142
wwjcloud/src/common/member/member.controller.ts
Normal file
142
wwjcloud/src/common/member/member.controller.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { MemberService } from './member.service';
|
||||
import { CreateMemberDto, UpdateMemberDto, QueryMemberDto } from './dto';
|
||||
import { Member } from './entities/member.entity';
|
||||
import { Request } from 'express';
|
||||
|
||||
@ApiTags('会员管理')
|
||||
@Controller('member')
|
||||
export class MemberController {
|
||||
constructor(private readonly memberService: MemberService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '创建会员' })
|
||||
@ApiResponse({ status: HttpStatus.CREATED, description: '创建成功', type: Member })
|
||||
@ApiResponse({ status: HttpStatus.CONFLICT, description: '用户名或手机号已存在' })
|
||||
async create(@Body() createMemberDto: CreateMemberDto) {
|
||||
const member = await this.memberService.create(createMemberDto);
|
||||
return {
|
||||
code: 200,
|
||||
message: '创建成功',
|
||||
data: member,
|
||||
};
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取会员列表' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '获取成功' })
|
||||
async findAll(@Query() queryDto: QueryMemberDto) {
|
||||
const result = await this.memberService.findAll(queryDto);
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: result,
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '获取会员详情' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '获取成功', type: Member })
|
||||
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '会员不存在' })
|
||||
async findOne(@Param('id', ParseIntPipe) id: number) {
|
||||
const member = await this.memberService.findOne(id);
|
||||
return {
|
||||
code: 200,
|
||||
message: '获取成功',
|
||||
data: member,
|
||||
};
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: '更新会员信息' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '更新成功', type: Member })
|
||||
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '会员不存在' })
|
||||
@ApiResponse({ status: HttpStatus.CONFLICT, description: '用户名或手机号已存在' })
|
||||
async update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateMemberDto: UpdateMemberDto,
|
||||
) {
|
||||
const member = await this.memberService.update(id, updateMemberDto);
|
||||
return {
|
||||
code: 200,
|
||||
message: '更新成功',
|
||||
data: member,
|
||||
};
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: '删除会员' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '删除成功' })
|
||||
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: '会员不存在' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
await this.memberService.remove(id);
|
||||
return {
|
||||
code: 200,
|
||||
message: '删除成功',
|
||||
};
|
||||
}
|
||||
|
||||
@Post('batch-delete')
|
||||
@ApiOperation({ summary: '批量删除会员' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '批量删除成功' })
|
||||
async batchRemove(@Body('ids') ids: number[]) {
|
||||
await this.memberService.batchRemove(ids);
|
||||
return {
|
||||
code: 200,
|
||||
message: '批量删除成功',
|
||||
};
|
||||
}
|
||||
|
||||
@Post(':id/update-last-visit')
|
||||
@ApiOperation({ summary: '更新最后登录信息' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '更新成功' })
|
||||
async updateLastVisit(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Req() request: Request,
|
||||
) {
|
||||
const ip = request.ip || request.connection.remoteAddress || '';
|
||||
await this.memberService.updateLastVisit(id, ip);
|
||||
return {
|
||||
code: 200,
|
||||
message: '更新成功',
|
||||
};
|
||||
}
|
||||
|
||||
@Get('search/by-username/:username')
|
||||
@ApiOperation({ summary: '根据用户名查询会员' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '查询成功' })
|
||||
async findByUsername(@Param('username') username: string) {
|
||||
const member = await this.memberService.findByUsername(username);
|
||||
return {
|
||||
code: 200,
|
||||
message: '查询成功',
|
||||
data: member,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('search/by-mobile/:mobile')
|
||||
@ApiOperation({ summary: '根据手机号查询会员' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '查询成功' })
|
||||
async findByMobile(@Param('mobile') mobile: string) {
|
||||
const member = await this.memberService.findByMobile(mobile);
|
||||
return {
|
||||
code: 200,
|
||||
message: '查询成功',
|
||||
data: member,
|
||||
};
|
||||
}
|
||||
}
|
||||
13
wwjcloud/src/common/member/member.module.ts
Normal file
13
wwjcloud/src/common/member/member.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MemberService } from './member.service';
|
||||
import { MemberController } from './member.controller';
|
||||
import { Member } from './entities/member.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Member])],
|
||||
controllers: [MemberController],
|
||||
providers: [MemberService],
|
||||
exports: [MemberService, TypeOrmModule],
|
||||
})
|
||||
export class MemberModule {}
|
||||
251
wwjcloud/src/common/member/member.service.ts
Normal file
251
wwjcloud/src/common/member/member.service.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Like, Between } from 'typeorm';
|
||||
import { Member } from './entities/member.entity';
|
||||
import { CreateMemberDto, UpdateMemberDto, QueryMemberDto } from './dto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@Injectable()
|
||||
export class MemberService {
|
||||
constructor(
|
||||
@InjectRepository(Member)
|
||||
private readonly memberRepository: Repository<Member>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建会员
|
||||
*/
|
||||
async create(createMemberDto: CreateMemberDto): Promise<Member> {
|
||||
// 检查用户名是否已存在
|
||||
if (createMemberDto.username) {
|
||||
const existingByUsername = await this.memberRepository.findOne({
|
||||
where: { username: createMemberDto.username, deleteTime: 0 },
|
||||
});
|
||||
if (existingByUsername) {
|
||||
throw new ConflictException('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (createMemberDto.mobile) {
|
||||
const existingByMobile = await this.memberRepository.findOne({
|
||||
where: { mobile: createMemberDto.mobile, deleteTime: 0 },
|
||||
});
|
||||
if (existingByMobile) {
|
||||
throw new ConflictException('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 密码加密
|
||||
const hashedPassword = await bcrypt.hash(createMemberDto.password, 10);
|
||||
|
||||
const member = this.memberRepository.create({
|
||||
...createMemberDto,
|
||||
password: hashedPassword,
|
||||
regTime: Math.floor(Date.now() / 1000),
|
||||
createTime: Math.floor(Date.now() / 1000),
|
||||
updateTime: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
return await this.memberRepository.save(member);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询会员列表
|
||||
*/
|
||||
async findAll(queryDto: QueryMemberDto) {
|
||||
const { page = 1, limit = 10, keyword, siteId, memberLevel, sex, status, regType, startTime, endTime } = queryDto;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const queryBuilder = this.memberRepository.createQueryBuilder('member')
|
||||
.where('member.deleteTime = :deleteTime', { deleteTime: 0 });
|
||||
|
||||
// 关键词搜索
|
||||
if (keyword) {
|
||||
queryBuilder.andWhere(
|
||||
'(member.username LIKE :keyword OR member.nickname LIKE :keyword OR member.mobile LIKE :keyword)',
|
||||
{ keyword: `%${keyword}%` }
|
||||
);
|
||||
}
|
||||
|
||||
// 站点ID筛选
|
||||
if (siteId !== undefined) {
|
||||
queryBuilder.andWhere('member.siteId = :siteId', { siteId });
|
||||
}
|
||||
|
||||
// 会员等级筛选
|
||||
if (memberLevel !== undefined) {
|
||||
queryBuilder.andWhere('member.memberLevel = :memberLevel', { memberLevel });
|
||||
}
|
||||
|
||||
// 性别筛选
|
||||
if (sex !== undefined) {
|
||||
queryBuilder.andWhere('member.sex = :sex', { sex });
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if (status !== undefined) {
|
||||
queryBuilder.andWhere('member.status = :status', { status });
|
||||
}
|
||||
|
||||
// 注册类型筛选
|
||||
if (regType) {
|
||||
queryBuilder.andWhere('member.regType = :regType', { regType });
|
||||
}
|
||||
|
||||
// 时间范围筛选
|
||||
if (startTime && endTime) {
|
||||
queryBuilder.andWhere('member.regTime BETWEEN :startTime AND :endTime', {
|
||||
startTime,
|
||||
endTime,
|
||||
});
|
||||
} else if (startTime) {
|
||||
queryBuilder.andWhere('member.regTime >= :startTime', { startTime });
|
||||
} else if (endTime) {
|
||||
queryBuilder.andWhere('member.regTime <= :endTime', { endTime });
|
||||
}
|
||||
|
||||
// 排序
|
||||
queryBuilder.orderBy('member.createTime', 'DESC');
|
||||
|
||||
// 分页
|
||||
const [list, total] = await queryBuilder
|
||||
.skip(skip)
|
||||
.take(limit)
|
||||
.getManyAndCount();
|
||||
|
||||
// 移除密码字段
|
||||
const safeList = list.map(member => {
|
||||
const { password, ...safeMember } = member;
|
||||
return safeMember;
|
||||
});
|
||||
|
||||
return {
|
||||
list: safeList,
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询会员详情
|
||||
*/
|
||||
async findOne(id: number): Promise<Member> {
|
||||
const member = await this.memberRepository.findOne({
|
||||
where: { memberId: id, deleteTime: 0 },
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new NotFoundException('会员不存在');
|
||||
}
|
||||
|
||||
// 移除密码字段
|
||||
const { password, ...safeMember } = member;
|
||||
return safeMember as Member;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询会员
|
||||
*/
|
||||
async findByUsername(username: string): Promise<Member | null> {
|
||||
return await this.memberRepository.findOne({
|
||||
where: { username, deleteTime: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号查询会员
|
||||
*/
|
||||
async findByMobile(mobile: string): Promise<Member | null> {
|
||||
return await this.memberRepository.findOne({
|
||||
where: { mobile, deleteTime: 0 },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会员信息
|
||||
*/
|
||||
async update(id: number, updateMemberDto: UpdateMemberDto): Promise<Member> {
|
||||
const member = await this.findOne(id);
|
||||
|
||||
// 检查用户名是否已被其他用户使用
|
||||
if (updateMemberDto.username && updateMemberDto.username !== member.username) {
|
||||
const existingByUsername = await this.memberRepository.findOne({
|
||||
where: { username: updateMemberDto.username, deleteTime: 0 },
|
||||
});
|
||||
if (existingByUsername && existingByUsername.memberId !== id) {
|
||||
throw new ConflictException('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已被其他用户使用
|
||||
if (updateMemberDto.mobile && updateMemberDto.mobile !== member.mobile) {
|
||||
const existingByMobile = await this.memberRepository.findOne({
|
||||
where: { mobile: updateMemberDto.mobile, deleteTime: 0 },
|
||||
});
|
||||
if (existingByMobile && existingByMobile.memberId !== id) {
|
||||
throw new ConflictException('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果更新密码,需要加密
|
||||
if (updateMemberDto.password) {
|
||||
updateMemberDto.password = await bcrypt.hash(updateMemberDto.password, 10);
|
||||
}
|
||||
|
||||
await this.memberRepository.update(id, {
|
||||
...updateMemberDto,
|
||||
updateTime: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
|
||||
return await this.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除会员
|
||||
*/
|
||||
async remove(id: number): Promise<void> {
|
||||
const member = await this.findOne(id);
|
||||
|
||||
await this.memberRepository.update(id, {
|
||||
deleteTime: Math.floor(Date.now() / 1000),
|
||||
updateTime: Math.floor(Date.now() / 1000),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量软删除会员
|
||||
*/
|
||||
async batchRemove(ids: number[]): Promise<void> {
|
||||
const deleteTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
await this.memberRepository.update(
|
||||
{ memberId: { $in: ids } as any },
|
||||
{
|
||||
deleteTime,
|
||||
updateTime: deleteTime,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新最后登录信息
|
||||
*/
|
||||
async updateLastVisit(id: number, ip: string): Promise<void> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
await this.memberRepository.update(id, {
|
||||
lastVisitTime: now,
|
||||
lastVisitIp: ip,
|
||||
updateTime: now,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
*/
|
||||
async validatePassword(member: Member, password: string): Promise<boolean> {
|
||||
return await bcrypt.compare(password, member.password);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user