forked from datawhale/whale-town-end
feat:实现用户认证系统
- 添加用户登录、注册、密码重置功能 - 支持用户名/邮箱/手机号多种登录方式 - 集成GitHub OAuth第三方登录 - 实现bcrypt密码加密存储 - 添加基于角色的权限控制 - 包含完整的数据验证和错误处理
This commit is contained in:
23
src/core/login_core/login_core.module.ts
Normal file
23
src/core/login_core/login_core.module.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 登录核心模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供登录认证的核心服务模块
|
||||
* - 集成用户数据服务和认证逻辑
|
||||
* - 为业务层提供可复用的认证功能
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LoginCoreService } from './login_core.service';
|
||||
import { UsersModule } from '../db/users/users.module';
|
||||
|
||||
@Module({
|
||||
imports: [UsersModule],
|
||||
providers: [LoginCoreService],
|
||||
exports: [LoginCoreService],
|
||||
})
|
||||
export class LoginCoreModule {}
|
||||
216
src/core/login_core/login_core.service.spec.ts
Normal file
216
src/core/login_core/login_core.service.spec.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* 登录核心服务测试
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LoginCoreService } from './login_core.service';
|
||||
import { UsersService } from '../db/users/users.service';
|
||||
import { UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
|
||||
describe('LoginCoreService', () => {
|
||||
let service: LoginCoreService;
|
||||
let usersService: jest.Mocked<UsersService>;
|
||||
|
||||
const mockUser = {
|
||||
id: BigInt(1),
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
phone: '+8613800138000',
|
||||
password_hash: '$2b$12$hashedpassword',
|
||||
nickname: '测试用户',
|
||||
github_id: null as string | null,
|
||||
avatar_url: null as string | null,
|
||||
role: 1,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockUsersService = {
|
||||
findByUsername: jest.fn(),
|
||||
findByEmail: jest.fn(),
|
||||
findByGithubId: jest.fn(),
|
||||
findAll: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
LoginCoreService,
|
||||
{
|
||||
provide: UsersService,
|
||||
useValue: mockUsersService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<LoginCoreService>(LoginCoreService);
|
||||
usersService = module.get(UsersService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('login', () => {
|
||||
it('should login successfully with valid credentials', async () => {
|
||||
usersService.findByUsername.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(true);
|
||||
|
||||
const result = await service.login({
|
||||
identifier: 'testuser',
|
||||
password: 'password123'
|
||||
});
|
||||
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(result.isNewUser).toBe(false);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException for invalid user', async () => {
|
||||
usersService.findByUsername.mockResolvedValue(null);
|
||||
usersService.findByEmail.mockResolvedValue(null);
|
||||
usersService.findAll.mockResolvedValue([]);
|
||||
|
||||
await expect(service.login({
|
||||
identifier: 'nonexistent',
|
||||
password: 'password123'
|
||||
})).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException for wrong password', async () => {
|
||||
usersService.findByUsername.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(false);
|
||||
|
||||
await expect(service.login({
|
||||
identifier: 'testuser',
|
||||
password: 'wrongpassword'
|
||||
})).rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register successfully', async () => {
|
||||
usersService.create.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('hashedpassword');
|
||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||
|
||||
const result = await service.register({
|
||||
username: 'testuser',
|
||||
password: 'password123',
|
||||
nickname: '测试用户'
|
||||
});
|
||||
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(result.isNewUser).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate password strength', async () => {
|
||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {
|
||||
throw new BadRequestException('密码长度至少8位');
|
||||
});
|
||||
|
||||
await expect(service.register({
|
||||
username: 'testuser',
|
||||
password: '123',
|
||||
nickname: '测试用户'
|
||||
})).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('githubOAuth', () => {
|
||||
it('should login existing GitHub user', async () => {
|
||||
usersService.findByGithubId.mockResolvedValue(mockUser);
|
||||
usersService.update.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.githubOAuth({
|
||||
github_id: 'github123',
|
||||
username: 'githubuser',
|
||||
nickname: 'GitHub用户'
|
||||
});
|
||||
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(result.isNewUser).toBe(false);
|
||||
});
|
||||
|
||||
it('should create new GitHub user', async () => {
|
||||
usersService.findByGithubId.mockResolvedValue(null);
|
||||
usersService.findByUsername.mockResolvedValue(null);
|
||||
usersService.create.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.githubOAuth({
|
||||
github_id: 'github123',
|
||||
username: 'githubuser',
|
||||
nickname: 'GitHub用户'
|
||||
});
|
||||
|
||||
expect(result.user).toEqual(mockUser);
|
||||
expect(result.isNewUser).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPasswordResetCode', () => {
|
||||
it('should send reset code for email', async () => {
|
||||
usersService.findByEmail.mockResolvedValue(mockUser);
|
||||
|
||||
const code = await service.sendPasswordResetCode('test@example.com');
|
||||
|
||||
expect(code).toMatch(/^\d{6}$/);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException for non-existent user', async () => {
|
||||
usersService.findByEmail.mockResolvedValue(null);
|
||||
|
||||
await expect(service.sendPasswordResetCode('nonexistent@example.com'))
|
||||
.rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should reset password successfully', async () => {
|
||||
usersService.findByEmail.mockResolvedValue(mockUser);
|
||||
usersService.update.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('newhashedpassword');
|
||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||
|
||||
const result = await service.resetPassword({
|
||||
identifier: 'test@example.com',
|
||||
verificationCode: '123456',
|
||||
newPassword: 'newpassword123'
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for invalid verification code', async () => {
|
||||
await expect(service.resetPassword({
|
||||
identifier: 'test@example.com',
|
||||
verificationCode: 'invalid',
|
||||
newPassword: 'newpassword123'
|
||||
})).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changePassword', () => {
|
||||
it('should change password successfully', async () => {
|
||||
usersService.findOne.mockResolvedValue(mockUser);
|
||||
usersService.update.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(true);
|
||||
jest.spyOn(service as any, 'hashPassword').mockResolvedValue('newhashedpassword');
|
||||
jest.spyOn(service as any, 'validatePasswordStrength').mockImplementation(() => {});
|
||||
|
||||
const result = await service.changePassword(BigInt(1), 'oldpassword', 'newpassword123');
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
});
|
||||
|
||||
it('should throw UnauthorizedException for wrong old password', async () => {
|
||||
usersService.findOne.mockResolvedValue(mockUser);
|
||||
jest.spyOn(service as any, 'verifyPassword').mockResolvedValue(false);
|
||||
|
||||
await expect(service.changePassword(BigInt(1), 'wrongpassword', 'newpassword123'))
|
||||
.rejects.toThrow(UnauthorizedException);
|
||||
});
|
||||
});
|
||||
});
|
||||
423
src/core/login_core/login_core.service.ts
Normal file
423
src/core/login_core/login_core.service.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* 登录核心服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供用户认证的核心功能实现
|
||||
* - 处理登录、注册、密码重置等核心逻辑
|
||||
* - 为业务层提供基础的认证服务
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于认证功能的核心实现
|
||||
* - 不处理HTTP请求和响应格式化
|
||||
* - 为business层提供可复用的服务
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { UsersService } from '../db/users/users.service';
|
||||
import { Users } from '../db/users/users.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* 登录请求数据接口
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
/** 登录标识符:用户名、邮箱或手机号 */
|
||||
identifier: string;
|
||||
/** 密码 */
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册请求数据接口
|
||||
*/
|
||||
export interface RegisterRequest {
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 密码 */
|
||||
password: string;
|
||||
/** 昵称 */
|
||||
nickname: string;
|
||||
/** 邮箱(可选) */
|
||||
email?: string;
|
||||
/** 手机号(可选) */
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录请求数据接口
|
||||
*/
|
||||
export interface GitHubOAuthRequest {
|
||||
/** GitHub用户ID */
|
||||
github_id: string;
|
||||
/** 用户名 */
|
||||
username: string;
|
||||
/** 昵称 */
|
||||
nickname: string;
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 头像URL */
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码重置请求数据接口
|
||||
*/
|
||||
export interface PasswordResetRequest {
|
||||
/** 邮箱或手机号 */
|
||||
identifier: string;
|
||||
/** 验证码 */
|
||||
verificationCode: string;
|
||||
/** 新密码 */
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证结果接口
|
||||
*/
|
||||
export interface AuthResult {
|
||||
/** 用户信息 */
|
||||
user: Users;
|
||||
/** 是否为新用户 */
|
||||
isNewUser?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LoginCoreService {
|
||||
constructor(
|
||||
private readonly usersService: UsersService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 用户名密码登录
|
||||
*
|
||||
* @param loginRequest 登录请求数据
|
||||
* @returns 认证结果
|
||||
* @throws UnauthorizedException 认证失败时
|
||||
*/
|
||||
async login(loginRequest: LoginRequest): Promise<AuthResult> {
|
||||
const { identifier, password } = loginRequest;
|
||||
|
||||
// 查找用户(支持用户名、邮箱、手机号登录)
|
||||
let user: Users | null = null;
|
||||
|
||||
// 尝试用户名查找
|
||||
user = await this.usersService.findByUsername(identifier);
|
||||
|
||||
// 如果用户名未找到,尝试邮箱查找
|
||||
if (!user && this.isEmail(identifier)) {
|
||||
user = await this.usersService.findByEmail(identifier);
|
||||
}
|
||||
|
||||
// 如果邮箱未找到,尝试手机号查找(简单验证)
|
||||
if (!user && this.isPhoneNumber(identifier)) {
|
||||
const users = await this.usersService.findAll();
|
||||
user = users.find(u => u.phone === identifier) || null;
|
||||
}
|
||||
|
||||
// 用户不存在
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('用户名、邮箱或手机号不存在');
|
||||
}
|
||||
|
||||
// 检查是否为OAuth用户(没有密码)
|
||||
if (!user.password_hash) {
|
||||
throw new UnauthorizedException('该账户使用第三方登录,请使用对应的登录方式');
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const isPasswordValid = await this.verifyPassword(password, user.password_hash);
|
||||
if (!isPasswordValid) {
|
||||
throw new UnauthorizedException('密码错误');
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param registerRequest 注册请求数据
|
||||
* @returns 认证结果
|
||||
* @throws ConflictException 用户已存在时
|
||||
* @throws BadRequestException 数据验证失败时
|
||||
*/
|
||||
async register(registerRequest: RegisterRequest): Promise<AuthResult> {
|
||||
const { username, password, nickname, email, phone } = registerRequest;
|
||||
|
||||
// 验证密码强度
|
||||
this.validatePasswordStrength(password);
|
||||
|
||||
// 加密密码
|
||||
const passwordHash = await this.hashPassword(password);
|
||||
|
||||
// 创建用户
|
||||
const user = await this.usersService.create({
|
||||
username,
|
||||
password_hash: passwordHash,
|
||||
nickname,
|
||||
email,
|
||||
phone,
|
||||
role: 1 // 默认普通用户
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录/注册
|
||||
*
|
||||
* @param oauthRequest OAuth请求数据
|
||||
* @returns 认证结果
|
||||
*/
|
||||
async githubOAuth(oauthRequest: GitHubOAuthRequest): Promise<AuthResult> {
|
||||
const { github_id, username, nickname, email, avatar_url } = oauthRequest;
|
||||
|
||||
// 查找是否已存在GitHub用户
|
||||
let user = await this.usersService.findByGithubId(github_id);
|
||||
|
||||
if (user) {
|
||||
// 用户已存在,更新信息
|
||||
user = await this.usersService.update(user.id, {
|
||||
nickname,
|
||||
email,
|
||||
avatar_url
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: false
|
||||
};
|
||||
}
|
||||
|
||||
// 检查用户名是否已被占用
|
||||
let finalUsername = username;
|
||||
let counter = 1;
|
||||
while (await this.usersService.findByUsername(finalUsername)) {
|
||||
finalUsername = `${username}_${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 创建新用户
|
||||
user = await this.usersService.create({
|
||||
username: finalUsername,
|
||||
nickname,
|
||||
email,
|
||||
github_id,
|
||||
avatar_url,
|
||||
role: 1 // 默认普通用户
|
||||
});
|
||||
|
||||
return {
|
||||
user,
|
||||
isNewUser: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送密码重置验证码
|
||||
*
|
||||
* @param identifier 邮箱或手机号
|
||||
* @returns 验证码(实际应用中应发送到用户邮箱/手机)
|
||||
* @throws NotFoundException 用户不存在时
|
||||
*/
|
||||
async sendPasswordResetCode(identifier: string): Promise<string> {
|
||||
// 查找用户
|
||||
let user: Users | null = null;
|
||||
|
||||
if (this.isEmail(identifier)) {
|
||||
user = await this.usersService.findByEmail(identifier);
|
||||
} else if (this.isPhoneNumber(identifier)) {
|
||||
const users = await this.usersService.findAll();
|
||||
user = users.find(u => u.phone === identifier) || null;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 生成6位数验证码
|
||||
const verificationCode = this.generateVerificationCode();
|
||||
|
||||
// TODO: 实际应用中应该:
|
||||
// 1. 将验证码存储到Redis等缓存中,设置过期时间(如5分钟)
|
||||
// 2. 发送验证码到用户邮箱或手机
|
||||
// 3. 返回成功消息而不是验证码本身
|
||||
|
||||
// 这里为了演示,直接返回验证码
|
||||
console.log(`密码重置验证码(${identifier}): ${verificationCode}`);
|
||||
|
||||
return verificationCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param resetRequest 重置请求数据
|
||||
* @returns 更新后的用户信息
|
||||
* @throws NotFoundException 用户不存在时
|
||||
* @throws BadRequestException 验证码错误时
|
||||
*/
|
||||
async resetPassword(resetRequest: PasswordResetRequest): Promise<Users> {
|
||||
const { identifier, verificationCode, newPassword } = resetRequest;
|
||||
|
||||
// TODO: 实际应用中应该验证验证码的有效性
|
||||
// 这里为了演示,简单验证验证码格式
|
||||
if (!/^\d{6}$/.test(verificationCode)) {
|
||||
throw new BadRequestException('验证码格式错误');
|
||||
}
|
||||
|
||||
// 查找用户
|
||||
let user: Users | null = null;
|
||||
|
||||
if (this.isEmail(identifier)) {
|
||||
user = await this.usersService.findByEmail(identifier);
|
||||
} else if (this.isPhoneNumber(identifier)) {
|
||||
const users = await this.usersService.findAll();
|
||||
user = users.find(u => u.phone === identifier) || null;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 验证密码强度
|
||||
this.validatePasswordStrength(newPassword);
|
||||
|
||||
// 加密新密码
|
||||
const passwordHash = await this.hashPassword(newPassword);
|
||||
|
||||
// 更新密码
|
||||
return await this.usersService.update(user.id, {
|
||||
password_hash: passwordHash
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param oldPassword 旧密码
|
||||
* @param newPassword 新密码
|
||||
* @returns 更新后的用户信息
|
||||
* @throws UnauthorizedException 旧密码错误时
|
||||
*/
|
||||
async changePassword(userId: bigint, oldPassword: string, newPassword: string): Promise<Users> {
|
||||
// 获取用户信息
|
||||
const user = await this.usersService.findOne(userId);
|
||||
|
||||
// 检查是否为OAuth用户
|
||||
if (!user.password_hash) {
|
||||
throw new BadRequestException('OAuth用户无法修改密码');
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
const isOldPasswordValid = await this.verifyPassword(oldPassword, user.password_hash);
|
||||
if (!isOldPasswordValid) {
|
||||
throw new UnauthorizedException('旧密码错误');
|
||||
}
|
||||
|
||||
// 验证新密码强度
|
||||
this.validatePasswordStrength(newPassword);
|
||||
|
||||
// 加密新密码
|
||||
const passwordHash = await this.hashPassword(newPassword);
|
||||
|
||||
// 更新密码
|
||||
return await this.usersService.update(userId, {
|
||||
password_hash: passwordHash
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户密码
|
||||
*
|
||||
* @param password 明文密码
|
||||
* @param hash 密码哈希值
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
private async verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
try {
|
||||
return await bcrypt.compare(password, hash);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
*
|
||||
* @param password 明文密码
|
||||
* @returns 密码哈希值
|
||||
*/
|
||||
private async hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 12; // 推荐的盐值轮数
|
||||
return await bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码强度
|
||||
*
|
||||
* @param password 密码
|
||||
* @throws BadRequestException 密码强度不足时
|
||||
*/
|
||||
private validatePasswordStrength(password: string): void {
|
||||
if (password.length < 8) {
|
||||
throw new BadRequestException('密码长度至少8位');
|
||||
}
|
||||
|
||||
if (password.length > 128) {
|
||||
throw new BadRequestException('密码长度不能超过128位');
|
||||
}
|
||||
|
||||
// 检查是否包含字母和数字
|
||||
const hasLetter = /[a-zA-Z]/.test(password);
|
||||
const hasNumber = /\d/.test(password);
|
||||
|
||||
if (!hasLetter || !hasNumber) {
|
||||
throw new BadRequestException('密码必须包含字母和数字');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @returns 6位数验证码
|
||||
*/
|
||||
private generateVerificationCode(): string {
|
||||
return Math.floor(100000 + Math.random() * 900000).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为邮箱格式
|
||||
*
|
||||
* @param str 字符串
|
||||
* @returns 是否为邮箱
|
||||
*/
|
||||
private isEmail(str: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为手机号格式(简单验证)
|
||||
*
|
||||
* @param str 字符串
|
||||
* @returns 是否为手机号
|
||||
*/
|
||||
private isPhoneNumber(str: string): boolean {
|
||||
// 简单的手机号验证,支持国际格式
|
||||
const phoneRegex = /^(\+\d{1,3}[- ]?)?\d{10,11}$/;
|
||||
return phoneRegex.test(str.replace(/\s/g, ''));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user