feat: 员工信息管理功能完善

- 将员工表org_no字段迁移至dept_id,关联系统部门表
- 更新员工信息相关DTO、VO和Controller,使用deptId替代orgNo
- 添加员工信息管理OpenSpec规范文档(proposal/design/spec/tasks)
- 更新API文档,反映部门关联变更
- 添加数据库迁移脚本employee_org_no_to_dept_id.sql
- 新增员工信息分页接口测试脚本(PowerShell/Python)
- 更新CLAUDE.md,添加MCP数据库工具使用说明

Co-Authored-By: Claude (glm-4.7) <noreply@anthropic.com>
This commit is contained in:
wkc
2026-01-28 16:57:38 +08:00
parent eac1112f9b
commit 47f9491941
23 changed files with 2090 additions and 40 deletions

View File

@@ -13,7 +13,13 @@
"Skill(document-skills:mcp-builder)",
"Bash(ping:*)",
"Bash(git commit:*)",
"Bash(taskkill:*)"
"Bash(taskkill:*)",
"Bash(cd:*)",
"mcp__database-server__read_query",
"mcp__database-server__list_tables",
"mcp__database-server__describe_table",
"mcp__database-server__list_insights",
"mcp__database-server__alter_table"
]
},
"enabledMcpjsonServers": [

View File

@@ -38,6 +38,7 @@
## 运行
- 使用mcp工具进行数据库相关操作
- 使用根目录中的ry.bat控制后端的启动不要自行在命令行中启动后端
- 测试方式为生成可执行的测试脚本
- 测试脚本在运行完成后需要保存所有接口输出并生成测试用例报告

View File

@@ -24,7 +24,7 @@
|--------|------|------|------|
| name | String | 否 | 姓名(模糊查询) |
| tellerNo | String | 否 | 柜员号(精确查询) |
| orgNo | String | 否 | 所属机构号 |
| deptId | Long | 否 | 所属部门ID |
| idCard | String | 否 | 身份证号(精确查询) |
| status | String | 否 | 状态0=在职, 1=离职) |
| pageNum | Integer | 否 | 页码默认1 |
@@ -40,7 +40,8 @@
"employeeId": 1,
"name": "张三",
"tellerNo": "001",
"orgNo": "1001",
"deptId": 100,
"deptName": "总部",
"idCard": "110101199001011234",
"phone": "13800138000",
"hireDate": "2020-01-01",
@@ -53,6 +54,22 @@
}
```
**响应字段说明**:
| 字段名 | 类型 | 说明 |
|--------|------|------|
| employeeId | Long | 员工ID |
| name | String | 姓名 |
| tellerNo | String | 柜员号 |
| deptId | Long | 所属部门ID |
| deptName | String | 所属部门名称(关联 sys_dept 表) |
| idCard | String | 身份证号 |
| phone | String | 电话 |
| hireDate | Date | 入职时间 |
| status | String | 状态0=在职, 1=离职) |
| statusDesc | String | 状态描述 |
| createTime | Date | 创建时间 |
---
### 2. 查询员工详情
@@ -76,7 +93,7 @@
"employeeId": 1,
"name": "张三",
"tellerNo": "001",
"orgNo": "1001",
"deptId": 100,
"idCard": "110101199001011234",
"phone": "13800138000",
"hireDate": "2020-01-01",
@@ -116,7 +133,7 @@ Authorization: Bearer {token}
{
"name": "张三",
"tellerNo": "001",
"orgNo": "1001",
"deptId": 100,
"idCard": "110101199001011234",
"phone": "13800138000",
"hireDate": "2020-01-01",
@@ -138,7 +155,7 @@ Authorization: Bearer {token}
|--------|------|------|------|----------|
| name | String | 是 | 姓名 | 最大100字符 |
| tellerNo | String | 是 | 柜员号 | 最大50字符唯一 |
| orgNo | String | 否 | 所属机构号 | 最大50字符 |
| deptId | Long | 否 | 所属部门ID | |
| idCard | String | 是 | 身份证号 | 18位符合国标唯一 |
| phone | String | 否 | 电话 | 11位手机号 |
| hireDate | Date | 否 | 入职时间 | yyyy-MM-dd |
@@ -176,7 +193,7 @@ Authorization: Bearer {token}
"employeeId": 1,
"name": "张三",
"tellerNo": "001",
"orgNo": "1001",
"deptId": 100,
"idCard": "110101199001011234",
"phone": "13800138000",
"hireDate": "2020-01-01",
@@ -266,9 +283,9 @@ Authorization: Bearer {token}
**Excel 格式**:
**Sheet1: 员工信息**
| 姓名 | 柜员号 | 所属机构号 | 身份证号 | 电话 | 入职时间 | 状态 |
| 姓名 | 柜员号 | 所属部门ID | 身份证号 | 电话 | 入职时间 | 状态 |
|------|--------|------------|----------|------|----------|------|
| 张三 | 001 | 1001 | 110101199001011234 | 13800138000 | 2020-01-01 | 在职 |
| 张三 | 001 | 100 | 110101199001011234 | 13800138000 | 2020-01-01 | 在职 |
**Sheet2: 亲属信息(可选)**
| 员工身份证号 | 亲属姓名 | 亲属身份证号 | 亲属手机号 | 与员工关系 |

View File

@@ -0,0 +1,223 @@
# Design: 员工信息管理前端设计
## 页面布局
### 主页面布局
```
┌─────────────────────────────────────────────────────────────┐
│ 信息维护 > 员工信息管理 │
├─────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 搜索区: [姓名] [柜员号] [所属机构号] [身份证号] [状态▼] │ │
│ │ [搜索] [重置] │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ [新增] [导入] [搜索框展开] │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ┌───┬─────┬───────┬───────┬─────────┬─────┬─────────┐ │ │
│ │ │□ │姓名 │柜员号 │身份证号│所属机构 │状态 │ 操作 │ │ │
│ │ ├───┼─────┼───────┼───────┼─────────┼─────┼─────────┤ │ │
│ │ │□ │张三 │001 │110... │1001 │在职 │详情|编辑│ │ │
│ │ │ │ │ │ │ │ │删除 │ │ │
│ │ └───┴─────┴───────┴───────┴─────────┴─────┴─────────┘ │ │
│ │ < 1 2 3 4 5 ... 10 > │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 新增/编辑弹窗布局
```
┌─────────────────────────────────────────────┐
│ 新增员工 / 编辑员工 [X]│
├─────────────────────────────────────────────┤
│ ┌───────────────────────────────────────┐ │
│ │ 姓名: [_____________] │ │
│ │ 柜员号: [_____________] │ │
│ │ 所属部门: [请选择部门 ▼] │ │
│ │ 身份证号: [__________________] │ │
│ │ 电话: [_____________] │ │
│ │ 入职时间: [_______________] │ │
│ │ 状态: ( ) 在职 ( ) 离职 │ │
│ └───────────────────────────────────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ 亲属信息 [+ 添加亲属] │ │
│ │ ┌───────────────────────────────────┐ │ │
│ │ │ 亲属姓名 | 身份证号 | 电话 | 关系 │ │ │
│ │ │ 李四 | 110... | 138..| 配偶 │ │ │
│ │ │ | [删除] | | │ │ │
│ │ └───────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
│ [确定] [取消] │
└─────────────────────────────────────────────┘
```
### 详情弹窗布局
```
┌─────────────────────────────────────────────┐
│ 员工详情 [X]│
├─────────────────────────────────────────────┤
│ ┌───────────────────────────────────────┐ │
│ │ 姓名: 张三 │ │
│ │ 柜员号: 001 │ │
│ │ 所属部门: 研发部门 │ │
│ │ 身份证号: 110101199001011234 │ │
│ │ 电话: 13800138000 │ │
│ │ 入职时间: 2020-01-01 │ │
│ │ 状态: 在职 │ │
│ │ 创建时间: 2026-01-28 10:00:00 │ │
│ └───────────────────────────────────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ 亲属信息 │ │
│ │ ┌───────────────────────────────────┐ │ │
│ │ │ 亲属姓名 │ 身份证号 │ 电话 │关系│ │ │
│ │ │ 李四 │ 110101... │ 138..│配偶│ │ │
│ │ │ │ │ │ │ │ │
│ │ └───────────────────────────────────┘ │ │
│ └───────────────────────────────────────┘ │
│ [关闭] │
└─────────────────────────────────────────────┘
```
## 组件结构
### 文件结构
```
ruoyi-ui/src/
├── api/
│ └── dpcEmployee.js # API 接口定义
└── views/
└── dpcEmployee/
└── index.vue # 主页面组件
```
### 数据模型
#### 员工列表项
```javascript
{
employeeId: Number,
name: String,
tellerNo: String,
orgNo: String, // 部门 ID (dept_id)
orgName: String, // 部门名称 (用于显示)
idCard: String,
phone: String,
hireDate: String,
status: String,
statusDesc: String,
createTime: String
}
```
#### 员工表单
```javascript
{
employeeId: Number | null,
name: String,
tellerNo: String,
orgNo: String, // 部门 ID (dept_id)
idCard: String,
phone: String,
hireDate: String,
status: String,
relatives: Array<{
relativeId: Number | null,
relativeName: String,
relativeIdCard: String,
relativePhone: String,
relationship: String
}>
}
```
### API 接口
| 接口名 | 方法 | 路径 | 说明 |
|--------|------|------|------|
| listEmployee | GET | /dpc/employee/list | 查询员工列表 |
| getEmployee | GET | /dpc/employee/{id} | 获取员工详情 |
| addEmployee | POST | /dpc/employee | 新增员工 |
| updateEmployee | PUT | /dpc/employee | 编辑员工 |
| delEmployee | DELETE | /dpc/employee/{ids} | 删除员工 |
| importTemplate | POST | /dpc/employee/importTemplate | 下载导入模板 |
| importData | POST | /dpc/employee/importData | 导入员工信息 |
## 交互设计
### 1. 列表页面
- **默认状态**: 显示所有员工列表,默认分页大小 10
- **搜索**: 支持按姓名(模糊)、柜员号(精确)、所属部门、身份证号(精确)、状态筛选
- **所属部门筛选**: 使用部门树选择器
- **新增**: 点击"新增"按钮打开新增弹窗
- **编辑**: 点击"编辑"按钮打开编辑弹窗
- **详情**: 点击"详情"按钮打开详情弹窗
- **删除**: 点击"删除"按钮,弹出确认对话框后删除
- **导入**: 点击"导入"按钮,打开导入弹窗
### 2. 新增/编辑弹窗
- **所属部门**: 使用部门树选择器(`el-tree-select`),存储 `dept_id`
- **亲属管理**:
- 点击"+ 添加亲属"添加空行
- 点击"删除"移除对应亲属行
- 亲属姓名、关系为必填
- **表单验证**:
- 姓名: 必填最大100字符
- 柜员号: 必填最大50字符
- 所属部门: 选填
- 身份证号: 必填18位格式校验
- 电话: 选填11位手机号格式
- 亲属姓名: 必填
- 亲属关系: 必填
### 3. 详情弹窗
- 只读展示员工信息和亲属列表
- 无亲属时显示"暂无亲属信息"
### 4. 导入弹窗
- 支持拖拽上传
- 支持"更新已存在数据"选项
- 显示导入结果消息
## 表单验证规则
### 员工基本信息
| 字段 | 验证规则 |
|------|----------|
| 姓名 | 必填最大100字符 |
| 柜员号 | 必填最大50字符 |
| 身份证号 | 必填18位符合国标 |
| 电话 | 选填11位手机号 |
| 入职时间 | 选填,日期格式 |
| 状态 | 必填0或1 |
### 亲属信息
| 字段 | 验证规则 |
|------|----------|
| 亲属姓名 | 必填 |
| 亲属身份证号 | 选填 |
| 亲属电话 | 选填11位手机号 |
| 与员工关系 | 必填 |
## 权限控制
| 按钮 | 权限标识 |
|------|----------|
| 新增 | dpc:employee:add |
| 编辑 | dpc:employee:edit |
| 删除 | dpc:employee:remove |
| 详情 | dpc:employee:query |
| 导入 | dpc:employee:import |
## 技术实现要点
1. **亲属子表单**: 使用 `v-for` 动态渲染亲属列表,支持增删操作
2. **部门选择器**: 使用 Element UI 的 `el-tree-select` 组件,关联 `sys_dept`
3. **日期选择器**: 使用 Element UI 的 `el-date-picker`
4. **状态下拉**: 使用 `el-select``el-radio-group`
5. **文件上传**: 使用 Element UI 的 `el-upload`
6. **分页**: 使用若依框架的 `pagination` 组件
## 参考
- 中介库管理页面: `ruoyi-ui/src/views/dpcIntermediary/index.vue`
- 若依用户管理页面: `ruoyi-ui/src/views/system/user/index.vue`

