feat: 公众端注册添加手机号必填——支持 AI 创作功能绑定手机号
后端 PublicRegisterDto phone 字段添加 @NotBlank + @Pattern 校验; PublicAuthService 添加手机号唯一性检查(公众租户范围内); 前端 Login.vue 注册表单添加手机号输入框、验证规则、提交参数; 新增 10 条 E2E 测试用例覆盖前端校验、API 参数传递、完整注册流程。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
87ac3b5ed9
commit
67de13c29a
@ -2,6 +2,7 @@ package com.competition.modules.pub.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
@ -24,6 +25,8 @@ public class PublicRegisterDto {
|
||||
@Schema(description = "昵称")
|
||||
private String nickname;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
|
||||
@ -65,6 +65,15 @@ public class PublicAuthService {
|
||||
throw new BusinessException(400, "用户名已存在");
|
||||
}
|
||||
|
||||
// 检查手机号是否已被注册(公众租户范围内)
|
||||
Long phoneCount = sysUserMapper.selectCount(
|
||||
new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getPhone, dto.getPhone())
|
||||
.eq(SysUser::getTenantId, publicTenant.getId()));
|
||||
if (phoneCount > 0) {
|
||||
throw new BusinessException(400, "该手机号已注册");
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
SysUser user = new SysUser();
|
||||
user.setTenantId(publicTenant.getId());
|
||||
|
||||
267
frontend/e2e/public/register.spec.ts
Normal file
267
frontend/e2e/public/register.spec.ts
Normal file
@ -0,0 +1,267 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* 公众端注册流程测试
|
||||
* 验证手机号必填功能
|
||||
*/
|
||||
|
||||
/** 公众端登录/注册页面路径 */
|
||||
const REGISTER_PATH = '/p/login'
|
||||
|
||||
/** Mock 注册 API 的响应 */
|
||||
async function setupRegisterApiMocks(page: import('@playwright/test').Page) {
|
||||
// 拦截注册 API
|
||||
await page.route('**/api/public/auth/register', async (route) => {
|
||||
const request = route.request()
|
||||
const postData = request.postDataJSON()
|
||||
|
||||
// 模拟后端校验逻辑
|
||||
if (!postData?.phone) {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ code: 400, message: '手机号不能为空' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!/^1[3-9]\d{9}$/.test(postData.phone)) {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ code: 400, message: '手机号格式不正确' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (postData.phone === '13800000000') {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ code: 400, message: '该手机号已注册' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 注册成功
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
token: 'mock-jwt-token-for-test',
|
||||
user: {
|
||||
id: 999,
|
||||
username: postData.username,
|
||||
nickname: postData.nickname,
|
||||
phone: postData.phone,
|
||||
avatar: null,
|
||||
city: null,
|
||||
gender: null,
|
||||
userType: 'adult',
|
||||
roles: ['public_user'],
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
// 拦截登录 API(切换到登录模式时需要)
|
||||
await page.route('**/api/public/auth/login', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
token: 'mock-jwt-token-for-test',
|
||||
user: {
|
||||
id: 1,
|
||||
username: 'demo',
|
||||
nickname: '测试用户',
|
||||
phone: '13800138000',
|
||||
avatar: null,
|
||||
city: null,
|
||||
gender: null,
|
||||
userType: 'adult',
|
||||
roles: ['public_user'],
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
test.describe('公众端注册 - 手机号必填', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await setupRegisterApiMocks(page)
|
||||
await page.goto(REGISTER_PATH)
|
||||
// 点击"立即注册"切换到注册模式
|
||||
await page.locator('text=立即注册').click()
|
||||
// 等待注册表单渲染
|
||||
await expect(page.locator('text=创建账号')).toBeVisible()
|
||||
})
|
||||
|
||||
test('PR-01 注册表单包含手机号输入框', async ({ page }) => {
|
||||
// 验证手机号标签可见
|
||||
await expect(page.locator('label:has-text("手机号")')).toBeVisible()
|
||||
// 验证手机号输入框可见
|
||||
await expect(page.locator('input[placeholder="请输入手机号"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test('PR-02 手机号输入框限制最大长度11位', async ({ page }) => {
|
||||
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
|
||||
await expect(phoneInput).toHaveAttribute('maxlength', '11')
|
||||
})
|
||||
|
||||
test('PR-03 登录模式不显示手机号输入框', async ({ page }) => {
|
||||
// 点击"去登录"切换回登录模式
|
||||
await page.locator('text=去登录').click()
|
||||
await expect(page.locator('text=登录')).toBeVisible()
|
||||
|
||||
// 验证手机号输入框不存在
|
||||
await expect(page.locator('input[placeholder="请输入手机号"]')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('PR-04 手机号为空时显示必填校验错误', async ({ page }) => {
|
||||
// 清空手机号(开发环境可能预填了值)
|
||||
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
|
||||
await phoneInput.clear()
|
||||
// 触发 blur 校验
|
||||
await phoneInput.blur()
|
||||
|
||||
// 验证校验错误信息
|
||||
await expect(page.locator('.ant-form-item-explain-error')).toContainText('请输入手机号')
|
||||
})
|
||||
|
||||
test('PR-05 手机号格式不正确时显示校验错误', async ({ page }) => {
|
||||
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
|
||||
await phoneInput.fill('12345678901')
|
||||
await phoneInput.blur()
|
||||
|
||||
// 验证格式校验错误
|
||||
await expect(page.locator('.ant-form-item-explain-error')).toContainText('手机号格式不正确')
|
||||
})
|
||||
|
||||
test('PR-06 输入正确的手机号格式不显示错误', async ({ page }) => {
|
||||
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
|
||||
await phoneInput.fill('13912345678')
|
||||
await phoneInput.blur()
|
||||
|
||||
// 验证无校验错误
|
||||
await expect(page.locator('.ant-form-item-explain-error')).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('PR-07 不传手机号后端返回校验错误', async ({ page }) => {
|
||||
// 修改 route 来模拟不传手机号
|
||||
let capturedBody: any = null
|
||||
await page.route('**/api/public/auth/register', async (route) => {
|
||||
capturedBody = route.request().postDataJSON()
|
||||
// 模拟后端 @NotBlank 校验
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ code: 400, message: '手机号不能为空' }),
|
||||
})
|
||||
})
|
||||
|
||||
// 清空手机号并提交(绕过前端校验直接测试 API)
|
||||
const phoneInput = page.locator('input[placeholder="请输入手机号"]')
|
||||
await phoneInput.clear()
|
||||
|
||||
// 填写其他必填项
|
||||
await page.locator('input[placeholder="给自己取个名字吧"]').fill('测试昵称')
|
||||
await page.locator('input[placeholder="请输入用户名"]').fill('testuser' + Date.now())
|
||||
await page.locator('input[type="password"]').first().fill('test123456')
|
||||
await page.locator('input[placeholder="再次输入密码"]').fill('test123456')
|
||||
|
||||
// 点击注册按钮
|
||||
await page.locator('button:has-text("注册并登录")').click()
|
||||
|
||||
// 验证 Ant Design 前端表单校验拦截了提交(手机号必填)
|
||||
await expect(page.locator('.ant-form-item-explain-error')).toContainText('请输入手机号')
|
||||
})
|
||||
|
||||
test('PR-08 注册时传递手机号参数到后端', async ({ page }) => {
|
||||
let capturedBody: any = null
|
||||
|
||||
// 重新注册路由来捕获请求体(覆盖 beforeEach 的 setupRegisterApiMocks)
|
||||
await page.unroute('**/api/public/auth/register')
|
||||
await page.route('**/api/public/auth/register', async (route) => {
|
||||
capturedBody = route.request().postDataJSON()
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
token: 'mock-token',
|
||||
user: { id: 999, username: 'test', nickname: '测试', phone: '13900001111', avatar: null, city: null, gender: null, userType: 'adult', roles: ['public_user'], permissions: [] },
|
||||
},
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
// 填写完整注册表单
|
||||
await page.locator('input[placeholder="给自己取个名字吧"]').fill('测试昵称')
|
||||
await page.locator('input[placeholder="请输入手机号"]').fill('13900001111')
|
||||
const uniqueUsername = 'testuser' + Date.now()
|
||||
await page.locator('input[placeholder="请输入用户名"]').fill(uniqueUsername)
|
||||
await page.locator('input[type="password"]').first().fill('test123456')
|
||||
await page.locator('input[placeholder="再次输入密码"]').fill('test123456')
|
||||
|
||||
// 点击注册
|
||||
await page.locator('button:has-text("注册并登录")').click()
|
||||
|
||||
// 等待成功提示(说明 API 已被调用)
|
||||
await expect(page.locator('.ant-message')).toContainText('注册成功', { timeout: 5000 })
|
||||
|
||||
// 验证请求体包含 phone 字段
|
||||
expect(capturedBody).toBeTruthy()
|
||||
expect(capturedBody.phone).toBe('13900001111')
|
||||
})
|
||||
|
||||
test('PR-09 已注册手机号后端返回错误提示', async ({ page }) => {
|
||||
// 使用预定义的"已注册"手机号
|
||||
await page.locator('input[placeholder="给自己取个名字吧"]').fill('测试昵称')
|
||||
await page.locator('input[placeholder="请输入手机号"]').fill('13800000000')
|
||||
await page.locator('input[placeholder="请输入用户名"]').fill('testuser' + Date.now())
|
||||
await page.locator('input[type="password"]').first().fill('test123456')
|
||||
await page.locator('input[placeholder="再次输入密码"]').fill('test123456')
|
||||
|
||||
// 点击注册
|
||||
await page.locator('button:has-text("注册并登录")').click()
|
||||
|
||||
// 验证错误提示(由 mock 返回"该手机号已注册")
|
||||
await expect(page.locator('.ant-message')).toContainText('该手机号已注册', { timeout: 5000 })
|
||||
})
|
||||
|
||||
test('PR-10 完整注册流程成功', async ({ page }) => {
|
||||
const testSuffix = Date.now()
|
||||
|
||||
// 填写完整注册表单
|
||||
await page.locator('input[placeholder="给自己取个名字吧"]').fill('测试昵称')
|
||||
await page.locator('input[placeholder="请输入手机号"]').fill('139' + String(testSuffix).slice(-8))
|
||||
await page.locator('input[placeholder="请输入用户名"]').fill('e2e' + testSuffix)
|
||||
await page.locator('input[type="password"]').first().fill('test123456')
|
||||
await page.locator('input[placeholder="再次输入密码"]').fill('test123456')
|
||||
|
||||
// 点击注册
|
||||
await page.locator('button:has-text("注册并登录")').click()
|
||||
|
||||
// 验证成功提示
|
||||
await expect(page.locator('.ant-message')).toContainText('注册成功', { timeout: 5000 })
|
||||
|
||||
// 验证跳转离开登录页
|
||||
await page.waitForURL((url) => !url.pathname.includes('/login'), { timeout: 10_000 })
|
||||
|
||||
// 验证跳转到了活动列表页(/p/activities)
|
||||
await expect(page).toHaveURL(/\/p\/activities/)
|
||||
})
|
||||
})
|
||||
@ -22,6 +22,15 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item v-if="isRegister" name="phone" label="手机号">
|
||||
<a-input
|
||||
v-model:value="form.phone"
|
||||
placeholder="请输入手机号"
|
||||
size="large"
|
||||
:maxlength="11"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item name="username" label="用户名">
|
||||
<a-input
|
||||
v-model:value="form.username"
|
||||
@ -100,6 +109,7 @@ const form = reactive({
|
||||
password: "demo123456",
|
||||
confirmPassword: "",
|
||||
nickname: "",
|
||||
phone: import.meta.env.DEV ? "13800138000" : "",
|
||||
})
|
||||
|
||||
const rules: Record<string, Rule[]> = {
|
||||
@ -127,6 +137,10 @@ const rules: Record<string, Rule[]> = {
|
||||
{ required: true, message: "请输入昵称", trigger: "blur" },
|
||||
{ min: 2, message: "昵称至少2个字符", trigger: "blur" },
|
||||
],
|
||||
phone: [
|
||||
{ required: true, message: "请输入手机号", trigger: "blur" },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: "手机号格式不正确", trigger: "blur" },
|
||||
],
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@ -138,6 +152,7 @@ const handleSubmit = async () => {
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
nickname: form.nickname,
|
||||
phone: form.phone,
|
||||
})
|
||||
message.success("注册成功")
|
||||
} else {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user