feat: 初始化 WWJ Cloud 企业级框架项目

- 后端:基于 NestJS 的分层架构设计
- 前端:基于 VbenAdmin + Element Plus 的管理系统
- 支持 SaaS + 独立版双架构模式
- 完整的用户权限管理系统
- 系统设置、文件上传、通知等核心功能
- 多租户支持和插件化扩展架构
This commit is contained in:
万物街
2025-08-23 13:20:01 +08:00
commit f30d64e6cc
172 changed files with 10179 additions and 0 deletions

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

View File

@@ -0,0 +1,3 @@
export { CreateMemberDto } from './create-member.dto';
export { UpdateMemberDto } from './update-member.dto';
export { QueryMemberDto } from './query-member.dto';

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

View File

@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateMemberDto } from './create-member.dto';
export class UpdateMemberDto extends PartialType(CreateMemberDto) {}

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

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

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

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

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