View File

@@ -0,0 +1,119 @@
# Proposal: 添加员工信息管理前端页面
## Change ID
`add-employee-info-ui`
## Summary
为员工信息管理模块开发前端页面,实现员工及其亲属信息的可视化管理。包括员工列表查询、新增/编辑员工支持亲属管理、详情查看、Excel 导入等功能。页面将放置在"信息维护"菜单下。
## Motivation
员工信息管理的后端 API 已在 `add-employee-info` 变更中完成开发。现在需要开发前端页面以提供用户交互界面。
用户需要:
1. 可视化查看和管理员工信息列表
2. 创建员工时同时维护亲属信息
3. 查看员工详情时展示所有亲属
4. 支持批量导入员工数据
5. 在"信息维护"菜单下访问此功能
## Scope
本提案实现以下前端功能:
### 包含的功能
- **员工列表页面**
- 搜索筛选区(姓名、柜员号、所属部门、身份证号、状态)
- 员工列表表格(分页显示)
- 操作按钮(新增、导入)
- 部门树选择器
- **新增员工弹窗**
- 员工基本信息表单
- 所属部门选择(关联 sys_dept 表)
- 亲属列表子表单(支持动态增删)
- 表单验证
- **编辑员工弹窗**
- 员工基本信息表单
- 所属部门选择
- 亲属列表子表单(支持动态增删)
- 表单验证
- **员工详情弹窗**
- 员工基本信息展示(只读)
- 显示部门名称
- 亲属列表展示(只读)
- **导入功能**
- 导入弹窗
- Excel 文件上传
- 导入结果反馈
- 模板下载
### 明确排除
- 菜单数据的数据库插入(需要前端开发完成后手动配置)
- 员工信息与其他模块的联动(属于后续功能)
## Proposed Design
详见 [design.md](./design.md)
## Alternatives Considered
### 选项1参考中介库管理页面实现
**优点**
- 保持项目内 UI/UX 一致性
- 复用已有的代码模式
- 开发效率高
**缺点**
- 需要额外实现亲属子表单功能
**决定**:采用。参考 `dpcIntermediary` 页面的实现模式,在此基础上增加亲属子表单功能。
### 选项2使用若依代码生成器
**优点**
- 快速生成基础 CRUD 页面
**缺点**
- 生成代码需要大量调整以支持亲属子表单
- 与现有前端风格可能不一致
**决定**:不采用。手动开发可以更好地实现亲属子表单功能,保持与中介库页面一致的风格。
## Impact
### 前端影响
- 新增视图:`ruoyi-ui/src/views/dpcEmployee/`
- `index.vue` - 员工列表页面
- 新增 API`ruoyi-ui/src/api/dpcEmployee.js`
### 后端影响
无,后端 API 已在 `add-employee-info` 中完成。
### 数据库影响
- 需要在 `sys_menu` 表中插入菜单数据(开发完成后手动配置)
## Dependencies
- 依赖 `add-employee-info` 变更完成后端 API
- 依赖若依框架的前端组件Element UI
## Related Changes
- `add-employee-info` - 提供后端 API 接口
## Open Questions
## Success Criteria
- [ ] 用户可以在"信息维护"菜单下访问员工信息管理页面
- [ ] 用户可以查看员工列表并支持分页
- [ ] 用户可以按多种条件搜索员工
- [ ] 用户可以新增员工并添加亲属
- [ ] 用户可以编辑员工信息和亲属
- [ ] 用户可以查看员工详情和亲属列表
- [ ] 用户可以删除员工
- [ ] 用户可以下载 Excel 导入模板
- [ ] 用户可以导入员工数据
## References
- [员工信息管理 API 文档](../../../doc/员工信息管理API文档.md)
- [模块设计文档 - 信息维护模块](../../../doc/modules/03-信息维护模块.md)
- [add-employee-info 变更](../add-employee-info/)

View File

