فهرست منبع

📝 docs(planning): 添加PRD文档用于招聘系统项目

- 创建产品需求文档框架,记录已完成步骤和输入文档
- 包含项目文档、测试规范、源代码和测试参考等关键信息

✅ test(e2e): 为残疾人管理功能添加端到端测试

- 新增残疾人管理页面对象模型,包含完整的表单操作和验证方法
- 添加调试测试用例,用于分析参数错误问题
- 在测试配置中注册新的页面对象模型
yourname 1 هفته پیش
والد
کامیت
b6f4488e07

+ 40 - 0
_bmad-output/planning-artifacts/prd.md

@@ -0,0 +1,40 @@
+---
+stepsCompleted: ['step-01-init']
+inputDocuments:
+  - name: 项目文档索引
+    path: docs/index.md
+    type: project-knowledge
+    loadedAt: '2026-01-07T12:00:00.000Z'
+  - name: 测试策略
+    path: docs/standards/testing-standards.md
+    type: testing-standards
+    loadedAt: '2026-01-07T12:00:00.000Z'
+  - name: Web UI测试规范
+    path: docs/standards/web-ui-testing-standards.md
+    type: testing-standards
+    loadedAt: '2026-01-07T12:00:00.000Z'
+  - name: 残疾人管理组件
+    path: allin-packages/disability-person-management-ui/src/components/DisabilityPersonManagement.tsx
+    type: source-code
+    loadedAt: '2026-01-07T12:00:00.000Z'
+  - name: 残疾人实体模型
+    path: allin-packages/disability-module/src/entities/disabled-person.entity.ts
+    type: source-code
+    loadedAt: '2026-01-07T12:00:00.000Z'
+  - name: E2E测试示例
+    path: web/tests/e2e/specs/admin/users.spec.ts
+    type: test-reference
+    loadedAt: '2026-01-07T12:00:00.000Z'
+documentCounts:
+  briefs: 0
+  research: 0
+  projectDocs: 3
+  testReferences: 1
+workflowType: 'prd'
+lastStep: 1
+---
+
+# Product Requirements Document - 188-179 招聘系统
+
+**作者:** Root
+**日期:** 2026-01-07

+ 170 - 0
web/tests/e2e/pages/admin/disability-person.page.ts

