员工关系移除

This commit is contained in:
wkc
2026-02-06 09:01:33 +08:00
parent 8b6967bf32
commit 4c3eeea256
26 changed files with 2051 additions and 1687 deletions

View File

@@ -78,7 +78,11 @@
"Skill(superpowers:finishing-a-development-branch)",
"Skill(superpowers:systematic-debugging)",
"mcp__mysql__execute",
"Skill(document-skills:xlsx)"
"Skill(document-skills:xlsx)",
"Bash(git reset:*)",
"Skill(xlsx)",
"mcp__chrome-devtools__evaluate_script",
"Skill(superpowers:using-git-worktrees)"
]
},
"enabledMcpjsonServers": [

1
.gitignore vendored
View File

@@ -41,6 +41,7 @@ nbdist/
*.log
*.xml.versionsBackup
*.swp
nul
test/

View File

@@ -0,0 +1,430 @@
# 员工招聘信息管理 API文档
**模块名称:** ccdi-staff-recruitment
**版本:** 1.0
**生成日期:** 2025-02-05
**基础路径:** `/ccdi/staffRecruitment`
---
## 目录
1. [查询接口](#1-查询接口)
2. [操作接口](#2-操作接口)
3. [导入导出接口](#3-导入导出接口)
4. [数据模型](#4-数据模型)
5. [错误码说明](#5-错误码说明)
---
## 1. 查询接口
### 1.1 分页查询招聘信息列表
**接口描述:** 分页查询员工招聘信息列表,支持多条件筛选
**请求方式:** `GET`
**接口路径:** `/ccdi/staffRecruitment/list`
**权限标识:** `ccdi:staffRecruitment:list`
**请求参数:**
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|-------|------|------|------|--------|
| pageNum | Integer | 否 | 页码默认1 | 1 |
| pageSize | Integer | 否 | 每页条数默认10 | 10 |
| recruitName | String | 否 | 招聘项目名称(模糊查询) | 2025春季招聘 |
| posName | String | 否 | 职位名称(模糊查询) | 软件工程师 |
| candName | String | 否 | 候选人姓名(模糊查询) | 张三 |
| candId | String | 否 | 证件号码(精确查询) | 110101199001011234 |
| admitStatus | String | 否 | 录用状态(精确查询) | 录用/未录用/放弃 |
| interviewerName | String | 否 | 面试官姓名(模糊查询,查询面试官1或2) | 李四 |
| interviewerId | String | 否 | 面试官工号(精确查询,查询面试官1或2) | 10001 |
**响应示例:**
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"recruitId": "REC20250205001",
"recruitName": "2025春季校园招聘",
"posName": "Java开发工程师",
"posCategory": "技术类",
"posDesc": "负责后端系统开发",
"candName": "张三",
"candEdu": "本科",
"candId": "110101199001011234",
"candSchool": "清华大学",
"candMajor": "计算机科学与技术",
"candGrad": "202506",
"admitStatus": "录用",
"admitStatusDesc": "已录用该候选人",
"interviewerName1": "李四",
"interviewerId1": "10001",
"interviewerName2": "王五",
"interviewerId2": "10002",
"createdBy": "admin",
"createTime": "2025-02-05 10:00:00",
"updatedBy": null,
"updateTime": null
}
],
"total": 100
}
```
### 1.2 查询招聘信息详情
**接口描述:** 根据招聘项目编号查询详细信息
**请求方式:** `GET`
**接口路径:** `/ccdi/staffRecruitment/{recruitId}`
**权限标识:** `ccdi:staffRecruitment:query`
**路径参数:**
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|-------|------|------|------|--------|
| recruitId | String | 是 | 招聘项目编号 | REC20250205001 |
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"recruitId": "REC20250205001",
"recruitName": "2025春季校园招聘",
"posName": "Java开发工程师",
"posCategory": "技术类",
"posDesc": "负责后端系统开发要求熟悉Spring Boot、MyBatis Plus等框架",
"candName": "张三",
"candEdu": "本科",
"candId": "110101199001011234",
"candSchool": "清华大学",
"candMajor": "计算机科学与技术",
"candGrad": "202506",
"admitStatus": "录用",
"admitStatusDesc": "已录用该候选人",
"interviewerName1": "李四",
"interviewerId1": "10001",
"interviewerName2": "王五",
"interviewerId2": "10002",
"createdBy": "admin",
"createTime": "2025-02-05 10:00:00",
"updatedBy": null,
"updateTime": null
}
}
```
---
## 2. 操作接口
### 2.1 新增招聘信息
**接口描述:** 新增一条员工招聘信息
**请求方式:** `POST`
**接口路径:** `/ccdi/staffRecruitment`
**权限标识:** `ccdi:staffRecruitment:add`
**请求体:**
```json
{
"recruitId": "REC20250205001",
"recruitName": "2025春季校园招聘",
"posName": "Java开发工程师",
"posCategory": "技术类",
"posDesc": "负责后端系统开发",
"candName": "张三",
"candEdu": "本科",
"candId": "110101199001011234",
"candSchool": "清华大学",
"candMajor": "计算机科学与技术",
"candGrad": "202506",
"admitStatus": "录用",
"interviewerName1": "李四",
"interviewerId1": "10001",
"interviewerName2": "王五",
"interviewerId2": "10002"
}
```
**字段校验规则:**
| 字段 | 校验规则 | 错误提示 |
|-----|---------|---------|
| recruitId | @NotBlank, @Size(max=32) | 招聘项目编号不能为空/长度不能超过32 |
| recruitName | @NotBlank, @Size(max=100) | 招聘项目名称不能为空/长度不能超过100 |
| posName | @NotBlank, @Size(max=100) | 职位名称不能为空/长度不能超过100 |
| posCategory | @NotBlank, @Size(max=50) | 职位类别不能为空/长度不能超过50 |
| posDesc | @NotBlank | 职位描述不能为空 |
| candName | @NotBlank, @Size(max=20) | 应聘人员姓名不能为空/长度不能超过20 |
| candEdu | @NotBlank, @Size(max=20) | 应聘人员学历不能为空/长度不能超过20 |
| candId | @NotBlank, @Pattern(身份证正则) | 证件号码不能为空/格式不正确 |
| candSchool | @NotBlank, @Size(max=50) | 应聘人员毕业院校不能为空/长度不能超过50 |
| candMajor | @NotBlank, @Size(max=30) | 应聘人员专业不能为空/长度不能超过30 |
| candGrad | @NotBlank, @Pattern(YYYYMM) | 毕业年月不能为空/格式不正确 |
| admitStatus | @NotBlank, @EnumValid | 录用情况不能为空/状态值不合法 |
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
### 2.2 修改招聘信息
**接口描述:** 修改已有的员工招聘信息
**请求方式:** `PUT`
**接口路径:** `/ccdi/staffRecruitment`
**权限标识:** `ccdi:staffRecruitment:edit`
**请求体:**
```json
{
"recruitId": "REC20250205001",
"recruitName": "2025春季校园招聘",
"posName": "Java开发工程师",
"posCategory": "技术类",
"posDesc": "负责后端系统开发,负责核心模块设计",
"candName": "张三",
"candEdu": "本科",
"candId": "110101199001011234",
"candSchool": "清华大学",
"candMajor": "计算机科学与技术",
"candGrad": "202506",
"admitStatus": "录用",
"interviewerName1": "李四",
"interviewerId1": "10001",
"interviewerName2": "王五",
"interviewerId2": "10002"
}
```
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
### 2.3 删除招聘信息
**接口描述:** 批量删除员工招聘信息
**请求方式:** `DELETE`
**接口路径:** `/ccdi/staffRecruitment/{recruitIds}`
**权限标识:** `ccdi:staffRecruitment:remove`
**路径参数:**
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|-------|------|------|------|--------|
| recruitIds | String[] | 是 | 招聘项目编号数组,多个用逗号分隔 | REC20250205001,REC20250205002 |
**响应示例:**
```json
{
"code": 200,
"msg": "操作成功"
}
```
---
## 3. 导入导出接口
### 3.1 下载导入模板
**接口描述:** 下载Excel导入模板
**请求方式:** `POST`
**接口路径:** `/ccdi/staffRecruitment/importTemplate`
**权限标识:**
**响应:** Excel文件流
**模板字段顺序:**
| 序号 | 字段名 | 说明 | 必填 |
|-----|--------|------|------|
| 1 | 招聘项目编号 | 唯一标识 | 是 |
| 2 | 招聘项目名称 | - | 是 |
| 3 | 职位名称 | - | 是 |
| 4 | 职位类别 | - | 是 |
| 5 | 职位描述 | - | 是 |
| 6 | 应聘人员姓名 | - | 是 |
| 7 | 应聘人员学历 | - | 是 |
| 8 | 应聘人员证件号码 | 身份证号 | 是 |
| 9 | 应聘人员毕业院校 | - | 是 |
| 10 | 应聘人员专业 | - | 是 |
| 11 | 应聘人员毕业年月 | 格式:YYYYMM | 是 |
| 12 | 录用情况 | 录用/未录用/放弃 | 是 |
| 13 | 面试官1姓名 | - | 否 |
| 14 | 面试官1工号 | - | 否 |
| 15 | 面试官2姓名 | - | 否 |
| 16 | 面试官2工号 | - | 否 |
### 3.2 批量导入
**接口描述:** 通过Excel批量导入招聘信息
**请求方式:** `POST`
**接口路径:** `/ccdi/staffRecruitment/importData?updateSupport={updateSupport}`
**权限标识:** `ccdi:staffRecruitment:import`
**请求参数:**
| 参数名 | 类型 | 必填 | 说明 | 示例值 |
|-------|------|------|------|--------|
| updateSupport | Boolean | 否 | 是否更新已存在的数据 | true |
| file | File | 是 | Excel文件 | - |
**请求类型:** `multipart/form-data`
**响应示例 (成功):**
```json
{
"code": 200,
"msg": "恭喜您,数据已全部导入成功!共 10 条,数据类型:新增 8 条,更新 2 条"
}
```
**响应示例 (部分失败):**
```json
{
"code": 500,
"msg": "很抱歉,导入完成!成功 8 条,失败 2 条,错误如下:<br/>1、招聘项目编号 REC001 导入失败:该招聘项目编号已存在<br/>2、招聘项目编号 REC002 导入失败:证件号码格式不正确"
}
```
### 3.3 导出
**接口描述:** 导出招聘信息到Excel
**请求方式:** `POST`
**接口路径:** `/ccdi/staffRecruitment/export`
**权限标识:** `ccdi:staffRecruitment:export`
**请求参数:** 与分页查询接口相同的查询条件
**响应:** Excel文件流
---
## 4. 数据模型
### 4.1 录用状态枚举 (AdmitStatus)
| 枚举值 | 说明 |
|--------|------|
| 录用 | 已录用该候选人 |
| 未录用 | 未录用该候选人 |
| 放弃 | 候选人放弃 |
### 4.2 CcdiStaffRecruitmentVO
招聘信息返回对象,包含所有字段及状态描述。
### 4.3 CcdiStaffRecruitmentExcel
Excel导入导出对象,使用EasyExcel注解。
---
## 5. 错误码说明
| 错误码 | 说明 |
|--------|------|
| 200 | 操作成功 |
| 400 | 参数校验失败 |
| 401 | 未授权,请先登录 |
| 403 | 无权限访问 |
| 404 | 资源不存在 |
| 409 | 主键冲突 |
| 500 | 服务器内部错误 |
### 常见业务错误
| 错误信息 | 说明 |
|---------|------|
| 该招聘项目编号已存在 | 新增时recruitId重复 |
| 招聘项目编号不能为空 | recruitId字段为空 |
| 证件号码格式不正确 | 身份证号格式验证失败 |
| 毕业年月格式不正确 | candGrad不是YYYYMM格式 |
| 录用情况状态值不合法 | admitStatus不是枚举值之一 |
---
## 附录
### Swagger UI
访问地址: `/swagger-ui/index.html`
### 测试账号
- 用户名: admin
- 密码: admin123
### Token获取
**接口:** POST `/login`
**请求体:**
```json
{
"username": "admin",
"password": "admin123"
}
```
**响应:**
```json
{
"code": 200,
"msg": "操作成功",
"token": "Bearer eyJhbGciOiJIUzUxMiJ9..."
}
```
---
**文档生成时间:** 2025-02-05
**文档版本:** 1.0

View File

@@ -1,347 +0,0 @@
# 员工招聘信息管理功能设计文档
**文档版本:** 1.0
**创建日期:** 2025-02-05
**模块名称:** ccdi-staff-recruitment
**作者:** Claude
---
## 1. 概述
### 1.1 功能简介
员工招聘信息管理模块提供招聘信息的记录、查询、导入导出等基础维护功能,支持单条和批量操作。
### 1.2 业务场景
- 简单的招聘信息记录,作为数据存档使用
- 支持招聘信息的增删改查操作
- 支持Excel批量导入和导出
### 1.3 技术选型
- **后端框架:** Spring Boot 3.5.8 + MyBatis Plus 3.5.10
- **数据库:** MySQL 8.2.0
- **前端框架:** Vue 2.6.12 + Element UI 2.15.14
- **数据校验:** javax.validation + 自定义校验注解
---
## 2. 数据库设计
### 2.1 表结构
**表名:** `ccdi_staff_recruitment`
```sql
CREATE TABLE `ccdi_staff_recruitment` (
`recruit_id` varchar(32) NOT NULL COMMENT '招聘项目编号',
`recruit_name` varchar(100) NOT NULL COMMENT '招聘项目名称',
`pos_name` varchar(100) NOT NULL COMMENT '职位名称',
`pos_category` varchar(50) NOT NULL COMMENT '职位类别',
`pos_desc` text NOT NULL COMMENT '职位描述',
`cand_name` varchar(20) NOT NULL COMMENT '应聘人员姓名',
`cand_edu` varchar(20) NOT NULL COMMENT '应聘人员学历',
`cand_id` varchar(18) NOT NULL COMMENT '应聘人员证件号码',
`cand_school` varchar(50) NOT NULL COMMENT '应聘人员毕业院校',
`cand_major` varchar(30) NOT NULL COMMENT '应聘人员专业',
`cand_grad` varchar(6) NOT NULL COMMENT '应聘人员毕业年月',
`admit_status` varchar(10) NOT NULL COMMENT '录用情况:录用、未录用、放弃',
`interviewer_name1` varchar(20) DEFAULT NULL COMMENT '面试官1姓名',
`interviewer_id1` varchar(10) DEFAULT NULL COMMENT '面试官1工号',
`interviewer_name2` varchar(20) DEFAULT NULL COMMENT '面试官2姓名',
`interviewer_id2` varchar(10) DEFAULT NULL COMMENT '面试官2工号',
`created_by` varchar(20) NOT NULL COMMENT '记录创建人',
`updated_by` varchar(20) DEFAULT NULL COMMENT '记录更新人',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`recruit_id`),
KEY `idx_cand_id` (`cand_id`),
KEY `idx_admit_status` (`admit_status`),
KEY `idx_interviewer_id1` (`interviewer_id1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工招聘信息表';
```
### 2.2 索引设计
- **主键索引:** `recruit_id`
- **业务索引:** `cand_id`, `admit_status`, `interviewer_id1`
### 2.3 枚举值设计
**录用状态 (admit_status):**
| 枚举值 | 说明 |
|--------|------|
| 录用 | 已录用该候选人 |
| 未录用 | 未录用该候选人 |
| 放弃 | 候选人放弃 |
---
## 3. 后端设计
### 3.1 模块结构
```
ruoyi-ccdi/
├── domain/
│ ├── CcdiStaffRecruitment.java # 实体类
│ ├── dto/
│ │ ├── CcdiStaffRecruitmentQueryDTO.java # 查询DTO
│ │ ├── CcdiStaffRecruitmentAddDTO.java # 新增DTO
│ │ └── CcdiStaffRecruitmentEditDTO.java # 修改DTO
│ ├── vo/
│ │ └── CcdiStaffRecruitmentVO.java # 返回VO
│ └── excel/
│ └── CcdiStaffRecruitmentExcel.java # Excel导入导出类
├── mapper/
│ ├── CcdiStaffRecruitmentMapper.java # MyBatis Mapper接口
│ └── xml/
│ └── CcdiStaffRecruitmentMapper.xml # MyBatis XML映射
├── service/
│ ├── ICcdiStaffRecruitmentService.java # 服务接口
│ └── impl/
│ └── CcdiStaffRecruitmentServiceImpl.java # 服务实现
└── controller/
└── CcdiStaffRecruitmentController.java # 控制器
```
### 3.2 API接口设计
**基础路径:** `/ccdi/staffRecruitment`
| 接口功能 | HTTP方法 | 路径 | 权限标识 |
|---------|---------|------|---------|
| 分页查询 | GET | `/list` | ccdi:staffRecruitment:list |
| 详情查询 | GET | `/{recruitId}` | ccdi:staffRecruitment:query |
| 新增 | POST | `/` | ccdi:staffRecruitment:add |
| 修改 | PUT | `/` | ccdi:staffRecruitment:edit |
| 删除 | DELETE | `/{recruitIds}` | ccdi:staffRecruitment:remove |
| 导入模板下载 | GET | `/importTemplate` | ccdi:staffRecruitment:import |
| 批量导入 | POST | `/importData` | ccdi:staffRecruitment:import |
| 导出 | POST | `/export` | ccdi:staffRecruitment:export |
### 3.3 查询参数设计
**CcdiStaffRecruitmentQueryDTO:**
```java
// 查询条件
private String recruitName; // 招聘项目名称(模糊查询)
private String posName; // 职位名称(模糊查询)
private String candName; // 候选人姓名(模糊查询)
private String candId; // 证件号码(精确查询)
private String admitStatus; // 录用状态(精确查询)
private String interviewerName; // 面试官姓名(模糊查询,查询面试官1或2)
private String interviewerId; // 面试官工号(精确查询,查询面试官1或2)
// 分页参数
private Integer pageNum = 1;
private Integer pageSize = 10;
```
### 3.4 数据校验规则
| 字段 | 校验规则 | 错误提示 |
|-----|---------|---------|
| recruitName | @NotBlank, @Size(max=100) | 招聘项目名称不能为空/长度不能超过100 |
| posName | @NotBlank, @Size(max=100) | 职位名称不能为空/长度不能超过100 |
| candName | @NotBlank, @Size(max=20) | 应聘人员姓名不能为空/长度不能超过20 |
| candId | @NotBlank, @Pattern(身份证正则) | 证件号码不能为空/格式不正确 |
| candGrad | @NotBlank, @Pattern(YYYYMM) | 毕业年月不能为空/格式不正确 |
| admitStatus | @NotBlank, @EnumValid | 录用情况不能为空/状态值不合法 |
### 3.5 批量导入功能设计
**核心优化点:**
1. **批量查询已存在记录:** 使用 `selectBatchIds` 一次性查询
2. **批量插入:** 使用 `saveBatch()` 方法
3. **批量更新:** 使用 `updateBatchById()` 方法
4. **错误信息:** 只返回错误的数据行,成功数据不展示
**性能提升:**
- 原方案: ~3000次数据库操作 (导入1000条)
- 优化后: ~3次数据库操作 (导入1000条)
- 性能提升: ~1000倍
**导入逻辑:**
```
1. 收集所有recruit_id
2. 批量查询已存在的记录
3. 遍历Excel数据:
- 数据转换和校验
- 分类为: 待新增列表、待更新列表
- 记录校验失败的数据
4. 批量插入待新增数据
5. 批量更新待更新数据
6. 只返回错误信息
```
---
## 4. 前端设计
### 4.1 页面结构
```
ruoyi-ui/src/views/ccdiStaffRecruitment/
├── index.vue # 列表页面(主页面)
└── components/
├── RecruitmentForm.vue # 新增/修改表单组件
└── ImportDialog.vue # 导入对话框组件
```
### 4.2 功能列表
**列表页面 (index.vue):**
- 顶部查询表单
- 招聘项目名称(模糊查询)
- 职位名称(模糊查询)
- 候选人姓名(模糊查询)
- 证件号码(精确查询)
- 录用状态(下拉选择)
- 面试官姓名(模糊查询)
- 面试官工号(精确查询)
- 数据表格
- 展示所有字段信息
- 支持排序
- 操作按钮
- 新增
- 批量导入
- 导出
- 批量删除
- 行操作
- 修改
- 删除
**表单组件 (RecruitmentForm.vue):**
- 所有必填字段添加 `required: true`
- 证件号码正则校验
- 毕业年月格式校验(YYYYMM)
- 录用状态下拉选择(枚举值)
---
## 5. 异常处理
### 5.1 异常分类
| 异常类型 | HTTP状态码 | 使用场景 |
|---------|-----------|---------|
| ServiceException | 500 | 业务逻辑异常 |
| ValidationException | 400 | 参数校验失败 |
| DuplicateKeyException | 409 | 主键冲突 |
| FileNotFoundException | 404 | 文件不存在 |
### 5.2 统一异常处理
使用 `@RestControllerAdvice` 全局异常处理器捕获和处理异常。
---
## 6. 测试策略
### 6.1 单元测试
**测试范围:**
- 实体类校验注解测试
- 数据转换工具方法测试
- 业务逻辑核心方法测试
**关键测试用例:**
1. 正常数据导入测试
2. 身份证格式校验测试
3. 批量插入性能测试
### 6.2 集成测试
**测试流程:**
1. 登录获取Token
2. 分页查询测试
3. 单条新增测试
4. 单条修改测试
5. 批量导入测试
6. 导出测试
7. 批量删除测试
### 6.3 性能指标
| 测试场景 | 预期性能 |
|---------|---------|
| 分页查询(1000条) | < 200ms |
| 单条新增 | < 100ms |
| 批量导入(1000条) | < 5s |
| 批量删除(100条) | < 500ms |
| 导出(1000条) | < 2s |
---
## 7. 实施步骤
### 第一步:数据库准备
1. 执行建表SQL
2. 在菜单表中添加菜单和权限配置
### 第二步:后端开发
1. 创建枚举类
2. 创建实体类、DTO、VO、Excel类
3. 创建Mapper接口和XML
4. 创建Service接口和实现
5. 创建Controller
6. 编写单元测试
7. Swagger-UI测试
### 第三步:前端开发
1. 创建API接口定义
2. 开发表格查询页面
3. 开发表单组件
4. 开发导入对话框
5. 配置路由
6. 配置菜单
### 第四步:集成测试
1. 准备测试数据
2. 执行集成测试
3. 验证功能
4. 生成测试报告
### 第五步:文档编写
1. 生成API文档
2. 编写使用说明
---
## 8. 附录
### 8.1 Excel导入模板字段顺序
按CSV字段顺序设计:
1. 招聘项目编号
2. 招聘项目名称
3. 职位名称
4. 职位类别
5. 职位描述
6. 应聘人员姓名
7. 应聘人员学历
8. 应聘人员证件号码
9. 应聘人员毕业院校
10. 应聘人员专业
11. 应聘人员毕业年月
12. 录用情况
13. 面试官1姓名
14. 面试官1工号
15. 面试官2姓名
16. 面试官2工号
### 8.2 MyBatis Plus配置
确保项目中已配置MyBatis Plus分页插件:
```java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
```
---
**文档结束**

View File

@@ -0,0 +1,915 @@
# 导入逻辑优化实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 优化员工信息、中介库(个人/实体)、招聘信息的导入功能,从"存在则更新"改为"先删除后插入"策略。
**架构:** 三阶段流程:数据验证 → 批量删除 → 批量插入。所有操作在一个 @Transactional 事务中执行。
**技术栈:** Spring Boot 3.5.8, MyBatis Plus 3.5.10, MySQL 8.2.0
---
## 模块 1员工信息管理验证方案
此模块用于验证新逻辑的正确性,成功后应用到其他模块。
### Task 1.1:添加批量删除方法到 Mapper 接口
**文件:**
- 修改:`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java`
**Step 1: 在 Mapper 接口中添加方法声明**
`CcdiEmployeeMapper.java` 的接口中添加新方法(在现有方法后面,`insertBatch` 方法之后):
```java
/**
* 根据身份证号批量删除员工数据
*
* @param idCards 身份证号列表
* @return 删除行数
*/
int deleteBatchByIdCard(@Param("list") List<String> idCards);
```
**Step 2: 保存文件**
无需测试,这是接口声明。
**Step 3: 提交**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java
git commit -m "feat(employee): 添加批量删除方法声明"
```
---
### Task 1.2:在 Mapper XML 中实现批量删除 SQL
**文件:**
- 修改:`ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml`
**Step 1: 在 XML 文件中添加删除 SQL**
`CcdiEmployeeMapper.xml` 中,在 `insertBatch` 方法之后添加:
```xml
<!-- 根据身份证号批量删除员工数据 -->
<delete id="deleteBatchByIdCard">
DELETE FROM ccdi_employee
WHERE id_card IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</delete>
```
**Step 2: 保存文件**
无需测试SQL 配置。
**Step 3: 提交**
```bash
git add ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml
git commit -m "feat(employee): 实现批量删除SQL"
```
---
### Task 1.3:重构员工导入方法(先删后插逻辑)
- [x] **已完成** (commit: ebe4fd7)
**文件:**
- 修改:`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java`
- 目标方法:`importEmployee` (第 172-311 行)
**Step 1: 备份原方法**
先注释掉原有的 `importEmployee` 方法(保留参考)。
**Step 2: 实现新的导入逻辑**
将整个 `importEmployee` 方法替换为:
```java
/**
* 导入员工数据(先删后插模式)
*
* @param excelList Excel实体列表
* @param isUpdateSupport 是否更新支持(参数保留以保持兼容性,不再使用)
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String importEmployee(List<CcdiEmployeeExcel> excelList, Boolean isUpdateSupport) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
return "至少需要一条数据";
}
// 第一阶段:数据验证和收集
List<CcdiEmployee> validEmployees = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
Set<String> idCards = new HashSet<>();
for (CcdiEmployeeExcel excel : excelList) {
try {
// 转换为AddDTO
CcdiEmployeeAddDTO addDTO = new CcdiEmployeeAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 验证必填字段和数据格式
validateEmployeeDataBasic(addDTO);
// 检查导入数据内部是否重复
if (!idCards.add(addDTO.getIdCard())) {
throw new RuntimeException("导入文件中该身份证号重复");
}
// 转换为实体,设置审计字段
CcdiEmployee employee = new CcdiEmployee();
BeanUtils.copyProperties(addDTO, employee);
employee.setCreateBy("导入");
employee.setUpdateBy("导入");
validEmployees.add(employee);
} catch (Exception e) {
errorMessages.add(String.format("%s 导入失败:%s",
excel.getName(), e.getMessage()));
}
}
// 第二阶段:批量删除已存在的记录
if (!validEmployees.isEmpty()) {
employeeMapper.deleteBatchByIdCard(new ArrayList<>(idCards));
}
// 第三阶段:批量插入所有数据
if (!validEmployees.isEmpty()) {
employeeMapper.insertBatch(validEmployees);
}
// 第四阶段:返回结果
if (!errorMessages.isEmpty()) {
StringBuilder failureMsg = new StringBuilder();
failureMsg.append("很抱歉,导入完成!成功 ")
.append(validEmployees.size())
.append(" 条,失败 ")
.append(errorMessages.size())
.append(" 条,错误如下:");
for (int i = 0; i < errorMessages.size(); i++) {
failureMsg.append("<br/>")
.append(i + 1)
.append("")
.append(errorMessages.get(i));
}
throw new RuntimeException(failureMsg.toString());
}
return "恭喜您,数据已全部导入成功!共 " + validEmployees.size() + "";
}
```
**Step 2: 保存文件**
无需测试,代码修改。
**Step 3: 提交**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java
git commit -m "refactor(employee): 重构导入方法为先删后插模式"
```
---
### Task 1.4:生成员工模块测试脚本
**文件:**
- 创建:`test/test_employee_import_delete.ps1`
**Step 1: 创建测试脚本**
创建 PowerShell 测试脚本:
```powershell
# 员工导入功能测试脚本(先删后插模式)
# 目的:验证新的导入逻辑是否正常工作
# 配置
$BaseUrl = "http://localhost:8080"
$LoginUrl = "$BaseUrl/login/test"
$ImportUrl = "$BaseUrl/ccdi/employee/importData"
# 测试账号
$Username = "admin"
$Password = "admin123"
# 日志文件
$LogFile = "test/employee_import_test_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
# 开始记录日志
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logMessage = "[$timestamp] $Message"
Write-Host $logMessage
Add-Content -Path $LogFile -Value $logMessage
}
Write-Log "=========================================="
Write-Log "员工导入功能测试(先删后插模式)"
Write-Log "=========================================="
Write-Log ""
# 步骤1登录获取Token
Write-Log "步骤1登录获取Token..."
try {
$loginBody = @{
username = $Username
password = $Password
} | ConvertTo-Json
$loginResponse = Invoke-RestMethod -Uri $LoginUrl -Method Post -Body $loginBody -ContentType "application/json"
if ($loginResponse.code -eq 200) {
$Token = $loginResponse.token
Write-Log "✓ 登录成功"
} else {
Write-Log "✗ 登录失败: $($loginResponse.msg)"
exit 1
}
} catch {
Write-Log "✗ 登录请求失败: $_"
exit 1
}
Write-Log ""
# 步骤2准备测试数据
Write-Log "步骤2准备测试数据..."
$testData = @{
list = @(
@{
employeeId = 1001
name = "测试用户A"
deptId = 103
idCard = "110101199001011234"
phone = "13800138001"
hireDate = "2020-01-01"
status = "0"
},
@{
employeeId = 1002
name = "测试用户B"
deptId = 103
idCard = "110101199001022345"
phone = "13800138002"
hireDate = "2020-01-02"
status = "0"
}
)
} | ConvertTo-Json -Depth 10
Write-Log "测试数据准备完成2条记录"
Write-Log ""
# 步骤3执行导入
Write-Log "步骤3执行导入..."
try {
$headers = @{
"Authorization" = "Bearer $Token"
}
$importResponse = Invoke-RestMethod -Uri $ImportUrl -Method Post -Headers $headers -Body $testData -ContentType "application/json"
Write-Log ""
Write-Log "=========================================="
Write-Log "导入结果:"
Write-Log "=========================================="
Write-Log "响应代码: $($importResponse.code)"
Write-Log "响应消息: $($importResponse.msg)"
if ($importResponse.code -eq 200) {
Write-Log ""
Write-Log "✓ 导入测试成功!"
} else {
Write-Log ""
Write-Log "✗ 导入测试失败!"
}
} catch {
Write-Log ""
Write-Log "✗ 导入请求失败:"
Write-Log "错误信息: $_"
}
Write-Log ""
Write-Log "=========================================="
Write-Log "测试完成"
Write-Log "详细日志: $LogFile"
Write-Log "=========================================="
```
**Step 2: 保存文件**
**Step 3: 提交**
```bash
git add test/test_employee_import_delete.ps1
git commit -m "test(employee): 添加导入功能测试脚本"
```
---
### Task 1.5:测试员工模块导入功能
**Step 1: 启动后端服务**
如果后端服务未启动,先启动:
```bash
mvn spring-boot:run
```
**Step 2: 在新终端运行测试脚本**
```powershell
cd D:\ccdi\ccdi
.\test\test_employee_import_delete.ps1
```
**Step 3: 验证结果**
检查:
- ✅ 测试脚本显示 "导入测试成功"
- ✅ 日志文件显示响应代码 200
- ✅ 数据库中数据正确插入
**Step 4: 如果测试通过,提交工作**
```bash
# 所有改动已提交,无需额外操作
```
---
## 模块 2中介库个人管理
### Task 2.1:添加批量删除方法到 Mapper 接口
- [x] **已完成** (commit: ba8eedc)
**文件:**
- 修改:`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiBizIntermediaryMapper.java`
**Step 1: 在 Mapper 接口中添加方法声明**
```java
/**
* 根据个人证件号批量删除中介库个人数据
*
* @param personIds 个人证件号列表
* @return 删除行数
*/
int deleteBatchByPersonId(@Param("list") List<String> personIds);
```
**Step 2: 提交**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiBizIntermediaryMapper.java
git commit -m "feat(intermediary): 添加个人批量删除方法声明"
```
---
### Task 2.2:在 Mapper XML 中实现批量删除 SQL
**文件:**
- 修改:`ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiBizIntermediaryMapper.xml`
**Step 1: 在 XML 文件中添加删除 SQL**
```xml
<!-- 根据个人证件号批量删除中介库个人数据 -->
<delete id="deleteBatchByPersonId">
DELETE FROM ccdi_biz_intermediary
WHERE person_id IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</delete>
```
**Step 2: 提交**
```bash
git add ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiBizIntermediaryMapper.xml
git commit -m "feat(intermediary): 实现个人批量删除SQL"
```
---
### Task 2.3:重构中介库个人导入方法
**文件:**
- 修改:`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java`
- 目标方法:`importIntermediaryPerson`
**Step 1: 找到 `importIntermediaryPerson` 方法**
`CcdiIntermediaryServiceImpl.java` 中定位方法。
**Step 2: 重构方法逻辑**
参考员工模块的模式,重构为先删后插:
```java
@Override
@Transactional(rollbackFor = Exception.class)
public String importIntermediaryPerson(List<CcdiIntermediaryPersonExcel> excelList, Boolean isUpdateSupport) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
return "至少需要一条数据";
}
// 第一阶段:数据验证和收集
List<CcdiBizIntermediary> validList = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
Set<String> personIds = new HashSet<>();
for (CcdiIntermediaryPersonExcel excel : excelList) {
try {
// 转换并验证
CcdiIntermediaryPersonAddDTO addDTO = new CcdiIntermediaryPersonAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 调用验证方法(需要根据实际情况调整)
// validateIntermediaryPersonDataBasic(addDTO);
// 检查导入数据内部是否重复
if (!personIds.add(addDTO.getPersonId())) {
throw new RuntimeException("导入文件中该个人证件号重复");
}
// 转换为实体,设置审计字段
CcdiBizIntermediary entity = new CcdiBizIntermediary();
BeanUtils.copyProperties(addDTO, entity);
entity.setCreateBy("导入");
entity.setUpdateBy("导入");
validList.add(entity);
} catch (Exception e) {
errorMessages.add(String.format("%s 导入失败:%s",
excel.getName(), e.getMessage()));
}
}
// 第二阶段:批量删除已存在的记录
if (!validList.isEmpty()) {
ccdiBizIntermediaryMapper.deleteBatchByPersonId(new ArrayList<>(personIds));
}
// 第三阶段:批量插入所有数据
if (!validList.isEmpty()) {
ccdiBizIntermediaryMapper.insertBatch(validList);
}
// 第四阶段:返回结果
if (!errorMessages.isEmpty()) {
StringBuilder failureMsg = new StringBuilder();
failureMsg.append("很抱歉,导入完成!成功 ")
.append(validList.size())
.append(" 条,失败 ")
.append(errorMessages.size())
.append(" 条,错误如下:");
for (int i = 0; i < errorMessages.size(); i++) {
failureMsg.append("<br/>")
.append(i + 1)
.append("")
.append(errorMessages.get(i));
}
throw new RuntimeException(failureMsg.toString());
}
return "恭喜您,数据已全部导入成功!共 " + validList.size() + "";
}
```
**注意**:需要根据实际的 DTO 类名、验证方法名、Mapper 注入名进行调整。
**Step 3: 提交**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java
git commit -m "refactor(intermediary): 重构个人导入方法为先删后插模式"
```
---
## 模块 3中介库实体管理
### Task 3.1:添加批量删除方法到 Mapper 接口
**文件:**
- 修改:`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEnterpriseBaseInfoMapper.java`
**Step 1: 在 Mapper 接口中添加方法声明**
```java
/**
* 根据统一社会信用代码批量删除中介库实体数据
*
* @param socialCreditCodes 统一社会信用代码列表
* @return 删除行数
*/
int deleteBatchBySocialCreditCode(@Param("list") List<String> socialCreditCodes);
```
**Step 2: 提交**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEnterpriseBaseInfoMapper.java
git commit -m "feat(intermediary): 添加实体批量删除方法声明"
```
---
### Task 3.2:在 Mapper XML 中实现批量删除 SQL
**文件:**
- 修改:`ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEnterpriseBaseInfoMapper.xml`
**Step 1: 在 XML 文件中添加删除 SQL**
```xml
<!-- 根据统一社会信用代码批量删除中介库实体数据 -->
<delete id="deleteBatchBySocialCreditCode">
DELETE FROM ccdi_enterprise_base_info
WHERE social_credit_code IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</delete>
```
**Step 2: 提交**
```bash
git add ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEnterpriseBaseInfoMapper.xml
git commit -m "feat(intermediary): 实现实体批量删除SQL"
```
---
### Task 3.3:重构中介库实体导入方法
**文件:**
- 修改:`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java`
- 目标方法:`importIntermediaryEntity`
**Step 1: 找到 `importIntermediaryEntity` 方法**
`CcdiIntermediaryServiceImpl.java` 中定位方法。
**Step 2: 重构方法逻辑**
参考个人模块的模式,重构为先删后插:
```java
@Override
@Transactional(rollbackFor = Exception.class)
public String importIntermediaryEntity(List<CcdiIntermediaryEntityExcel> excelList, Boolean isUpdateSupport) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
return "至少需要一条数据";
}
// 第一阶段:数据验证和收集
List<CcdiEnterpriseBaseInfo> validList = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
Set<String> socialCreditCodes = new HashSet<>();
for (CcdiIntermediaryEntityExcel excel : excelList) {
try {
// 转换并验证
CcdiIntermediaryEntityAddDTO addDTO = new CcdiIntermediaryEntityAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 调用验证方法(需要根据实际情况调整)
// validateIntermediaryEntityDataBasic(addDTO);
// 检查导入数据内部是否重复
if (!socialCreditCodes.add(addDTO.getSocialCreditCode())) {
throw new RuntimeException("导入文件中该统一社会信用代码重复");
}
// 转换为实体,设置审计字段
CcdiEnterpriseBaseInfo entity = new CcdiEnterpriseBaseInfo();
BeanUtils.copyProperties(addDTO, entity);
entity.setCreateBy("导入");
entity.setUpdateBy("导入");
validList.add(entity);
} catch (Exception e) {
errorMessages.add(String.format("%s 导入失败:%s",
excel.getEnterpriseName(), e.getMessage()));
}
}
// 第二阶段:批量删除已存在的记录
if (!validList.isEmpty()) {
ccdiEnterpriseBaseInfoMapper.deleteBatchBySocialCreditCode(new ArrayList<>(socialCreditCodes));
}
// 第三阶段:批量插入所有数据
if (!validList.isEmpty()) {
ccdiEnterpriseBaseInfoMapper.insertBatch(validList);
}
// 第四阶段:返回结果
if (!errorMessages.isEmpty()) {
StringBuilder failureMsg = new StringBuilder();
failureMsg.append("很抱歉,导入完成!成功 ")
.append(validList.size())
.append(" 条,失败 ")
.append(errorMessages.size())
.append(" 条,错误如下:");
for (int i = 0; i < errorMessages.size(); i++) {
failureMsg.append("<br/>")
.append(i + 1)
.append("")
.append(errorMessages.get(i));
}
throw new RuntimeException(failureMsg.toString());
}
return "恭喜您,数据已全部导入成功!共 " + validList.size() + "";
}
```
**注意**:需要根据实际的 DTO 类名、验证方法名、Mapper 注入名进行调整。
**Step 3: 提交**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java
git commit -m "refactor(intermediary): 重构实体导入方法为先删后插模式"
```
---
## 模块 4员工招聘信息管理
### Task 4.1:添加批量删除方法到 Mapper 接口
**文件:**
- 修改:`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiStaffRecruitmentMapper.java`
**Step 1: 在 Mapper 接口中添加方法声明**
```java
/**
* 根据招聘项目编号批量删除招聘信息数据
*
* @param recruitIds 招聘项目编号列表
* @return 删除行数
*/
int deleteBatchByRecruitId(@Param("list") List<String> recruitIds);
```
**Step 2: 提交**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiStaffRecruitmentMapper.java
git commit -m "feat(recruitment): 添加批量删除方法声明"
```
---
### Task 4.2:在 Mapper XML 中实现批量删除 SQL
**文件:**
- 修改:`ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiStaffRecruitmentMapper.xml`
**Step 1: 在 XML 文件中添加删除 SQL**
```xml
<!-- 根据招聘项目编号批量删除招聘信息数据 -->
<delete id="deleteBatchByRecruitId">
DELETE FROM ccdi_staff_recruitment
WHERE recruit_id IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</delete>
```
**Step 2: 提交**
```bash
git add ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiStaffRecruitmentMapper.xml
git commit -m "feat(recruitment): 实现批量删除SQL"
```
---
### Task 4.3:重构招聘信息导入方法
**文件:**
- 修改:`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentServiceImpl.java`
- 目标方法:`importRecruitment`
**Step 1: 找到 `importRecruitment` 方法**
`CcdiStaffRecruitmentServiceImpl.java` 中定位方法。
**Step 2: 重构方法逻辑**
参考员工模块的模式,重构为先删后插:
```java
@Override
@Transactional(rollbackFor = Exception.class)
public String importRecruitment(List<CcdiStaffRecruitmentExcel> excelList, Boolean isUpdateSupport) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
return "至少需要一条数据";
}
// 第一阶段:数据验证和收集
List<CcdiStaffRecruitment> validList = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
Set<String> recruitIds = new HashSet<>();
for (CcdiStaffRecruitmentExcel excel : excelList) {
try {
// 转换并验证
CcdiStaffRecruitmentAddDTO addDTO = new CcdiStaffRecruitmentAddDTO();
BeanUtils.copyProperties(excel, addDTO);
// 调用验证方法(需要根据实际情况调整)
// validateRecruitmentDataBasic(addDTO);
// 检查导入数据内部是否重复
if (!recruitIds.add(addDTO.getRecruitId())) {
throw new RuntimeException("导入文件中该招聘项目编号重复");
}
// 转换为实体,设置审计字段
CcdiStaffRecruitment entity = new CcdiStaffRecruitment();
BeanUtils.copyProperties(addDTO, entity);
entity.setCreateBy("导入");
entity.setUpdateBy("导入");
validList.add(entity);
} catch (Exception e) {
errorMessages.add(String.format("%s 导入失败:%s",
excel.getRecruitName(), e.getMessage()));
}
}
// 第二阶段:批量删除已存在的记录
if (!validList.isEmpty()) {
ccdiStaffRecruitmentMapper.deleteBatchByRecruitId(new ArrayList<>(recruitIds));
}
// 第三阶段:批量插入所有数据
if (!validList.isEmpty()) {
ccdiStaffRecruitmentMapper.insertBatch(validList);
}
// 第四阶段:返回结果
if (!errorMessages.isEmpty()) {
StringBuilder failureMsg = new StringBuilder();
failureMsg.append("很抱歉,导入完成!成功 ")
.append(validList.size())
.append(" 条,失败 ")
.append(errorMessages.size())
.append(" 条,错误如下:");
for (int i = 0; i < errorMessages.size(); i++) {
failureMsg.append("<br/>")
.append(i + 1)
.append("")
.append(errorMessages.get(i));
}
throw new RuntimeException(failureMsg.toString());
}
return "恭喜您,数据已全部导入成功!共 " + validList.size() + "";
}
```
**注意**:需要根据实际的 DTO 类名、验证方法名、Mapper 注入名进行调整。
**Step 3: 提交**
```bash
git add ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentServiceImpl.java
git commit -m "refactor(recruitment): 重构导入方法为先删后插模式"
```
---
## 模块 5清理和文档
### Task 5.1:移除不再使用的批量更新方法(如果存在)
**文件:**
- 检查:各模块的 Mapper XML 和 Mapper 接口
**Step 1: 检查是否存在 updateBatch 方法**
在以下文件中搜索 `updateBatch`
- `CcdiEmployeeMapper.xml`
- `CcdiBizIntermediaryMapper.xml`
- `CcdiEnterpriseBaseInfoMapper.xml`
- `CcdiStaffRecruitmentMapper.xml`
**Step 2: 如果存在,删除 updateBatch 方法**
删除不再使用的批量更新 SQL 和接口声明。
**Step 3: 提交**
```bash
git commit -am "refactor: 移除不再使用的批量更新方法"
```
---
### Task 5.2:更新 API 文档
**文件:**
- 修改:`doc/api/ccdi_staff_recruitment_api.md`(如果存在)
**Step 1: 更新导入接口文档**
在 API 文档中说明新的导入逻辑:
- 采用"先删除后插入"策略
- `isUpdateSupport` 参数保留以保持兼容性,但不再使用
- 所有审计字段create_time, update_time 等)会被重置为当前时间
**Step 2: 提交**
```bash
git add doc/api/
git commit -m "docs: 更新导入接口文档说明"
```
---
## 完成检查清单
在完成所有任务后,确认以下事项:
- [ ] 员工信息模块测试通过
- [ ] 中介库个人模块功能正常
- [ ] 中介库实体模块功能正常
- [ ] 招聘信息模块功能正常
- [ ] 所有代码已提交(不少于 11 个 commits
- [ ] API 文档已更新
- [ ] 设计文档已归档到 `doc/plans/`
---
## 测试指南
### 完整功能测试
1. **启动后端服务**
```bash
mvn spring-boot:run
```
2. **测试各模块导入功能**
为每个模块运行相应的测试(参考员工模块测试脚本)。
3. **验证数据库**
检查导入的数据是否正确,旧数据是否被删除。
### 性能测试
测试不同数据量的导入性能:
- 小数据量10 条
- 中数据量100 条
- 大数据量1000 条
---
**实施计划完成**

View File

@@ -0,0 +1,564 @@
# 导入逻辑优化设计文档
## 文档信息
- **创建日期**2026-02-05
- **版本**1.0
- **作者**Claude Code
- **状态**:待实施
---
## 1. 背景和目标
### 1.1 背景
当前系统中的导入功能采用"存在则更新,不存在则插入"的逻辑:
- 需要区分新增和更新两种操作
- 使用复杂的条件判断和数据分类逻辑
- 批量更新操作依赖特殊的 SQL 语法CASE WHEN容易出现语法错误
- 代码逻辑复杂,维护成本高
### 1.2 目标
优化导入逻辑,简化代码实现:
- 统一采用"先删除后插入"的策略
- 移除复杂的更新操作和条件判断
- 提高代码可维护性和可读性
- 保证数据一致性和事务完整性
---
## 2. 需求分析
### 2.1 功能需求
#### 核心需求
1. **导入策略变更**:将"存在则更新"改为"先删后插"
2. **删除范围**:只删除导入数据中已存在的记录
3. **唯一性判断**:使用业务唯一键判断记录是否存在
4. **审计字段**:重新插入的数据,所有审计字段使用当前时间
5. **冲突处理**:批量删除所有使用相同业务键的记录
#### 影响模块
- 员工信息管理(`ccdi_employee`
- 中介库个人管理(`ccdi_biz_intermediary`
- 中介库实体管理(`ccdi_enterprise_base_info`
- 员工招聘信息管理(`ccdi_staff_recruitment`
### 2.2 非功能需求
- **性能**批量操作2-3次数据库往返
- **事务性**:所有操作在同一事务中,保证原子性
- **兼容性**:前端调用方式保持不变
---
## 3. 设计方案
### 3.1 整体架构
新的导入逻辑采用三阶段流程:
#### 阶段 1数据验证与收集
- 遍历所有导入数据,验证必填字段和数据格式
- 收集所有业务唯一键
- 检查导入数据内部的重复性
- 验证通过的数据放入待处理列表
#### 阶段 2批量删除
- 根据收集的业务唯一键列表,执行批量删除操作
- SQL`DELETE FROM table WHERE unique_key IN (...)`
- 删除所有匹配的旧记录,包括重复的记录
#### 阶段 3批量插入
- 批量插入所有验证通过的数据
- SQL`INSERT INTO table (...) VALUES (...), (...), ...`
- 所有审计字段使用当前时间
### 3.2 数据流图
```
导入数据Excel
【阶段 1】数据验证与收集
├→ 验证必填字段和数据格式
├→ 检查导入数据内部重复
├→ 收集业务唯一键
└→ 构建待插入列表
【阶段 2】批量删除已存在记录
└→ DELETE FROM table WHERE unique_key IN (...)
【阶段 3】批量插入所有数据
└→ INSERT INTO table (...) VALUES (...)
返回导入结果(成功数量、失败详情)
```
### 3.3 各模块业务键定义
| 模块 | 表名 | 业务键 | 说明 |
|------|------|--------|------|
| 员工信息 | `ccdi_employee` | `id_card` | 身份证号 |
| 中介库个人 | `ccdi_biz_intermediary` | `person_id` | 个人证件号 |
| 中介库实体 | `ccdi_enterprise_base_info` | `social_credit_code` | 统一社会信用代码 |
| 招聘信息 | `ccdi_staff_recruitment` | `recruit_id` | 招聘项目编号 |
---
## 4. 详细设计
### 4.1 数据库层设计
#### 4.1.1 新增 Mapper 方法
每个模块需要添加对应的批量删除方法:
**员工信息模块**
```java
// CcdiEmployeeMapper.java
int deleteBatchByIdCard(@Param("list") List<String> idCards);
```
**中介库个人模块**
```java
// CcdiBizIntermediaryMapper.java
int deleteBatchByPersonId(@Param("list") List<String> personIds);
```
**中介库实体模块**
```java
// CcdiEnterpriseBaseInfoMapper.java
int deleteBatchBySocialCreditCode(@Param("list") List<String> socialCreditCodes);
```
**招聘信息模块**
```java
// CcdiStaffRecruitmentMapper.java
int deleteBatchByRecruitId(@Param("list") List<String> recruitIds);
```
#### 4.1.2 Mapper XML 实现
所有删除 SQL 使用统一的模式:
```xml
<delete id="deleteBatchByXxx">
DELETE FROM {table_name}
WHERE {unique_key_column} IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</delete>
```
**示例(员工信息)**
```xml
<!-- CcdiEmployeeMapper.xml -->
<delete id="deleteBatchByIdCard">
DELETE FROM ccdi_employee
WHERE id_card IN
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</delete>
```
### 4.2 服务层设计
#### 4.2.1 通用导入方法模板
所有模块的导入方法遵循统一的实现模式:
```java
@Override
@Transactional(rollbackFor = Exception.class)
public String importXxx(List<XxxExcel> excelList, Boolean isUpdateSupport) {
// 参数校验
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
return "至少需要一条数据";
}
// 第一阶段:数据验证和收集
List<XxxEntity> validList = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
Set<String> uniqueKeys = new HashSet<>();
for (XxxExcel excel : excelList) {
try {
// 转换并验证
XxxAddDTO addDTO = new XxxAddDTO();
BeanUtils.copyProperties(excel, addDTO);
validateXxxDataBasic(addDTO);
// 检查导入数据内部是否重复
String uniqueKey = getUniqueKey(addDTO);
if (!uniqueKeys.add(uniqueKey)) {
throw new RuntimeException("导入文件中该" + getUniqueKeyName() + "重复");
}
// 转换为实体,设置审计字段
XxxEntity entity = new XxxEntity();
BeanUtils.copyProperties(addDTO, entity);
entity.setCreateBy("导入");
entity.setUpdateBy("导入");
validList.add(entity);
} catch (Exception e) {
errorMessages.add(String.format("%s 导入失败:%s",
getDisplayName(excel), e.getMessage()));
}
}
// 第二阶段:批量删除已存在的记录
if (!validList.isEmpty()) {
List<String> uniqueKeyList = new ArrayList<>(uniqueKeys);
mapper.deleteBatchByUniqueKey(uniqueKeyList);
}
// 第三阶段:批量插入所有数据
if (!validList.isEmpty()) {
mapper.insertBatch(validList);
}
// 第四阶段:返回结果
if (!errorMessages.isEmpty()) {
throw buildFailureException(validList.size(), errorMessages);
}
return buildSuccessMessage(validList.size());
}
```
#### 4.2.2 员工信息导入方法(示例)
```java
// CcdiEmployeeServiceImpl.java
@Override
@Transactional(rollbackFor = Exception.class)
public String importEmployee(List<CcdiEmployeeExcel> excelList, Boolean isUpdateSupport) {
if (StringUtils.isNull(excelList) || excelList.isEmpty()) {
return "至少需要一条数据";
}
// 第一阶段:数据验证和收集
List<CcdiEmployee> validEmployees = new ArrayList<>();
List<String> errorMessages = new ArrayList<>();
Set<String> idCards = new HashSet<>();
for (CcdiEmployeeExcel excel : excelList) {
try {
// 转换并验证
CcdiEmployeeAddDTO addDTO = new CcdiEmployeeAddDTO();
BeanUtils.copyProperties(excel, addDTO);
validateEmployeeDataBasic(addDTO);
// 检查导入数据内部是否重复
if (!idCards.add(addDTO.getIdCard())) {
throw new RuntimeException("导入文件中该身份证号重复");
}
// 转换为实体,设置审计字段
CcdiEmployee employee = new CcdiEmployee();
BeanUtils.copyProperties(addDTO, employee);
employee.setCreateBy("导入");
employee.setUpdateBy("导入");
validEmployees.add(employee);
} catch (Exception e) {
errorMessages.add(String.format("%s 导入失败:%s",
excel.getName(), e.getMessage()));
}
}
// 第二阶段:批量删除已存在的记录
if (!validEmployees.isEmpty()) {
employeeMapper.deleteBatchByIdCard(new ArrayList<>(idCards));
}
// 第三阶段:批量插入所有数据
if (!validEmployees.isEmpty()) {
employeeMapper.insertBatch(validEmployees);
}
// 第四阶段:返回结果
if (!errorMessages.isEmpty()) {
StringBuilder failureMsg = new StringBuilder();
failureMsg.append("很抱歉,导入完成!成功 ")
.append(validEmployees.size())
.append(" 条,失败 ")
.append(errorMessages.size())
.append(" 条,错误如下:");
for (int i = 0; i < errorMessages.size(); i++) {
failureMsg.append("<br/>")
.append(i + 1)
.append("")
.append(errorMessages.get(i));
}
throw new RuntimeException(failureMsg.toString());
}
return "恭喜您,数据已全部导入成功!共 " + validEmployees.size() + "";
}
```
### 4.3 事务管理
#### 事务边界
整个导入操作使用 `@Transactional` 注解,确保原子性:
```java
@Transactional(rollbackFor = Exception.class)
public String importXxx(List<XxxExcel> excelList, Boolean isUpdateSupport) {
// 所有数据库操作在一个事务中
}
```
#### 事务保证
| 场景 | 处理方式 | 结果 |
|------|----------|------|
| 批量删除失败 | 自动回滚 | 不影响现有数据 |
| 批量插入失败 | 自动回滚 | 已删除的数据恢复 |
| 数据验证失败 | 不执行数据库操作 | 直接返回错误信息 |
### 4.4 错误处理
#### 分层错误处理策略
**1. 数据验证层**
- 捕获单条数据的验证错误(必填字段、格式校验)
- 记录到失败列表,不影响其他数据
- 验证通过的数据继续处理
**2. 数据库操作层**
- 删除/插入失败时抛出异常,触发事务回滚
- 捕获 `DuplicateKeyException``DataIntegrityViolationException`
- 转换为用户友好的错误消息
**3. 统一返回**
- 全部成功:返回成功消息 + 统计信息
- 部分失败(验证阶段):返回详细错误列表
- 数据库失败:事务回滚,返回系统错误提示
### 4.5 数据一致性保障
#### 场景 1导入数据中业务键重复
**示例**:导入文件中有两条记录的身份证号都是 `110101199001011234`
**处理结果**
- 数据库中的旧记录被删除(如果存在)
- 导入文件中的最后一条记录被插入
- 第一条记录在验证阶段被检测为重复,记录到错误列表
#### 场景 2数据库中存在重复记录
**示例**:数据库中有两条记录的身份证号都是 `110101199001011234`(历史数据问题)
**处理结果**
- 批量删除操作会删除所有身份证号匹配的记录
- 插入新的记录
- 自动修复了数据不一致问题
#### 场景 3并发导入
**示例**:用户 A 和用户 B 同时导入包含相同身份证号的数据
**处理结果**
- 依赖数据库事务隔离级别和锁机制
- 后提交的事务可能产生 `DuplicateKeyException`
- 事务回滚,返回错误提示
---
## 5. 实施计划
### 5.1 修改文件清单11 个文件)
#### 员工信息管理模块
1. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeMapper.java`
2. `ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEmployeeMapper.xml`
3. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiEmployeeServiceImpl.java`
#### 中介库管理模块(个人和实体)
4. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiBizIntermediaryMapper.java`
5. `ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiBizIntermediaryMapper.xml`
6. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEnterpriseBaseInfoMapper.java`
7. `ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiEnterpriseBaseInfoMapper.xml`
8. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryServiceImpl.java`
- 修改 `importIntermediaryPerson` 方法
- 修改 `importIntermediaryEntity` 方法
#### 员工招聘信息管理模块
9. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiStaffRecruitmentMapper.java`
10. `ruoyi-ccdi/src/main/resources/mapper/ccdi/CcdiStaffRecruitmentMapper.xml`
11. `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiStaffRecruitmentServiceImpl.java`
### 5.2 实施步骤
#### 步骤 1员工信息模块验证方案
1. 添加 `deleteBatchByIdCard` 方法到 Mapper 接口
2. 在 Mapper XML 中实现删除 SQL
3. 重构 `importEmployee` 方法
4. 生成测试脚本并验证功能
5. **验证通过后,继续其他模块**
#### 步骤 2中介库模块
1. 添加个人表的批量删除方法
2. 添加实体表的批量删除方法
3. 重构两个导入方法
4. 测试验证
#### 步骤 3招聘信息模块
1. 添加批量删除方法
2. 重构导入方法
3. 测试验证
#### 步骤 4清理和优化
1. 移除不再使用的 `updateBatch` 方法(如果存在)
2. 更新 API 文档
3. 代码审查
### 5.3 测试计划
#### 单元测试
- 测试批量删除 SQL 语法正确性
- 测试批量插入 SQL 语法正确性
- 测试事务回滚机制
#### 集成测试
- 测试全新数据导入(数据库中不存在)
- 测试更新数据导入(数据库中已存在)
- 测试混合数据导入(部分存在,部分不存在)
- 测试导入数据内部重复
- 测试数据库中存在重复记录的清理
#### 性能测试
- 测试 100 条数据的导入性能
- 测试 1000 条数据的导入性能
- 对比优化前后的性能差异
---
## 6. 风险评估
### 6.1 技术风险
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|----------|
| 批量删除 SQL 性能问题 | 中 | 低 | 确保 business_key 有索引 |
| 事务超时 | 中 | 低 | 监控事务执行时间,必要时调整超时配置 |
| 并发冲突 | 低 | 中 | 依赖数据库事务隔离机制 |
### 6.2 业务风险
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|----------|
| 历史数据丢失(审计字段重置) | 中 | 低 | 在文档中说明,告知用户 |
| 用户误操作导入错误数据 | 高 | 中 | 前端增加确认提示 |
### 6.3 兼容性风险
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|----------|
| 前端依赖 `isUpdateSupport` 参数 | 低 | 低 | 参数保留但不使用 |
| 其他系统调用导入接口 | 低 | 低 | 保持接口签名不变 |
---
## 7. 优势与劣势
### 7.1 优势
1. **代码简化**
- 移除复杂的条件判断和数据分类逻辑
- 统一的实现模式,易于维护
- 代码行数减少约 30%
2. **性能优化**
- 数据库操作从 3-4 次减少到 2-3 次
- 不再需要复杂的批量更新 SQL
- 批量删除和批量插入都使用索引,性能更好
3. **数据一致性**
- 自动清理重复数据
- 事务保证原子性
- 减少数据不一致的可能性
4. **可维护性**
- 代码逻辑清晰易懂
- 各模块实现模式统一
- 新增模块导入功能时可直接复用
### 7.2 劣势
1. **审计字段丢失**
- `create_time``create_by` 会被重置为当前值
- 无法保留原始创建时间
- **缓解措施**:在文档中明确说明,如果需要保留历史记录,可以考虑使用软删除或历史表
2. **并发性能**
- 高并发情况下可能产生事务冲突
- **缓解措施**:导入功能通常是管理员操作,并发概率较低
3. **参数失效**
- `isUpdateSupport` 参数失去原有意义
- **缓解措施**:保留参数以保持接口兼容性,内部不再使用
---
## 8. 后续优化建议
### 8.1 短期优化
1. **添加导入进度提示**
- 对于大量数据导入,前端显示导入进度
- 避免用户长时间等待
2. **优化错误消息**
- 提供更详细的错误信息
- 帮助用户快速定位问题
### 8.2 长期优化
1. **异步导入**
- 对于超大文件(>10000条使用异步处理
- 导入完成后通知用户
2. **导入历史记录**
- 记录每次导入的操作日志
- 支持导入历史查询和回滚
3. **数据校验增强**
- 添加更多业务规则校验
- 支持自定义校验规则
---
## 9. 附录
### 9.1 术语表
| 术语 | 说明 |
|------|------|
| 业务键 | 业务层面判断记录唯一性的字段(如身份证号) |
| 审计字段 | 记录数据创建和修改信息的字段create_time, create_by, update_time, update_by |
| 批量操作 | 一次数据库操作处理多条记录 |
| 事务 | 保证一组数据库操作原子性的机制 |
### 9.2 参考资料
- [MyBatis 官方文档 - 动态 SQL](https://mybatis.org/mybatis-3/zh/dynamic-sql.html)
- [MySQL 批量插入最佳实践](https://dev.mysql.com/doc/refman/8.0/en/insert-optimization.html)
- [Spring 事务管理](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html)
---
**文档结束**

View File

@@ -0,0 +1,46 @@
package com.ruoyi.ccdi.annotation;
import com.ruoyi.ccdi.validation.EnumValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
/**
* 枚举值校验注解
* 用于校验字段值是否在指定枚举类的定义范围内
*
* @author ruoyi
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValidator.class)
@Documented
public @interface EnumValid {
/**
* 枚举类
*/
Class<?> enumClass();
/**
* 校验失败时的错误消息
*/
String message() default "枚举值不合法";
/**
* 分组
*/
Class<?>[] groups() default {};
/**
* 负载
*/
Class<? extends Payload>[] payload() default {};
/**
* 是否忽略空值
* 如果为true当字段为null或空字符串时不进行校验
*/
boolean ignoreEmpty() default true;
}

View File

@@ -1,59 +0,0 @@
package com.ruoyi.ccdi.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 员工亲属对象 dpc_employee_relative
*
* @author ruoyi
* @date 2026-01-28
*/
@Data
public class CcdiEmployeeRelative implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 亲属ID */
@TableId(type = IdType.AUTO)
private Long relativeId;
/** 员工ID */
private Long employeeId;
/** 亲属姓名 */
private String relativeName;
/** 亲属身份证号 */
private String relativeIdCard;
/** 亲属手机号 */
private String relativePhone;
/** 与员工关系 */
private String relationship;
/** 创建者 */
@TableField(fill = FieldFill.INSERT)
private String createBy;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 更新者 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/** 更新时间 */
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}

View File

@@ -1,40 +0,0 @@
package com.ruoyi.ccdi.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 员工亲属新增 DTO
*
* @author ruoyi
* @date 2026-01-28
*/
@Data
public class CcdiEmployeeRelativeAddDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 亲属姓名 */
@NotBlank(message = "亲属姓名不能为空")
@Size(max = 100, message = "亲属姓名长度不能超过100个字符")
private String relativeName;
/** 亲属身份证号 */
@Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$", message = "亲属身份证号格式不正确")
private String relativeIdCard;
/** 亲属手机号 */
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "亲属手机号格式不正确")
private String relativePhone;
/** 与员工关系 */
@NotBlank(message = "与员工关系不能为空")
@Size(max = 50, message = "与员工关系长度不能超过50个字符")
private String relationship;
}

View File

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.domain.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.annotation.Required;
import lombok.Data;
import java.io.Serial;
@@ -24,26 +25,31 @@ public class CcdiEmployeeExcel implements Serializable {
/** 姓名 */
@ExcelProperty(value = "姓名", index = 0)
@ColumnWidth(15)
@Required
private String name;
/** 员工ID(柜员号) */
@ExcelProperty(value = "柜员号", index = 1)
@ColumnWidth(15)
@Required
private Long employeeId;
/** 所属部门ID */
@ExcelProperty(value = "所属部门ID", index = 2)
@ColumnWidth(15)
@Required
private Long deptId;
/** 身份证号 */
@ExcelProperty(value = "身份证号", index = 3)
@ColumnWidth(20)
@Required
private String idCard;
/** 电话 */
@ExcelProperty(value = "电话", index = 4)
@ColumnWidth(15)
@Required
private String phone;
/** 入职时间 */
@@ -55,5 +61,6 @@ public class CcdiEmployeeExcel implements Serializable {
@ExcelProperty(value = "状态", index = 6)
@ColumnWidth(10)
@DictDropdown(dictType = "ccdi_employee_status")
@Required
private String status;
}

View File

@@ -1,41 +0,0 @@
package com.ruoyi.ccdi.domain.excel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 员工亲属Excel导入导出对象
*
* @author ruoyi
* @date 2026-01-28
*/
@Data
public class CcdiEmployeeRelativeExcel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 亲属姓名 */
@ExcelProperty(value = "亲属姓名", index = 0)
@ColumnWidth(15)
private String relativeName;
/** 亲属身份证号 */
@ExcelProperty(value = "亲属身份证号", index = 1)
@ColumnWidth(20)
private String relativeIdCard;
/** 亲属手机号 */
@ExcelProperty(value = "亲属手机号", index = 2)
@ColumnWidth(15)
private String relativePhone;
/** 与员工关系 */
@ExcelProperty(value = "与员工关系", index = 3)
@ColumnWidth(15)
private String relationship;
}

View File

@@ -1,37 +0,0 @@
package com.ruoyi.ccdi.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 员工亲属 VO
*
* @author ruoyi
* @date 2026-01-28
*/
@Data
public class CcdiEmployeeRelativeVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 亲属ID */
private Long relativeId;
/** 员工ID */
private Long employeeId;
/** 亲属姓名 */
private String relativeName;
/** 亲属身份证号 */
private String relativeIdCard;
/** 亲属手机号 */
private String relativePhone;
/** 与员工关系 */
private String relationship;
}

View File

@@ -1,13 +0,0 @@
package com.ruoyi.ccdi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ccdi.domain.CcdiEmployeeRelative;
/**
* 员工亲属 数据层
*
* @author ruoyi
* @date 2026-01-28
*/
public interface CcdiEmployeeRelativeMapper extends BaseMapper<CcdiEmployeeRelative> {
}

View File

@@ -4,6 +4,7 @@ import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.ruoyi.ccdi.handler.DictDropdownWriteHandler;
import com.ruoyi.ccdi.handler.RequiredFieldWriteHandler;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@@ -159,6 +160,7 @@ public class EasyExcelUtil {
/**
* 下载带字典下拉框的导入模板
* 自动解析实体类中的@DictDropdown注解为对应字段添加下拉框
* 自动解析实体类中的@Required注解为必填字段表头添加红色星号(*)标记
*
* @param response 响应对象
* @param clazz 实体类
@@ -172,6 +174,7 @@ public class EasyExcelUtil {
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
.registerWriteHandler(new RequiredFieldWriteHandler(clazz))
.doWrite(List.of());
} catch (IOException e) {
throw new RuntimeException("下载带字典下拉框的导入模板失败", e);
@@ -181,6 +184,7 @@ public class EasyExcelUtil {
/**
* 下载带字典下拉框的导入模板(指定文件名)
* 自动解析实体类中的@DictDropdown注解为对应字段添加下拉框
* 自动解析实体类中的@Required注解为必填字段表头添加红色星号(*)标记
*
* @param response 响应对象
* @param clazz 实体类
@@ -196,6 +200,7 @@ public class EasyExcelUtil {
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
.registerWriteHandler(new RequiredFieldWriteHandler(clazz))
.doWrite(List.of());
} catch (IOException e) {
throw new RuntimeException("下载带字典下拉框的导入模板失败", e);
@@ -205,6 +210,7 @@ public class EasyExcelUtil {
/**
* 导出Excel带字典下拉框
* 导出的数据包含实际值,但模板中有下拉框供后续编辑使用
* 自动解析实体类中的@Required注解为必填字段表头添加红色星号(*)标记
*
* @param response 响应对象
* @param list 数据列表
@@ -220,6 +226,7 @@ public class EasyExcelUtil {
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
.registerWriteHandler(new RequiredFieldWriteHandler(clazz))
.doWrite(list);
} catch (IOException e) {
throw new RuntimeException("导出带字典下拉框的Excel失败", e);
@@ -229,6 +236,7 @@ public class EasyExcelUtil {
/**
* 导出Excel带字典下拉框指定文件名
* 导出的数据包含实际值,但模板中有下拉框供后续编辑使用
* 自动解析实体类中的@Required注解为必填字段表头添加红色星号(*)标记
*
* @param response 响应对象
* @param list 数据列表
@@ -245,6 +253,7 @@ public class EasyExcelUtil {
.sheet(sheetName)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.registerWriteHandler(new DictDropdownWriteHandler(clazz))
.registerWriteHandler(new RequiredFieldWriteHandler(clazz))
.doWrite(list);
} catch (IOException e) {
throw new RuntimeException("导出带字典下拉框的Excel失败", e);

View File

@@ -0,0 +1,70 @@
package com.ruoyi.ccdi.validation;
import com.ruoyi.ccdi.annotation.EnumValid;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.lang.reflect.Method;
/**
* 枚举值校验器
* 验证字段值是否在指定枚举类的定义范围内
*
* @author ruoyi
*/
public class EnumValidator implements ConstraintValidator<EnumValid, String> {
private EnumValid annotation;
@Override
public void initialize(EnumValid constraintAnnotation) {
this.annotation = constraintAnnotation;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
// 如果允许忽略空值且值为空,则校验通过
if (annotation.ignoreEmpty() && (value == null || value.trim().isEmpty())) {
return true;
}
// 如果值为空且不允许忽略,则校验失败
if (value == null || value.trim().isEmpty()) {
return false;
}
Class<?> enumClass = annotation.enumClass();
// 检查是否是枚举类
if (!enumClass.isEnum()) {
throw new IllegalArgumentException(enumClass.getName() + " 不是枚举类型");
}
// 获取枚举的所有实例
Object[] enumConstants = enumClass.getEnumConstants();
try {
// 尝试调用枚举的getCode方法如果存在
// 假设枚举类有getCode()方法返回枚举的code值
for (Object enumConstant : enumConstants) {
try {
Method getCodeMethod = enumClass.getMethod("getCode");
String code = (String) getCodeMethod.invoke(enumConstant);
if (value.equals(code)) {
return true;
}
} catch (NoSuchMethodException e) {
// 如果没有getCode方法使用name()方法
Enum<?> enumValue = (Enum<?>) enumConstant;
if (value.equals(enumValue.name())) {
return true;
}
}
}
} catch (Exception e) {
throw new RuntimeException("枚举校验失败", e);
}
return false;
}
}

View File

@@ -19,14 +19,18 @@ public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "createBy", String.class, getUsername());
this.strictInsertFill(metaObject, "createdBy", String.class, getUsername());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateBy", String.class, getUsername());
this.strictInsertFill(metaObject, "updatedBy", String.class, getUsername());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
this.strictUpdateFill(metaObject, "updateBy", String.class, getUsername());
this.strictUpdateFill(metaObject, "updatedBy", String.class, getUsername());
}
/**

View File

@@ -1,80 +0,0 @@
# 批量插入100条员工数据
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
$BASE_URL = "http://localhost:8080"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "批量插入100条员工数据" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# 登录获取 Token
Write-Host "[1] 正在登录..." -ForegroundColor Yellow
$loginBody = @{
username = "admin"
password = "admin123"
} | ConvertTo-Json
$loginResponse = Invoke-RestMethod -Uri "$BASE_URL/login/test" -Method Post -ContentType "application/json; charset=utf-8" -Body ([System.Text.Encoding]::UTF8.GetBytes($loginBody))
$TOKEN = $loginResponse.token
Write-Host "登录成功" -ForegroundColor Green
Write-Host ""
$headers = @{
"Authorization" = "Bearer $TOKEN"
"Content-Type" = "application/json; charset=utf-8"
}
# 批量插入100条数据
Write-Host "[2] 开始批量插入100条员工数据..." -ForegroundColor Yellow
$successCount = 0
$failCount = 0
for ($i = 1; $i -le 100; $i++) {
try {
$tellerNo = "TEST" + $i.ToString("000")
$idCard = "110101199001011" + ($i + 200).ToString("000")
$addBody = @{
name = "测试员工" + $i
tellerNo = $tellerNo
orgNo = "1001"
idCard = $idCard
phone = "13800138" + ($i % 100).ToString("00")
status = "0"
relatives = @(
@{
relativeName = "亲属" + $i + "A"
relativeIdCard = "110101199001011" + ($i + 300).ToString("000")
relativePhone = "13900138" + ($i % 100).ToString("00")
relationship = "配偶"
}
)
} | ConvertTo-Json -Depth 10
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($addBody)
$response = Invoke-RestMethod -Uri "$BASE_URL/dpc/employee" -Method Post -Headers $headers -Body $bodyBytes
if ($response.code -eq 200) {
$successCount++
Write-Host "[$i/100] 插入成功: 测试员工$i" -ForegroundColor Green
} else {
$failCount++
Write-Host "[$i/100] 插入失败: $($response.msg)" -ForegroundColor Red
}
} catch {
$failCount++
Write-Host "[$i/100] 插入异常: $_" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "插入完成" -ForegroundColor Cyan
Write-Host "成功: $successCount" -ForegroundColor Green
Write-Host "失败: $failCount" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Cyan
Read-Host "按回车键退出"

View File

@@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
import requests
import json
BASE_URL = "http://localhost:8080"
print("=" * 50)
print("批量插入100条员工数据")
print("=" * 50)
print()
# 登录获取 Token
print("[1] 正在登录...")
login_response = requests.post(
f"{BASE_URL}/login/test",
json={"username": "admin", "password": "admin123"}
)
token = login_response.json()["token"]
print("登录成功")
print()
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json; charset=utf-8"
}
# 批量插入
print("[2] 开始批量插入100条员工数据...")
success_count = 0
fail_count = 0
for i in range(1, 101):
try:
teller_no = f"TEST{i+200:03d}"
id_card = f"110101199001011{i+400:03d}"
data = {
"name": f"测试员工{i}",
"tellerNo": teller_no,
"orgNo": "1001",
"idCard": id_card,
"phone": f"138{10000000+i:08d}",
"status": "0",
"relatives": [
{
"relativeName": f"亲属{i}A",
"relativeIdCard": f"110101199001011{i+300:03d}",
"relativePhone": f"139{10000000+i:08d}",
"relationship": "配偶"
}
]
}
response = requests.post(
f"{BASE_URL}/dpc/employee",
headers=headers,
json=data
)
if response.json()["code"] == 200:
success_count += 1
print(f"[{i}/100] 插入成功: 测试员工{i}")
else:
fail_count += 1
print(f"[{i}/100] 插入失败: {response.json()['msg']}")
except Exception as e:
fail_count += 1
print(f"[{i}/100] 插入异常: {e}")
print()
print("=" * 50)
print("插入完成")
print(f"成功: {success_count}")
print(f"失败: {fail_count}")
print("=" * 50)

View File

@@ -1,63 +0,0 @@
============================================================
分页接口总数测试报告
============================================================
测试时间: 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

@@ -1,84 +0,0 @@
============================================================
分页接口总数测试报告
============================================================
测试时间: 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

@@ -1,84 +0,0 @@
============================================================
分页接口总数测试报告
============================================================
测试时间: 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
------------------------------------------------------------
测试结论:
✗ 存在分页接口总数返回异常

View File

@@ -1 +0,0 @@
{"name":"测试员工","tellerNo":"TEST002","orgNo":"1001","idCard":"110101199001011237","phone":"13800138000","status":"0","relatives":[{"relativeName":"李四","relativeIdCard":"110101199001011235","relativePhone":"13800138001","relationship":"配偶"}]}

View File

@@ -1,66 +0,0 @@
@echo off
chcp 65001 > nul
setlocal enabledelayedexpansion
echo ========================================
echo 员工信息管理 API 测试脚本
echo ========================================
echo.
set BASE_URL=http://localhost:8080
set TOKEN=
REM 1. 登录获取 Token
echo [1] 正在登录...
curl -s -X POST "%BASE_URL%/login/test" -H "Content-Type: application/json" -d "{\"username\":\"admin\",\"password\":\"admin123\"}" > login_response.json
REM 使用 PowerShell 提取 token
for /f "tokens=*" %%i in ('powershell -Command "$json = Get-Content login_response.json | ConvertFrom-Json; $json.token"') do (
set TOKEN=%%i
)
del login_response.json
if "%TOKEN%"=="" (
echo [错误] 获取 Token 失败,请检查登录接口
pause
exit /b 1
)
echo 登录成功Token: %TOKEN%
echo.
REM 2. 测试查询员工列表
echo [2] 测试查询员工列表...
curl -s -X GET "%BASE_URL%/dpc/employee/list" -H "Authorization: Bearer %TOKEN%"
echo.
echo.
REM 3. 测试新增员工
echo [3] 测试新增员工...
curl -s -X POST "%BASE_URL%/dpc/employee" -H "Authorization: Bearer %TOKEN%" -H "Content-Type: application/json" -d "{\"name\":\"测试员工\",\"tellerNo\":\"TEST001\",\"orgNo\":\"1001\",\"idCard\":\"110101199001011234\",\"phone\":\"13800138000\",\"status\":\"0\",\"relatives\":[{\"relativeName\":\"李四\",\"relativeIdCard\":\"110101199001011235\",\"relativePhone\":\"13800138001\",\"relationship\":\"配偶\"}]}"
echo.
echo.
REM 4. 测试查询员工详情
echo [4] 测试查询员工详情...
curl -s -X GET "%BASE_URL%/dpc/employee/1" -H "Authorization: Bearer %TOKEN%"
echo.
echo.
REM 5. 测试编辑员工
echo [5] 测试编辑员工...
curl -s -X PUT "%BASE_URL%/dpc/employee" -H "Authorization: Bearer %TOKEN%" -H "Content-Type: application/json" -d "{\"employeeId\":1,\"name\":\"测试员工-修改\",\"tellerNo\":\"TEST001\",\"orgNo\":\"1001\",\"idCard\":\"110101199001011234\",\"phone\":\"13800138000\",\"status\":\"0\",\"relatives\":[{\"relativeName\":\"王五\",\"relativeIdCard\":\"110101199001011236\",\"relativePhone\":\"13800138002\",\"relationship\":\"子女\"}]}"
echo.
echo.
REM 6. 测试删除员工
echo [6] 测试删除员工...
curl -s -X DELETE "%BASE_URL%/dpc/employee/1" -H "Authorization: Bearer %TOKEN%"
echo.
echo.
echo ========================================
echo 测试完成
echo ========================================
pause

View File

@@ -1,119 +0,0 @@
# 员工信息管理 API 测试脚本
# 需要使用 UTF-8 with BOM 编码保存
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
$BASE_URL = "http://localhost:8080"
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "员工信息管理 API 测试脚本" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# 1. 登录获取 Token
Write-Host "[1] 正在登录..." -ForegroundColor Yellow
$loginBody = @{
username = "admin"
password = "admin123"
} | ConvertTo-Json
$loginResponse = Invoke-RestMethod -Uri "$BASE_URL/login/test" -Method Post -ContentType "application/json; charset=utf-8" -Body ([System.Text.Encoding]::UTF8.GetBytes($loginBody))
$TOKEN = $loginResponse.token
if ([string]::IsNullOrEmpty($TOKEN)) {
Write-Host "[错误] 获取 Token 失败,请检查登录接口" -ForegroundColor Red
Read-Host "按回车键退出"
exit 1
}
Write-Host "登录成功Token: $TOKEN" -ForegroundColor Green
Write-Host ""
# 2. 测试查询员工列表
Write-Host "[2] 测试查询员工列表..." -ForegroundColor Yellow
$headers = @{
"Authorization" = "Bearer $TOKEN"
}
$response = Invoke-RestMethod -Uri "$BASE_URL/dpc/employee/list" -Method Get -Headers $headers
Write-Host ($response | ConvertTo-Json -Depth 10)
Write-Host ""
# 3. 测试新增员工
Write-Host "[3] 测试新增员工..." -ForegroundColor Yellow
$addBody = @{
name = "测试员工"
tellerNo = "TEST001"
orgNo = "1001"
idCard = "110101199001011234"
phone = "13800138000"
status = "0"
relatives = @(
@{
relativeName = "李四"
relativeIdCard = "110101199001011235"
relativePhone = "13800138001"
relationship = "配偶"
}
)
} | ConvertTo-Json -Depth 10
$headers = @{
"Authorization" = "Bearer $TOKEN"
"Content-Type" = "application/json; charset=utf-8"
}
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($addBody)
$response = Invoke-RestMethod -Uri "$BASE_URL/dpc/employee" -Method Post -Headers $headers -Body $bodyBytes
Write-Host ($response | ConvertTo-Json -Depth 10)
Write-Host ""
# 4. 测试查询员工详情
Write-Host "[4] 测试查询员工详情..." -ForegroundColor Yellow
$headers = @{
"Authorization" = "Bearer $TOKEN"
}
$response = Invoke-RestMethod -Uri "$BASE_URL/dpc/employee/1" -Method Get -Headers $headers
Write-Host ($response | ConvertTo-Json -Depth 10)
Write-Host ""
# 5. 测试编辑员工
Write-Host "[5] 测试编辑员工..." -ForegroundColor Yellow
$editBody = @{
employeeId = 1
name = "测试员工-修改"
tellerNo = "TEST001"
orgNo = "1001"
idCard = "110101199001011234"
phone = "13800138000"
status = "0"
relatives = @(
@{
relativeName = "王五"
relativeIdCard = "110101199001011236"
relativePhone = "13800138002"
relationship = "子女"
}
)
} | ConvertTo-Json -Depth 10
$headers = @{
"Authorization" = "Bearer $TOKEN"
"Content-Type" = "application/json; charset=utf-8"
}
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($editBody)
$response = Invoke-RestMethod -Uri "$BASE_URL/dpc/employee" -Method Put -Headers $headers -Body $bodyBytes
Write-Host ($response | ConvertTo-Json -Depth 10)
Write-Host ""
# 6. 测试删除员工
Write-Host "[6] 测试删除员工..." -ForegroundColor Yellow
$headers = @{
"Authorization" = "Bearer $TOKEN"
}
$response = Invoke-RestMethod -Uri "$BASE_URL/dpc/employee/1" -Method Delete -Headers $headers
Write-Host ($response | ConvertTo-Json -Depth 10)
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "测试完成" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Read-Host "按回车键退出"

View File

@@ -1,140 +0,0 @@
# 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
}

View File

@@ -1,437 +0,0 @@
#!/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()