@@ -0,0 +1,475 @@
# Spec: 员工信息管理前端用户界面
## ADDED Requirements
### Requirement: 前端SHALL提供员工信息列表页面
前端MUST提供员工信息列表页面允许用户查看、搜索和管理员工信息。
#### Scenario: 页面加载时显示员工列表
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**When** 用户访问员工信息管理页面
**Then** 页面应显示员工列表
**And** 列表应默认显示第1页每页10条记录
**And** 页面应显示搜索筛选区和操作按钮
#### Scenario: 列表显示正确的员工信息
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**When** 系统返回员工列表数据
**Then** 列表应显示以下列:姓名、柜员号、身份证号、所属部门、电话、状态、操作
**And** 状态列应显示"在职"或"离职"的文字描述
**And** 操作列应显示"详情"、"编辑"、"删除"按钮
#### Scenario: 列表支持分页
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**And** 系统中存在25条员工记录
**When** 用户访问员工信息管理页面
**Then** 页面应显示分页控件
**And** 默认显示第1页的10条记录
**When** 用户点击第2页
**Then** 页面应显示第11-20条记录
#### Scenario: 搜索筛选区默认展开
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**When** 用户访问员工信息管理页面
**Then** 搜索筛选区应默认显示
**And** 搜索筛选区应包含:姓名输入框、柜员号输入框、所属部门选择器、身份证号输入框、状态下拉框
---
### Requirement: 前端SHALL支持多条件搜索员工
前端MUST支持用户通过多种条件组合搜索员工信息。
#### Scenario: 按姓名模糊搜索
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**And** 系统中存在姓名为"张三"的员工
**When** 用户在姓名输入框输入"张"
**And** 点击"搜索"按钮
**Then** 列表应仅显示姓名中包含"张"的员工记录
**And** 分页控件应显示匹配的记录总数
#### Scenario: 按柜员号精确搜索
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**And** 系统中存在柜员号为"001"的员工
**When** 用户在柜员号输入框输入"001"
**And** 点击"搜索"按钮
**Then** 列表应仅显示柜员号为"001"的员工记录
#### Scenario: 按状态筛选
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**And** 系统中存在在职和离职两种状态的员工
**When** 用户在状态下拉框选择"在职"
**And** 点击"搜索"按钮
**Then** 列表应仅显示状态为"在职"的员工记录
#### Scenario: 按所属部门筛选
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**And** 系统中存在多个部门的员工
**When** 用户在所属部门选择器中选择"研发部门"
**And** 点击"搜索"按钮
**Then** 列表应仅显示该部门的员工记录
#### Scenario: 组合条件搜索
**Given** 用户已登录系统且具有 `dpc:employee:list` 权限
**When** 用户同时输入姓名"张"、柜员号"001"
**And** 点击"搜索"按钮
**Then** 列表应仅显示同时满足所有条件的记录
#### Scenario: 重置搜索条件
**Given** 用户已输入多个搜索条件并执行了搜索
**When** 用户点击"重置"按钮
**Then** 所有搜索条件输入框应清空
**And** 列表应显示所有员工记录(恢复默认)
---
### Requirement: 前端SHALL提供新增员工弹窗
前端MUST提供新增员工弹窗允许用户输入员工基本信息并添加亲属信息。
#### Scenario: 点击新增按钮打开弹窗
**Given** 用户已登录系统且具有 `dpc:employee:add` 权限
**When** 用户点击"新增"按钮
**Then** 应打开新增员工弹窗
**And** 弹窗标题为"添加员工"
**And** 所有表单字段应为空
**And** 亲属列表应为空
#### Scenario: 填写员工基本信息
**Given** 新增员工弹窗已打开
**When** 用户填写姓名为"张三"
**And** 填写柜员号为"001"
**And** 在所属部门选择器中选择"研发部门"
**And** 填写身份证号为"110101199001011234"
**And** 填写电话为"13800138000"
**And** 选择入职时间为"2020-01-01"
**And** 选择状态为"在职"
**Then** 表单应正确显示填写的信息
#### Scenario: 添加单个亲属
**Given** 新增员工弹窗已打开
**And** 用户已填写员工基本信息
**When** 用户点击"+ 添加亲属"按钮
**Then** 亲属列表应新增一个空行
**When** 用户填写亲属姓名为"李四"
**And** 填写亲属身份证号为"110101199001011235"
**And** 填写亲属电话为"13800138001"
**And** 选择关系为"配偶"
**Then** 亲属列表应显示该亲属信息
#### Scenario: 添加多个亲属
**Given** 新增员工弹窗已打开
**And** 用户已填写员工基本信息
**When** 用户点击3次"+ 添加亲属"按钮
**And** 填写3个亲属的信息
**Then** 亲属列表应显示3个亲属
**And** 每个亲属应有独立的删除按钮
#### Scenario: 删除亲属
**Given** 新增员工弹窗已打开
**And** 亲属列表中存在2个亲属
**When** 用户点击第一个亲属的"删除"按钮
**Then** 亲属列表应仅显示1个亲属
**And** 被删除的亲属应从列表中移除
#### Scenario: 提交成功
**Given** 新增员工弹窗已打开
**And** 用户已填写所有必填信息
**When** 用户点击"确定"按钮
**Then** 系统应发送请求到后端
**And** 弹窗应关闭
**And** 应显示"新增成功"的提示消息
**And** 列表应刷新并显示新增的记录
#### Scenario: 表单验证-姓名为空
**Given** 新增员工弹窗已打开
**When** 用户不填写姓名
**And** 点击"确定"按钮
**Then** 系统应显示"姓名不能为空"的错误提示
**And** 弹窗应保持打开状态
**And** 不发送请求到后端
#### Scenario: 表单验证-身份证号格式错误
**Given** 新增员工弹窗已打开
**When** 用户填写身份证号为"123"
**And** 点击"确定"按钮
**Then** 系统应显示"身份证号格式不正确"的错误提示
**And** 弹窗应保持打开状态
#### Scenario: 亲属姓名为空验证
**Given** 新增员工弹窗已打开
**And** 用户已添加一个亲属但未填写亲属姓名
**When** 用户点击"确定"按钮
**Then** 系统应显示"亲属姓名不能为空"的错误提示
#### Scenario: 取消新增
**Given** 新增员工弹窗已打开
**And** 用户已填写部分信息
**When** 用户点击"取消"按钮
**Then** 弹窗应关闭
**And** 不保存任何数据
**And** 列表应保持原状态
---
### Requirement: 前端SHALL提供编辑员工弹窗
前端MUST提供编辑员工弹窗允许用户修改员工信息和亲属信息。
#### Scenario: 点击编辑按钮打开弹窗
**Given** 用户已登录系统且具有 `dpc:employee:edit` 权限
**And** 列表中存在一条员工记录
**When** 用户点击该记录的"编辑"按钮
**Then** 应打开编辑员工弹窗
**And** 弹窗标题为"修改员工"
**And** 表单应显示该员工的现有信息
**And** 亲属列表应显示该员工的所有亲属
#### Scenario: 修改员工基本信息
**Given** 编辑员工弹窗已打开
**When** 用户修改姓名为"李四"
**And** 点击"确定"按钮
**Then** 系统应发送更新请求到后端
**And** 弹窗应关闭
**And** 应显示"修改成功"的提示消息
**And** 列表应刷新并显示更新后的姓名
#### Scenario: 编辑时新增亲属
**Given** 编辑员工弹窗已打开
**And** 该员工当前有1个亲属
**When** 用户点击"+ 添加亲属"按钮
**And** 填写新亲属信息
**And** 点击"确定"按钮
**Then** 系统应发送更新请求到后端
**And** 再次打开编辑弹窗时应显示2个亲属
#### Scenario: 编辑时删除亲属
**Given** 编辑员工弹窗已打开
**And** 该员工当前有2个亲属
**When** 用户点击其中一个亲属的"删除"按钮
**And** 点击"确定"按钮
**Then** 系统应发送更新请求到后端
**And** 再次打开编辑弹窗时应仅显示1个亲属
#### Scenario: 编辑时修改亲属信息
**Given** 编辑员工弹窗已打开
**And** 该员工有亲属"李四"
**When** 用户修改亲属姓名为"王五"
**And** 点击"确定"按钮
**Then** 系统应发送更新请求到后端
**And** 再次打开编辑弹窗时应显示亲属姓名为"王五"
#### Scenario: 编辑时清除所有亲属
**Given** 编辑员工弹窗已打开
**And** 该员工有亲属信息
**When** 用户逐个删除所有亲属
**And** 点击"确定"按钮
**Then** 系统应发送更新请求到后端
**And** 再次打开编辑弹窗时亲属列表应为空
---
### Requirement: 前端SHALL提供员工详情弹窗
前端MUST提供员工详情弹窗以只读方式展示员工完整信息和亲属列表。
#### Scenario: 点击详情按钮打开弹窗
**Given** 用户已登录系统且具有 `dpc:employee:query` 权限
**And** 列表中存在一条员工记录
**When** 用户点击该记录的"详情"按钮
**Then** 应打开员工详情弹窗
**And** 弹窗标题为"员工详情"
**And** 所有字段应为只读状态
#### Scenario: 显示员工基本信息
**Given** 员工详情弹窗已打开
**Then** 应显示以下信息:姓名、柜员号、所属部门、身份证号、电话、入职时间、状态、创建时间
**And** 状态应显示为"在职"或"离职"的文字描述
**And** 所有字段应不可编辑
#### Scenario: 显示亲属列表
**Given** 员工详情弹窗已打开
**And** 该员工有2个亲属
**Then** 亲属列表应显示2个亲属
**And** 每个亲属应显示:亲属姓名、亲属身份证号、亲属电话、与员工关系
**And** 亲属列表应不可编辑
#### Scenario: 无亲属时显示提示
**Given** 员工详情弹窗已打开
**And** 该员工无亲属信息
**Then** 亲属列表区域应显示"暂无亲属信息"的提示
---
### Requirement: 前端SHALL支持删除员工
前端MUST支持删除员工操作包括单个删除和批量删除。
#### Scenario: 删除单条记录
**Given** 用户已登录系统且具有 `dpc:employee:remove` 权限
**And** 列表中存在一条员工记录
**When** 用户点击该记录的"删除"按钮
**Then** 应弹出确认对话框,提示是否确认删除
**When** 用户确认删除
**Then** 系统应发送删除请求到后端
**And** 应显示"删除成功"的提示消息
**And** 列表应刷新,该记录应从列表中移除
#### Scenario: 取消删除
**Given** 用户已点击"删除"按钮
**And** 确认对话框已显示
**When** 用户点击"取消"按钮
**Then** 对话框应关闭
**And** 不发送删除请求
**And** 记录应保留在列表中
#### Scenario: 批量删除
**Given** 用户已登录系统且具有 `dpc:employee:remove` 权限
**And** 列表中存在多条员工记录
**When** 用户勾选3条记录
**And** 点击"删除"按钮
**And** 确认删除
**Then** 系统应发送批量删除请求到后端
**And** 应显示"删除成功"的提示消息
**And** 列表应刷新,被删除的记录应从列表中移除
---
### Requirement: 前端SHALL支持导入员工数据
前端MUST支持通过 Excel 文件批量导入员工数据。
#### Scenario: 点击导入按钮打开导入弹窗
**Given** 用户已登录系统且具有 `dpc:employee:import` 权限
**When** 用户点击"导入"按钮
**Then** 应打开导入数据弹窗
**And** 弹窗标题为"员工数据导入"
**And** 应显示文件上传区域
#### Scenario: 上传Excel文件
**Given** 导入弹窗已打开
**When** 用户选择一个 Excel 文件
**And** 点击"确定"按钮
**Then** 系统应上传文件到后端
**And** 应显示上传进度
**And** 上传完成后应显示导入结果消息
#### Scenario: 导入成功提示
**Given** 用户上传了一个包含有效数据的 Excel 文件
**When** 文件上传完成
**Then** 应显示导入成功消息,包含成功导入的记录数
**And** 弹窗应自动关闭
**And** 列表应刷新,显示新导入的记录
#### Scenario: 导入部分失败提示
**Given** 用户上传了一个包含10条数据的 Excel 文件
**And** 其中2条数据格式错误
**When** 文件上传完成
**Then** 应显示导入结果消息说明成功8条失败2条
**And** 消息中应包含失败行的错误详情
#### Scenario: 更新已存在数据选项
**Given** 导入弹窗已打开
**When** 用户勾选"是否更新已经存在的员工数据"选项
**And** 上传文件
**Then** 请求应携带 `updateSupport=true` 参数
#### Scenario: 下载导入模板
**Given** 导入弹窗已打开
**When** 用户点击"下载模板"链接
**Then** 浏览器应下载 Excel 模板文件
**And** 导入弹窗应保持打开
#### Scenario: 支持拖拽上传
**Given** 导入弹窗已打开
**When** 用户将 Excel 文件拖拽到上传区域
**Then** 文件应自动添加到上传队列
---
### Requirement: 前端SHALL根据权限控制按钮显示
前端MUST根据用户权限显示或隐藏相应的操作按钮。
#### Scenario: 无新增权限时隐藏新增按钮
**Given** 用户已登录系统
**And** 该用户不具有 `dpc:employee:add` 权限
**When** 用户访问员工信息管理页面
**Then** "新增"按钮应隐藏或禁用
#### Scenario: 无编辑权限时隐藏编辑按钮
**Given** 用户已登录系统
**And** 该用户不具有 `dpc:employee:edit` 权限
**When** 用户访问员工信息管理页面
**Then** 列表操作列中的"编辑"按钮应隐藏或禁用
#### Scenario: 无删除权限时隐藏删除按钮
**Given** 用户已登录系统
**And** 该用户不具有 `dpc:employee:remove` 权限
**When** 用户访问员工信息管理页面
**Then** 列表操作列中的"删除"按钮应隐藏或禁用
#### Scenario: 无导入权限时隐藏导入按钮
**Given** 用户已登录系统
**And** 该用户不具有 `dpc:employee:import` 权限
**When** 用户访问员工信息管理页面
**Then** "导入"按钮应隐藏或禁用
---
### Requirement: 前端SHALL提供友好的用户反馈
前端MUST在各种操作中提供适当的用户反馈。
#### Scenario: 加载状态显示
**Given** 用户执行任何需要请求后端的操作
**When** 请求正在处理中
**Then** 列表或弹窗应显示加载状态(如 loading 遮罩)
#### Scenario: 操作成功提示
**Given** 用户执行新增、修改或删除操作
**When** 操作成功
**Then** 应显示成功提示消息
**And** 提示消息应在几秒后自动消失
#### Scenario: 操作失败提示
**Given** 用户执行新增、修改或删除操作
**When** 操作失败
**Then** 应显示错误提示消息
**And** 错误消息应说明失败原因
#### Scenario: 网络错误处理
**Given** 用户执行任何需要请求后端的操作
**When** 网络请求失败
**Then** 应显示网络错误提示消息
**And** 弹窗应保持打开状态(如果是新增/编辑操作)
---
### Requirement: 前端SHALL支持响应式布局
前端MUST确保页面在不同屏幕尺寸下正常显示。
#### Scenario: 小屏幕适配
**Given** 用户使用小屏幕设备访问页面
**When** 页面加载完成
**Then** 搜索筛选区应合理布局
**And** 表格应支持横向滚动
**And** 弹窗应适配屏幕尺寸
#### Scenario: 大屏幕优化
**Given** 用户使用大屏幕设备访问页面
**When** 页面加载完成
**Then** 搜索筛选区应在一行内显示
**And** 表格应充分利用屏幕宽度