@@ -0,0 +1,170 @@
+import { Page, Locator } from '@playwright/test';
+
+export class DisabilityPersonManagementPage {
+  readonly page: Page;
+  readonly pageTitle: Locator;
+  readonly addPersonButton: Locator;
+  readonly keywordSearchInput: Locator;
+  readonly searchButton: Locator;
+  readonly personTable: Locator;
+
+  constructor(page: Page) {
+    this.page = page;
+    this.pageTitle = page.getByRole('heading', { name: '残疾人个人管理' });
+    this.addPersonButton = page.getByTestId('add-disabled-person-button');
+    this.keywordSearchInput = page.getByTestId('keyword-search-input');
+    this.searchButton = page.getByRole('button', { name: '搜索' });
+    this.personTable = page.locator('table');
+  }
+
+  async goto() {
+    await this.page.goto('/admin/disability-persons');
+    await this.page.waitForLoadState('domcontentloaded');
+    await this.page.waitForSelector('h1:has-text("残疾人个人管理")', { state: 'visible', timeout: 15000 });
+    await this.page.waitForSelector('table tbody tr', { state: 'visible', timeout: 20000 });
+    await this.expectToBeVisible();
+  }
+
+  async expectToBeVisible() {
+    await this.pageTitle.waitFor({ state: 'visible', timeout: 15000 });
+    await this.addPersonButton.waitFor({ state: 'visible', timeout: 10000 });
+  }
+
+  async openCreateDialog() {
+    // 监听网络请求
+    const responses: any[] = [];
+    this.page.on('response', async (response) => {
+      if (response.url().includes('disabled-person') || response.url().includes('aggregated')) {
+        try {
+          const responseData = await response.json().catch(() => ({ status: response.status() }));
+          responses.push({
+            url: response.url(),
+            status: response.status(),
+            data: responseData
+          });
+        } catch (e) {
+          responses.push({
+            url: response.url(),
+            status: response.status(),
+            error: e
+          });
+        }
+      }
+    });
+
+    await this.addPersonButton.click();
+    await this.page.waitForSelector('[data-testid="create-disabled-person-dialog-title"]', { state: 'visible', timeout: 5000 });
+
+    return responses;
+  }
+
+  async fillBasicForm(data: {
+    name: string;
+    gender: string;
+    idCard: string;
+    disabilityId: string;
+    disabilityType: string;
+    disabilityLevel: string;
+    phone: string;
+    idAddress: string;
+    province: string;
+    city: string;
+  }) {
+    // 等待表单出现
+    await this.page.waitForSelector('form#-create-form', { state: 'visible', timeout: 5000 });
+
+    // 填写基本信息
+    await this.page.getByLabel('姓名 *').fill(data.name);
+    await this.page.getByLabel('性别 *').selectOption(data.gender);
+    await this.page.getByLabel('身份证号 *').fill(data.idCard);
+    await this.page.getByLabel('残疾证号 *').fill(data.disabilityId);
+    await this.page.getByLabel('残疾类型 *').selectOption(data.disabilityType);
+    await this.page.getByLabel('残疾等级 *').selectOption(data.disabilityLevel);
+    await this.page.getByLabel('联系电话 *').fill(data.phone);
+    await this.page.getByLabel('身份证地址 *').fill(data.idAddress);
+
+    // 居住地址 - 使用省份选择
+    await this.page.locator('[data-testid="province-select"]').selectOption(data.province);
+    await this.page.waitForTimeout(500); // 等待城市加载
+    await this.page.locator('[data-testid="city-select"]').selectOption(data.city);
+  }
+
+  async submitForm() {
+    // 收集网络响应
+    const responses: any[] = [];
+
+    // 监听所有网络请求
+    this.page.on('response', async (response) => {
+      const url = response.url();
+      if (url.includes('disabled-person') || url.includes('aggregated')) {
+        const requestBody = response.request()?.postData();
+        const responseBody = await response.text().catch(() => '');
+        let jsonBody = null;
+        try {
+          jsonBody = JSON.parse(responseBody);
+        } catch (e) {
+          // 不是 JSON
+        }
+
+        responses.push({
+          url,
+          method: response.request()?.method(),
+          status: response.status(),
+          ok: response.ok(),
+          requestHeaders: await response.allHeaders().catch(() => ({})),
+          responseHeaders: await response.allHeaders().catch(() => ({})),
+          requestBody: requestBody ? JSON.parse(requestBody) : null,
+          responseBody: jsonBody || responseBody,
+          statusText: response.statusText()
+        });
+      }
+    });
+
+    // 点击创建按钮
+    const submitButton = this.page.locator('form#create-form button[type="submit"]');
+    await submitButton.click();
+
+    // 等待网络请求完成
+    await this.page.waitForLoadState('networkidle', { timeout: 10000 });
+
+    // 等待一段时间让 Toast 消息显示
+    await this.page.waitForTimeout(2000);
+
+    // 检查是否有错误提示
+    const errorToast = this.page.locator('[data-sonner-toast][data-type="error"]');
+    const successToast = this.page.locator('[data-sonner-toast][data-type="success"]');
+
+    const hasError = await errorToast.count() > 0;
+    const hasSuccess = await successToast.count() > 0;
+
+    let errorMessage = null;
+    let successMessage = null;
+
+    if (hasError) {
+      errorMessage = await errorToast.textContent();
+    }
+    if (hasSuccess) {
+      successMessage = await successToast.textContent();
+    }
+
+    return {
+      responses,
+      hasError,
+      hasSuccess,
+      errorMessage,
+      successMessage
+    };
+  }
+
+  async searchByName(name: string) {
+    await this.keywordSearchInput.fill(name);
+    await this.searchButton.click();
+    await this.page.waitForLoadState('networkidle');
+    await this.page.waitForTimeout(1000);
+  }
+
+  async personExists(name: string): Promise<boolean> {
+    const personRow = this.personTable.locator('tbody tr').filter({ hasText: name }).first();
+    return (await personRow.count()) > 0;
+  }
+}

+ 149 - 0
web/tests/e2e/specs/admin/disability-person-debug.spec.ts

