From 4c3eeea256b816af728028e66590286107579ad5 Mon Sep 17 00:00:00 2001
From: wkc <978997012@qq.com>
Date: Fri, 6 Feb 2026 09:01:33 +0800
Subject: [PATCH] =?UTF-8?q?=E5=91=98=E5=B7=A5=E5=85=B3=E7=B3=BB=E7=A7=BB?=
=?UTF-8?q?=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.claude/settings.local.json | 6 +-
.gitignore | 1 +
doc/api/ccdi_staff_recruitment_api.md | 430 ++++++++
doc/plans/2025-02-05-员工招聘信息管理设计.md | 347 -------
doc/plans/2026-02-05-导入逻辑优化实施计划.md | 915 ++++++++++++++++++
doc/plans/2026-02-05-导入逻辑优化设计.md | 564 +++++++++++
.../com/ruoyi/ccdi/annotation/EnumValid.java | 46 +
.../ccdi/domain/CcdiEmployeeRelative.java | 59 --
.../dto/CcdiEmployeeRelativeAddDTO.java | 40 -
.../ccdi/domain/excel/CcdiEmployeeExcel.java | 7 +
.../excel/CcdiEmployeeRelativeExcel.java | 41 -
.../domain/vo/CcdiEmployeeRelativeVO.java | 37 -
.../mapper/CcdiEmployeeRelativeMapper.java | 13 -
.../com/ruoyi/ccdi/utils/EasyExcelUtil.java | 9 +
.../ruoyi/ccdi/validation/EnumValidator.java | 70 ++
.../config/MybatisPlusMetaObjectHandler.java | 4 +
test/batch_insert.ps1 | 80 --
test/batch_insert.py | 75 --
...pagination_test_report_20260128_152606.txt | 63 --
...pagination_test_report_20260128_152638.txt | 84 --
...pagination_test_report_20260128_153235.txt | 84 --
test/test_data.json | 1 -
test/test_employee_api.bat | 66 --
test/test_employee_api.ps1 | 119 ---
test/test_pagination.ps1 | 140 ---
test/test_pagination.py | 437 ---------
26 files changed, 2051 insertions(+), 1687 deletions(-)
create mode 100644 doc/api/ccdi_staff_recruitment_api.md
delete mode 100644 doc/plans/2025-02-05-员工招聘信息管理设计.md
create mode 100644 doc/plans/2026-02-05-导入逻辑优化实施计划.md
create mode 100644 doc/plans/2026-02-05-导入逻辑优化设计.md
create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/annotation/EnumValid.java
delete mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiEmployeeRelative.java
delete mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/dto/CcdiEmployeeRelativeAddDTO.java
delete mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiEmployeeRelativeExcel.java
delete mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/vo/CcdiEmployeeRelativeVO.java
delete mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEmployeeRelativeMapper.java
create mode 100644 ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/validation/EnumValidator.java
delete mode 100644 test/batch_insert.ps1
delete mode 100644 test/batch_insert.py
delete mode 100644 test/pagination_test_report_20260128_152606.txt
delete mode 100644 test/pagination_test_report_20260128_152638.txt
delete mode 100644 test/pagination_test_report_20260128_153235.txt
delete mode 100644 test/test_data.json
delete mode 100644 test/test_employee_api.bat
delete mode 100644 test/test_employee_api.ps1
delete mode 100644 test/test_pagination.ps1
delete mode 100644 test/test_pagination.py
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 extends Payload>[] 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()