View File

@@ -0,0 +1,291 @@
# Tasks: 员工信息管理前端页面
## Overview
本文档将员工信息管理前端页面的开发工作分解为可验证的任务项。任务按照依赖关系排序,确保开发过程顺畅。
---
## 1. API 接口层开发
### 1.1 创建 API 接口文件
- [ ] 创建 `ruoyi-ui/src/api/dpcEmployee.js` 文件
- [ ] 实现 `listEmployee(query)` - 查询员工列表
- [ ] 实现 `getEmployee(employeeId)` - 获取员工详情
- [ ] 实现 `addEmployee(data)` - 新增员工
- [ ] 实现 `updateEmployee(data)` - 编辑员工
- [ ] 实现 `delEmployee(employeeIds)` - 删除员工
- [ ] 实现 `importTemplate()` - 下载导入模板
- [ ] 实现 `importData(data, updateSupport)` - 导入员工信息
**验证**: API 文件包含所有7个接口方法路径与后端 API 文档一致。
---
## 2. 主页面组件开发
### 2.1 创建主页面文件
- [ ] 创建 `ruoyi-ui/src/views/dpcEmployee/` 目录
- [ ] 创建 `ruoyi-ui/src/views/dpcEmployee/index.vue` 文件
**验证**: 文件创建成功,可正常访问。
### 2.2 实现页面基础结构
- [ ] 实现 `<template>` 基础布局
- [ ] 实现搜索筛选区的表单(含部门树选择器)
- [ ] 实现操作按钮区(新增、导入)
- [ ] 实现员工列表表格
- [ ] 实现分页组件
**验证**: 页面可正常渲染,布局符合设计文档。
### 2.3 实现数据管理逻辑
- [ ] 定义 `data()` 数据结构
- `loading` - 加载状态
- `employeeList` - 员工列表数据
- `total` - 总记录数
- `queryParams` - 查询参数
- `showSearch` - 搜索区显示状态
- [ ] 实现 `getList()` 方法 - 查询员工列表
- [ ] 实现 `handleQuery()` 方法 - 搜索
- [ ] 实现 `resetQuery()` 方法 - 重置搜索条件
**验证**: 页面加载时正确显示员工列表,搜索和重置功能正常。
### 2.4 实现列表表格
- [ ] 添加表格列:复选框、姓名、柜员号、身份证号、所属部门、电话、状态、操作
- [ ] 实现状态标签显示(在职/离职)
- [ ] 实现操作列按钮:详情、编辑、删除
- [ ] 实现 `handleSelectionChange()` 方法 - 多选处理
**验证**: 列表正确显示所有字段,状态标签正确着色。
---
## 3. 新增/编辑弹窗开发
### 3.1 实现弹窗结构
- [ ] 添加新增/编辑弹窗的 `<el-dialog>`
- [ ] 实现弹窗标题动态显示
- [ ] 实现员工基本信息表单
- 姓名输入框必填最大100字符
- 柜员号输入框必填最大50字符
- 所属部门选择器el-tree-select关联 sys_dept
- 身份证号输入框必填18位校验
- 电话输入框选填11位手机号校验
- 入职时间日期选择器
- 状态单选框组(在职/离职)
**验证**: 弹窗可正常打开和关闭,表单字段正确显示。
### 3.2 实现亲属子表单
- [ ] 添加亲属信息区域
- [ ] 实现"+ 添加亲属"按钮
- [ ] 实现亲属列表表格(亲属姓名、身份证号、电话、关系、操作)
- [ ] 实现亲属行的删除按钮
- [ ] 添加 `relatives` 数据字段
**验证**: 可以动态添加和删除亲属行。
### 3.3 实现表单验证
- [ ] 定义 `rules` 验证规则
- 姓名必填最大100字符
- 柜员号必填最大50字符
- 身份证号必填格式校验18位
- 电话选填格式校验11位手机号
- 亲属姓名:必填
- [ ] 实现 `reset()` 方法 - 重置表单
- [ ] 实现 `cancel()` 方法 - 取消操作
**验证**: 表单验证规则正确触发,错误提示显示正确。
### 3.4 实现新增逻辑
- [ ] 实现 `handleAdd()` 方法 - 打开新增弹窗
- [ ] 实现 `submitForm()` 方法 - 提交新增
- [ ] 集成 `addEmployee` API
- [ ] 处理成功/失败响应
**验证**: 可以成功新增员工及其亲属信息。
### 3.5 实现编辑逻辑
- [ ] 实现 `handleUpdate(row)` 方法 - 打开编辑弹窗
- [ ] 集成 `getEmployee` API 获取详情
- [ ] 回显员工信息和亲属列表
- [ ] 修改 `submitForm()` 方法处理编辑提交
- [ ] 集成 `updateEmployee` API
**验证**: 可以成功编辑员工信息和亲属信息,数据正确回显。
---
## 4. 详情弹窗开发
### 4.1 实现详情弹窗结构
- [ ] 添加详情弹窗的 `<el-dialog>`
- [ ] 实现员工基本信息展示区(只读)
- [ ] 实现亲属列表展示区(只读)
- [ ] 实现"暂无亲属信息"提示
**验证**: 详情弹窗正确显示员工信息和亲属列表。
### 4.2 实现详情查看逻辑
- [ ] 添加 `detailOpen` 数据字段
- [ ] 添加 `employeeDetail` 数据字段
- [ ] 实现 `handleDetail(row)` 方法
- [ ] 集成 `getEmployee` API
**验证**: 点击详情按钮可正确查看员工完整信息。
---
## 5. 删除功能开发
### 5.1 实现删除逻辑
- [ ] 实现 `handleDelete(row)` 方法
- [ ] 添加删除确认对话框
- [ ] 集成 `delEmployee` API
- [ ] 处理成功/失败响应
- [ ] 删除后刷新列表
**验证**: 可以成功删除员工记录,删除确认对话框正常显示。
---
## 6. 导入功能开发
### 6.1 实现导入弹窗
- [ ] 添加导入弹窗的 `<el-dialog>`
- [ ] 实现 `<el-upload>` 组件
- [ ] 配置上传参数headers, action, 限制文件类型)
- [ ] 实现"更新已存在数据"复选框
- [ ] 实现"下载模板"链接
**验证**: 导入弹窗正常显示,可以正确选择文件。
### 6.2 实现导入逻辑
- [ ] 添加 `upload` 数据对象
- [ ] 实现 `handleImport()` 方法 - 打开导入弹窗
- [ ] 实现 `importTemplate()` 方法 - 下载模板
- [ ] 实现 `handleFileUploadProgress()` - 上传进度
- [ ] 实现 `handleFileSuccess()` - 上传成功处理
- [ ] 实现 `submitFileForm()` - 提交文件
**验证**: 可以成功下载模板,上传文件后正确显示导入结果。
---
## 7. 权限控制
### 7.1 实现按钮权限控制
- [ ] 新增按钮添加 `v-hasPermi="['dpc:employee:add']"`
- [ ] 编辑按钮添加 `v-hasPermi="['dpc:employee:edit']"`
- [ ] 删除按钮添加 `v-hasPermi="['dpc:employee:remove']"`
- [ ] 详情按钮添加 `v-hasPermi="['dpc:employee:query']"`
- [ ] 导入按钮添加 `v-hasPermi="['dpc:employee:import']"`
**验证**: 按钮根据用户权限正确显示或隐藏。
---
## 8. 样式和交互优化
### 9.1 实现加载状态
- [ ] 列表加载时显示 loading 遮罩
- [ ] 提交表单时禁用提交按钮
- [ ] 文件上传时显示上传进度
**验证**: 各种加载状态正确显示。
### 9.2 实现用户反馈
- [ ] 操作成功显示成功提示
- [ ] 操作失败显示错误提示
- [ ] 导入结果显示详细信息
**验证**: 用户反馈消息清晰友好。
### 9.3 响应式布局
- [ ] 搜索筛选区响应式布局
- [ ] 表格支持横向滚动
- [ ] 弹窗适配小屏幕
**验证**: 页面在不同屏幕尺寸下正常显示。
---
## 10. 测试和修复
### 9.1 功能测试
- [ ] 测试列表查询和分页
- [ ] 测试搜索和筛选功能
- [ ] 测试新增员工(无亲属)
- [ ] 测试新增员工(单个亲属)
- [ ] 测试新增员工(多个亲属)
- [ ] 测试编辑员工基本信息
- [ ] 测试编辑员工亲属(新增、修改、删除)
- [ ] 测试查看详情
- [ ] 测试删除员工(单个)
- [ ] 测试批量删除
- [ ] 测试下载导入模板
- [ ] 测试导入数据(成功场景)
- [ ] 测试导入数据(部分失败场景)
- [ ] 测试表单验证(各种错误场景)
### 9.2 浏览器兼容性测试
- [ ] Chrome 测试
- [ ] Firefox 测试
- [ ] Edge 测试
### 9.3 Bug 修复
- [ ] 修复测试中发现的问题
---
## 10. 菜单配置
### 11.1 数据库菜单配置
- [ ]`sys_menu` 表中插入"信息维护"父菜单(如果不存在)
- [ ]`sys_menu` 表中插入"员工信息管理"子菜单
- [ ] 配置菜单路由为 `dpcEmployee`
- [ ] 配置菜单组件为 `dpcEmployee/index`
- [ ] 配置菜单权限标识
- [ ] 配置菜单图标
**验证**: 菜单正确显示,点击可跳转到员工信息管理页面。
---
## 依赖关系
```
1. API 接口层 (1.1)
2. 主页面组件基础 (2.1 - 2.4)
3. 新增/编辑弹窗 (3.1 - 3.5) ← 并行
4. 详情弹窗 (4.1 - 4.2) ← 并行
5. 删除功能 (5.1) ← 并行
6. 导入功能 (6.1 - 6.2) ← 并行
7. 权限控制 (7.1)
8. 样式优化 (8.1 - 8.3)
9. 测试和修复 (9.1 - 9.3)
10. 菜单配置 (10.1)
```
## 预估工作量
| 任务组 | 预估时间 |
|--------|----------|
| API 接口层 | 0.5 小时 |
| 主页面组件 | 1.5 小时 |
| 新增/编辑弹窗 | 3 小时 |
| 详情弹窗 | 1 小时 |
| 删除功能 | 0.5 小时 |
| 导入功能 | 1.5 小时 |
| 权限控制 | 0.5 小时 |
| 样式优化 | 1 小时 |
| 测试和修复 | 1 小时 |
| 菜单配置 | 0.5 小时 |
| **总计** | **11 小时** |