@@ -0,0 +1,149 @@
+import { test } from '../../utils/test-setup';
+import { readFileSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const testUsers = JSON.parse(readFileSync(join(__dirname, '../../fixtures/test-users.json'), 'utf-8'));
+
+test.describe.serial('残疾人管理 - 参数错误调试测试', () => {
+  test.beforeEach(async ({ adminLoginPage, disabilityPersonPage }) => {
+    // 以管理员身份登录后台
+    await adminLoginPage.goto();
+    await adminLoginPage.login(testUsers.admin.username, testUsers.admin.password);
+    await adminLoginPage.expectLoginSuccess();
+    await disabilityPersonPage.goto();
+  });
+
+  test('调试: 新增残疾人时的参数错误问题', async ({ disabilityPersonPage, page }) => {
+    // 生成唯一的测试数据
+    const timestamp = Date.now();
+    const testData = {
+      name: `调试测试_${timestamp}`,
+      gender: '男',
+      idCard: `11010119900101123${timestamp % 10}`,
+      disabilityId: `5110011990010${timestamp % 10}`,
+      disabilityType: '视力残疾',
+      disabilityLevel: '一级',
+      phone: `1380013800${timestamp % 10}`,
+      idAddress: '北京市东城区测试街道1号',
+      province: '北京市',
+      city: '北京市'
+    };
+
+    console.log('\n========== 开始调试测试 ==========');
+    console.log('测试数据:', JSON.stringify(testData, null, 2));
+
+    // 1. 打开创建对话框
+    console.log('\n[步骤1] 打开新增残疾人对话框...');
+    await disabilityPersonPage.openCreateDialog();
+    console.log('✓ 对话框已打开');
+
+    // 2. 填写表单
+    console.log('\n[步骤2] 填写表单...');
+    await disabilityPersonPage.fillBasicForm(testData);
+    console.log('✓ 表单已填写');
+
+    // 3. 提交表单并捕获网络请求
+    console.log('\n[步骤3] 提交表单并监听网络请求...');
+
+    const result = await disabilityPersonPage.submitForm();
+
+    console.log('\n========== 网络请求分析 ==========');
+
+    if (result.responses.length === 0) {
+      console.log('⚠️  没有捕获到任何相关网络请求!');
+    } else {
+      result.responses.forEach((resp, index) => {
+        console.log(`\n[请求 ${index + 1}]`);
+        console.log('  URL:', resp.url);
+        console.log('  方法:', resp.method);
+        console.log('  状态码:', resp.status);
+        console.log('  OK:', resp.ok);
+        console.log('  状态文本:', resp.statusText);
+
+        if (resp.requestBody) {
+          console.log('  请求体:', JSON.stringify(resp.requestBody, null, 2));
+        }
+
+        if (resp.responseBody) {
+          console.log('  响应体:', JSON.stringify(resp.responseBody, null, 2));
+        }
+
+        // 关键检查:状态码与响应内容
+        console.log('\n  🔍 状态码分析:');
+        if (resp.status === 200) {
+          console.log('    ✓ HTTP 状态码是 200 (成功)');
+          if (resp.responseBody) {
+            if (resp.responseBody.message) {
+              console.log('    响应消息:', resp.responseBody.message);
+            }
+            if (resp.responseBody.error) {
+              console.log('    ⚠️  响应包含错误信息:', resp.responseBody.error);
+            }
+          }
+        } else if (resp.status === 201) {
+          console.log('    ✓ HTTP 状态码是 201 (已创建)');
+        } else if (resp.status >= 400) {
+          console.log('    ✗ HTTP 错误状态码:', resp.status);
+        }
+
+        console.log('\n  响应头:', JSON.stringify(resp.responseHeaders, null, 2));
+      });
+    }
+
+    console.log('\n========== UI 提示分析 ==========');
+    console.log('有错误提示:', result.hasError);
+    console.log('有成功提示:', result.hasSuccess);
+
+    if (result.errorMessage) {
+      console.log('❌ 错误消息:', result.errorMessage);
+    }
+
+    if (result.successMessage) {
+      console.log('✅ 成功消息:', result.successMessage);
+    }
+
+    // 4. 验证数据是否真的创建成功
+    console.log('\n[步骤4] 验证数据是否创建成功...');
+
+    // 关闭对话框(如果还在)
+    const dialogVisible = await page.locator('[role="dialog"]').isVisible().catch(() => false);
+    if (dialogVisible) {
+      await page.keyboard.press('Escape');
+      await page.waitForTimeout(500);
+    }
+
+    // 刷新页面
+    await page.reload();
+    await page.waitForLoadState('networkidle');
+    await disabilityPersonPage.goto();
+
+    // 搜索刚创建的残疾人
+    await disabilityPersonPage.searchByName(testData.name);
+    await page.waitForTimeout(1000);
+
+    const personExists = await disabilityPersonPage.personExists(testData.name);
+
+    console.log('\n========== 最终结论 ==========');
+    console.log('数据实际创建成功:', personExists);
+
+    if (result.hasError && personExists) {
+      console.log('\n🔴 问题确认!');
+      console.log('  - 前端显示:创建失败(参数错误)');
+      console.log('  - 实际情况:数据已成功创建到数据库');
+      console.log('\n  可能原因:');
+      console.log('  1. 前端判断响应状态码的逻辑错误');
+      console.log('  2. 后端返回 200 但前端期望 201');
+      console.log('  3. 响应数据结构不符合前端预期');
+      console.log('  4. 前端错误处理逻辑将成功误判为失败');
+    } else if (result.hasSuccess && personExists) {
+      console.log('\n✅ 测试通过:前端显示成功,数据也创建成功');
+    } else if (!result.hasError && !result.hasSuccess) {
+      console.log('\n⚠️  没有任何提示消息');
+    }
+
+    console.log('================================\n');
+  });
+});

+ 6 - 1
web/tests/e2e/utils/test-setup.ts

@@ -2,11 +2,13 @@ import { test as base } from '@playwright/test';
 import { AdminLoginPage } from '../pages/admin/login.page';
 import { DashboardPage } from '../pages/admin/dashboard.page';
 import { UserManagementPage } from '../pages/admin/user-management.page';
+import { DisabilityPersonManagementPage } from '../pages/admin/disability-person.page';
 
 type Fixtures = {
   adminLoginPage: AdminLoginPage;
   dashboardPage: DashboardPage;
   userManagementPage: UserManagementPage;
+  disabilityPersonPage: DisabilityPersonManagementPage;
 };
 
 export const test = base.extend<Fixtures>({
@@ -19,6 +21,9 @@ export const test = base.extend<Fixtures>({
   userManagementPage: async ({ page }, use) => {
     await use(new UserManagementPage(page));
   },
+  disabilityPersonPage: async ({ page }, use) => {
+    await use(new DisabilityPersonManagementPage(page));
+  },
 });
 
-export { expect } from '@playwright/test';
+export { expect } from '@playwright/test';