diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 29b1a95..6361c5b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [ diff --git a/.gitignore b/.gitignore index fddc8ca..9327d07 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ nbdist/ *.log *.xml.versionsBackup *.swp +nul test/ diff --git a/doc/api/ccdi_staff_recruitment_api.md b/doc/api/ccdi_staff_recruitment_api.md new file mode 100644 index 0000000..d7e5451 --- /dev/null +++ b/doc/api/ccdi_staff_recruitment_api.md @@ -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 条,错误如下:
1、招聘项目编号 REC001 导入失败:该招聘项目编号已存在
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 diff --git a/doc/plans/2025-02-05-员工招聘信息管理设计.md b/doc/plans/2025-02-05-员工招聘信息管理设计.md deleted file mode 100644 index 87f09e0..0000000 --- a/doc/plans/2025-02-05-员工招聘信息管理设计.md +++ /dev/null @@ -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; -} -``` - ---- - -**文档结束** diff --git a/doc/plans/2026-02-05-导入逻辑优化实施计划.md b/doc/plans/2026-02-05-导入逻辑优化实施计划.md new file mode 100644 index 0000000..3dcf749 --- /dev/null +++ b/doc/plans/2026-02-05-导入逻辑优化实施计划.md @@ -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 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 FROM ccdi_employee + WHERE id_card IN + + #{item} + + +``` + +**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 excelList, Boolean isUpdateSupport) { + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + return "至少需要一条数据"; + } + + // 第一阶段:数据验证和收集 + List validEmployees = new ArrayList<>(); + List errorMessages = new ArrayList<>(); + Set 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("
") + .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 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 FROM ccdi_biz_intermediary + WHERE person_id IN + + #{item} + + +``` + +**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 excelList, Boolean isUpdateSupport) { + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + return "至少需要一条数据"; + } + + // 第一阶段:数据验证和收集 + List validList = new ArrayList<>(); + List errorMessages = new ArrayList<>(); + Set 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("
") + .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 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 FROM ccdi_enterprise_base_info + WHERE social_credit_code IN + + #{item} + + +``` + +**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 excelList, Boolean isUpdateSupport) { + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + return "至少需要一条数据"; + } + + // 第一阶段:数据验证和收集 + List validList = new ArrayList<>(); + List errorMessages = new ArrayList<>(); + Set 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("
") + .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 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 FROM ccdi_staff_recruitment + WHERE recruit_id IN + + #{item} + + +``` + +**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 excelList, Boolean isUpdateSupport) { + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + return "至少需要一条数据"; + } + + // 第一阶段:数据验证和收集 + List validList = new ArrayList<>(); + List errorMessages = new ArrayList<>(); + Set 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("
") + .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 条 + +--- + +**实施计划完成** diff --git a/doc/plans/2026-02-05-导入逻辑优化设计.md b/doc/plans/2026-02-05-导入逻辑优化设计.md new file mode 100644 index 0000000..faa5e12 --- /dev/null +++ b/doc/plans/2026-02-05-导入逻辑优化设计.md @@ -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 idCards); +``` + +**中介库个人模块**: +```java +// CcdiBizIntermediaryMapper.java +int deleteBatchByPersonId(@Param("list") List personIds); +``` + +**中介库实体模块**: +```java +// CcdiEnterpriseBaseInfoMapper.java +int deleteBatchBySocialCreditCode(@Param("list") List socialCreditCodes); +``` + +**招聘信息模块**: +```java +// CcdiStaffRecruitmentMapper.java +int deleteBatchByRecruitId(@Param("list") List recruitIds); +``` + +#### 4.1.2 Mapper XML 实现 + +所有删除 SQL 使用统一的模式: + +```xml + + DELETE FROM {table_name} + WHERE {unique_key_column} IN + + #{item} + + +``` + +**示例(员工信息)**: +```xml + + + DELETE FROM ccdi_employee + WHERE id_card IN + + #{item} + + +``` + +### 4.2 服务层设计 + +#### 4.2.1 通用导入方法模板 + +所有模块的导入方法遵循统一的实现模式: + +```java +@Override +@Transactional(rollbackFor = Exception.class) +public String importXxx(List excelList, Boolean isUpdateSupport) { + // 参数校验 + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + return "至少需要一条数据"; + } + + // 第一阶段:数据验证和收集 + List validList = new ArrayList<>(); + List errorMessages = new ArrayList<>(); + Set 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 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 excelList, Boolean isUpdateSupport) { + if (StringUtils.isNull(excelList) || excelList.isEmpty()) { + return "至少需要一条数据"; + } + + // 第一阶段:数据验证和收集 + List validEmployees = new ArrayList<>(); + List errorMessages = new ArrayList<>(); + Set 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("
") + .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 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) + +--- + +**文档结束** diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/annotation/EnumValid.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/annotation/EnumValid.java new file mode 100644 index 0000000..cb1a6cc --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/annotation/EnumValid.java @@ -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[] payload() default {}; + + /** + * 是否忽略空值 + * 如果为true,当字段为null或空字符串时不进行校验 + */ + boolean ignoreEmpty() default true; +} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiEmployeeRelative.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiEmployeeRelative.java deleted file mode 100644 index a825d44..0000000 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiEmployeeRelative.java +++ /dev/null @@ -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; -} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiEmployeeRelativeAddDTO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiEmployeeRelativeAddDTO.java deleted file mode 100644 index c98796e..0000000 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiEmployeeRelativeAddDTO.java +++ /dev/null @@ -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; -} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiEmployeeExcel.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiEmployeeExcel.java index f07e66e..19a11d5 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiEmployeeExcel.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiEmployeeExcel.java @@ -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; } diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiEmployeeRelativeExcel.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiEmployeeRelativeExcel.java deleted file mode 100644 index bab3ce5..0000000 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiEmployeeRelativeExcel.java +++ /dev/null @@ -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; -} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiEmployeeRelativeVO.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiEmployeeRelativeVO.java deleted file mode 100644 index c919d03..0000000 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiEmployeeRelativeVO.java +++ /dev/null @@ -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; -} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeRelativeMapper.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeRelativeMapper.java deleted file mode 100644 index cc2d291..0000000 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeRelativeMapper.java +++ /dev/null @@ -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 { -} diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/utils/EasyExcelUtil.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/utils/EasyExcelUtil.java index a015f70..ce8a7e7 100644 --- a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/utils/EasyExcelUtil.java +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/utils/EasyExcelUtil.java @@ -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); diff --git a/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/validation/EnumValidator.java b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/validation/EnumValidator.java new file mode 100644 index 0000000..59d9ad7 --- /dev/null +++ b/ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/validation/EnumValidator.java @@ -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 { + + 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; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusMetaObjectHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusMetaObjectHandler.java index 7849942..bddd646 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusMetaObjectHandler.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusMetaObjectHandler.java @@ -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()); } /** diff --git a/test/batch_insert.ps1 b/test/batch_insert.ps1 deleted file mode 100644 index ea8f3c2..0000000 --- a/test/batch_insert.ps1 +++ /dev/null @@ -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 "按回车键退出" diff --git a/test/batch_insert.py b/test/batch_insert.py deleted file mode 100644 index e16dfa8..0000000 --- a/test/batch_insert.py +++ /dev/null @@ -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) diff --git a/test/pagination_test_report_20260128_152606.txt b/test/pagination_test_report_20260128_152606.txt deleted file mode 100644 index 442da9f..0000000 --- a/test/pagination_test_report_20260128_152606.txt +++ /dev/null @@ -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字段 - ------------------------------------------------------------- -测试结论: -✗ 存在分页接口总数返回异常 diff --git a/test/pagination_test_report_20260128_152638.txt b/test/pagination_test_report_20260128_152638.txt deleted file mode 100644 index 6601644..0000000 --- a/test/pagination_test_report_20260128_152638.txt +++ /dev/null @@ -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 - ------------------------------------------------------------- -测试结论: -✗ 存在分页接口总数返回异常 diff --git a/test/pagination_test_report_20260128_153235.txt b/test/pagination_test_report_20260128_153235.txt deleted file mode 100644 index bed0a9b..0000000 --- a/test/pagination_test_report_20260128_153235.txt +++ /dev/null @@ -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 - ------------------------------------------------------------- -测试结论: -✗ 存在分页接口总数返回异常 diff --git a/test/test_data.json b/test/test_data.json deleted file mode 100644 index c88d49c..0000000 --- a/test/test_data.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"测试员工","tellerNo":"TEST002","orgNo":"1001","idCard":"110101199001011237","phone":"13800138000","status":"0","relatives":[{"relativeName":"李四","relativeIdCard":"110101199001011235","relativePhone":"13800138001","relationship":"配偶"}]} diff --git a/test/test_employee_api.bat b/test/test_employee_api.bat deleted file mode 100644 index fb4bc1c..0000000 --- a/test/test_employee_api.bat +++ /dev/null @@ -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 diff --git a/test/test_employee_api.ps1 b/test/test_employee_api.ps1 deleted file mode 100644 index 4b8868e..0000000 --- a/test/test_employee_api.ps1 +++ /dev/null @@ -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 "按回车键退出" diff --git a/test/test_pagination.ps1 b/test/test_pagination.ps1 deleted file mode 100644 index 234570b..0000000 --- a/test/test_pagination.ps1 +++ /dev/null @@ -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 -} diff --git a/test/test_pagination.py b/test/test_pagination.py deleted file mode 100644 index d326733..0000000 --- a/test/test_pagination.py +++ /dev/null @@ -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()