View File

@@ -143,7 +143,7 @@ public class DpcEmployeeController extends BaseController {
DpcEmployeeAddDTO dto = new DpcEmployeeAddDTO();
dto.setName(entity.getName());
dto.setTellerNo(entity.getTellerNo());
dto.setOrgNo(entity.getOrgNo());
dto.setDeptId(entity.getDeptId());
dto.setIdCard(entity.getIdCard());
dto.setPhone(entity.getPhone());
dto.setHireDate(entity.getHireDate());

View File

@@ -35,9 +35,9 @@ public class DpcEmployee implements Serializable {
@Excel(name = "柜员号")
private String tellerNo;
/** 所属机构号 */
@Excel(name = "所属机构号")
private String orgNo;
/** 所属部门ID */
@Excel(name = "所属部门ID")
private Long deptId;
/** 身份证号 */
@Excel(name = "身份证号")

View File

@@ -35,10 +35,9 @@ public class DpcEmployeeAddDTO implements Serializable {
@Size(max = 50, message = "柜员号长度不能超过50个字符")
private String tellerNo;
/** 所属机构号 */
@Excel(name = "所属机构号")
@Size(max = 50, message = "所属机构号长度不能超过50个字符")
private String orgNo;
/** 所属部门ID */
@Excel(name = "所属部门ID")
private Long deptId;
/** 身份证号 */
@Excel(name = "身份证号")

View File

@@ -37,10 +37,9 @@ public class DpcEmployeeEditDTO implements Serializable {
@Size(max = 50, message = "柜员号长度不能超过50个字符")
private String tellerNo;
/** 所属机构号 */
@Excel(name = "所属机构号")
@Size(max = 50, message = "所属机构号长度不能超过50个字符")
private String orgNo;
/** 所属部门ID */
@Excel(name = "所属部门ID")
private Long deptId;
/** 身份证号 */
@Excel(name = "身份证号")

View File

@@ -23,8 +23,8 @@ public class DpcEmployeeQueryDTO implements Serializable {
/** 柜员号(精确查询) */
private String tellerNo;
/** 所属机构号 */
private String orgNo;
/** 所属部门ID */
private Long deptId;
/** 身份证号(精确查询) */
private String idCard;

View File

@@ -28,8 +28,11 @@ public class DpcEmployeeVO implements Serializable {
/** 柜员号 */
private String tellerNo;
/** 所属机构号 */
private String orgNo;
/** 所属部门ID */
private Long deptId;
/** 所属部门名称 */
private String deptName;
/** 身份证号 */
private String idCard;

View File

@@ -1,7 +1,9 @@
package com.ruoyi.dpc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.dpc.domain.DpcEmployee;
import com.ruoyi.dpc.domain.dto.DpcEmployeeQueryDTO;
import com.ruoyi.dpc.domain.vo.DpcEmployeeVO;
import org.apache.ibatis.annotations.Param;
@@ -13,6 +15,16 @@ import org.apache.ibatis.annotations.Param;
*/
public interface DpcEmployeeMapper extends BaseMapper<DpcEmployee> {
/**
* 分页查询员工列表(包含部门名称)
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 员工VO分页结果
*/
Page<DpcEmployeeVO> selectEmployeePageWithDept(@Param("page") Page<DpcEmployeeVO> page,
@Param("query") DpcEmployeeQueryDTO queryDTO);
/**
* 查询员工详情(包含亲属列表)
*

View File

@@ -63,18 +63,20 @@ public class DpcEmployeeServiceImpl implements IDpcEmployeeService {
*/
@Override
public Page<DpcEmployeeVO> selectEmployeePage(Page<DpcEmployee> page, DpcEmployeeQueryDTO queryDTO) {
LambdaQueryWrapper<DpcEmployee> wrapper = buildQueryWrapper(queryDTO);
Page<DpcEmployee> resultPage = employeeMapper.selectPage(page, wrapper);
// 使用关联查询获取部门名称
Page<DpcEmployeeVO> voPage = new Page<>(page.getCurrent(), page.getSize());
Page<DpcEmployeeVO> resultPage = employeeMapper.selectEmployeePageWithDept(voPage, queryDTO);
// 直接复用resultPage只转换records
Page<DpcEmployeeVO> voPage = new Page<>(resultPage.getCurrent(), resultPage.getSize(), resultPage.getTotal());
voPage.setRecords(resultPage.getRecords().stream()
.map(this::convertToVO)
.collect(Collectors.toList()));
// 复制其他分页信息
voPage.setPages(resultPage.getPages());
// 设置状态描述
resultPage.getRecords().forEach(vo -> {
if ("0".equals(vo.getStatus())) {
vo.setStatusDesc("在职");
} else if ("1".equals(vo.getStatus())) {
vo.setStatusDesc("离职");
}
});
return voPage;
return resultPage;
}
/**
@@ -262,7 +264,7 @@ public class DpcEmployeeServiceImpl implements IDpcEmployeeService {
LambdaQueryWrapper<DpcEmployee> wrapper = new LambdaQueryWrapper<>();
wrapper.like(StringUtils.isNotEmpty(queryDTO.getName()), DpcEmployee::getName, queryDTO.getName())
.eq(StringUtils.isNotEmpty(queryDTO.getTellerNo()), DpcEmployee::getTellerNo, queryDTO.getTellerNo())
.eq(StringUtils.isNotEmpty(queryDTO.getOrgNo()), DpcEmployee::getOrgNo, queryDTO.getOrgNo())
.eq(queryDTO.getDeptId() != null, DpcEmployee::getDeptId, queryDTO.getDeptId())
.eq(StringUtils.isNotEmpty(queryDTO.getIdCard()), DpcEmployee::getIdCard, queryDTO.getIdCard())
.eq(StringUtils.isNotEmpty(queryDTO.getStatus()), DpcEmployee::getStatus, queryDTO.getStatus())
.orderByDesc(DpcEmployee::getCreateTime);

View File

@@ -8,7 +8,8 @@
<id property="employeeId" column="employee_id"/>
<result property="name" column="name"/>
<result property="tellerNo" column="teller_no"/>
<result property="orgNo" column="org_no"/>
<result property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<result property="idCard" column="id_card"/>
<result property="phone" column="phone"/>
<result property="hireDate" column="hire_date"/>
@@ -24,9 +25,35 @@
</collection>
</resultMap>
<select id="selectEmployeePageWithDept" resultMap="DpcEmployeeVOResult">
SELECT
e.employee_id, e.name, e.teller_no, e.dept_id, e.id_card, e.phone, e.hire_date, e.status, e.create_time,
d.dept_name
FROM dpc_employee e
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id
<where>
<if test="query.name != null and query.name != ''">
AND e.name LIKE CONCAT('%', #{query.name}, '%')
</if>
<if test="query.tellerNo != null and query.tellerNo != ''">
AND e.teller_no = #{query.tellerNo}
</if>
<if test="query.deptId != null">
AND e.dept_id = #{query.deptId}
</if>
<if test="query.idCard != null and query.idCard != ''">
AND e.id_card = #{query.idCard}
</if>
<if test="query.status != null and query.status != ''">
AND e.status = #{query.status}
</if>
</where>
ORDER BY e.create_time DESC
</select>
<select id="selectEmployeeWithRelatives" parameterType="Long" resultMap="DpcEmployeeVOResult">
SELECT
e.employee_id, e.name, e.teller_no, e.org_no, e.id_card, e.phone, e.hire_date, e.status, e.create_time,
e.employee_id, e.name, e.teller_no, e.dept_id, e.id_card, e.phone, e.hire_date, e.status, e.create_time,
r.relative_id, r.employee_id, r.relative_name, r.relative_id_card, r.relative_phone, r.relationship
FROM dpc_employee e
LEFT JOIN dpc_employee_relative r ON e.employee_id = r.employee_id

View File

@@ -15,7 +15,7 @@ CREATE TABLE `dpc_employee` (
`employee_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '员工ID',
`name` VARCHAR(100) NOT NULL COMMENT '姓名',
`teller_no` VARCHAR(50) NOT NULL COMMENT '柜员号',
`org_no` VARCHAR(50) DEFAULT NULL COMMENT '所属机构号',
`dept_id` BIGINT DEFAULT NULL COMMENT '所属部门ID',
`id_card` VARCHAR(18) NOT NULL COMMENT '身份证号',
`phone` VARCHAR(11) DEFAULT NULL COMMENT '电话',
`hire_date` DATE DEFAULT NULL COMMENT '入职时间',
@@ -27,7 +27,7 @@ CREATE TABLE `dpc_employee` (
PRIMARY KEY (`employee_id`),
UNIQUE KEY `uk_teller_no` (`teller_no`),
UNIQUE KEY `uk_id_card` (`id_card`),
KEY `idx_org_no` (`org_no`),
KEY `idx_dept_id` (`dept_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='员工信息表';

View File

@@ -0,0 +1,68 @@
-- ================================
-- 纪检初核系统 - 员工表字段迁移脚本
-- 功能: 将 org_no 字段改为 dept_id
-- 创建日期: 2026-01-28
-- ================================
-- 备份说明:执行前请先备份数据库
USE `discipline-prelim-check`;
-- ----------------------------
-- 步骤1: 删除旧索引(如果存在)
-- ----------------------------
-- 使用存储过程检查并删除索引
DROP PROCEDURE IF EXISTS drop_index_if_exists;
DELIMITER //
CREATE PROCEDURE drop_index_if_exists()
BEGIN
DECLARE index_count INT;
SELECT COUNT(*) INTO index_count
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = 'discipline-prelim-check'
AND TABLE_NAME = 'dpc_employee'
AND INDEX_NAME = 'idx_org_no';
IF index_count > 0 THEN
ALTER TABLE `dpc_employee` DROP INDEX `idx_org_no`;
END IF;
END //
DELIMITER ;
CALL drop_index_if_exists();
DROP PROCEDURE IF EXISTS drop_index_if_exists;
-- ----------------------------
-- 步骤2: 修改字段名和类型
-- ----------------------------
ALTER TABLE `dpc_employee`
CHANGE COLUMN `org_no` `dept_id` BIGINT DEFAULT NULL COMMENT '所属部门ID';
-- ----------------------------
-- 步骤3: 创建新索引
-- ----------------------------
CREATE INDEX `idx_dept_id` ON `dpc_employee` (`dept_id`);
-- ----------------------------
-- 验证脚本
-- ----------------------------
-- 检查字段是否修改成功
SELECT
COLUMN_NAME,
DATA_TYPE,
COLUMN_TYPE,
IS_NULLABLE,
COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'discipline-prelim-check'
AND TABLE_NAME = 'dpc_employee'
AND COLUMN_NAME = 'dept_id';
-- 检查索引是否创建成功
SELECT
INDEX_NAME,
COLUMN_NAME
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = 'discipline-prelim-check'
AND TABLE_NAME = 'dpc_employee'
AND INDEX_NAME = 'idx_dept_id';

View File

@@ -0,0 +1,63 @@
============================================================
分页接口总数测试报告
============================================================
测试时间: 2026-01-28 15:26:06
测试统计:
总测试数: 8
通过: 0
失败: 8
错误: 0
测试接口:
1. /dpc/employee/list - 员工列表MyBatis Plus分页
2. /dpc/intermediary/list - 中介黑名单列表若依startPage分页
------------------------------------------------------------
详细结果:
------------------------------------------------------------
测试: 员工列表 - 第1页(10条/页)
API类型: MyBatis Plus
状态: FAIL
错误: 响应缺少data字段
测试: 员工列表 - 第2页(10条/页)
API类型: MyBatis Plus
状态: FAIL
错误: 响应缺少data字段
测试: 员工列表 - 第1页(5条/页)
API类型: MyBatis Plus
状态: FAIL
错误: 响应缺少data字段
测试: 员工列表 - 第1页(20条/页)
API类型: MyBatis Plus
状态: FAIL
错误: 响应缺少data字段
测试: 中介黑名单 - 第1页(10条/页)
API类型: 若依startPage
状态: FAIL
错误: 响应缺少data字段
测试: 中介黑名单 - 第2页(10条/页)
API类型: 若依startPage
状态: FAIL
错误: 响应缺少data字段
测试: 中介黑名单 - 第1页(5条/页)
API类型: 若依startPage
状态: FAIL
错误: 响应缺少data字段
测试: 中介黑名单 - 第1页(20条/页)
API类型: 若依startPage
状态: FAIL
错误: 响应缺少data字段
------------------------------------------------------------
测试结论:
✗ 存在分页接口总数返回异常

View File

@@ -0,0 +1,84 @@
============================================================
分页接口总数测试报告
============================================================
测试时间: 2026-01-28 15:26:38
测试统计:
总测试数: 8
通过: 7
失败: 1
错误: 0
测试接口:
1. /dpc/employee/list - 员工列表MyBatis Plus分页
2. /dpc/intermediary/list - 中介黑名单列表若依startPage分页
------------------------------------------------------------
详细结果:
------------------------------------------------------------
测试: 员工列表 - 第1页(10条/页)
API类型: MyBatis Plus
状态: PASS
页码: 1/10
返回行数: 10
总数: 199
预期行数: 10
测试: 员工列表 - 第2页(10条/页)
API类型: MyBatis Plus
状态: PASS
页码: 2/10
返回行数: 10
总数: 199
预期行数: 10
测试: 员工列表 - 第1页(5条/页)
API类型: MyBatis Plus
状态: PASS
页码: 1/5
返回行数: 5
总数: 199
预期行数: 5
测试: 员工列表 - 第1页(20条/页)
API类型: MyBatis Plus
状态: PASS
页码: 1/20
返回行数: 20
总数: 199
预期行数: 20
测试: 中介黑名单 - 第1页(10条/页)
API类型: 若依startPage
状态: PASS
页码: 1/10
返回行数: 1
总数: 1
预期行数: 1
测试: 中介黑名单 - 第2页(10条/页)
API类型: 若依startPage
状态: FAIL
错误: 行数不匹配
测试: 中介黑名单 - 第1页(5条/页)
API类型: 若依startPage
状态: PASS
页码: 1/5
返回行数: 1
总数: 1
预期行数: 1
测试: 中介黑名单 - 第1页(20条/页)
API类型: 若依startPage
状态: PASS
页码: 1/20
返回行数: 1
总数: 1
预期行数: 1
------------------------------------------------------------
测试结论:
✗ 存在分页接口总数返回异常

View File

@@ -0,0 +1,84 @@
============================================================
分页接口总数测试报告
============================================================
测试时间: 2026-01-28 15:32:35
测试统计:
总测试数: 8
通过: 7
失败: 1
错误: 0
测试接口:
1. /dpc/employee/list - 员工列表MyBatis Plus分页
2. /dpc/intermediary/list - 中介黑名单列表若依startPage分页
------------------------------------------------------------
详细结果:
------------------------------------------------------------
测试: 员工列表 - 第1页(10条/页)
API类型: MyBatis Plus
状态: PASS
页码: 1/10
返回行数: 10
总数: 199
预期行数: 10
测试: 员工列表 - 第2页(10条/页)
API类型: MyBatis Plus
状态: PASS
页码: 2/10
返回行数: 10
总数: 199
预期行数: 10
测试: 员工列表 - 第1页(5条/页)
API类型: MyBatis Plus
状态: PASS
页码: 1/5
返回行数: 5
总数: 199
预期行数: 5
测试: 员工列表 - 第1页(20条/页)
API类型: MyBatis Plus
状态: PASS
页码: 1/20
返回行数: 20
总数: 199
预期行数: 20
测试: 中介黑名单 - 第1页(10条/页)
API类型: 若依startPage
状态: PASS
页码: 1/10
返回行数: 1
总数: 1
预期行数: 1
测试: 中介黑名单 - 第2页(10条/页)
API类型: 若依startPage
状态: FAIL
错误: 行数不匹配
测试: 中介黑名单 - 第1页(5条/页)
API类型: 若依startPage
状态: PASS
页码: 1/5
返回行数: 1
总数: 1
预期行数: 1
测试: 中介黑名单 - 第1页(20条/页)
API类型: 若依startPage
状态: PASS
页码: 1/20
返回行数: 1
总数: 1
预期行数: 1
------------------------------------------------------------
测试结论:
✗ 存在分页接口总数返回异常

140
test/test_pagination.ps1 Normal file
View File

@@ -0,0 +1,140 @@
# Pagination API Test Script
# Test employee list pagination and total count
$BaseUrl = "http://localhost:8080"
$LoginUrl = "$BaseUrl/login/test"
$ListUrl = "$BaseUrl/dpc/employee/list"
# Login to get token
$loginBody = @{
username = "admin"
password = "admin123"
} | ConvertTo-Json
Write-Host "Logging in..." -ForegroundColor Cyan
$loginResponse = Invoke-RestMethod -Uri $LoginUrl -Method Post -Body $loginBody -ContentType "application/json"
$token = $loginResponse.token
Write-Host "Login success!" -ForegroundColor Green
Write-Host ""
$headers = @{
Authorization = "Bearer $token"
}
# Test function
function Test-Page {
param(
[int]$PageNum,
[int]$PageSize
)
Write-Host "========== Testing: pageNum=$PageNum, pageSize=$PageSize ==========" -ForegroundColor Yellow
$queryParams = @{
pageNum = $PageNum
pageSize = $PageSize
}
$queryString = ($queryParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join '&'
$url = "$ListUrl?$queryString"
try {
$response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers
$rows = $response.rows.Count
$total = $response.total
$code = $response.code
Write-Host " Response Code: $code" -ForegroundColor Cyan
Write-Host " Returned Rows: $rows" -ForegroundColor Cyan
Write-Host " Total Records: $total" -ForegroundColor Cyan
# Calculate expected values
$expectedTotalPages = [Math]::Ceiling($total / $pageSize)
$expectedRows = if ($PageNum -lt $expectedTotalPages) { $pageSize } elseif ($PageNum -eq $expectedTotalPages) { $total - ($pageSize * ($PageNum - 1)) } else { 0 }
Write-Host " Expected Rows: $expectedRows" -ForegroundColor Cyan
Write-Host " Expected Total Pages: $expectedTotalPages" -ForegroundColor Cyan
# Verify
$isCorrect = $rows -eq $expectedRows
if ($isCorrect) {
Write-Host " Result: CORRECT" -ForegroundColor Green
} else {
Write-Host " Result: WRONG! Got $rows rows, expected $expectedRows" -ForegroundColor Red
}
# Show returned employee IDs
if ($response.rows -and $response.rows.Count -gt 0) {
$ids = ($response.rows | ForEach-Object { $_.employeeId }) -join ', '
Write-Host " Employee IDs: $ids" -ForegroundColor Gray
}
return @{
Success = ($code -eq 200)
Rows = $rows
Total = $total
ExpectedRows = $expectedRows
IsCorrect = $isCorrect
Response = $response
}
}
catch {
Write-Host " Error: $_" -ForegroundColor Red
return @{
Success = $false
Error = $_.Exception.Message
}
}
}
Write-Host ""
Write-Host "================ Starting Pagination Tests ================" -ForegroundColor Yellow
Write-Host ""
# Test cases
$testCases = @(
@{ PageNum = 1; PageSize = 10; Description = "Page 1, Size 10" }
@{ PageNum = 2; PageSize = 10; Description = "Page 2, Size 10" }
@{ PageNum = 1; PageSize = 5; Description = "Page 1, Size 5" }
@{ PageNum = 3; PageSize = 5; Description = "Page 3, Size 5" }
@{ PageNum = 1; PageSize = 20; Description = "Page 1, Size 20" }
@{ PageNum = 1; PageSize = 3; Description = "Page 1, Size 3" }
)
$results = @()
$allCorrect = $true
foreach ($testCase in $testCases) {
Write-Host ""
$result = Test-Page -PageNum $testCase.PageNum -PageSize $testCase.PageSize
$results += [PSCustomObject]@{
Test = $testCase.Description
PageNum = $testCase.PageNum
PageSize = $testCase.PageSize
ReturnedRows = $result.Rows
TotalRecords = $result.Total
ExpectedRows = $result.ExpectedRows
Correct = if ($result.IsCorrect) { "PASS" } else { "FAIL" }
}
if (-not $result.IsCorrect) {
$allCorrect = $false
}
Start-Sleep -Milliseconds 200
}
# Show summary
Write-Host ""
Write-Host "================ Test Results Summary ================" -ForegroundColor Yellow
Write-Host ""
$results | Format-Table -AutoSize
Write-Host ""
if ($allCorrect) {
Write-Host "PASS - All tests passed! Pagination is working correctly." -ForegroundColor Green
} else {
Write-Host "FAIL - Some tests failed! Please check pagination logic." -ForegroundColor Red
}

437
test/test_pagination.py Normal file
View File

@@ -0,0 +1,437 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""分页接口总数测试脚本
测试接口:
1. /dpc/employee/list - 员工列表MyBatis Plus分页
2. /dpc/intermediary/list - 中介黑名单列表若依startPage分页
"""
import sys
import io
import requests
import json
from datetime import datetime
# 设置stdout编码为UTF-8
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
BASE_URL = "http://localhost:8080"
LOGIN_URL = f"{BASE_URL}/login/test"
EMPLOYEE_LIST_URL = f"{BASE_URL}/dpc/employee/list"
INTERMEDIARY_LIST_URL = f"{BASE_URL}/dpc/intermediary/list"
# 测试结果存储
test_results = []
def login():
"""登录获取token"""
print("=" * 60)
print("步骤1: 获取认证Token")
print("=" * 60)
login_body = {
"username": "admin",
"password": "admin123"
}
try:
# 使用json参数发送JSON格式请求体
response = requests.post(LOGIN_URL, json=login_body)
print(f"请求URL: {LOGIN_URL}")
print(f"请求参数: {login_body}")
print(f"响应状态码: {response.status_code}")
data = response.json()
if data.get("code") == 200:
token = data.get("token")
print(f"✓ Token获取成功: {token[:20]}...")
return token
else:
print(f"✗ Token获取失败: {data.get('msg')}")
return None
except Exception as e:
print(f"✗ 异常: {e}")
return None
def test_page(url, token, page_num, page_size, test_name, api_type):
"""测试指定分页参数的接口"""
print(f"\n========== 测试: {test_name} ==========")
print(f"API类型: {api_type}")
print(f"URL: {url}")
print(f"参数: pageNum={page_num}, pageSize={page_size}")
params = {
"pageNum": page_num,
"pageSize": page_size
}
headers = {
"Authorization": f"Bearer {token}"
}
try:
response = requests.get(url, params=params, headers=headers)
print(f"响应状态码: {response.status_code}")
if response.status_code != 200:
print(f"✗ HTTP错误: {response.text}")
test_results.append({
"test_name": test_name,
"api_type": api_type,
"status": "FAIL",
"error": f"HTTP {response.status_code}"
})
return None
data = response.json()
print(f"响应内容:\n{json.dumps(data, indent=2, ensure_ascii=False)}")
code = data.get("code", 0)
if code != 200:
print(f"✗ 业务错误: {data.get('msg')}")
test_results.append({
"test_name": test_name,
"api_type": api_type,
"status": "FAIL",
"error": data.get("msg", "Unknown error")
})
return None
# 检查响应结构 - 支持两种格式
# 格式1: {data: {total, rows}, code, msg}
# 格式2: {total, rows, code, msg}
if "data" in data:
response_data = data["data"]
rows = response_data.get("rows", [])
total = response_data.get("total")
else:
# 扁平结构格式
rows = data.get("rows", [])
total = data.get("total")
rows_count = len(rows)
print(f"\n--- 分页数据分析 ---")
print(f"返回行数(rows): {rows_count}")
print(f"总数(total): {total}")
# 验证total字段
if total is None:
print(f"✗ 响应缺少total字段")
test_results.append({
"test_name": test_name,
"api_type": api_type,
"status": "FAIL",
"error": "响应缺少total字段"
})
return None
if not isinstance(total, int):
print(f"✗ total类型错误: {type(total)}")
test_results.append({
"test_name": test_name,
"api_type": api_type,
"status": "FAIL",
"error": f"total类型错误: {type(total)}"
})
return None
if total < 0:
print(f"✗ total值无效: {total}")
test_results.append({
"test_name": test_name,
"api_type": api_type,
"status": "FAIL",
"error": f"total值无效: {total}"
})
return None
# 计算预期值
expected_total_pages = (total + page_size - 1) // page_size
if page_num < expected_total_pages:
expected_rows = page_size
elif page_num == expected_total_pages:
expected_rows = total - (page_size * (page_num - 1))
else:
expected_rows = 0
print(f"预期行数: {expected_rows}")
print(f"预期总页数: {expected_total_pages}")
# 验证行数是否正确
is_correct = rows_count == expected_rows
if is_correct:
print(f"✓ 测试通过 - 分页总数返回正常")
test_results.append({
"test_name": test_name,
"api_type": api_type,
"status": "PASS",
"page_num": page_num,
"page_size": page_size,
"rows_count": rows_count,
"total": total,
"expected_rows": expected_rows
})
else:
print(f"✗ 测试失败 - 预期{expected_rows}行,实际{rows_count}")
test_results.append({
"test_name": test_name,
"api_type": api_type,
"status": "FAIL",
"page_num": page_num,
"page_size": page_size,
"rows_count": rows_count,
"total": total,
"expected_rows": expected_rows,
"error": f"行数不匹配"
})
# 显示返回的ID
if rows:
if "employeeId" in rows[0]:
ids = ', '.join([str(r.get("employeeId")) for r in rows])
print(f"员工ID: {ids}")
elif "intermediaryId" in rows[0]:
ids = ', '.join([str(r.get("intermediaryId")) for r in rows])
print(f"中介ID: {ids}")
return {
"success": True,
"rows": rows_count,
"total": total,
"expected_rows": expected_rows,
"is_correct": is_correct
}
except Exception as e:
print(f"✗ 异常: {e}")
test_results.append({
"test_name": test_name,
"api_type": api_type,
"status": "ERROR",
"error": str(e)
})
return None
def test_consistency(url, token, test_name, api_type):
"""测试不同pageSize下total是否一致"""
print(f"\n========== 测试总数一致性: {test_name} ==========")
page_sizes = [10, 20, 50]
totals = []
for size in page_sizes:
params = {"pageNum": 1, "pageSize": size}
headers = {"Authorization": f"Bearer {token}"}
try:
response = requests.get(url, params=params, headers=headers)
if response.status_code == 200:
data = response.json()
if data.get("code") == 200:
# 支持两种响应格式
if "data" in data:
total = data.get("data", {}).get("total")
else:
total = data.get("total")
totals.append(total)
print(f"pageSize={size}: total={total}")
except Exception as e:
print(f"✗ 异常: {e}")
if len(set(totals)) == 1 and totals[0] is not None:
print(f"✓ 不同pageSize下总数一致: {totals[0]}")
return True
else:
print(f"✗ 不同pageSize下总数不一致: {totals}")
return False
def generate_report():
"""生成测试报告"""
print("\n" + "=" * 60)
print("测试报告")
print("=" * 60)
# 按API类型分组
employee_results = [r for r in test_results if "员工" in r.get("test_name", "")]
intermediary_results = [r for r in test_results if "中介" in r.get("test_name", "")]
pass_count = sum(1 for r in test_results if r["status"] == "PASS")
fail_count = sum(1 for r in test_results if r["status"] == "FAIL")
error_count = sum(1 for r in test_results if r["status"] == "ERROR")
print(f"\n总测试数: {len(test_results)}")
print(f"通过: {pass_count}")
print(f"失败: {fail_count}")
print(f"错误: {error_count}")
# 员工接口结果
print(f"\n--- 员工列表接口 (MyBatis Plus) ---")
print(f"测试数: {len(employee_results)}")
for r in employee_results:
status_icon = "" if r["status"] == "PASS" else ""
print(f"{status_icon} {r['test_name']}: {r['status']}")
if r["status"] == "PASS":
print(f" 页码: {r.get('page_num')}/{r.get('page_size')}, "
f"返回行数: {r.get('rows_count')}, 总数: {r.get('total')}")
else:
print(f" 错误: {r.get('error', 'Unknown')}")
# 中介黑名单接口结果
print(f"\n--- 中介黑名单接口 (若依startPage) ---")
print(f"测试数: {len(intermediary_results)}")
for r in intermediary_results:
status_icon = "" if r["status"] == "PASS" else ""
print(f"{status_icon} {r['test_name']}: {r['status']}")
if r["status"] == "PASS":
print(f" 页码: {r.get('page_num')}/{r.get('page_size')}, "
f"返回行数: {r.get('rows_count')}, 总数: {r.get('total')}")
else:
print(f" 错误: {r.get('error', 'Unknown')}")
# 总体结论
print(f"\n--- 测试结论 ---")
if fail_count == 0 and error_count == 0:
print("✓ 所有分页接口总数返回正常")
else:
print("✗ 存在分页接口总数返回异常")
# 保存报告到文件
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
report_file = f"test/pagination_test_report_{timestamp}.txt"
with open(report_file, "w", encoding="utf-8") as f:
f.write("=" * 60 + "\n")
f.write("分页接口总数测试报告\n")
f.write("=" * 60 + "\n\n")
f.write(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
f.write("测试统计:\n")
f.write(f" 总测试数: {len(test_results)}\n")
f.write(f" 通过: {pass_count}\n")
f.write(f" 失败: {fail_count}\n")
f.write(f" 错误: {error_count}\n\n")
f.write("测试接口:\n")
f.write(" 1. /dpc/employee/list - 员工列表MyBatis Plus分页\n")
f.write(" 2. /dpc/intermediary/list - 中介黑名单列表若依startPage分页\n\n")
f.write("-" * 60 + "\n")
f.write("详细结果:\n")
f.write("-" * 60 + "\n\n")
for r in test_results:
f.write(f"测试: {r['test_name']}\n")
f.write(f"API类型: {r['api_type']}\n")
f.write(f"状态: {r['status']}\n")
if r['status'] == 'PASS':
f.write(f" 页码: {r.get('page_num')}/{r.get('page_size')}\n")
f.write(f" 返回行数: {r.get('rows_count')}\n")
f.write(f" 总数: {r.get('total')}\n")
f.write(f" 预期行数: {r.get('expected_rows')}\n")
else:
f.write(f" 错误: {r.get('error', 'Unknown')}\n")
f.write("\n")
f.write("-" * 60 + "\n")
f.write("测试结论:\n")
if fail_count == 0 and error_count == 0:
f.write("✓ 所有分页接口总数返回正常\n")
else:
f.write("✗ 存在分页接口总数返回异常\n")
print(f"\n报告已保存至: {report_file}")
def main():
"""主函数"""
print("\n" + "=" * 60)
print("分页接口总数测试")
print("=" * 60)
print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 获取token
token = login()
if not token:
print("\n✗ 无法获取token测试终止")
return
print("\n✓ 登录成功,开始测试")
# 员工列表接口测试用例
employee_test_cases = [
{"page_num": 1, "page_size": 10, "desc": "员工列表 - 第1页(10条/页)"},
{"page_num": 2, "page_size": 10, "desc": "员工列表 - 第2页(10条/页)"},
{"page_num": 1, "page_size": 5, "desc": "员工列表 - 第1页(5条/页)"},
{"page_num": 1, "page_size": 20, "desc": "员工列表 - 第1页(20条/页)"},
]
print("\n" + "=" * 60)
print("测试员工列表接口MyBatis Plus分页")
print("=" * 60)
for test_case in employee_test_cases:
test_page(
EMPLOYEE_LIST_URL,
token,
test_case["page_num"],
test_case["page_size"],
test_case["desc"],
"MyBatis Plus"
)
# 测试总数一致性
test_consistency(
EMPLOYEE_LIST_URL,
token,
"员工列表-总数一致性",
"MyBatis Plus"
)
# 中介黑名单接口测试用例
intermediary_test_cases = [
{"page_num": 1, "page_size": 10, "desc": "中介黑名单 - 第1页(10条/页)"},
{"page_num": 2, "page_size": 10, "desc": "中介黑名单 - 第2页(10条/页)"},
{"page_num": 1, "page_size": 5, "desc": "中介黑名单 - 第1页(5条/页)"},
{"page_num": 1, "page_size": 20, "desc": "中介黑名单 - 第1页(20条/页)"},
]
print("\n" + "=" * 60)
print("测试中介黑名单接口若依startPage分页")
print("=" * 60)
for test_case in intermediary_test_cases:
test_page(
INTERMEDIARY_LIST_URL,
token,
test_case["page_num"],
test_case["page_size"],
test_case["desc"],
"若依startPage"
)
# 测试总数一致性
test_consistency(
INTERMEDIARY_LIST_URL,
token,
"中介黑名单-总数一致性",
"若依startPage"
)
# 生成报告
generate_report()
print("\n" + "=" * 60)
print("测试完成")
print("=" * 60)
if __name__ == "__main__":
main()