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 io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@ -24,6 +25,8 @@ public class PublicRegisterDto {
|
|||||||
@Schema(description = "昵称")
|
@Schema(description = "昵称")
|
||||||
private String nickname;
|
private String nickname;
|
||||||
|
|
||||||
|
@NotBlank(message = "手机号不能为空")
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
@Schema(description = "手机号")
|
@Schema(description = "手机号")
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
|
|||||||
@ -65,6 +65,15 @@ public class PublicAuthService {
|
|||||||
throw new BusinessException(400, "用户名已存在");
|
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();
|
SysUser user = new SysUser();
|
||||||
user.setTenantId(publicTenant.getId());
|
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>
|
||||||
|
|
||||||
|
<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-form-item name="username" label="用户名">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="form.username"
|
v-model:value="form.username"
|
||||||
@ -100,6 +109,7 @@ const form = reactive({
|
|||||||
password: "demo123456",
|
password: "demo123456",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
nickname: "",
|
nickname: "",
|
||||||
|
phone: import.meta.env.DEV ? "13800138000" : "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules: Record<string, Rule[]> = {
|
const rules: Record<string, Rule[]> = {
|
||||||
@ -127,6 +137,10 @@ const rules: Record<string, Rule[]> = {
|
|||||||
{ required: true, message: "请输入昵称", trigger: "blur" },
|
{ required: true, message: "请输入昵称", trigger: "blur" },
|
||||||
{ min: 2, message: "昵称至少2个字符", 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 () => {
|
const handleSubmit = async () => {
|
||||||
@ -138,6 +152,7 @@ const handleSubmit = async () => {
|
|||||||
username: form.username,
|
username: form.username,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
nickname: form.nickname,
|
nickname: form.nickname,
|
||||||
|
phone: form.phone,
|
||||||
})
|
})
|
||||||
message.success("注册成功")
|
message.success("注册成功")
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user