文件夹整理

This commit is contained in:
wkc
2026-02-09 14:32:35 +08:00
parent 02249c402e
commit 4ce4a717db
350 changed files with 285 additions and 71908 deletions

View File

@@ -1,252 +0,0 @@
# 员工信息表重命名测试报告
**测试日期**: 2026-02-09
**测试人**: Claude
**测试类型**: 数据库结构验证 + 权限配置验证
---
## 1. 测试概述
本次测试验证员工信息表从 `ccdi_employee` 重命名为 `ccdi_base_staff` 的实施结果,包括:
- 数据库表结构变更
- 字段变更(主键重命名、字段删除)
- 菜单权限配置更新
---
## 2. 测试结果汇总
| 测试项 | 结果 | 详情 |
|--------|------|------|
| 表存在性验证 | ✅ 通过 | ccdi_base_staff 表存在 |
| 主键字段验证 | ✅ 通过 | staff_id 字段存在且为主键 |
| 字段删除验证 | ✅ 通过 | teller_no 字段已删除 |
| 必需字段验证 | ✅ 通过 | 所有必需字段存在 |
| 菜单权限验证 | ✅ 通过 | 7个权限全部更新 |
| 旧权限清理验证 | ✅ 通过 | 旧权限已全部删除 |
**总测试数**: 6
**通过数**: 6
**失败数**: 0
**通过率**: 100%
---
## 3. 详细测试结果
### 3.1 表结构验证
**验证项目**: 表存在性和主键字段
**验证方法**:
```sql
DESC ccdi_base_staff;
```
**验证结果**: ✅ 通过
**表结构详情**:
| 字段名 | 类型 | 是否为空 | 键 | 默认值 | 额外 |
|--------|------|----------|-----|--------|------|
| staff_id | bigint(20) | NO | PRI | - | - |
| name | varchar(100) | NO | - | - | - |
| dept_id | bigint(20) | YES | MUL | - | - |
| id_card | varchar(18) | NO | - | - | - |
| phone | varchar(11) | YES | - | - | - |
| hire_date | date | YES | - | - | - |
| status | char(1) | NO | MUL | 0 | - |
| create_by | varchar(64) | YES | - | - | - |
| create_time | datetime | YES | - | - | - |
| update_by | varchar(64) | YES | - | - | - |
| update_time | datetime | YES | - | - | - |
**结论**:
- ✅ 表名正确:`ccdi_base_staff`
- ✅ 主键字段正确:`staff_id`
- ✅ 必需字段全部存在
- ✅ 字段类型正确
---
### 3.2 字段变更验证
**验证项目**:
1. 主键从 `employee_id` 改为 `staff_id`
2. 删除 `teller_no` 字段
**验证方法**:
```sql
SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_KEY
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'ccdi'
AND TABLE_NAME = 'ccdi_base_staff'
AND COLUMN_NAME IN ('staff_id', 'employee_id', 'teller_no');
```
**验证结果**: ✅ 通过
| 变更项 | 期望值 | 实际值 | 状态 |
|--------|--------|--------|------|
| 主键字段名 | staff_id | staff_id | ✅ |
| employee_id | 不存在 | 不存在 | ✅ |
| teller_no | 不存在 | 不存在 | ✅ |
**结论**:
- ✅ 主键字段已成功从 `employee_id` 改为 `staff_id`
-`teller_no` 字段已成功删除
---
### 3.3 菜单权限验证
**验证项目**: 菜单权限字符更新
**验证方法**:
```sql
SELECT menu_id, menu_name, perms, menu_type
FROM sys_menu
WHERE perms LIKE '%baseStaff%' OR perms LIKE '%employee%'
ORDER BY menu_id;
```
**验证结果**: ✅ 通过
**权限配置详情**:
| menu_id | menu_name | 新权限 | 原权限 | 状态 |
|---------|-----------|--------|--------|------|
| 2002 | 员工信息维护 | ccdi:baseStaff:list | ccdi:employee:list | ✅ |
| 2020 | 员工信息查询 | ccdi:baseStaff:query | ccdi:employee:query | ✅ |
| 2021 | 员工信息新增 | ccdi:baseStaff:add | ccdi:employee:add | ✅ |
| 2022 | 员工信息修改 | ccdi:baseStaff:edit | ccdi:employee:edit | ✅ |
| 2023 | 员工信息删除 | ccdi:baseStaff:remove | ccdi:employee:remove | ✅ |
| 2024 | 员工信息导出 | ccdi:baseStaff:export | ccdi:employee:export | ✅ |
| 2025 | 员工信息导入 | ccdi:baseStaff:import | ccdi:employee:import | ✅ |
**结论**:
- ✅ 7个菜单权限全部成功更新为 `ccdi:baseStaff:*`
- ✅ 旧的 `ccdi:employee:*` 权限已全部删除
- ✅ 权限配置完整,无遗漏
---
### 3.4 索引验证
**验证项目**: 表索引正确性
**验证方法**:
```sql
SHOW INDEX FROM ccdi_base_staff;
```
**验证结果**: ✅ 通过
| 索引名 | 字段名 | 索引类型 | 唯一 | 状态 |
|--------|--------|----------|------|------|
| PRIMARY | staff_id | BTREE | 是 | ✅ |
| idx_dept_id | dept_id | BTREE | 否 | ✅ |
| idx_status | status | BTREE | 否 | ✅ |
**结论**:
- ✅ 主键索引正确
- ✅ 业务索引完整
---
## 4. 代码实施清单
### 4.1 新增文件14个
**Entity 层 (1个)**:
- `CcdiBaseStaff.java` - 员工信息实体类
**DTO/VO 层 (5个)**:
- `CcdiBaseStaffAddDTO.java`
- `CcdiBaseStaffEditDTO.java`
- `CcdiBaseStaffQueryDTO.java`
- `CcdiBaseStaffVO.java`
- `CcdiBaseStaffExcel.java`
**Mapper 层 (2个)**:
- `CcdiBaseStaffMapper.java`
- `CcdiBaseStaffMapper.xml`
**Service 层 (4个)**:
- `ICcdiBaseStaffService.java`
- `CcdiBaseStaffServiceImpl.java`
- `ICcdiBaseStaffImportService.java`
- `CcdiBaseStaffImportServiceImpl.java`
**Controller 层 (1个)**:
- `CcdiBaseStaffController.java`
**前端 API 层 (1个)**:
- `ccdiBaseStaff.js`
### 4.2 API 接口清单
| 接口路径 | 方法 | 功能 | 权限 |
|----------|------|------|------|
| /ccdi/baseStaff/list | GET | 查询列表 | ccdi:baseStaff:list |
| /ccdi/baseStaff/{staffId} | GET | 查询详情 | ccdi:baseStaff:query |
| /ccdi/baseStaff | POST | 新增员工 | ccdi:baseStaff:add |
| /ccdi/baseStaff | PUT | 修改员工 | ccdi:baseStaff:edit |
| /ccdi/baseStaff/{staffIds} | DELETE | 删除员工 | ccdi:baseStaff:remove |
| /ccdi/baseStaff/export | POST | 导出数据 | ccdi:baseStaff:export |
| /ccdi/baseStaff/importTemplate | POST | 下载模板 | - |
| /ccdi/baseStaff/importData | POST | 导入数据 | ccdi:baseStaff:import |
| /ccdi/baseStaff/importStatus/{taskId} | GET | 导入状态 | ccdi:baseStaff:import |
| /ccdi/baseStaff/importFailures/{taskId} | GET | 失败记录 | ccdi:baseStaff:import |
---
## 5. 测试结论
### 5.1 总体评价
**测试通过** - 所有变更均已正确实施,无遗留问题。
### 5.2 变更完整性
| 变更项 | 状态 | 备注 |
|--------|------|------|
| 数据库表重命名 | ✅ | ccdi_base_staff |
| 主键字段重命名 | ✅ | employee_id → staff_id |
| 字段删除 | ✅ | teller_no 已删除 |
| 后端代码更新 | ✅ | 14个新文件 |
| 前端API更新 | ✅ | ccdiBaseStaff.js |
| 权限配置更新 | ✅ | 7个权限全部更新 |
### 5.3 风险评估
**低风险**
- 新旧代码并存,不影响现有功能
- 数据库变更已完成,无数据迁移风险
- 权限配置完整,无安全风险
### 5.4 建议
1. **编译验证**: 建议编译后端代码,确保无语法错误
2. **API测试**: 建议启动后端服务测试API接口可用性
3. **前端联调**: 如需前端页面建议更新组件引用新的API文件
4. **旧代码清理**: 确认新代码稳定后,可删除旧的 `CcdiEmployee*`
---
## 6. 附录
### 6.1 测试脚本
- `test_base_staff_db.sh` - 数据库验证脚本(需修正数据库名)
- `test_base_staff_rename.sh` - 完整测试脚本含API测试
### 6.2 相关文档
- `doc/requirements/designs/2026-02-09-employee-table-rename-to-base-staff.md` - 设计文档
---
**测试报告生成时间**: 2026-02-09
**报告版本**: v1.0
**测试状态**: ✅ 全部通过

View File

@@ -1,489 +0,0 @@
# 中介库导入失败记录查看功能设计
## 1. 需求背景
当前中介库导入功能在导入失败后,只显示通知消息,但没有提供查看失败记录的入口,用户无法了解具体哪些数据导入失败以及失败原因。
## 2. 功能描述
为中介库管理页面添加**导入失败记录查看**功能,支持个人中介和实体中介两种类型的失败记录查看。
### 2.1 核心功能
1. **双按钮独立管理**
- "查看个人导入失败记录"按钮 - 仅在个人中介导入存在失败记录时显示
- "查看实体导入失败记录"按钮 - 仅在实体中介导入存在失败记录时显示
- 按钮带tooltip提示上次导入时间
2. **localStorage持久化存储**
- 分别存储个人中介和实体中介的导入任务信息
- 存储期限:7天,过期自动清除
- 存储内容:任务ID、导入时间、成功数、失败数、hasFailures标志
3. **失败记录对话框**
- 显示导入统计摘要(总数/成功/失败)
- 表格展示所有失败记录,支持分页(每页10条)
- 提供清除历史记录按钮
- 记录过期时自动提示并清除
## 3. 技术设计
### 3.1 组件结构
```
index.vue (中介库管理页面)
├── 工具栏按钮区域
│ ├── 新增按钮
│ ├── 导入按钮
│ ├── 查看个人导入失败记录按钮 (条件显示)
│ └── 查看实体导入失败记录按钮 (条件显示)
├── 数据表格
├── 个人中介导入失败记录对话框
└── 实体中介导入失败记录对话框
```
### 3.2 数据流程
```
用户选择文件上传
ImportDialog 组件提交导入
后端返回 taskId (异步处理)
前端开始轮询导入状态
导入完成,ImportDialog 触发 @import-complete 事件
index.vue 接收事件,根据 importType 判断类型
保存任务信息到 localStorage (person 或 entity)
更新对应的失败记录按钮显示状态
用户点击"查看失败记录"按钮
调用后端接口获取失败记录列表 (支持分页)
在对话框中展示失败记录和错误原因
```
### 3.3 localStorage存储设计
#### 3.3.1 个人中介导入任务
**Key**: `intermediary_person_import_last_task`
**数据结构**:
```javascript
{
taskId: "uuid", // 任务ID
saveTime: 1234567890, // 保存时间戳
hasFailures: true, // 是否有失败记录
totalCount: 100, // 总数
successCount: 95, // 成功数
failureCount: 5 // 失败数
}
```
#### 3.3.2 实体中介导入任务
**Key**: `intermediary_entity_import_last_task`
**数据结构**: 同个人中介
### 3.4 页面状态管理
```javascript
data() {
return {
// 按钮显示状态
showPersonFailureButton: false,
showEntityFailureButton: false,
// 当前任务ID
currentPersonTaskId: null,
currentEntityTaskId: null,
// 个人失败记录对话框
personFailureDialogVisible: false,
personFailureList: [],
personFailureLoading: false,
personFailureTotal: 0,
personFailureQueryParams: {
pageNum: 1,
pageSize: 10
},
// 实体失败记录对话框
entityFailureDialogVisible: false,
entityFailureList: [],
entityFailureLoading: false,
entityFailureTotal: 0,
entityFailureQueryParams: {
pageNum: 1,
pageSize: 10
}
}
}
```
## 4. 接口依赖
### 4.1 已有后端接口
#### 4.1.1 查询个人中介导入失败记录
**接口**: `GET /ccdi/intermediary/importPersonFailures/{taskId}`
**参数**:
- `taskId`: 任务ID (路径参数)
- `pageNum`: 页码 (默认1)
- `pageSize`: 每页大小 (默认10)
**返回**: `IntermediaryPersonImportFailureVO[]`
**字段**:
- `name`: 姓名
- `personId`: 证件号码
- `personType`: 人员类型
- `gender`: 性别
- `mobile`: 手机号码
- `company`: 所在公司
- `errorMessage`: 错误信息
#### 4.1.2 查询实体中介导入失败记录
**接口**: `GET /ccdi/intermediary/importEntityFailures/{taskId}`
**参数**:
- `taskId`: 任务ID (路径参数)
- `pageNum`: 页码 (默认1)
- `pageSize`: 每页大小 (默认10)
**返回**: `IntermediaryEntityImportFailureVO[]`
**字段**:
- `enterpriseName`: 机构名称
- `socialCreditCode`: 统一社会信用代码
- `enterpriseType`: 主体类型
- `enterpriseNature`: 企业性质
- `legalRepresentative`: 法定代表人
- `establishDate`: 成立日期
- `errorMessage`: 错误信息
### 4.2 前端API方法
已有API方法 (位于 `@/api/ccdiIntermediary.js`):
- `getPersonImportFailures(taskId, pageNum, pageSize)` - 查询个人导入失败记录
- `getEntityImportFailures(taskId, pageNum, pageSize)` - 查询实体导入失败记录
## 5. UI设计
### 5.1 工具栏按钮
```vue
<el-col :span="1.5" v-if="showPersonFailureButton">
<el-tooltip :content="getPersonImportTooltip()" placement="top">
<el-button
type="warning"
plain
icon="el-icon-warning"
size="mini"
@click="viewPersonImportFailures"
>查看个人导入失败记录</el-button>
</el-tooltip>
</el-col>
<el-col :span="1.5" v-if="showEntityFailureButton">
<el-tooltip :content="getEntityImportTooltip()" placement="top">
<el-button
type="warning"
plain
icon="el-icon-warning"
size="mini"
@click="viewEntityImportFailures"
>查看实体导入失败记录</el-button>
</el-tooltip>
</el-col>
```
### 5.2 失败记录对话框
**个人中介失败记录对话框**:
- 标题: "个人中介导入失败记录"
- 顶部提示: 显示导入统计信息
- 表格列: 姓名、证件号码、人员类型、性别、手机号码、所在公司、**失败原因**(最小宽度200px,溢出显示tooltip)
- 分页组件: 支持翻页
- 底部按钮: "关闭"、"清除历史记录"
**实体中介失败记录对话框**:
- 标题: "实体中介导入失败记录"
- 顶部提示: 显示导入统计信息
- 表格列: 机构名称、统一社会信用代码、主体类型、企业性质、法定代表人、成立日期、**失败原因**(最小宽度200px,溢出显示tooltip)
- 分页组件: 支持翻页
- 底部按钮: "关闭"、"清除历史记录"
## 6. 核心方法设计
### 6.1 localStorage管理方法
#### 6.1.1 个人中介导入任务
```javascript
/** 保存个人导入任务到localStorage */
savePersonImportTaskToStorage(taskData) {
const data = {
...taskData,
saveTime: Date.now()
}
localStorage.setItem('intermediary_person_import_last_task', JSON.stringify(data))
}
/** 从localStorage读取个人导入任务 */
getPersonImportTaskFromStorage() {
try {
const data = localStorage.getItem('intermediary_person_import_last_task')
if (!data) return null
const task = JSON.parse(data)
// 7天过期检查
const sevenDays = 7 * 24 * 60 * 60 * 1000
if (Date.now() - task.saveTime > sevenDays) {
this.clearPersonImportTaskFromStorage()
return null
}
return task
} catch (error) {
console.error('读取个人导入任务失败:', error)
this.clearPersonImportTaskFromStorage()
return null
}
}
/** 清除个人导入任务 */
clearPersonImportTaskFromStorage() {
localStorage.removeItem('intermediary_person_import_last_task')
}
```
#### 6.1.2 实体中介导入任务
结构同个人中介,方法名为:
- `saveEntityImportTaskToStorage(taskData)`
- `getEntityImportTaskFromStorage()`
- `clearEntityImportTaskFromStorage()`
### 6.2 导入完成处理
```javascript
/** 处理导入完成 */
handleImportComplete(importData) {
const { taskId, hasFailures, importType, totalCount, successCount, failureCount } = importData
if (importType === 'person') {
// 保存个人导入任务
this.savePersonImportTaskToStorage({
taskId,
hasFailures,
totalCount,
successCount,
failureCount
})
// 更新按钮显示
this.showPersonFailureButton = hasFailures
this.currentPersonTaskId = taskId
} else if (importType === 'entity') {
// 保存实体导入任务
this.saveEntityImportTaskToStorage({
taskId,
hasFailures,
totalCount,
successCount,
failureCount
})
// 更新按钮显示
this.showEntityFailureButton = hasFailures
this.currentEntityTaskId = taskId
}
// 刷新列表
this.getList()
}
```
### 6.3 查看失败记录
```javascript
/** 查看个人导入失败记录 */
viewPersonImportFailures() {
this.personFailureDialogVisible = true
this.getPersonFailureList()
}
/** 查询个人失败记录列表 */
getPersonFailureList() {
this.personFailureLoading = true
getPersonImportFailures(
this.currentPersonTaskId,
this.personFailureQueryParams.pageNum,
this.personFailureQueryParams.pageSize
).then(response => {
this.personFailureList = response.rows
this.personFailureTotal = response.total
this.personFailureLoading = false
}).catch(error => {
this.personFailureLoading = false
// 错误处理: 404表示记录已过期
if (error.response?.status === 404) {
this.$modal.msgWarning('导入记录已过期,无法查看失败记录')
this.clearPersonImportTaskFromStorage()
this.showPersonFailureButton = false
this.personFailureDialogVisible = false
} else {
this.$modal.msgError('查询失败记录失败')
}
})
}
```
### 6.4 清除历史记录
```javascript
/** 清除个人导入历史记录 */
clearPersonImportHistory() {
this.$confirm('确认清除上次导入记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.clearPersonImportTaskFromStorage()
this.showPersonFailureButton = false
this.currentPersonTaskId = null
this.personFailureDialogVisible = false
this.$message.success('已清除')
}).catch(() => {})
}
```
## 7. 生命周期管理
### 7.1 created钩子
```javascript
created() {
this.getList()
this.loadEnumOptions()
this.restoreImportState() // 恢复导入状态
}
```
### 7.2 恢复导入状态
```javascript
/** 恢复导入状态 */
restoreImportState() {
// 恢复个人中介导入状态
const personTask = this.getPersonImportTaskFromStorage()
if (personTask && personTask.hasFailures && personTask.taskId) {
this.currentPersonTaskId = personTask.taskId
this.showPersonFailureButton = true
}
// 恢复实体中介导入状态
const entityTask = this.getEntityImportTaskFromStorage()
if (entityTask && entityTask.hasFailures && entityTask.taskId) {
this.currentEntityTaskId = entityTask.taskId
this.showEntityFailureButton = true
}
}
```
## 8. 边界情况处理
### 8.1 记录过期
- localStorage中存储的记录超过7天,自动清除
- 后端接口返回404时,提示用户"导入记录已过期",并清除本地存储
- 清除后隐藏对应的"查看失败记录"按钮
### 8.2 并发导入
- 每次新导入开始前,清除旧的导入记录
- 同一类型的导入进行时,取消之前的轮询
- 只保留最近一次的导入任务信息
### 8.3 网络错误
- 查询失败记录时网络错误,显示友好的错误提示
- 不影响页面其他功能的正常使用
## 9. 测试要点
### 9.1 功能测试
1. **个人中介导入失败场景**
- 导入包含错误数据的Excel文件
- 验证失败记录按钮是否显示
- 点击按钮查看失败记录
- 验证失败原因是否正确显示
2. **实体中介导入失败场景**
- 导入包含错误数据的Excel文件
- 验证失败记录按钮是否显示
- 点击按钮查看失败记录
- 验证失败原因是否正确显示
3. **localStorage持久化**
- 导入失败后刷新页面
- 验证"查看失败记录"按钮是否仍然显示
- 验证点击后能否正常查看失败记录
4. **分页功能**
- 失败记录超过10条时
- 验证分页组件是否正常工作
- 验证翻页后数据是否正确
5. **清除历史记录**
- 点击"清除历史记录"按钮
- 验证localStorage是否清除
- 验证按钮是否隐藏
- 再次点击导入,验证新记录是否正常
6. **记录过期处理**
- 手动修改localStorage中的saveTime模拟过期
- 刷新页面,验证按钮是否隐藏
- 或点击查看,验证是否提示"记录已过期"
### 9.2 兼容性测试
1. **浏览器兼容性**
- Chrome
- Firefox
- Edge
- Safari
2. **数据量大时性能测试**
- 导入1000条数据,其中100条失败
- 验证查询速度和渲染性能
## 10. 参考实现
本设计参考了员工管理页面 (`ccdiEmployee/index.vue`) 的导入失败记录查看功能的实现,主要参考点:
1. localStorage存储模式
2. 失败记录对话框布局
3. 分页查询逻辑
4. 错误处理机制
5. 过期记录清理逻辑
## 11. 变更历史
| 日期 | 版本 | 变更内容 | 作者 |
|------|------|----------|------|
| 2026-02-08 | 1.0 | 初始设计 | Claude |

View File

@@ -1,324 +0,0 @@
# 中介库导入失败记录查看功能 - 测试清单
## 测试环境
- 前端: Vue 2.6.12 + Element UI
- 后端: Spring Boot 3.5.8
- 测试数据目录: `doc/test-data/purchase_transaction/`
## 测试前准备
### 1. 准备测试数据
准备包含错误数据的Excel文件,用于测试导入失败场景:
**个人中介测试数据应包含的错误类型:**
- 缺少必填字段(姓名、证件号)
- 证件号格式错误
- 手机号格式错误
- 重复数据(唯一键冲突)
**实体中介测试数据应包含的错误类型:**
- 缺少必填字段(机构名称、统一社会信用代码)
- 统一社会信用代码格式错误
- 重复数据(唯一键冲突)
### 2. 清理环境
打开浏览器开发者工具 → Application → Local Storage,清除以下key:
- `intermediary_person_import_last_task`
- `intermediary_entity_import_last_task`
## 功能测试清单
### 测试1: 个人中介导入失败记录查看
#### 步骤
1. 访问中介库管理页面
2. 点击"导入"按钮
3. 选择"个人中介"导入类型
4. 上传包含错误数据的个人中介Excel文件
5. 等待导入完成(观察通知消息)
6. 验证"查看个人导入失败记录"按钮是否显示
7. 点击按钮查看失败记录
#### 预期结果
- ✅ 导入完成后显示通知:"成功X条,失败Y条"
- ✅ 工具栏显示"查看个人导入失败记录"按钮(黄色警告样式)
- ✅ 按钮tooltip显示上次导入时间
- ✅ 点击按钮打开对话框
- ✅ 对话框标题:"个人中介导入失败记录"
- ✅ 顶部显示统计信息:"导入时间: XXX | 总数: X条 | 成功: X条 | 失败: X条"
- ✅ 表格显示失败记录,包含以下列:
- 姓名
- 证件号码
- 人员类型
- 性别
- 手机号码
- 所在公司
- **失败原因**(最小宽度200px,溢出显示tooltip)
- ✅ 如果失败记录超过10条,分页组件正常显示
### 测试2: 实体中介导入失败记录查看
#### 步骤
1. 访问中介库管理页面
2. 点击"导入"按钮
3. 选择"实体中介"导入类型
4. 上传包含错误数据的实体中介Excel文件
5. 等待导入完成(观察通知消息)
6. 验证"查看实体导入失败记录"按钮是否显示
7. 点击按钮查看失败记录
#### 预期结果
- ✅ 导入完成后显示通知:"成功X条,失败Y条"
- ✅ 工具栏显示"查看实体导入失败记录"按钮(黄色警告样式)
- ✅ 按钮tooltip显示上次导入时间
- ✅ 点击按钮打开对话框
- ✅ 对话框标题:"实体中介导入失败记录"
- ✅ 顶部显示统计信息:"导入时间: XXX | 总数: X条 | 成功: X条 | 失败: X条"
- ✅ 表格显示失败记录,包含以下列:
- 机构名称
- 统一社会信用代码
- 主体类型
- 企业性质
- 法定代表人
- 成立日期(格式: YYYY-MM-DD)
- **失败原因**(最小宽度200px,溢出显示tooltip)
- ✅ 如果失败记录超过10条,分页组件正常显示
### 测试3: localStorage持久化
#### 步骤
1. 执行个人中介导入,包含失败记录
2. 观察按钮显示
3. 刷新页面(F5)
4. 观察"查看个人导入失败记录"按钮是否仍然显示
5. 点击按钮验证能否正常查看失败记录
#### 预期结果
- ✅ 刷新页面后按钮仍然显示
- ✅ 点击按钮能正常查看失败记录
- ✅ localStorage中存在`intermediary_person_import_last_task``intermediary_entity_import_last_task`
### 测试4: 分页功能
#### 步骤
1. 准备至少20条失败记录的数据
2. 导入并等待完成
3. 打开失败记录对话框
4. 测试翻页功能
#### 预期结果
- ✅ 分页组件显示正确的总记录数
- ✅ 每页显示10条记录
- ✅ 点击下一页/上一页按钮正常切换
- ✅ 修改每页显示数量正常工作
### 测试5: 清除历史记录
#### 步骤
1. 打开失败记录对话框
2. 点击"清除历史记录"按钮
3. 确认清除操作
4. 关闭对话框
5. 观察工具栏按钮是否隐藏
6. 检查localStorage是否已清除
#### 预期结果
- ✅ 弹出确认对话框:"确认清除上次导入记录?"
- ✅ 确认后显示成功提示:"已清除"
- ✅ 对话框关闭
- ✅ 工具栏对应的"查看失败记录"按钮隐藏
- ✅ localStorage中的对应key已删除
### 测试6: 记录过期处理
#### 方法1: 手动修改localStorage模拟过期
1. 打开开发者工具 → Application → Local Storage
2. 找到`intermediary_person_import_last_task``intermediary_entity_import_last_task`
3. 修改`saveTime`为8天前的时间戳
4. 刷新页面
5. 观察按钮是否隐藏
#### 方法2: 等待后端记录过期
1. 导入数据并等待失败记录显示
2. 等待后端清理过期记录(根据后端配置的过期时间)
3. 点击"查看失败记录"按钮
4. 观察错误提示
#### 预期结果
- ✅ 方法1: 刷新后按钮自动隐藏
- ✅ 方法2: 显示提示"导入记录已过期,无法查看失败记录"
- ✅ 方法2: localStorage自动清除
- ✅ 方法2: 按钮自动隐藏
### 测试7: 两种类型导入互不影响
#### 步骤
1. 先导入个人中介(有失败记录)
2. 再导入实体中介(有失败记录)
3. 验证两个按钮是否同时显示
4. 分别点击两个按钮,验证显示的失败记录是否正确
#### 预期结果
- ✅ 两个按钮同时显示
- ✅ "查看个人导入失败记录"按钮显示个人中介的失败记录
- ✅ "查看实体导入失败记录"按钮显示实体中介的失败记录
- ✅ 两个localStorage存储独立,互不影响
### 测试8: 导入成功场景
#### 步骤
1. 准备完全正确的Excel文件(所有数据都符合要求)
2. 导入数据
3. 等待导入完成
#### 预期结果
- ✅ 显示成功通知:"全部成功!共导入X条数据"
- ✅ 不显示"查看失败记录"按钮
- ✅ localStorage中不存储该任务(或hasFailures为false)
### 测试9: 网络错误处理
#### 步骤
1. 导入数据(有失败记录)
2. 打开失败记录对话框
3. 断开网络或使用浏览器开发者工具模拟离线
4. 尝试翻页或重新加载失败记录
#### 预期结果
- ✅ 显示友好的错误提示:"网络连接失败,请检查网络"
- ✅ 不影响页面其他功能的正常使用
### 测试10: 服务器错误处理
#### 步骤
1. 导入数据(有失败记录)
2. 使用浏览器开发者工具模拟服务器错误(500)
3. 尝试加载失败记录
#### 预期结果
- ✅ 显示错误提示:"服务器错误,请稍后重试"
## 边界情况测试
### 测试11: 大数据量性能测试
#### 步骤
1. 准备1000条数据,其中100条失败
2. 导入并等待完成
3. 打开失败记录对话框
4. 测试翻页性能
#### 预期结果
- ✅ 导入在合理时间内完成(参考员工模块:1000条约1-2分钟)
- ✅ 查询失败记录响应时间 < 2秒
- ✅ 翻页流畅,无卡顿
### 测试12: 并发导入
#### 步骤
1. 快速连续执行两次个人中介导入
2. 观察localStorage中的数据
3. 观察按钮显示状态
#### 预期结果
- ✅ 只有最近一次导入的数据被保存
- ✅ 按钮显示状态基于最新的导入结果
## 浏览器兼容性测试
### 测试13: 不同浏览器测试
在以下浏览器中重复执行测试1和测试2:
- ✅ Chrome (推荐)
- ✅ Firefox
- ✅ Edge
- ✅ Safari (Mac)
## 回归测试
### 测试14: 原有功能不受影响
验证以下原有功能仍正常工作:
- ✅ 新增中介(个人/实体)
- ✅ 编辑中介(个人/实体)
- ✅ 查看详情
- ✅ 删除中介
- ✅ 搜索功能
- ✅ 导入成功场景
- ✅ 导入模板下载
## 性能测试
### 测试15: 内存泄漏检查
1. 打开浏览器开发者工具 → Performance
2. 开始录制
3. 执行多次导入和查看失败记录操作
4. 停止录制
5. 检查内存使用情况
#### 预期结果
- ✅ 内存使用稳定,无明显泄漏
- ✅ 定时器在组件销毁时正确清理
## 自动化测试脚本(可选)
### 测试16: API接口测试
使用Postman或curl测试以下接口:
```bash
# 1. 测试个人中介导入失败记录查询
curl -X GET "http://localhost:8080/ccdi/intermediary/importPersonFailures/{taskId}?pageNum=1&pageSize=10" \
-H "Authorization: Bearer {token}"
# 2. 测试实体中介导入失败记录查询
curl -X GET "http://localhost:8080/ccdi/intermediary/importEntityFailures/{taskId}?pageNum=1&pageSize=10" \
-H "Authorization: Bearer {token}"
# 3. 测试过期记录查询(应返回404)
curl -X GET "http://localhost:8080/ccdi/intermediary/importPersonFailures/expired-task-id?pageNum=1&pageSize=10" \
-H "Authorization: Bearer {token}"
```
## 测试结果记录表
| 测试项 | 测试结果 | 问题描述 | 解决方案 | 验证日期 |
|--------|---------|---------|---------|---------|
| 测试1: 个人中介导入失败记录查看 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试2: 实体中介导入失败记录查看 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试3: localStorage持久化 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试4: 分页功能 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试5: 清除历史记录 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试6: 记录过期处理 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试7: 两种类型导入互不影响 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试8: 导入成功场景 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试9: 网络错误处理 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试10: 服务器错误处理 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试11: 大数据量性能测试 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试12: 并发导入 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试13: 浏览器兼容性 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试14: 原有功能不受影响 | ⬜ 通过 / ⬜ 失败 | | | |
| 测试15: 内存泄漏检查 | ⬜ 通过 / ⬜ 失败 | | | |
## 已知问题
记录测试过程中发现的已知问题:
| 问题编号 | 问题描述 | 严重程度 | 状态 | 解决方案 |
|---------|---------|---------|------|---------|
| | | | | |
## 测试总结
### 通过率统计
- 总测试项: 15项
- 通过: X项
- 失败: Y项
- 通过率: X%
### 测试结论
- ⬜ 测试通过,可以发布
- ⬜ 存在问题,需要修复后再测试
### 测试签名
- 测试人员: ___________
- 测试日期: ___________
- 审核人员: ___________
- 审核日期: ___________

View File

@@ -1,54 +0,0 @@
# 测试数据目录
本目录用于存放测试相关的Excel数据文件。
## 目录结构
```
doc/test-data/
├── temp/ # 临时测试数据(由测试脚本自动生成)
│ ├── purchase_duplicate.xlsx
│ ├── employee_employee_id_duplicate.xlsx
│ ├── employee_id_card_duplicate.xlsx
│ ├── purchase_mixed_duplicate.xlsx
│ └── employee_mixed_duplicate.xlsx
├── employee/ # 员工信息测试数据
│ └── employee_test_data.xlsx
└── recruitment/ # 招聘信息测试数据
└── recruitment_test_data.xlsx
```
## 说明
### temp/ 目录
- 由测试脚本自动生成和管理
- 每次运行测试时会重新生成
- 可以手动删除,不影响测试功能
### employee/ 和 recruitment/ 目录
- 存放用于功能测试的标准测试数据
- 包含正常场景和异常场景的数据
- 可用于手动测试
## 使用方法
### 自动生成测试数据
运行测试脚本时会自动在temp目录生成测试数据:
```bash
python doc/test-scripts/test_import_duplicate_detection.py
```
### 手动使用测试数据
1. 进入采购交易/员工信息管理页面
2. 点击"导入"按钮
3. 选择本目录下的Excel文件
4. 上传并查看导入结果
## 清理
测试完成后可以删除temp目录下的文件:
```bash
rm -rf doc/test-data/temp/*.xlsx
```
或手动删除temp文件夹中的所有Excel文件。

View File

@@ -1,191 +0,0 @@
# getExistingIdCards 方法实现文档
## 方法概述
**位置**: `CcdiEmployeeImportServiceImpl.java` 第200-222行
**功能**: 批量查询数据库中已存在的身份证号用于Excel导入时的重复检测
## 方法签名
```java
/**
* 批量查询数据库中已存在的身份证号
* @param excelList Excel数据列表
* @return 已存在的身份证号集合
*/
private Set<String> getExistingIdCards(List<CcdiEmployeeExcel> excelList)
```
## 实现代码
```java
private Set<String> getExistingIdCards(List<CcdiEmployeeExcel> excelList) {
// 1. 提取所有身份证号
List<String> idCards = excelList.stream()
.map(CcdiEmployeeExcel::getIdCard)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
// 2. 空值检查
if (idCards.isEmpty()) {
return Collections.emptySet();
}
// 3. 批量查询数据库
LambdaQueryWrapper<CcdiEmployee> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiEmployee::getIdCard, idCards);
List<CcdiEmployee> existingEmployees = employeeMapper.selectList(wrapper);
// 4. 返回已存在的身份证号集合
return existingEmployees.stream()
.map(CcdiEmployee::getIdCard)
.collect(Collectors.toSet());
}
```
## 实现特点
### 1. 流式处理
- 使用 Java Stream API 进行数据处理
- 代码简洁、可读性强
- 符合现代Java编程风格
### 2. 空值过滤
- 使用 `StringUtils.isNotEmpty` 过滤空字符串
- 避免无效数据查询
- 提高查询效率
### 3. 批量查询优化
- 使用 MyBatis Plus 的 `LambdaQueryWrapper`
- 使用 `in` 条件一次性查询所有数据
- 比循环单条查询效率高得多
### 4. 返回 Set 集合
- 自动去重
- O(1) 时间复杂度的查找操作
- 便于后续的重复检测
## 与参考方法对比
### 参考1: getExistingEmployeeIds (员工ID查询)
```java
private Set<Long> getExistingEmployeeIds(List<CcdiEmployeeExcel> excelList) {
List<Long> employeeIds = excelList.stream()
.map(CcdiEmployeeExcel::getEmployeeId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (employeeIds.isEmpty()) {
return Collections.emptySet();
}
List<CcdiEmployee> existingEmployees = employeeMapper.selectBatchIds(employeeIds);
return existingEmployees.stream()
.map(CcdiEmployee::getEmployeeId)
.collect(Collectors.toSet());
}
```
### 参考2: getExistingPersonIds (中介人员证件号查询)
```java
private Set<String> getExistingPersonIds(List<CcdiIntermediaryPersonExcel> excelList) {
List<String> personIds = excelList.stream()
.map(CcdiIntermediaryPersonExcel::getPersonId)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
if (personIds.isEmpty()) {
return Collections.emptySet();
}
LambdaQueryWrapper<CcdiBizIntermediary> wrapper = new LambdaQueryWrapper<>();
wrapper.in(CcdiBizIntermediary::getPersonId, personIds);
List<CcdiBizIntermediary> existingIntermediaries = intermediaryMapper.selectList(wrapper);
return existingIntermediaries.stream()
.map(CcdiBizIntermediary::getPersonId)
.collect(Collectors.toSet());
}
```
### 实现对比
| 特性 | getExistingEmployeeIds | getExistingIdCards | getExistingPersonIds |
|------|----------------------|-------------------|---------------------|
| 查询字段 | employeeId (Long) | idCard (String) | personId (String) |
| 空值过滤 | Objects::nonNull | StringUtils::isNotEmpty | StringUtils::isNotEmpty |
| 查询方式 | selectBatchIds | selectList(wrapper.in) | selectList(wrapper.in) |
| 返回类型 | Set<Long> | Set<String> | Set<String> |
**新方法实现特点**:
-`getExistingPersonIds` 风格完全一致
- 都处理字符串类型的ID字段
- 都使用 `StringUtils.isNotEmpty` 过滤空值
- 都使用 `LambdaQueryWrapper.in` 批量查询
## 使用场景
此方法将在后续的身份证号重复检测功能中使用,例如:
```java
// 在导入验证中调用
Set<String> existingIdCards = getExistingIdCards(excelList);
// 检查Excel中的身份证号是否已存在
for (CcdiEmployeeExcel excel : excelList) {
if (existingIdCards.contains(excel.getIdCard())) {
// 身份证号重复,标记为失败
failure.setErrorMessage("该身份证号已存在");
}
}
```
## 性能优势
假设导入1000条数据
**单条查询方式**:
- 1000次数据库查询
- 预计耗时: 1000ms × 1000 = 1000秒不可接受
**批量查询方式** (当前实现):
- 1次数据库查询
- 使用 in 条件查询1000个ID
- 预计耗时: 100ms以内
**性能提升**: 约10000倍
## 编译验证
```bash
mvn clean compile -pl ruoyi-ccdi -am -DskipTests
```
**结果**: ✅ BUILD SUCCESS
## 代码规范检查
✅ 符合若依框架编码规范
✅ 使用正确的注解(@Resource
✅ 添加了清晰的JavaDoc注释
✅ 方法命名规范(驼峰命名)
✅ 与现有代码风格一致
✅ 使用MyBatis Plus最佳实践
## 后续集成
此方法已实现完成,将在以下任务中被调用:
1. **任务2**: 修改 importEmployeeAsync 方法,调用 getExistingIdCards
2. **任务3**: 在数据验证逻辑中使用查询结果
3. **任务4**: 处理重复身份证号的错误提示
## 总结
- ✅ 方法已成功实现
- ✅ 代码编译通过
- ✅ 遵循项目编码规范
- ✅ 与参考实现风格一致
- ✅ 性能优化到位(批量查询)
- ✅ 准备好用于后续集成

View File

@@ -1,301 +0,0 @@
# 中介导入功能重构测试报告
## 测试目标
验证Service层重构后,使用 `importPersonBatch``importEntityBatch` 方法
(基于 `ON DUPLICATE KEY UPDATE`) 的导入功能是否正常工作。
## 重构内容
### Task 5: 重构个人中介导入Service
**文件:** `CcdiIntermediaryPersonImportServiceImpl.java`
**核心变更:**
- 移除"先查询后分类再删除再插入"的逻辑
- 更新模式(`isUpdateSupport=true`): 直接调用 `intermediaryMapper.importPersonBatch(validRecords)`
- 仅新增模式(`isUpdateSupport=false`): 先查询冲突,然后只插入无冲突数据
- 新增辅助方法:
- `saveBatchWithUpsert()`: 使用 `importPersonBatch` 进行批量UPSERT
- `getExistingPersonIdsFromDb()`: 从数据库获取已存在的证件号
- `createFailureVO()`: 创建失败记录VO(两个重载方法)
### Task 6: 重构实体中介导入Service
**文件:** `CcdiIntermediaryEntityImportServiceImpl.java`
**同样的重构逻辑**
## 测试场景
### 场景1: 个人中介 - 更新模式(第一次导入)
**目的:** 验证批量INSERT功能
**操作:**
- 上传测试数据文件(1000条个人中介数据)
- 设置 `updateSupport=true`
**预期结果:**
- 所有数据成功插入
- 状态: SUCCESS
- 成功数 = 总数
- 失败数 = 0
**实际结果:** _待测试_
**状态:** ⏳ 待执行
---
### 场景2: 个人中介 - 仅新增模式(重复导入)
**目的:** 验证冲突检测功能
**操作:**
- 再次上传相同的测试数据
- 设置 `updateSupport=false`
**预期结果:**
- 所有数据因为冲突而失败
- 状态: PARTIAL_SUCCESS 或 FAILURE
- 成功数 = 0
- 失败数 = 总数
- 失败原因: "该证件号码已存在"
**实际结果:** _待测试_
**状态:** ⏳ 待执行
---
### 场景3: 实体中介 - 更新模式(第一次导入)
**目的:** 验证实体中介批量INSERT功能
**操作:**
- 上传测试数据文件(1000条实体中介数据)
- 设置 `updateSupport=true`
**预期结果:**
- 所有数据成功插入
- 状态: SUCCESS
- 成功数 = 总数
- 失败数 = 0
**实际结果:** _待测试_
**状态:** ⏳ 待执行
---
### 场景4: 实体中介 - 仅新增模式(重复导入)
**目的:** 验证实体中介冲突检测功能
**操作:**
- 再次上传相同的测试数据
- 设置 `updateSupport=false`
**预期结果:**
- 所有数据因为冲突而失败
- 状态: PARTIAL_SUCCESS 或 FAILURE
- 成功数 = 0
- 失败数 = 总数
- 失败原因: "该统一社会信用代码已存在"
**实际结果:** _待测试_
**状态:** ⏳ 待执行
---
### 场景5: 个人中介 - 再次更新模式
**目的:** 验证 `ON DUPLICATE KEY UPDATE` 功能
**操作:**
- 第三次上传相同的测试数据
- 设置 `updateSupport=true`
**预期结果:**
- 所有数据成功更新(而不是先删除再插入)
- 状态: SUCCESS
- 成功数 = 总数
- 失败数 = 0
- 数据库中不会出现重复记录
**实际结果:** _待测试_
**状态:** ⏳ 待执行
---
## 测试方法
### 手动测试
1. **启动后端服务**
```bash
cd ruoyi-ccdi
mvn spring-boot:run
```
2. **访问Swagger UI**
- URL: http://localhost:8080/swagger-ui/index.html
- 找到 `/ccdi/intermediary/importPersonData` 和 `/ccdi/intermediary/importEntityData` 接口
3. **执行测试场景**
- 使用"Try it out"功能上传测试文件
- 观察响应结果
- 使用任务ID查询导入状态
- 查看失败记录
### 自动化测试
运行测试脚本:
```bash
cd doc/test-data/intermediary
node test-import-upsert.js
```
测试脚本会自动执行所有测试场景并生成报告。
## 测试数据
### 个人中介测试数据
- 文件: `doc/test-data/intermediary/个人中介黑名单测试数据_1000条_第1批.xlsx`
- 记录数: 1000
- 特点: 包含有效的身份证号码
### 实体中介测试数据
- 文件: `doc/test-data/intermediary/机构中介黑名单测试数据_1000条_第1批.xlsx`
- 记录数: 1000
- 特点: 包含有效的统一社会信用代码
## 关键验证点
### 1. 数据库层面验证
**更新模式下的UPSERT操作:**
- 检查 `ccdi_biz_intermediary` 表,确保持有相同 `person_id` 的记录只有1条
- 检查 `ccdi_enterprise_base_info` 表,确保持有相同 `social_credit_code` 的记录只有1条
**验证SQL:**
```sql
-- 检查个人中介重复记录
SELECT person_id, COUNT(*) as cnt
FROM ccdi_biz_intermediary
GROUP BY person_id
HAVING cnt > 1;
-- 检查实体中介重复记录
SELECT social_credit_code, COUNT(*) as cnt
FROM ccdi_enterprise_base_info
GROUP BY social_credit_code
HAVING cnt > 1;
```
### 2. 性能验证
**对比重构前后的性能差异:**
| 场景 | 重构前(先删后插) | 重构后(UPSERT) | 性能提升 |
|------|----------------|---------------|---------|
| 1000条首次导入 | _待测试_ | _待测试_ | _待计算_ |
| 1000条重复导入 | _待测试_ | _待测试_ | _待计算_ |
### 3. 错误处理验证
**验证失败记录的正确性:**
- 失败原因是否准确
- 失败记录的完整信息是否保留
- Redis中失败记录的存储和读取
## 测试结果汇总
| 场景 | 状态 | 通过/失败 | 备注 |
|------|------|----------|------|
| 场景1 | ⏳ 待执行 | - | 个人中介首次导入 |
| 场景2 | ⏳ 待执行 | - | 个人中介重复导入(仅新增) |
| 场景3 | ⏳ 待执行 | - | 实体中介首次导入 |
| 场景4 | ⏳ 待执行 | - | 实体中介重复导入(仅新增) |
| 场景5 | ⏳ 待执行 | - | 个人中介重复导入(更新) |
**总通过率:** 0/5 (0%)
## 问题记录
### 问题1: _问题描述_
**场景:** _相关场景_
**现象:** _具体表现_
**原因:** _根本原因_
**解决方案:** _修复方法_
**状态:** ⏳ 待解决 / ✅ 已解决
---
## 结论
_测试完成后填写总体结论_
### 代码质量评估
- **可读性:** _评分_ / 10
- **可维护性:** _评分_ / 10
- **性能:** _评分_ / 10
- **错误处理:** _评分_ / 10
### 优化建议
_根据测试结果提出优化建议_
## 附录
### A. 测试环境信息
- **操作系统:** Windows 11
- **Java版本:** 17
- **Spring Boot版本:** 3.5.8
- **MySQL版本:** 8.2.0
- **Redis版本:** _待填写_
### B. 相关文件清单
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryPersonImportServiceImpl.java`
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiIntermediaryEntityImportServiceImpl.java`
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiBizIntermediaryMapper.java`
- `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/mapper/CcdiEnterpriseBaseInfoMapper.java`
- `doc/test-data/intermediary/test-import-upsert.js`
### C. Git提交信息
```
commit 7d534de
refactor: 重构Service层使用ON DUPLICATE KEY UPDATE
- 更新模式直接调用importPersonBatch/importEntityBatch
- 移除'先删除再插入'逻辑,代码简化约50%
- 添加辅助方法saveBatchWithUpsert/getExistingPersonIdsFromDb
- 添加createFailureVO重载方法简化失败记录创建
变更详情:
- CcdiIntermediaryPersonImportServiceImpl: 重构importPersonAsync方法
- CcdiIntermediaryEntityImportServiceImpl: 重构importEntityAsync方法
- 两个Service均采用统一的处理模式
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
```
---
**报告生成时间:** 2026-02-08
**测试执行人:** _待填写_
**审核人:** _待填写_

View File

@@ -1,151 +0,0 @@
import pandas as pd
import random
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
def calculate_id_check_code(id_17):
"""
计算身份证校验码符合GB 11643-1999标准
"""
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
check_codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
weighted_sum = sum(int(id_17[i]) * weights[i] for i in range(17))
mod = weighted_sum % 11
return check_codes[mod]
def generate_valid_person_id():
"""
生成符合校验标准的18位身份证号
"""
area_code = f"{random.randint(110000, 659999)}"
birth_year = random.randint(1960, 2000)
birth_month = f"{random.randint(1, 12):02d}"
birth_day = f"{random.randint(1, 28):02d}"
sequence_code = f"{random.randint(0, 999):03d}"
id_17 = f"{area_code}{birth_year}{birth_month}{birth_day}{sequence_code}"
check_code = calculate_id_check_code(id_17)
return f"{id_17}{check_code}"
def validate_id_check_code(person_id):
"""
验证身份证校验码是否正确
"""
if len(str(person_id)) != 18:
return False
id_17 = str(person_id)[:17]
check_code = str(person_id)[17]
return calculate_id_check_code(id_17) == check_code.upper()
# 读取现有文件
input_file = 'doc/test-data/intermediary/intermediary_test_data_1000_valid.xlsx'
output_file = 'doc/test-data/intermediary/intermediary_test_data_1000_valid.xlsx'
print(f"正在读取文件: {input_file}")
df = pd.read_excel(input_file)
print(f"总行数: {len(df)}\n")
# 统计各证件类型
print("=== 原始证件类型分布 ===")
for id_type, count in df['证件类型'].value_counts().items():
print(f"{id_type}: {count}")
# 找出所有非身份证类型的记录
non_id_mask = df['证件类型'] != '身份证'
non_id_count = non_id_mask.sum()
id_card_count = (~non_id_mask).sum()
print(f"\n需要转换的证件数量: {non_id_count}")
print(f"现有身份证数量: {id_card_count}条(保持不变)")
# 备份现有身份证号码
existing_id_cards = df[~non_id_mask]['证件号码*'].copy()
print(f"\n已备份 {len(existing_id_cards)} 条现有身份证号码")
# 转换证件类型并生成新身份证号
print(f"\n正在转换证件类型并生成身份证号码...")
updated_count = 0
for idx in df[non_id_mask].index:
# 修改证件类型为身份证
df.loc[idx, '证件类型'] = '身份证'
# 生成新的身份证号
new_id = generate_valid_person_id()
df.loc[idx, '证件号码*'] = new_id
updated_count += 1
if (updated_count % 100 == 0) or (updated_count == non_id_count):
print(f"已处理 {updated_count}/{non_id_count}")
# 保存到Excel
df.to_excel(output_file, index=False, engine='openpyxl')
# 格式化Excel文件
wb = load_workbook(output_file)
ws = wb.active
# 设置列宽
ws.column_dimensions['A'].width = 15
ws.column_dimensions['B'].width = 12
ws.column_dimensions['C'].width = 12
ws.column_dimensions['D'].width = 8
ws.column_dimensions['E'].width = 12
ws.column_dimensions['F'].width = 20
ws.column_dimensions['G'].width = 15
ws.column_dimensions['H'].width = 15
ws.column_dimensions['I'].width = 30
ws.column_dimensions['J'].width = 20
ws.column_dimensions['K'].width = 20
ws.column_dimensions['L'].width = 12
ws.column_dimensions['M'].width = 15
ws.column_dimensions['N'].width = 12
ws.column_dimensions['O'].width = 20
# 设置表头样式
header_fill = PatternFill(start_color='D3D3D3', end_color='D3D3D3', fill_type='solid')
header_font = Font(bold=True)
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal='center', vertical='center')
# 冻结首行
ws.freeze_panes = 'A2'
wb.save(output_file)
# 最终验证
print("\n正在进行最终验证...")
df_verify = pd.read_excel(output_file)
# 验证所有记录都是身份证
all_id_card = (df_verify['证件类型'] == '身份证').all()
print(f"所有证件类型均为身份证: {'✅ 是' if all_id_card else '❌ 否'}")
# 验证所有身份证号码
all_valid = True
invalid_count = 0
for idx, person_id in df_verify['证件号码*'].items():
if not validate_id_check_code(str(person_id)):
all_valid = False
invalid_count += 1
if invalid_count <= 5:
print(f"❌ 错误: {person_id}")
print(f"\n身份证号码验证:")
print(f"总数: {len(df_verify)}")
print(f"校验通过: {len(df_verify) - invalid_count}条 ✅")
if invalid_count > 0:
print(f"校验失败: {invalid_count}条 ❌")
print(f"\n=== 更新完成 ===")
print(f"文件: {output_file}")
print(f"转换证件数量: {updated_count}")
print(f"保持不变: {len(existing_id_cards)}")
print(f"总记录数: {len(df_verify)}")
print(f"\n✅ 所有1000条记录现在都使用身份证类型")
print(f"✅ 所有身份证号码已通过GB 11643-1999标准校验")

View File

@@ -1,143 +0,0 @@
import pandas as pd
import random
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
def calculate_id_check_code(id_17):
"""
计算身份证校验码符合GB 11643-1999标准
"""
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
check_codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
weighted_sum = sum(int(id_17[i]) * weights[i] for i in range(17))
mod = weighted_sum % 11
return check_codes[mod]
def generate_valid_person_id():
"""
生成符合校验标准的18位身份证号
"""
area_code = f"{random.randint(110000, 659999)}"
birth_year = random.randint(1960, 2000)
birth_month = f"{random.randint(1, 12):02d}"
birth_day = f"{random.randint(1, 28):02d}"
sequence_code = f"{random.randint(0, 999):03d}"
id_17 = f"{area_code}{birth_year}{birth_month}{birth_day}{sequence_code}"
check_code = calculate_id_check_code(id_17)
return f"{id_17}{check_code}"
def validate_id_check_code(person_id):
"""
验证身份证校验码是否正确
"""
if len(person_id) != 18:
return False
id_17 = person_id[:17]
check_code = person_id[17]
return calculate_id_check_code(id_17) == check_code.upper()
# 读取现有文件
input_file = 'doc/test-data/intermediary/intermediary_test_data_1000_valid.xlsx'
output_file = 'doc/test-data/intermediary/intermediary_test_data_1000_valid.xlsx'
print(f"正在读取文件: {input_file}")
df = pd.read_excel(input_file)
print(f"总行数: {len(df)}")
# 找出所有身份证类型的记录
id_card_mask = df['证件类型'] == '身份证'
id_card_count = id_card_mask.sum()
print(f"\n找到 {id_card_count} 条身份证记录")
# 验证现有身份证
print("\n正在验证现有身份证校验码...")
invalid_count = 0
invalid_indices = []
for idx in df[id_card_mask].index:
person_id = str(df.loc[idx, '证件号码*'])
if not validate_id_check_code(person_id):
invalid_count += 1
invalid_indices.append(idx)
print(f"校验正确: {id_card_count - invalid_count}")
print(f"校验错误: {invalid_count}")
if invalid_count > 0:
print(f"\n需要重新生成 {invalid_count} 条身份证号码")
# 重新生成所有身份证号码
print(f"\n正在重新生成所有身份证号码...")
updated_count = 0
for idx in df[id_card_mask].index:
old_id = df.loc[idx, '证件号码*']
new_id = generate_valid_person_id()
df.loc[idx, '证件号码*'] = new_id
updated_count += 1
if (updated_count % 50 == 0) or (updated_count == id_card_count):
print(f"已更新 {updated_count}/{id_card_count}")
# 保存到Excel
df.to_excel(output_file, index=False, engine='openpyxl')
# 格式化Excel文件
wb = load_workbook(output_file)
ws = wb.active
# 设置列宽
ws.column_dimensions['A'].width = 15
ws.column_dimensions['B'].width = 12
ws.column_dimensions['C'].width = 12
ws.column_dimensions['D'].width = 8
ws.column_dimensions['E'].width = 12
ws.column_dimensions['F'].width = 20
ws.column_dimensions['G'].width = 15
ws.column_dimensions['H'].width = 15
ws.column_dimensions['I'].width = 30
ws.column_dimensions['J'].width = 20
ws.column_dimensions['K'].width = 20
ws.column_dimensions['L'].width = 12
ws.column_dimensions['M'].width = 15
ws.column_dimensions['N'].width = 12
ws.column_dimensions['O'].width = 20
# 设置表头样式
header_fill = PatternFill(start_color='D3D3D3', end_color='D3D3D3', fill_type='solid')
header_font = Font(bold=True)
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal='center', vertical='center')
# 冻结首行
ws.freeze_panes = 'A2'
wb.save(output_file)
# 最终验证
print("\n正在进行最终验证...")
df_verify = pd.read_excel(output_file)
id_cards = df_verify[df_verify['证件类型'] == '身份证']['证件号码*']
all_valid = True
for idx, person_id in id_cards.items():
if not validate_id_check_code(str(person_id)):
all_valid = False
print(f"❌ 错误: {person_id}")
if all_valid:
print(f"✅ 所有 {len(id_cards)} 条身份证号码校验通过!")
else:
print("❌ 存在校验失败的身份证号码")
print(f"\n=== 更新完成 ===")
print(f"文件: {output_file}")
print(f"更新身份证数量: {updated_count}")
print(f"其他证件类型保持不变")

View File

@@ -1,215 +0,0 @@
import pandas as pd
import random
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
def calculate_id_check_code(id_17):
"""
计算身份证校验码符合GB 11643-1999标准
:param id_17: 前17位身份证号
:return: 校验码0-9或X
"""
# 权重因子
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
# 校验码对应表
check_codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
# 计算加权和
weighted_sum = sum(int(id_17[i]) * weights[i] for i in range(17))
# 取模得到索引
mod = weighted_sum % 11
# 返回对应的校验码
return check_codes[mod]
def generate_valid_person_id(id_type):
"""
生成符合校验标准的证件号码
"""
if id_type == '身份证':
# 6位地区码 + 4位年份 + 2位月份 + 2位日期 + 3位顺序码
area_code = f"{random.randint(110000, 659999)}"
birth_year = random.randint(1960, 2000)
birth_month = f"{random.randint(1, 12):02d}"
birth_day = f"{random.randint(1, 28):02d}"
sequence_code = f"{random.randint(0, 999):03d}"
# 前17位
id_17 = f"{area_code}{birth_year}{birth_month}{birth_day}{sequence_code}"
# 计算校验码
check_code = calculate_id_check_code(id_17)
return f"{id_17}{check_code}"
else:
# 护照、台胞证、港澳通行证8位数字
return str(random.randint(10000000, 99999999))
# 验证身份证校验码
def validate_id_check_code(person_id):
"""
验证身份证校验码是否正确
"""
if len(person_id) != 18:
return False
id_17 = person_id[:17]
check_code = person_id[17]
return calculate_id_check_code(id_17) == check_code.upper()
# 定义数据生成规则
last_names = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
first_names_male = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
first_names_female = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
person_types = ['中介']
person_sub_types = ['本人', '配偶', '子女', '父母', '其他']
genders = ['M', 'F', 'O']
id_types = ['身份证', '护照', '台胞证', '港澳通行证']
companies = ['房屋租赁公司', '房产经纪公司', '投资咨询公司', '置业咨询公司', '不动产咨询公司', '物业管理公司', '资产评估公司', '土地评估公司', '地产代理公司', '房产咨询公司']
positions = ['区域经理', '店长', '高级经纪人', '房产经纪人', '销售经理', '置业顾问', '物业顾问', '评估师', '业务员', '总监', '主管', None]
relation_types = ['配偶', '子女', '父母', '兄弟姐妹', None, None]
provinces = ['北京市', '上海市', '广东省', '江苏省', '浙江省', '四川省', '河南省', '福建省', '湖北省', '湖南省']
districts = ['海淀区', '朝阳区', '天河区', '浦东新区', '西湖区', '黄浦区', '静安区', '徐汇区', '福田区', '罗湖区']
streets = ['', '大街', '大道', '街道', '', '广场', '大厦', '花园']
buildings = ['1号楼', '2号楼', '3号楼', '4号楼', '5号楼', '6号楼', '7号楼', '8号楼', 'A座', 'B座']
def generate_name(gender):
first_names = first_names_male if gender == 'M' else first_names_female
return random.choice(last_names) + random.choice(first_names)
def generate_mobile():
return f"1{random.choice([3, 5, 7, 8, 9])}{random.randint(0, 9)}{random.randint(10000000, 99999999)}"
def generate_wechat():
return f"wx_{''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=8))}"
def generate_address():
return f"{random.choice(provinces)}{random.choice(districts)}{random.choice(streets)}{random.randint(1, 100)}"
def generate_social_credit_code():
return f"91{random.randint(0, 9)}{random.randint(10000000000000000, 99999999999999999)}"
def generate_related_num_id():
return f"ID{random.randint(10000, 99999)}"
def generate_row(index):
gender = random.choice(genders)
person_sub_type = random.choice(person_sub_types)
id_type = random.choice(id_types)
return {
'姓名*': generate_name(gender),
'人员类型': '中介',
'人员子类型': person_sub_type,
'性别': gender,
'证件类型': id_type,
'证件号码*': generate_valid_person_id(id_type),
'手机号码': generate_mobile(),
'微信号': random.choice([generate_wechat(), None, None]),
'联系地址': generate_address(),
'所在公司': random.choice(companies),
'企业统一信用码': random.choice([generate_social_credit_code(), None, None]),
'职位': random.choice(positions),
'关联人员ID': random.choice([generate_related_num_id(), None, None, None]),
'关系类型': random.choice(relation_types),
'备注': None
}
# 生成1000条数据
print("正在生成1000条测试数据...")
data = []
for i in range(1000):
row = generate_row(i)
data.append(row)
if (i + 1) % 100 == 0:
print(f"已生成 {i + 1} 条...")
# 创建DataFrame
df = pd.DataFrame(data)
# 输出文件
output_file = 'doc/test-data/intermediary/intermediary_test_data_1000_valid.xlsx'
# 保存到Excel
df.to_excel(output_file, index=False, engine='openpyxl')
# 格式化Excel文件
wb = load_workbook(output_file)
ws = wb.active
# 设置列宽
ws.column_dimensions['A'].width = 15
ws.column_dimensions['B'].width = 12
ws.column_dimensions['C'].width = 12
ws.column_dimensions['D'].width = 8
ws.column_dimensions['E'].width = 12
ws.column_dimensions['F'].width = 20
ws.column_dimensions['G'].width = 15
ws.column_dimensions['H'].width = 15
ws.column_dimensions['I'].width = 30
ws.column_dimensions['J'].width = 20
ws.column_dimensions['K'].width = 20
ws.column_dimensions['L'].width = 12
ws.column_dimensions['M'].width = 15
ws.column_dimensions['N'].width = 12
ws.column_dimensions['O'].width = 20
# 设置表头样式
header_fill = PatternFill(start_color='D3D3D3', end_color='D3D3D3', fill_type='solid')
header_font = Font(bold=True)
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal='center', vertical='center')
# 冻结首行
ws.freeze_panes = 'A2'
wb.save(output_file)
# 验证身份证校验码
print("\n正在验证身份证校验码...")
df_read = pd.read_excel(output_file)
id_cards = df_read[df_read['证件类型'] == '身份证']['证件号码*']
valid_count = 0
invalid_count = 0
invalid_ids = []
for idx, person_id in id_cards.items():
if validate_id_check_code(str(person_id)):
valid_count += 1
else:
invalid_count += 1
invalid_ids.append(person_id)
print(f"\n✅ 成功生成1000条测试数据到: {output_file}")
print(f"\n=== 身份证校验码验证 ===")
print(f"身份证总数: {len(id_cards)}")
print(f"校验正确: {valid_count}条 ✅")
print(f"校验错误: {invalid_count}")
if invalid_count > 0:
print(f"\n错误的身份证号:")
for pid in invalid_ids[:10]:
print(f" {pid}")
print(f"\n=== 数据统计 ===")
print(f"人员类型: {df_read['人员类型'].unique()}")
print(f"性别分布: {dict(df_read['性别'].value_counts())}")
print(f"证件类型分布: {dict(df_read['证件类型'].value_counts())}")
print(f"人员子类型分布: {dict(df_read['人员子类型'].value_counts())}")
print(f"\n=== 身份证号码样本(已验证校验码)===")
valid_id_samples = id_cards.head(5).tolist()
for sample in valid_id_samples:
is_valid = "" if validate_id_check_code(str(sample)) else ""
print(f"{sample} {is_valid}")

View File

@@ -1,163 +0,0 @@
import pandas as pd
import random
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment
# 读取模板文件
template_file = 'doc/test-data/intermediary/person_1770542031351.xlsx'
output_file = 'doc/test-data/intermediary/intermediary_test_data_1000.xlsx'
# 定义数据生成规则
last_names = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
first_names_male = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
first_names_female = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
person_types = ['中介']
person_sub_types = ['本人', '配偶', '子女', '父母', '其他']
genders = ['M', 'F', 'O']
id_types = ['身份证', '护照', '台胞证', '港澳通行证']
companies = ['房屋租赁公司', '房产经纪公司', '投资咨询公司', '置业咨询公司', '不动产咨询公司', '物业管理公司', '资产评估公司', '土地评估公司', '地产代理公司', '房产咨询公司']
positions = ['区域经理', '店长', '高级经纪人', '房产经纪人', '销售经理', '置业顾问', '物业顾问', '评估师', '业务员', '总监', '主管', None]
relation_types = ['配偶', '子女', '父母', '兄弟姐妹', None, None]
provinces = ['北京市', '上海市', '广东省', '江苏省', '浙江省', '四川省', '河南省', '福建省', '湖北省', '湖南省']
districts = ['海淀区', '朝阳区', '天河区', '浦东新区', '西湖区', '黄浦区', '静安区', '徐汇区', '福田区', '罗湖区']
streets = ['', '大街', '大道', '街道', '', '广场', '大厦', '花园']
buildings = ['1号楼', '2号楼', '3号楼', '4号楼', '5号楼', '6号楼', '7号楼', '8号楼', 'A座', 'B座']
# 现有数据样本(从数据库获取的格式)
existing_data_samples = [
{'name': '林玉兰', 'person_type': '中介', 'person_sub_type': '本人', 'gender': 'F', 'id_type': '护照', 'person_id': '45273944', 'mobile': '18080309834', 'wechat_no': 'wx_rt54d59p', 'contact_address': '福建省黄浦区巷4号', 'company': '房屋租赁公司', 'social_credit_code': '911981352496905281', 'position': '区域经理', 'related_num_id': 'ID92351', 'relation_type': None},
{'name': '刘平', 'person_type': '中介', 'person_sub_type': '本人', 'gender': 'F', 'id_type': '台胞证', 'person_id': '38639164', 'mobile': '19360856434', 'wechat_no': None, 'contact_address': '四川省海淀区路3号', 'company': '房产经纪公司', 'social_credit_code': '918316437629447909', 'position': None, 'related_num_id': None, 'relation_type': None},
{'name': '何娜', 'person_type': '中介', 'person_sub_type': '本人', 'gender': 'O', 'id_type': '港澳通行证', 'person_id': '83433341', 'mobile': '18229577387', 'wechat_no': 'wx_8ikozqjx', 'contact_address': '河南省天河区巷4号', 'company': '房产经纪公司', 'social_credit_code': '918315578905616368', 'position': '店长', 'related_num_id': None, 'relation_type': '父母'},
{'name': '王毅', 'person_type': '中介', 'person_sub_type': '本人', 'gender': 'M', 'id_type': '台胞证', 'person_id': '76369869', 'mobile': '17892993806', 'wechat_no': None, 'contact_address': '江苏省西湖区街道1号', 'company': '投资咨询公司', 'social_credit_code': None, 'position': '高级经纪人', 'related_num_id': 'ID61198', 'relation_type': None},
{'name': '李桂英', 'person_type': '中介', 'person_sub_type': '配偶', 'gender': 'F', 'id_type': '护照', 'person_id': '75874216', 'mobile': '15648713336', 'wechat_no': 'wx_5n0e926w', 'contact_address': '浙江省海淀区大道2号', 'company': '投资咨询公司', 'social_credit_code': None, 'position': '店长', 'related_num_id': None, 'relation_type': None},
]
def generate_name(gender):
first_names = first_names_male if gender == 'M' else first_names_female
return random.choice(last_names) + random.choice(first_names)
def generate_mobile():
return f"1{random.choice([3, 5, 7, 8, 9])}{random.randint(0, 9)}{random.randint(10000000, 99999999)}"
def generate_wechat():
return f"wx_{''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=8))}"
def generate_person_id(id_type):
if id_type == '身份证':
# 18位身份证号6位地区码 + 4位年份 + 2位月份 + 2位日期 + 3位顺序码 + 1位校验码
area_code = f"{random.randint(110000, 659999)}"
birth_year = random.randint(1960, 2000)
birth_month = f"{random.randint(1, 12):02d}"
birth_day = f"{random.randint(1, 28):02d}"
sequence_code = f"{random.randint(0, 999):03d}"
# 简单校验码随机0-9或X
check_code = random.choice(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'X'])
return f"{area_code}{birth_year}{birth_month}{birth_day}{sequence_code}{check_code}"
else:
return str(random.randint(10000000, 99999999))
def generate_social_credit_code():
return f"91{random.randint(0, 9)}{random.randint(10000000000000000, 99999999999999999)}"
def generate_address():
return f"{random.choice(provinces)}{random.choice(districts)}{random.choice(streets)}{random.randint(1, 100)}"
def generate_related_num_id():
return f"ID{random.randint(10000, 99999)}"
def generate_row(index, is_existing):
if is_existing:
sample = existing_data_samples[index % len(existing_data_samples)]
return {
'姓名*': sample['name'],
'人员类型': sample['person_type'],
'人员子类型': sample['person_sub_type'],
'性别': sample['gender'],
'证件类型': sample['id_type'],
'证件号码*': sample['person_id'],
'手机号码': sample['mobile'],
'微信号': sample['wechat_no'],
'联系地址': sample['contact_address'],
'所在公司': sample['company'],
'企业统一信用码': sample['social_credit_code'],
'职位': sample['position'],
'关联人员ID': sample['related_num_id'],
'关系类型': sample['relation_type'],
'备注': None
}
else:
gender = random.choice(genders)
person_sub_type = random.choice(person_sub_types)
id_type = random.choice(id_types)
return {
'姓名*': generate_name(gender),
'人员类型': '中介',
'人员子类型': person_sub_type,
'性别': gender,
'证件类型': id_type,
'证件号码*': generate_person_id(id_type),
'手机号码': generate_mobile(),
'微信号': random.choice([generate_wechat(), None, None]),
'联系地址': generate_address(),
'所在公司': random.choice(companies),
'企业统一信用码': random.choice([generate_social_credit_code(), None, None]),
'职位': random.choice(positions),
'关联人员ID': random.choice([generate_related_num_id(), None, None, None]),
'关系类型': random.choice(relation_types),
'备注': None
}
# 生成1000条数据
data = []
for i in range(1000):
is_existing = i < 500
row = generate_row(i, is_existing)
data.append(row)
# 创建DataFrame
df = pd.DataFrame(data)
# 保存到Excel
df.to_excel(output_file, index=False, engine='openpyxl')
# 格式化Excel文件
wb = load_workbook(output_file)
ws = wb.active
# 设置列宽
ws.column_dimensions['A'].width = 15
ws.column_dimensions['B'].width = 12
ws.column_dimensions['C'].width = 12
ws.column_dimensions['D'].width = 8
ws.column_dimensions['E'].width = 12
ws.column_dimensions['F'].width = 20
ws.column_dimensions['G'].width = 15
ws.column_dimensions['H'].width = 15
ws.column_dimensions['I'].width = 30
ws.column_dimensions['J'].width = 20
ws.column_dimensions['K'].width = 20
ws.column_dimensions['L'].width = 12
ws.column_dimensions['M'].width = 15
ws.column_dimensions['N'].width = 12
ws.column_dimensions['O'].width = 20
# 设置表头样式
header_fill = PatternFill(start_color='D3D3D3', end_color='D3D3D3', fill_type='solid')
header_font = Font(bold=True)
for cell in ws[1]:
cell.fill = header_fill
cell.font = header_font
cell.alignment = Alignment(horizontal='center', vertical='center')
# 冻结首行
ws.freeze_panes = 'A2'
wb.save(output_file)
print(f'成功生成1000条测试数据到: {output_file}')
print('- 500条现有数据前500行')
print('- 500条新数据后500行')

View File

@@ -1,181 +0,0 @@
import random
import string
from datetime import datetime, timedelta
import pandas as pd
# 机构名称前缀
company_prefixes = ['北京市', '上海市', '广州市', '深圳市', '杭州市', '成都市', '武汉市', '南京市', '西安市', '重庆市']
company_keywords = ['房产', '地产', '置业', '中介', '经纪', '咨询', '投资', '资产', '物业', '不动产']
company_suffixes = ['有限公司', '股份有限公司', '集团', '企业', '合伙企业', '有限责任公司']
# 主体类型
entity_types = ['企业', '个体工商户', '农民专业合作社', '其他组织']
# 企业性质
enterprise_natures = ['国有企业', '集体企业', '私营企业', '混合所有制企业', '外商投资企业', '港澳台投资企业']
# 行业分类
industry_classes = ['房地产业', '金融业', '租赁和商务服务业', '建筑业', '批发和零售业']
# 所属行业
industry_names = [
'房地产中介服务', '房地产经纪', '房地产开发经营', '物业管理',
'投资咨询', '资产管理', '商务咨询', '市场调查',
'建筑工程', '装饰装修', '园林绿化'
]
# 法定代表人姓名
surnames = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
given_names = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '秀英', '', '']
# 证件类型
cert_types = ['身份证', '护照', '港澳通行证', '台胞证', '其他']
# 常用地址
provinces = ['北京市', '上海市', '广东省', '浙江省', '江苏省', '四川省', '湖北省', '河南省', '山东省', '福建省']
cities = ['朝阳区', '海淀区', '浦东新区', '黄浦区', '天河区', '福田区', '西湖区', '滨江区', '鼓楼区', '玄武区',
'武侯区', '江汉区', '金水区', '市南区', '思明区']
districts = ['街道', '大道', '', '', '小区', '花园', '广场', '大厦']
street_numbers = ['1号', '2号', '3号', '88号', '66号', '108号', '188号', '888号', '666号', '168号']
# 股东姓名
shareholder_names = [
'张伟', '李芳', '王强', '刘军', '陈静', '杨洋', '黄勇', '赵艳',
'周杰', '吴娟', '徐涛', '孙明', '马超', '胡秀英', '朱霞', '郭平',
'何桂英', '罗玉兰', '高萍', '林毅', '王浩', '李宇', '张轩', '刘然'
]
def generate_company_name():
"""生成机构名称"""
prefix = random.choice(company_prefixes)
keyword = random.choice(company_keywords)
suffix = random.choice(company_suffixes)
return f"{prefix}{keyword}{suffix}"
def generate_social_credit_code():
"""生成统一社会信用代码18位"""
# 统一社会信用代码规则18位第一位为登记管理部门代码1-5第二位为机构类别代码1-9
dept_code = random.choice(['1', '2', '3', '4', '5'])
org_code = random.choice(['1', '2', '3', '4', '5', '6', '7', '8', '9'])
rest = ''.join([str(random.randint(0, 9)) for _ in range(16)])
return f"{dept_code}{org_code}{rest}"
def generate_id_card():
"""生成身份证号码18位简化版"""
# 地区码前6位
area_code = f"{random.randint(110000, 650000):06d}"
# 出生日期8位
birth_year = random.randint(1960, 1990)
birth_month = f"{random.randint(1, 12):02d}"
birth_day = f"{random.randint(1, 28):02d}"
birth_date = f"{birth_year}{birth_month}{birth_day}"
# 顺序码3位
sequence = f"{random.randint(1, 999):03d}"
# 校验码1位
check_code = random.randint(0, 9)
return f"{area_code}{birth_date}{sequence}{check_code}"
def generate_other_id():
"""生成其他证件号码"""
return f"{random.randint(10000000, 99999999):08d}"
def generate_register_address():
"""生成注册地址"""
province = random.choice(provinces)
city = random.choice(cities)
district = random.choice(districts)
number = random.choice(street_numbers)
return f"{province}{city}{district}{number}"
def generate_establish_date():
"""生成成立日期2000-2024年之间"""
start_date = datetime(2000, 1, 1)
end_date = datetime(2024, 12, 31)
time_between = end_date - start_date
days_between = time_between.days
random_days = random.randrange(days_between)
return start_date + timedelta(days=random_days)
def generate_legal_representative():
"""生成法定代表人"""
name = random.choice(surnames) + random.choice(given_names)
cert_type = random.choice(cert_types)
cert_no = generate_id_card() if cert_type == '身份证' else generate_other_id()
return name, cert_type, cert_no
def generate_shareholders():
"""生成股东列表1-5个股东"""
shareholder_count = random.randint(1, 5)
selected_shareholders = random.sample(shareholder_names, shareholder_count)
shareholders = [None] * 5
for i, shareholder in enumerate(selected_shareholders):
shareholders[i] = shareholder
return shareholders
def generate_entity(index):
"""生成单条机构中介数据"""
# 基本信息
enterprise_name = generate_company_name()
social_credit_code = generate_social_credit_code()
entity_type = random.choice(entity_types)
enterprise_nature = random.choice(enterprise_natures)
industry_class = random.choice(industry_classes)
industry_name = random.choice(industry_names)
# 成立日期
establish_date = generate_establish_date()
# 注册地址
register_address = generate_register_address()
# 法定代表人信息
legal_name, legal_cert_type, legal_cert_no = generate_legal_representative()
# 股东
shareholders = generate_shareholders()
return {
'机构名称*': enterprise_name,
'统一社会信用代码*': social_credit_code,
'主体类型': entity_type,
'企业性质': enterprise_nature if random.random() > 0.3 else '',
'行业分类': industry_class if random.random() > 0.3 else '',
'所属行业': industry_name if random.random() > 0.2 else '',
'成立日期': establish_date.strftime('%Y-%m-%d') if random.random() > 0.4 else '',
'注册地址': register_address,
'法定代表人': legal_name,
'法定代表人证件类型': legal_cert_type,
'法定代表人证件号码': legal_cert_no,
'股东1': shareholders[0] if shareholders[0] else '',
'股东2': shareholders[1] if shareholders[1] else '',
'股东3': shareholders[2] if shareholders[2] else '',
'股东4': shareholders[3] if shareholders[3] else '',
'股东5': shareholders[4] if shareholders[4] else '',
'备注': f'测试数据{index}' if random.random() > 0.5 else ''
}
# 生成第一个1000条数据
print("正在生成第一批1000条机构中介黑名单数据...")
data = [generate_entity(i) for i in range(1, 1001)]
df = pd.DataFrame(data)
# 保存第一个文件
output1 = r'D:\ccdi\ccdi\doc\test-data\intermediary\机构中介黑名单测试数据_1000条_第1批.xlsx'
df.to_excel(output1, index=False, engine='openpyxl')
print(f"已生成第一个文件: {output1}")
# 生成第二个1000条数据
print("正在生成第二批1000条机构中介黑名单数据...")
data2 = [generate_entity(i) for i in range(1, 1001)]
df2 = pd.DataFrame(data2)
# 保存第二个文件
output2 = r'D:\ccdi\ccdi\doc\test-data\intermediary\机构中介黑名单测试数据_1000条_第2批.xlsx'
df2.to_excel(output2, index=False, engine='openpyxl')
print(f"已生成第二个文件: {output2}")
print("\n✅ 生成完成!")
print(f"文件1: {output1}")
print(f"文件2: {output2}")
print(f"\n每个文件包含1000条测试数据")
print(f"数据格式与CcdiIntermediaryEntityExcel.java定义一致")

View File

@@ -1,110 +0,0 @@
import random
import string
from datetime import datetime
import pandas as pd
# 常用姓氏和名字
surnames = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '']
given_names = ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '秀英', '', '', '', '桂英', '玉兰', '', '', '', '', '', '', '']
# 人员类型
person_types = ['中介', '职业背债人', '房产中介']
person_sub_types = ['本人', '配偶', '子女', '其他']
genders = ['M', 'F', 'O']
id_types = ['身份证', '护照', '港澳通行证', '台胞证', '军官证']
relation_types = ['配偶', '子女', '父母', '兄弟姐妹', '其他']
# 常用地址
provinces = ['北京市', '上海市', '广东省', '浙江省', '江苏省', '四川省', '湖北省', '河南省', '山东省', '福建省']
cities = ['朝阳区', '海淀区', '浦东新区', '黄浦区', '天河区', '福田区', '西湖区', '滨江区', '鼓楼区', '玄武区']
districts = ['街道1号', '大道2号', '路3号', '巷4号', '小区5栋', '花园6号', '广场7号', '大厦8号楼']
# 公司和职位
companies = ['房产中介有限公司', '置业咨询公司', '房产经纪公司', '地产代理公司', '不动产咨询公司', '房屋租赁公司', '物业管理公司', '投资咨询公司']
positions = ['房产经纪人', '销售经理', '业务员', '置业顾问', '店长', '区域经理', '高级经纪人', '项目经理']
# 生成身份证号码(简化版,仅用于测试)
def generate_id_card():
# 地区码前6位
area_code = f"{random.randint(110000, 650000):06d}"
# 出生日期8位
birth_year = random.randint(1960, 2000)
birth_month = f"{random.randint(1, 12):02d}"
birth_day = f"{random.randint(1, 28):02d}"
birth_date = f"{birth_year}{birth_month}{birth_day}"
# 顺序码3位
sequence = f"{random.randint(1, 999):03d}"
# 校验码1位
check_code = random.randint(0, 9)
return f"{area_code}{birth_date}{sequence}{check_code}"
# 生成手机号
def generate_phone():
second_digits = ['3', '5', '7', '8', '9']
second = random.choice(second_digits)
return f"1{second}{''.join([str(random.randint(0, 9)) for _ in range(9)])}"
# 生成统一信用代码
def generate_credit_code():
return f"91{''.join([str(random.randint(0, 9)) for _ in range(16)])}"
# 生成微信号
def generate_wechat():
return f"wx_{''.join([random.choice(string.ascii_lowercase + string.digits) for _ in range(8)])}"
# 生成单条数据
def generate_person(index):
person_type = random.choice(person_types)
gender = random.choice(genders)
# 根据性别选择更合适的名字
if gender == 'M':
name = random.choice(surnames) + random.choice(['', '', '', '', '', '', '', '', '', '', '', '', ''])
else:
name = random.choice(surnames) + random.choice(['', '', '', '', '', '', '', '秀英', '', '', '桂英', '玉兰', ''])
id_type = random.choice(id_types)
id_card = generate_id_card() if id_type == '身份证' else f"{random.randint(10000000, 99999999):08d}"
return {
'姓名': name,
'人员类型': person_type,
'人员子类型': random.choice(person_sub_types),
'性别': gender,
'证件类型': id_type,
'证件号码': id_card,
'手机号码': generate_phone(),
'微信号': generate_wechat() if random.random() > 0.3 else '',
'联系地址': f"{random.choice(provinces)}{random.choice(cities)}{random.choice(districts)}",
'所在公司': random.choice(companies) if random.random() > 0.2 else '',
'企业统一信用码': generate_credit_code() if random.random() > 0.5 else '',
'职位': random.choice(positions) if random.random() > 0.3 else '',
'关联人员ID': f"ID{random.randint(10000, 99999)}" if random.random() > 0.6 else '',
'关系类型': random.choice(relation_types) if random.random() > 0.6 else '',
'备注': f'测试数据{index}' if random.random() > 0.5 else ''
}
# 生成1000条数据
print("正在生成1000条个人中介黑名单数据...")
data = [generate_person(i) for i in range(1, 1001)]
df = pd.DataFrame(data)
# 保存第一个文件
output1 = r'D:\ccdi\ccdi\doc\test-data\intermediary\个人中介黑名单测试数据_1000条_第1批.xlsx'
df.to_excel(output1, index=False)
print(f"已生成第一个文件: {output1}")
# 生成第二个1000条数据
print("正在生成第二批1000条个人中介黑名单数据...")
data2 = [generate_person(i) for i in range(1, 1001)]
df2 = pd.DataFrame(data2)
# 保存第二个文件
output2 = r'D:\ccdi\ccdi\doc\test-data\intermediary\个人中介黑名单测试数据_1000条_第2批.xlsx'
df2.to_excel(output2, index=False)
print(f"已生成第二个文件: {output2}")
print("\n生成完成!")
print(f"文件1: {output1}")
print(f"文件2: {output2}")
print(f"\n每个文件包含1000条测试数据")

View File

@@ -1,446 +0,0 @@
/**
* 中介导入功能测试脚本 - 验证ON DUPLICATE KEY UPDATE重构
*
* 测试场景:
* 1. 更新模式 - 测试importPersonBatch/importEntityBatch的INSERT ON DUPLICATE KEY UPDATE
* 2. 仅新增模式 - 测试冲突检测和失败记录
* 3. 边界情况 - 空列表、全部冲突、部分冲突等
*/
const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
// 配置
const BASE_URL = 'http://localhost:8080';
const LOGIN_URL = `${BASE_URL}/login/test`;
const PERSON_IMPORT_URL = `${BASE_URL}/ccdi/intermediary/importPersonData`;
const ENTITY_IMPORT_URL = `${BASE_URL}/ccdi/intermediary/importEntityData`;
const PERSON_STATUS_URL = `${BASE_URL}/ccdi/intermediary/person/import/status`;
const ENTITY_STATUS_URL = `${BASE_URL}/ccdi/intermediary/entity/import/status`;
const PERSON_FAILURES_URL = `${BASE_URL}/ccdi/intermediary/person/import/failures`;
const ENTITY_FAILURES_URL = `${BASE_URL}/ccdi/intermediary/entity/import/failures`;
// 测试数据文件路径
const TEST_DATA_DIR = path.join(__dirname, '../test-data/intermediary');
const PERSON_TEST_FILE = path.join(TEST_DATA_DIR, '个人中介黑名单测试数据_1000条_第1批.xlsx');
const ENTITY_TEST_FILE = path.join(TEST_DATA_DIR, '机构中介黑名单测试数据_1000条_第1批.xlsx');
let authToken = '';
// 颜色输出
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[36m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
function logSuccess(message) {
log(`${message}`, 'green');
}
function logError(message) {
log(`${message}`, 'red');
}
function logInfo(message) {
log(` ${message}`, 'blue');
}
function logSection(title) {
console.log('\n' + '='.repeat(60));
log(title, 'yellow');
console.log('='.repeat(60));
}
/**
* 登录获取Token
*/
async function login() {
logSection('登录系统');
try {
const response = await axios.post(LOGIN_URL, {
username: 'admin',
password: 'admin123'
});
if (response.data.code === 200) {
authToken = response.data.data;
logSuccess('登录成功');
logInfo(`Token: ${authToken.substring(0, 20)}...`);
return true;
} else {
logError(`登录失败: ${response.data.msg}`);
return false;
}
} catch (error) {
logError(`登录请求失败: ${error.message}`);
return false;
}
}
/**
* 上传文件并开始导入
*/
async function importData(file, url, updateSupport, description) {
logSection(description);
if (!fs.existsSync(file)) {
logError(`测试文件不存在: ${file}`);
return null;
}
logInfo(`上传文件: ${path.basename(file)}`);
logInfo(`更新模式: ${updateSupport ? '是' : '否'}`);
try {
const form = new FormData();
form.append('file', fs.createReadStream(file));
form.append('updateSupport', updateSupport.toString());
const response = await axios.post(url, form, {
headers: {
...form.getHeaders(),
'Authorization': `Bearer ${authToken}`
}
});
if (response.data.code === 200) {
logSuccess('导入任务已提交');
logInfo(`响应信息: ${response.data.msg}`);
// 从响应中提取taskId
const match = response.data.msg.match(/任务ID: ([a-zA-Z0-9-]+)/);
if (match) {
const taskId = match[1];
logInfo(`任务ID: ${taskId}`);
return taskId;
}
} else {
logError(`导入失败: ${response.data.msg}`);
}
} catch (error) {
logError(`导入请求失败: ${error.message}`);
if (error.response) {
logError(`状态码: ${error.response.status}`);
logError(`响应数据: ${JSON.stringify(error.response.data)}`);
}
}
return null;
}
/**
* 轮询查询导入状态
*/
async function pollImportStatus(taskId, url, description, maxAttempts = 30, interval = 2000) {
logInfo(`等待导入完成...`);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const response = await axios.get(`${url}?taskId=${taskId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.data.code === 200) {
const status = response.data.data;
logInfo(`[尝试 ${attempt}/${maxAttempts}] 状态: ${status.status}, 进度: ${status.progress}%`);
if (status.status === 'SUCCESS' || status.status === 'PARTIAL_SUCCESS') {
logSuccess(`${description}完成!`);
logInfo(`总数: ${status.totalCount}, 成功: ${status.successCount}, 失败: ${status.failureCount}`);
return status;
} else if (status.status === 'FAILURE') {
logError(`${description}失败`);
return status;
}
}
} catch (error) {
logError(`查询状态失败: ${error.message}`);
}
await sleep(interval);
}
logError('导入超时');
return null;
}
/**
* 获取导入失败记录
*/
async function getImportFailures(taskId, url, description) {
logSection(`获取${description}失败记录`);
try {
const response = await axios.get(`${url}?taskId=${taskId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.data.code === 200) {
const failures = response.data.data;
logInfo(`失败记录数: ${failures.length}`);
if (failures.length > 0) {
logInfo('前3条失败记录:');
failures.slice(0, 3).forEach((failure, index) => {
console.log(` ${index + 1}. ${failure.errorMessage || '未知错误'}`);
});
// 保存失败记录到文件
const failureFile = path.join(__dirname, `failures_${taskId}.json`);
fs.writeFileSync(failureFile, JSON.stringify(failures, null, 2));
logInfo(`失败记录已保存到: ${failureFile}`);
}
return failures;
}
} catch (error) {
logError(`获取失败记录失败: ${error.message}`);
}
return [];
}
/**
* 辅助函数: 延迟
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 测试场景1: 个人中介 - 更新模式(第一次导入)
*/
async function testPersonImportUpdateMode() {
logSection('测试场景1: 个人中介 - 更新模式(第一次导入)');
const taskId = await importData(
PERSON_TEST_FILE,
PERSON_IMPORT_URL,
true, // 更新模式
'个人中介导入(更新模式)'
);
if (!taskId) {
logError('导入任务未创建');
return false;
}
const status = await pollImportStatus(taskId, PERSON_STATUS_URL, '个人中介导入');
if (status && (status.status === 'SUCCESS' || status.status === 'PARTIAL_SUCCESS')) {
const failures = await getImportFailures(taskId, PERSON_FAILURES_URL, '个人中介');
logSuccess(`测试场景1完成 - 成功: ${status.successCount}, 失败: ${status.failureCount}`);
return true;
}
return false;
}
/**
* 测试场景2: 个人中介 - 仅新增模式(重复导入应失败)
*/
async function testPersonImportInsertOnly() {
logSection('测试场景2: 个人中介 - 仅新增模式(重复导入)');
const taskId = await importData(
PERSON_TEST_FILE,
PERSON_IMPORT_URL,
false, // 仅新增模式
'个人中介导入(仅新增)'
);
if (!taskId) {
logError('导入任务未创建');
return false;
}
const status = await pollImportStatus(taskId, PERSON_STATUS_URL, '个人中介导入');
if (status && (status.status === 'SUCCESS' || status.status === 'PARTIAL_SUCCESS')) {
const failures = await getImportFailures(taskId, PERSON_FAILURES_URL, '个人中介');
// 在仅新增模式下,重复导入应该全部失败
if (failures.length > 0) {
logSuccess(`测试场景2完成 - 预期有失败记录, 实际失败: ${failures.length}`);
return true;
} else {
logError('测试场景2失败 - 预期有失败记录, 但实际没有');
return false;
}
}
return false;
}
/**
* 测试场景3: 实体中介 - 更新模式(第一次导入)
*/
async function testEntityImportUpdateMode() {
logSection('测试场景3: 实体中介 - 更新模式(第一次导入)');
const taskId = await importData(
ENTITY_TEST_FILE,
ENTITY_IMPORT_URL,
true, // 更新模式
'实体中介导入(更新模式)'
);
if (!taskId) {
logError('导入任务未创建');
return false;
}
const status = await pollImportStatus(taskId, ENTITY_STATUS_URL, '实体中介导入');
if (status && (status.status === 'SUCCESS' || status.status === 'PARTIAL_SUCCESS')) {
const failures = await getImportFailures(taskId, ENTITY_FAILURES_URL, '实体中介');
logSuccess(`测试场景3完成 - 成功: ${status.successCount}, 失败: ${status.failureCount}`);
return true;
}
return false;
}
/**
* 测试场景4: 实体中介 - 仅新增模式(重复导入应失败)
*/
async function testEntityImportInsertOnly() {
logSection('测试场景4: 实体中介 - 仅新增模式(重复导入)');
const taskId = await importData(
ENTITY_TEST_FILE,
ENTITY_IMPORT_URL,
false, // 仅新增模式
'实体中介导入(仅新增)'
);
if (!taskId) {
logError('导入任务未创建');
return false;
}
const status = await pollImportStatus(taskId, ENTITY_STATUS_URL, '实体中介导入');
if (status && (status.status === 'SUCCESS' || status.status === 'PARTIAL_SUCCESS')) {
const failures = await getImportFailures(taskId, ENTITY_FAILURES_URL, '实体中介');
// 在仅新增模式下,重复导入应该全部失败
if (failures.length > 0) {
logSuccess(`测试场景4完成 - 预期有失败记录, 实际失败: ${failures.length}`);
return true;
} else {
logError('测试场景4失败 - 预期有失败记录, 但实际没有');
return false;
}
}
return false;
}
/**
* 测试场景5: 个人中介 - 再次更新模式(应该更新已有数据)
*/
async function testPersonImportUpdateAgain() {
logSection('测试场景5: 个人中介 - 再次更新模式');
const taskId = await importData(
PERSON_TEST_FILE,
PERSON_IMPORT_URL,
true, // 更新模式
'个人中介导入(再次更新)'
);
if (!taskId) {
logError('导入任务未创建');
return false;
}
const status = await pollImportStatus(taskId, PERSON_STATUS_URL, '个人中介导入');
if (status && (status.status === 'SUCCESS' || status.status === 'PARTIAL_SUCCESS')) {
const failures = await getImportFailures(taskId, PERSON_FAILURES_URL, '个人中介');
logSuccess(`测试场景5完成 - 成功: ${status.successCount}, 失败: ${status.failureCount}`);
return true;
}
return false;
}
/**
* 主测试流程
*/
async function runTests() {
console.log('\n╔════════════════════════════════════════════════════════════╗');
console.log('║ 中介导入功能测试 - ON DUPLICATE KEY UPDATE验证 ║');
console.log('╚════════════════════════════════════════════════════════════╝');
const startTime = Date.now();
const results = {
passed: 0,
failed: 0
};
// 登录
const loginSuccess = await login();
if (!loginSuccess) {
logError('无法登录,终止测试');
return;
}
// 执行测试
const tests = [
{ name: '场景1: 个人中介-更新模式(首次)', fn: testPersonImportUpdateMode },
{ name: '场景2: 个人中介-仅新增(重复)', fn: testPersonImportInsertOnly },
{ name: '场景3: 实体中介-更新模式(首次)', fn: testEntityImportUpdateMode },
{ name: '场景4: 实体中介-仅新增(重复)', fn: testEntityImportInsertOnly },
{ name: '场景5: 个人中介-再次更新', fn: testPersonImportUpdateAgain }
];
for (const test of tests) {
try {
const passed = await test.fn();
if (passed) {
results.passed++;
} else {
results.failed++;
}
await sleep(2000); // 测试之间间隔
} catch (error) {
logError(`${test.name} 执行异常: ${error.message}`);
results.failed++;
}
}
// 输出测试结果摘要
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
console.log('\n' + '='.repeat(60));
log('测试结果摘要', 'yellow');
console.log('='.repeat(60));
logSuccess(`通过: ${results.passed}/${tests.length}`);
if (results.failed > 0) {
logError(`失败: ${results.failed}/${tests.length}`);
}
logInfo(`总耗时: ${duration}`);
console.log('='.repeat(60) + '\n');
}
// 运行测试
runTests().catch(error => {
logError(`测试运行失败: ${error.message}`);
console.error(error);
process.exit(1);
});

View File

@@ -1,201 +0,0 @@
# 采购交易Excel类字段类型修复说明
## 问题描述
`CcdiPurchaseTransactionExcel``CcdiPurchaseTransaction` 存在字段类型不匹配问题,导致使用 `BeanUtils.copyProperties()` 进行属性复制时可能出现类型转换错误。
## 类型不匹配详情
### 1. 数值字段类型不匹配
| 字段名 | Excel类(修复前) | 实体类 | 修复后Excel类 |
|--------|----------------|--------|---------------|
| purchaseQty | String | BigDecimal | BigDecimal |
| budgetAmount | String | BigDecimal | BigDecimal |
| bidAmount | String | BigDecimal | BigDecimal |
| actualAmount | String | BigDecimal | BigDecimal |
| contractAmount | String | BigDecimal | BigDecimal |
| settlementAmount | String | BigDecimal | BigDecimal |
### 2. 日期字段类型不匹配
| 字段名 | Excel类(修复前) | 实体类 | 修复后Excel类 |
|--------|----------------|--------|---------------|
| applyDate | String | Date | Date |
| planApproveDate | String | Date | Date |
| announceDate | String | Date | Date |
| bidOpenDate | String | Date | Date |
| contractSignDate | String | Date | Date |
| expectedDeliveryDate | String | Date | Date |
| actualDeliveryDate | String | Date | Date |
| acceptanceDate | String | Date | Date |
| settlementDate | String | Date | Date |
## 修复内容
### 文件: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiPurchaseTransactionExcel.java`
#### 1. 添加必要的导入
```java
import java.math.BigDecimal;
import java.util.Date;
```
#### 2. 修改数值字段类型 (第53-83行)
**修复前**:
```java
private String purchaseQty;
private String budgetAmount;
private String bidAmount;
private String actualAmount;
private String contractAmount;
private String settlementAmount;
```
**修复后**:
```java
private BigDecimal purchaseQty;
private BigDecimal budgetAmount;
private BigDecimal bidAmount;
private BigDecimal actualAmount;
private BigDecimal contractAmount;
private BigDecimal settlementAmount;
```
#### 3. 修改日期字段类型 (第116-160行)
**修复前**:
```java
private String applyDate;
private String planApproveDate;
private String announceDate;
private String bidOpenDate;
private String contractSignDate;
private String expectedDeliveryDate;
private String actualDeliveryDate;
private String acceptanceDate;
private String settlementDate;
```
**修复后**:
```java
private Date applyDate;
private Date planApproveDate;
private Date announceDate;
private Date bidOpenDate;
private Date contractSignDate;
private Date expectedDeliveryDate;
private Date actualDeliveryDate;
private Date acceptanceDate;
private Date settlementDate;
```
## EasyExcel 类型转换说明
EasyExcel 支持以下自动类型转换:
### 数值类型
- Excel中的数值 → BigDecimal
- Excel中的数值 → Integer, Long, Double等
- 空单元格 → null
### 日期类型
- Excel中的日期 → Date
- Excel中的日期字符串 (yyyy-MM-dd) → Date
- 空单元格 → null
### 自定义日期格式
如果需要自定义日期格式,可以在字段上添加 `@DateTimeFormat` 注解:
```java
@ExcelProperty(value = "采购申请日期", index = 17)
@DateTimeFormat("yyyy-MM-dd")
private Date applyDate;
```
## 影响范围
### 正面影响
-`BeanUtils.copyProperties()` 可以正确复制属性
- ✅ 类型安全,避免运行时类型转换异常
- ✅ 与实体类字段类型保持一致
### 注意事项
- ⚠️ 导入Excel时,数值和日期列格式需要正确
- ⚠️ 如果Excel中的数值格式不正确,可能导致解析失败
- ⚠️ 如果Excel中的日期格式不正确,可能导致解析为null
### Excel导入注意事项
1. **数值列**: 确保Excel单元格格式为"数值"类型
2. **日期列**:
- 推荐格式: `yyyy-MM-dd` (如: 2026-02-09)
- 或使用Excel日期格式
- 空值会被解析为 `null`
3. **必填字段**: 标有 `@Required` 注解的字段不能为空
- purchaseId
- purchaseCategory
- subjectName
- purchaseQty
- budgetAmount
- purchaseMethod
- applyDate
- applicantId
- applicantName
- applyDepartment
## 验证方法
### 方法1: 导入测试
1. 准备正确格式的Excel文件
2. 通过系统界面导入
3. 验证数据是否正确保存到数据库
### 方法2: 单元测试
```java
@Test
public void testExcelToEntityConversion() {
CcdiPurchaseTransactionExcel excel = new CcdiPurchaseTransactionExcel();
excel.setPurchaseId("TEST001");
excel.setPurchaseQty(new BigDecimal("100.5"));
excel.setBudgetAmount(new BigDecimal("50000.00"));
excel.setApplyDate(new Date());
CcdiPurchaseTransaction entity = new CcdiPurchaseTransaction();
// 属性复制应该正常工作,不会抛出类型转换异常
BeanUtils.copyProperties(excel, entity);
// 验证字段类型正确
assertTrue(entity.getPurchaseQty() instanceof BigDecimal);
assertTrue(entity.getBudgetAmount() instanceof BigDecimal);
assertTrue(entity.getApplyDate() instanceof Date);
// 验证值正确
assertEquals(new BigDecimal("100.5"), entity.getPurchaseQty());
assertEquals(new BigDecimal("50000.00"), entity.getBudgetAmount());
}
```
## 兼容性说明
此修复使Excel类与实体类的字段类型完全一致,符合以下模块的规范:
- ✅ 中介管理 (CcdiIntermediaryPersonExcel, CcdiIntermediaryEntityExcel)
- ✅ 员工管理 (CcdiEmployeeExcel)
## 相关文件
- **Excel类**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiPurchaseTransactionExcel.java`
- **实体类**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/CcdiPurchaseTransaction.java`
- **导入Service**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiPurchaseTransactionImportServiceImpl.java`
## 变更历史
| 日期 | 版本 | 变更内容 | 作者 |
|------|------|----------|------|
| 2026-02-09 | 1.0 | 修复字段类型不匹配问题 | Claude |

View File

@@ -1,215 +0,0 @@
# 采购交易导入失败记录接口修复说明
## 问题描述
采购交易管理的导入失败记录列表无法展示。对话框能打开,但表格为空。
## 根本原因
通过代码对比分析,发现采购交易管理的导入失败记录接口与项目中其他模块(员工、中介)的实现不一致:
### 问题代码
**文件**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiPurchaseTransactionController.java`
**原代码 (第179-183行)**:
```java
@GetMapping("/importFailures/{taskId}")
public AjaxResult getImportFailures(@PathVariable String taskId) {
List<PurchaseTransactionImportFailureVO> failures = transactionImportService.getImportFailures(taskId);
return success(failures); // ❌ 直接返回所有数据,没有分页
}
```
**问题点**:
1. 返回类型是 `AjaxResult`,而不是 `TableDataInfo`
2. 没有 `pageNum``pageSize` 分页参数
3. 没有实现分页逻辑
4. 返回数据结构是 `{code: 200, data: [...]}` 而不是 `{code: 200, rows: [...], total: xxx}`
### 正确实现 (参考中介模块)
**文件**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiIntermediaryController.java`
```java
@GetMapping("/importPersonFailures/{taskId}")
public TableDataInfo getPersonImportFailures(
@PathVariable String taskId,
@RequestParam(defaultValue = "1") Integer pageNum, // ✅ 支持分页
@RequestParam(defaultValue = "10") Integer pageSize) {
List<IntermediaryPersonImportFailureVO> failures = personImportService.getImportFailures(taskId);
// ✅ 手动分页
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, failures.size());
List<IntermediaryPersonImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
return getDataTable(pageData, failures.size()); // ✅ 返回TableDataInfo
}
```
## 修复方案
修改 `CcdiPurchaseTransactionController.java``getImportFailures` 方法:
### 修改后的代码
**文件**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiPurchaseTransactionController.java:173-196`
```java
/**
* 查询导入失败记录
*/
@Operation(summary = "查询导入失败记录")
@Parameter(name = "taskId", description = "任务ID", required = true)
@Parameter(name = "pageNum", description = "页码", required = false)
@Parameter(name = "pageSize", description = "每页条数", required = false)
@PreAuthorize("@ss.hasPermi('ccdi:purchaseTransaction:import')")
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
List<PurchaseTransactionImportFailureVO> failures = transactionImportService.getImportFailures(taskId);
// 手动分页
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, failures.size());
List<PurchaseTransactionImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
return getDataTable(pageData, failures.size());
}
```
### 修改内容
1. ✅ 修改返回类型: `AjaxResult``TableDataInfo`
2. ✅ 添加分页参数: `pageNum``pageSize`
3. ✅ 实现手动分页逻辑
4. ✅ 使用 `getDataTable()` 方法返回标准分页结构
### 返回数据结构对比
**修复前 (AjaxResult)**:
```json
{
"code": 200,
"msg": "操作成功",
"data": [
{...},
{...},
...
]
}
```
**修复后 (TableDataInfo)**:
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{...},
{...},
...
],
"total": 100
}
```
## 测试验证
### 方法1: 使用自动化测试脚本
1. **启动后端服务**
```bash
mvn spring-boot:run
```
2. **准备测试数据**
- 准备一个包含错误数据的Excel文件
- 通过系统界面上传并导入
- 记录返回的 `taskId`
3. **运行测试脚本**
```bash
cd doc/test-data/purchase_transaction
node test-import-failures-api.js <taskId>
```
4. **查看测试结果**
- 脚本会验证:
- 响应状态码是否为 200
- `rows` 字段是否存在且为数组
- `total` 字段是否存在
- 分页功能是否正常工作
### 方法2: 使用 Postman/curl 测试
```bash
# 1. 登录获取token
curl -X POST "http://localhost:8080/login/test" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# 2. 查询导入失败记录 (替换 <taskId> 和 <token>)
curl -X GET "http://localhost:8080/ccdi/purchaseTransaction/importFailures/<taskId>?pageNum=1&pageSize=10" \
-H "Authorization: Bearer <token>"
```
**预期响应**:
```json
{
"code": 200,
"msg": "查询成功",
"rows": [
{
"purchaseId": "PO001",
"projectName": "测试项目",
"subjectName": "测试标的物",
"errorMessage": "采购数量必须大于0"
}
],
"total": 1
}
```
### 方法3: 前端界面测试
1. 访问采购交易管理页面
2. 准备包含错误数据的Excel文件并导入
3. 等待导入完成
4. 点击"查看导入失败记录"按钮
5. 验证:
- ✅ 对话框能正常打开
- ✅ 表格显示失败记录数据
- ✅ 顶部显示统计信息
- ✅ 分页组件正常显示和工作
## 影响范围
- ✅ **后端代码**: `CcdiPurchaseTransactionController.java`
- ✅ **前端代码**: 无需修改 (前端代码已正确处理 `TableDataInfo` 格式)
- ✅ **数据库**: 无影响
- ✅ **其他模块**: 无影响
## 兼容性说明
此修复使采购交易模块的导入失败记录接口与项目中其他模块(员工、中介)保持一致,符合项目的统一规范。
## 相关文件
- **Controller**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiPurchaseTransactionController.java`
- **前端页面**: `ruoyi-ui/src/views/ccdiPurchaseTransaction/index.vue`
- **前端API**: `ruoyi-ui/src/api/ccdiPurchaseTransaction.js`
- **Service实现**: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/service/impl/CcdiPurchaseTransactionImportServiceImpl.java`
- **测试脚本**: `doc/test-data/purchase_transaction/test-import-failures-api.js`
## 变更历史
| 日期 | 版本 | 变更内容 | 作者 |
|------|------|----------|------|
| 2026-02-09 | 1.0 | 初始版本,修复导入失败记录接口 | Claude |

View File

@@ -1,280 +0,0 @@
# 采购交易管理问题修复总结
## 修复日期
2026-02-09
## 修复内容概览
本次修复解决了采购交易管理模块的两个关键问题:
### 1. 导入失败记录列表无法展示 ✅
### 2. Excel类与实体类字段类型不匹配 ✅
---
## 问题1: 导入失败记录列表无法展示
### 问题描述
- 对话框能正常打开
- 表格为空,不显示任何数据
- 分页组件也不显示
### 根本原因
Controller层接口返回类型不正确:
- **返回类型**: `AjaxResult` 而不是 `TableDataInfo`
- **缺少分页**: 没有 `pageNum``pageSize` 参数
- **数据结构**: 返回 `{data: [...]}` 而不是 `{rows: [...], total: xxx}`
### 修复方案
修改 `CcdiPurchaseTransactionController.java``getImportFailures` 方法
#### 修复前 (第179-183行)
```java
@GetMapping("/importFailures/{taskId}")
public AjaxResult getImportFailures(@PathVariable String taskId) {
List<PurchaseTransactionImportFailureVO> failures = transactionImportService.getImportFailures(taskId);
return success(failures); // ❌ 直接返回所有数据,没有分页
}
```
#### 修复后 (第173-196行)
```java
@GetMapping("/importFailures/{taskId}")
public TableDataInfo getImportFailures(
@PathVariable String taskId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
List<PurchaseTransactionImportFailureVO> failures = transactionImportService.getImportFailures(taskId);
// 手动分页
int fromIndex = (pageNum - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, failures.size());
List<PurchaseTransactionImportFailureVO> pageData = failures.subList(fromIndex, toIndex);
return getDataTable(pageData, failures.size()); // ✅ 返回标准分页数据
}
```
### 修复效果
- ✅ 返回正确的分页数据结构
- ✅ 前端能正确读取 `response.rows``response.total`
- ✅ 表格正常显示失败记录
- ✅ 分页组件正常工作
- ✅ 与其他模块(员工、中介)保持一致
---
## 问题2: Excel类与实体类字段类型不匹配
### 问题描述
`CcdiPurchaseTransactionExcel``CcdiPurchaseTransaction` 存在字段类型不匹配,可能导致:
- `BeanUtils.copyProperties()` 属性复制失败
- 运行时类型转换异常
- 数据导入失败
### 类型不匹配详情
#### 数值字段
| 字段名 | Excel类(修复前) | 实体类 | 修复后Excel类 |
|--------|----------------|--------|---------------|
| purchaseQty | String | BigDecimal | ✅ BigDecimal |
| budgetAmount | String | BigDecimal | ✅ BigDecimal |
| bidAmount | String | BigDecimal | ✅ BigDecimal |
| actualAmount | String | BigDecimal | ✅ BigDecimal |
| contractAmount | String | BigDecimal | ✅ BigDecimal |
| settlementAmount | String | BigDecimal | ✅ BigDecimal |
#### 日期字段
| 字段名 | Excel类(修复前) | 实体类 | 修复后Excel类 |
|--------|----------------|--------|---------------|
| applyDate | String | Date | ✅ Date |
| planApproveDate | String | Date | ✅ Date |
| announceDate | String | Date | ✅ Date |
| bidOpenDate | String | Date | ✅ Date |
| contractSignDate | String | Date | ✅ Date |
| expectedDeliveryDate | String | Date | ✅ Date |
| actualDeliveryDate | String | Date | ✅ Date |
| acceptanceDate | String | Date | ✅ Date |
| settlementDate | String | Date | ✅ Date |
### 修复内容
#### 文件: `ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiPurchaseTransactionExcel.java`
**1. 添加必要的导入**
```java
import java.math.BigDecimal;
import java.util.Date;
```
**2. 修改数值字段类型 (第53-83行)**
```java
// 修复前
private String purchaseQty;
private String budgetAmount;
// ... 其他金额字段
// 修复后
private BigDecimal purchaseQty;
private BigDecimal budgetAmount;
// ... 其他金额字段
```
**3. 修改日期字段类型 (第116-160行)**
```java
// 修复前
private String applyDate;
private String planApproveDate;
// ... 其他日期字段
// 修复后
private Date applyDate;
private Date planApproveDate;
// ... 其他日期字段
```
### 修复效果
- ✅ Excel类与实体类字段类型完全一致
-`BeanUtils.copyProperties()` 正常工作
- ✅ 避免运行时类型转换异常
- ✅ EasyExcel 自动类型转换正常工作
- ✅ 与其他模块(员工、中介)保持一致
---
## 测试验证
### 测试文件
已生成以下测试文件:
1. **CSV测试数据**: `doc/test-data/purchase_transaction/generated/purchase_transaction_test_data.csv`
2. **JSON测试数据**: `doc/test-data/purchase_transaction/generated/purchase_transaction_test_data.json`
3. **测试说明**: `doc/test-data/purchase_transaction/generated/README.md`
4. **API测试脚本**: `doc/test-data/purchase_transaction/test-import-failures-api.js`
### 测试数据说明
#### 正确数据 (2条)
- **PT202602090001**: 货物采购 - 包含完整的数值和日期字段
- **PT202602090002**: 服务采购 - 部分金额字段为0
#### 错误数据 (2条)
- **PT202602090003**: 测试必填字段和数值范围校验
- **PT202602090004**: 测试工号格式校验
### 测试步骤
#### 1. 测试导入失败记录显示
```bash
# 步骤1: 准备Excel文件
# 将CSV文件导入Excel,保存为xlsx格式
# 步骤2: 导入数据
# 通过系统界面上传导入
# 步骤3: 获取taskId
# 记录返回的任务ID
# 步骤4: 测试API
cd doc/test-data/purchase_transaction
node test-import-failures-api.js <taskId>
# 步骤5: 验证结果
# - 检查响应是否包含 rows 和 total 字段
# - 检查前端对话框是否正确显示数据
# - 测试分页功能
```
#### 2. 测试字段类型转换
```bash
# 步骤1: 导入包含正确数值和日期格式的Excel
# 步骤2: 验证数据库
# 检查数值字段是否正确存储为DECIMAL类型
# 检查日期字段是否正确存储为DATETIME类型
# 步骤3: 验证失败记录
# 检查错误数据是否被正确捕获
# 验证错误提示信息是否准确
```
---
## 影响范围
### 修改的文件
1.`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/controller/CcdiPurchaseTransactionController.java`
2.`ruoyi-ccdi/src/main/java/com/ruoyi/ccdi/domain/excel/CcdiPurchaseTransactionExcel.java`
### 无需修改的文件
- ✅ 前端代码: 已正确处理 `TableDataInfo` 格式
- ✅ Service层: 无需修改
- ✅ Mapper层: 无需修改
- ✅ 数据库: 无影响
### 兼容性
- ✅ 与员工管理模块保持一致
- ✅ 与中介管理模块保持一致
- ✅ 符合项目统一规范
---
## 文档更新
### 新增文档
1.`doc/test-data/purchase_transaction/FIX_IMPORT_FAILURES_API.md` - 导入失败记录接口修复说明
2.`doc/test-data/purchase_transaction/FIX_EXCEL_FIELD_TYPES.md` - Excel字段类型修复说明
3.`doc/test-data/purchase_transaction/test-import-failures-api.js` - API测试脚本
4.`doc/test-data/purchase_transaction/generate-type-test-data.js` - 测试数据生成脚本
5.`doc/test-data/purchase_transaction/generated/README.md` - 测试数据说明
---
## 验证清单
### 功能验证
- [ ] 导入包含错误数据的Excel文件
- [ ] 导入完成后显示失败记录按钮
- [ ] 点击按钮打开对话框
- [ ] 对话框显示失败记录列表
- [ ] 分页组件正常显示和工作
- [ ] 失败原因正确显示
- [ ] 数值字段正确解析和存储
- [ ] 日期字段正确解析和存储
- [ ] 必填字段校验正常工作
- [ ] 错误提示信息准确
### 接口验证
- [ ] `/importFailures/{taskId}` 返回正确的数据结构
- [ ] `pageNum``pageSize` 参数正常工作
- [ ] `response.rows` 包含分页数据
- [ ] `response.total` 包含总记录数
- [ ] 404错误正确处理(记录过期)
- [ ] 500错误正确处理(服务器错误)
### 类型验证
- [ ] BigDecimal字段正确转换
- [ ] Date字段正确转换
- [ ] 空值正确处理(null)
- [ ] 格式错误正确处理
---
## 相关问题
如果有以下问题,可能需要进一步检查:
1. Excel文件格式不正确
2. 数值单元格格式不是"数值"类型
3. 日期单元格格式不正确
4. 缺少必填字段
5. 工号格式不是7位数字
---
## 总结
本次修复解决了采购交易管理模块的两个关键问题,使其与项目中其他模块保持一致,提高了代码的健壮性和可维护性。所有修复都经过了充分的分析和测试验证,确保不会引入新的问题。
**修复人员**: Claude
**审核状态**: 待审核
**部署状态**: 待部署

View File

@@ -1,379 +0,0 @@
# 采购交易信息管理 - 测试说明
## 1. 测试环境说明
### 1.1 系统环境
- **操作系统**: Windows/Linux
- **Java版本**: JDK 17
- **数据库**: MySQL 8.2.0
- **后端框架**: Spring Boot 3.5.8
- **前端框架**: Vue 2.6.12 + Element UI 2.15.14
### 1.2 服务地址
- **后端地址**: http://localhost:8080
- **前端地址**: http://localhost:80
- **Swagger UI**: http://localhost:8080/swagger-ui/index.html
## 2. 测试账号信息
### 2.1 管理员账号
- **用户名**: `admin`
- **密码**: `admin123`
- **权限**: 拥有所有权限
### 2.2 获取Token
使用以下接口获取访问令牌:
```
POST /login/test
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}
```
响应示例:
```json
{
"code": 200,
"msg": "操作成功",
"token": "Bearer eyJhbGciOiJIUzI1NiJ9..."
}
```
## 3. 接口测试说明
### 3.1 接口列表
采购交易管理模块共10个接口
| 序号 | 接口名称 | 方法 | 路径 | 权限标识 |
|------|---------|------|------|----------|
| 1 | 查询采购交易列表 | GET | /ccdi/purchaseTransaction/list | ccdi:purchaseTransaction:list |
| 2 | 获取采购交易详情 | GET | /ccdi/purchaseTransaction/{purchaseId} | ccdi:purchaseTransaction:query |
| 3 | 新增采购交易 | POST | /ccdi/purchaseTransaction | ccdi:purchaseTransaction:add |
| 4 | 修改采购交易 | PUT | /ccdi/purchaseTransaction | ccdi:purchaseTransaction:edit |
| 5 | 删除采购交易 | DELETE | /ccdi/purchaseTransaction/{purchaseIds} | ccdi:purchaseTransaction:remove |
| 6 | 导出采购交易 | POST | /ccdi/purchaseTransaction/export | ccdi:purchaseTransaction:export |
| 7 | 下载导入模板 | POST | /ccdi/purchaseTransaction/importTemplate | 无需权限 |
| 8 | 导入采购交易 | POST | /ccdi/purchaseTransaction/importData | ccdi:purchaseTransaction:import |
| 9 | 查询导入状态 | GET | /ccdi/purchaseTransaction/importStatus/{taskId} | ccdi:purchaseTransaction:import |
| 10 | 查询导入失败记录 | GET | /ccdi/purchaseTransaction/importFailures/{taskId} | ccdi:purchaseTransaction:import |
### 3.2 接口测试工具推荐
1. **Postman**: 图形化接口测试工具
2. **Swagger UI**: 在线接口文档和测试工具
3. **curl**: 命令行工具
### 3.3 接口测试要点
#### 3.3.1 分页查询测试
```bash
# 测试分页查询
GET /ccdi/purchaseTransaction/list?pageNum=1&pageSize=10
# 测试条件查询
GET /ccdi/purchaseTransaction/list?projectName=测试&applicantName=张三
# 测试日期范围查询
GET /ccdi/purchaseTransaction/list?params[beginApplyDate]=2025-01-01&params[endApplyDate]=2025-12-31
```
#### 3.3.2 数据验证测试
- 测试必填字段校验purchaseId为必填
- 测试字段长度限制
- 测试数值类型字段(金额、数量等)
- 测试日期格式校验
#### 3.3.3 异步导入测试
```bash
# 1. 提交导入任务
POST /ccdi/purchaseTransaction/importData?updateSupport=false
Content-Type: multipart/form-data
# 上传Excel文件
# 2. 获取返回的taskId
# 响应: {"code": 200, "msg": "导入任务已提交任务IDtask-xxx"}
# 3. 轮询查询导入状态
GET /ccdi/purchaseTransaction/importStatus/task-xxx
# 4. 如果有失败记录,查询失败详情
GET /ccdi/purchaseTransaction/importFailures/task-xxx
```
## 4. 前端功能测试说明
### 4.1 页面访问测试
1. 登录系统后,在左侧菜单找到"CCDI管理" -> "采购交易管理"
2. 点击菜单,确认页面正常加载
3. 确认表格、查询条件、操作按钮正常显示
### 4.2 查询功能测试
1. **基础查询**:
- 输入项目名称进行模糊查询
- 输入标的物名称进行模糊查询
- 输入申请人进行模糊查询
2. **日期范围查询**:
- 选择申请日期范围
- 点击"搜索"按钮
- 验证查询结果是否在指定日期范围内
3. **分页查询**:
- 切换每页显示条数10/20/50/100
- 点击页码切换
- 验证分页数据正确性
4. **重置查询**:
- 输入查询条件后点击"重置"
- 验证查询条件清空,列表恢复全部数据
### 4.3 新增功能测试
1. 点击"新增"按钮
2. 填写表单数据(测试不同场景):
- **正常数据**: 填写完整正确信息
- **必填验证**: 不填写purchaseId提交时验证提示
- **字段长度**: 输入超长字符串,验证长度限制
- **数值字段**: 输入负数、小数点等
- **日期字段**: 选择各个日期,验证日期顺序
3. 点击"确定"提交
4. 验证成功提示和列表刷新
### 4.4 编辑功能测试
1. 点击某条记录的"编辑"按钮
2. 验证表单数据回显正确
3. 修改部分字段
4. 提交保存
5. 验证修改成功和数据更新
### 4.5 详情功能测试
1. 点击某条记录的"详情"按钮
2. 验证详情对话框显示完整
3. 验证所有字段正确显示
4. 验证金额格式化显示(千分位)
5. 验证日期格式化显示
### 4.6 删除功能测试
1. **单条删除**:
- 点击某条记录的"删除"按钮
- 确认删除提示
- 验证删除成功
2. **批量删除**:
- 勾选多条记录
- 点击"删除"按钮
- 确认删除提示
- 验证批量删除成功
### 4.7 导出功能测试
1. 点击"导出"按钮
2. 验证Excel文件下载
3. 打开Excel文件验证:
- 表头正确
- 数据完整
- 格式正确(日期、金额等)
- 字典项显示正确
### 4.8 导入功能测试
1. **下载模板**:
- 点击"导入"按钮
- 点击"下载模板"链接
- 验证模板文件包含下拉框
2. **填写导入数据**:
- 使用下拉框选择字典值
- 填写测试数据(包含正常、异常数据)
3. **导入测试**:
- 上传Excel文件
- 选择是否更新已存在数据
- 提交导入
- 验证异步导入提示
- 等待导入完成
- 查看导入结果(成功/失败数量)
- 如果有失败,查看失败原因
4. **导入验证**:
- 刷新列表,验证数据导入成功
- 验证数据正确性
- 验证字典值正确
## 5. 导入导出测试说明
### 5.1 导出功能测试要点
1. **全部导出**:
- 不设置任何查询条件
- 点击导出
- 验证导出所有数据
2. **条件导出**:
- 设置查询条件
- 点击导出
- 验证只导出符合条件的数据
3. **数据格式验证**:
- 金额字段显示为数字格式保留2位小数
- 日期字段:格式为 yyyy-MM-dd
- 字典字段:显示字典标签而非值
### 5.2 导入功能测试要点
#### 5.2.1 模板验证
1. 下载模板,验证包含所有必填字段
2. 验证字典字段包含下拉框(使用@DictDropdown注解
3. 验证字段列顺序与实体类一致
#### 5.2.2 正常数据导入测试
准备包含以下特征的测试数据:
- 完整填写所有字段
- 使用下拉框选择字典值
- 日期格式正确
- 金额数值合理
#### 5.2.3 异常数据导入测试
准备包含以下错误的数据:
1. **必填字段缺失**:
- purchaseId为空
- 验证导入时提示必填
2. **字段长度超限**:
- 项目名称超过200字符
- 验证导入时提示长度超限
3. **数据格式错误**:
- 日期格式不正确
- 金额填写非数字
- 验证导入时提示格式错误
4. **重复数据**:
- purchaseId重复
- 测试"是否更新"选项:
- 不更新:跳过重复数据
- 更新:更新已有数据
#### 5.2.4 批量导入测试
准备1000+条测试数据:
- 验证导入性能
- 验证异步导入不阻塞
- 验证导入进度提示
- 验证导入结果统计正确
#### 5.2.5 导入失败验证
导入后:
1. 查看导入结果对话框
2. 验证显示成功/失败数量
3. 如果有失败:
- 查看失败记录列表
- 验证显示行号
- 验证显示具体错误信息
- 修正错误数据后重新导入
## 6. 性能测试建议
### 6.1 分页查询性能
- 测试不同数据量100/1000/10000条的查询响应时间
- 测试复杂条件查询性能
- 验证MyBatis Plus分页效率
### 6.2 导入性能测试
- 测试100条数据导入时间
- 测试1000条数据导入时间
- 测试5000条数据导入时间
- 监控数据库连接池使用情况
- 监控内存使用情况
### 6.3 导出性能测试
- 测试100条数据导出时间
- 测试1000条数据导出时间
- 测试10000条数据导出时间
- 验证大文件导出不卡顿
## 7. 常见问题及解决方案
### 7.1 导入失败
**问题**: 导入时提示文件格式错误
**解决**:
- 确认文件格式为.xlsx或.xls
- 不要修改模板的表头
- 不要删除或添加列
### 7.2 导入卡顿
**问题**: 导入大量数据时界面卡顿
**解决**:
- 本系统采用异步导入,不会卡顿
- 导入后会有进度提示
- 导入完成后会显示结果
### 7.3 数据导出乱码
**问题**: 导出的Excel中文乱码
**解决**:
- 系统使用UTF-8编码
- 确保Excel软件支持UTF-8
- 建议使用WPS或Microsoft Office打开
### 7.4 权限不足
**问题**: 提示无权限访问
**解决**:
- 确认用户已分配相应角色
- 确认角色已分配菜单权限
- 确认角色已分配按钮权限
## 8. 测试报告模板
测试完成后,建议记录以下内容:
### 8.1 功能测试报告
| 功能模块 | 测试用例数 | 通过数 | 失败数 | 通过率 |
|---------|-----------|--------|--------|--------|
| 列表查询 | 10 | 10 | 0 | 100% |
| 新增功能 | 8 | 8 | 0 | 100% |
| 编辑功能 | 6 | 6 | 0 | 100% |
| 删除功能 | 4 | 4 | 0 | 100% |
| 导出功能 | 3 | 3 | 0 | 100% |
| 导入功能 | 12 | 12 | 0 | 100% |
| **合计** | **43** | **43** | **0** | **100%** |
### 8.2 性能测试报告
| 测试项 | 数据量 | 响应时间 | 状态 |
|--------|--------|----------|------|
| 分页查询 | 1000条 | <200ms | 通过 |
| 分页查询 | 10000条 | <500ms | 通过 |
| 数据导入 | 1000条 | <5s | 通过 |
| 数据导出 | 1000条 | <2s | 通过 |
| 数据导出 | 10000条 | <10s | 通过 |
## 9. 测试完成标准
### 9.1 功能完整性
- [ ] 所有接口测试通过
- [ ] 所有前端功能测试通过
- [ ] 所有验证规则生效
- [ ] 导入导出功能正常
### 9.2 数据正确性
- [ ] 数据保存完整
- [ ] 数据查询准确
- [ ] 数据更新成功
- [ ] 数据删除正确
### 9.3 用户体验
- [ ] 操作响应及时
- [ ] 提示信息清晰
- [ ] 错误处理友好
- [ ] 界面布局合理
### 9.4 性能要求
- [ ] 分页查询 <500ms
- [ ] 单条CRUD <200ms
- [ ] 导入1000条 <5s
- [ ] 导出1000条 <2s
## 10. 测试注意事项
1. **测试数据准备**: 准备各种边界情况的测试数据
2. **环境一致性**: 确保测试环境与生产环境配置一致
3. **数据备份**: 测试前备份重要数据
4. **日志记录**: 测试过程中记录遇到的问题和解决方案
5. **回归测试**: 修改bug后进行回归测试
6. **用户验收**: 建议邀请业务人员进行用户验收测试

View File

@@ -1,20 +0,0 @@
# 测试环境信息
## 测试日期
2026-02-08
## 后端服务
- URL: http://localhost:8080
- Swagger: http://localhost:8080/swagger-ui/index.html
## 测试账号
- username: admin
- password: admin123
## 测试接口
1. 导入: POST /ccdi/purchaseTransaction/importData
2. 查询状态: GET /ccdi/purchaseTransaction/importStatus/{taskId}
3. 查询失败记录: GET /ccdi/purchaseTransaction/importFailures/{taskId}
## 测试数据文件
- purchase_test_data_2000.xlsx (2000条测试数据)

View File

@@ -1,226 +0,0 @@
const Excel = require('exceljs');
// 配置
const OUTPUT_FILE = 'purchase_test_data_2000_v2.xlsx';
const RECORD_COUNT = 2000;
// 数据池
const PURCHASE_CATEGORIES = ['货物类', '工程类', '服务类', '软件系统', '办公设备', '家具用具', '专用设备', '通讯设备'];
const PURCHASE_METHODS = ['公开招标', '邀请招标', '询价采购', '单一来源', '竞争性谈判'];
const DEPARTMENTS = ['人事部', '行政部', '财务部', '技术部', '市场部', '采购部', '研发部'];
const EMPLOYEES = [
{ id: 'EMP0001', name: '张伟' },
{ id: 'EMP0002', name: '王芳' },
{ id: 'EMP0003', name: '李娜' },
{ id: 'EMP0004', name: '刘洋' },
{ id: 'EMP0005', name: '陈静' },
{ id: 'EMP0006', name: '杨强' },
{ id: 'EMP0007', name: '赵敏' },
{ id: 'EMP0008', name: '孙杰' },
{ id: 'EMP0009', name: '周涛' },
{ id: 'EMP0010', name: '吴刚' },
{ id: 'EMP0011', name: '郑丽' },
{ id: 'EMP0012', name: '钱勇' },
{ id: 'EMP0013', name: '何静' },
{ id: 'EMP0014', name: '朱涛' },
{ id: 'EMP0015', name: '马超' }
];
// 生成随机整数
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// 生成随机浮点数
function randomFloat(min, max, decimals = 2) {
const num = Math.random() * (max - min) + min;
return parseFloat(num.toFixed(decimals));
}
// 从数组中随机选择
function randomChoice(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
// 生成随机日期
function randomDate(start, end) {
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime()));
}
// 生成采购事项ID
function generatePurchaseId(index) {
const timestamp = Date.now();
const num = String(index + 1).padStart(4, '0');
return `PUR${timestamp}${num}`;
}
// 生成测试数据
function generateTestData(count) {
const data = [];
const startDate = new Date('2023-01-01');
const endDate = new Date('2025-12-31');
for (let i = 0; i < count; i++) {
const purchaseQty = randomFloat(1, 5000, 2);
const unitPrice = randomFloat(100, 50000, 2);
const budgetAmount = parseFloat((purchaseQty * unitPrice).toFixed(2));
const discount = randomFloat(0.85, 0.98, 2);
const actualAmount = parseFloat((budgetAmount * discount).toFixed(2));
const employee = randomChoice(EMPLOYEES);
// 生成Date对象
const applyDateObj = randomDate(startDate, endDate);
// 生成后续日期(都比申请日期晚)
const planApproveDate = new Date(applyDateObj);
planApproveDate.setDate(planApproveDate.getDate() + randomInt(1, 7));
const announceDate = new Date(planApproveDate);
announceDate.setDate(announceDate.getDate() + randomInt(3, 15));
const bidOpenDate = new Date(announceDate);
bidOpenDate.setDate(bidOpenDate.getDate() + randomInt(5, 20));
const contractSignDate = new Date(bidOpenDate);
contractSignDate.setDate(contractSignDate.getDate() + randomInt(3, 10));
const expectedDeliveryDate = new Date(contractSignDate);
expectedDeliveryDate.setDate(expectedDeliveryDate.getDate() + randomInt(15, 60));
const actualDeliveryDate = new Date(expectedDeliveryDate);
actualDeliveryDate.setDate(actualDeliveryDate.getDate() + randomInt(-2, 5));
const acceptanceDate = new Date(actualDeliveryDate);
acceptanceDate.setDate(acceptanceDate.getDate() + randomInt(1, 7));
const settlementDate = new Date(acceptanceDate);
settlementDate.setDate(settlementDate.getDate() + randomInt(7, 30));
data.push({
purchaseId: generatePurchaseId(i),
purchaseCategory: randomChoice(PURCHASE_CATEGORIES),
projectName: `${randomChoice(PURCHASE_CATEGORIES)}采购项目-${String(i + 1).padStart(4, '0')}`,
subjectName: `${randomChoice(PURCHASE_CATEGORIES).replace('类', '')}配件-${String(i + 1).padStart(4, '0')}`,
subjectDesc: `${randomChoice(PURCHASE_CATEGORIES)}采购项目标的物详细描述-${String(i + 1).padStart(4, '0')}`,
purchaseQty: purchaseQty,
budgetAmount: budgetAmount,
bidAmount: actualAmount,
actualAmount: actualAmount,
contractAmount: actualAmount,
settlementAmount: actualAmount,
purchaseMethod: randomChoice(PURCHASE_METHODS),
supplierName: `供应商公司-${String(i + 1).padStart(4, '0')}有限公司`,
contactPerson: `联系人-${String(i + 1).padStart(4, '0')}`,
contactPhone: `13${randomInt(0, 9)}${String(randomInt(10000000, 99999999))}`,
supplierUscc: `91${randomInt(10000000, 99999999)}MA${String(randomInt(1000, 9999))}`,
supplierBankAccount: `6222${String(randomInt(100000000000000, 999999999999999))}`,
applyDate: applyDateObj, // Date对象
planApproveDate: planApproveDate,
announceDate: announceDate,
bidOpenDate: bidOpenDate,
contractSignDate: contractSignDate,
expectedDeliveryDate: expectedDeliveryDate,
actualDeliveryDate: actualDeliveryDate,
acceptanceDate: acceptanceDate,
settlementDate: settlementDate,
applicantId: employee.id,
applicantName: employee.name,
applyDepartment: randomChoice(DEPARTMENTS),
purchaseLeaderId: randomChoice(EMPLOYEES).id,
purchaseLeaderName: randomChoice(EMPLOYEES).name,
purchaseDepartment: '采购部'
});
}
return data;
}
// 创建Excel文件
async function createExcelFile() {
console.log('开始生成测试数据...');
console.log(`记录数: ${RECORD_COUNT}`);
// 生成测试数据
const testData = generateTestData(RECORD_COUNT);
console.log('测试数据生成完成');
// 创建工作簿
const workbook = new Excel.Workbook();
const worksheet = workbook.addWorksheet('采购交易数据');
// 定义列按照Excel实体类的index顺序
worksheet.columns = [
{ header: '采购事项ID', key: 'purchaseId', width: 25 },
{ header: '采购类别', key: 'purchaseCategory', width: 15 },
{ header: '项目名称', key: 'projectName', width: 30 },
{ header: '标的物名称', key: 'subjectName', width: 30 },
{ header: '标的物描述', key: 'subjectDesc', width: 35 },
{ header: '采购数量', key: 'purchaseQty', width: 15 },
{ header: '预算金额', key: 'budgetAmount', width: 18 },
{ header: '中标金额', key: 'bidAmount', width: 18 },
{ header: '实际采购金额', key: 'actualAmount', width: 18 },
{ header: '合同金额', key: 'contractAmount', width: 18 },
{ header: '结算金额', key: 'settlementAmount', width: 18 },
{ header: '采购方式', key: 'purchaseMethod', width: 15 },
{ header: '中标供应商名称', key: 'supplierName', width: 30 },
{ header: '供应商联系人', key: 'contactPerson', width: 15 },
{ header: '供应商联系电话', key: 'contactPhone', width: 18 },
{ header: '供应商统一信用代码', key: 'supplierUscc', width: 25 },
{ header: '供应商银行账户', key: 'supplierBankAccount', width: 25 },
{ header: '采购申请日期', key: 'applyDate', width: 18 },
{ header: '采购计划批准日期', key: 'planApproveDate', width: 18 },
{ header: '采购公告发布日期', key: 'announceDate', width: 18 },
{ header: '开标日期', key: 'bidOpenDate', width: 18 },
{ header: '合同签订日期', key: 'contractSignDate', width: 18 },
{ header: '预计交货日期', key: 'expectedDeliveryDate', width: 18 },
{ header: '实际交货日期', key: 'actualDeliveryDate', width: 18 },
{ header: '验收日期', key: 'acceptanceDate', width: 18 },
{ header: '结算日期', key: 'settlementDate', width: 18 },
{ header: '申请人工号', key: 'applicantId', width: 15 },
{ header: '申请人姓名', key: 'applicantName', width: 15 },
{ header: '申请部门', key: 'applyDepartment', width: 18 },
{ header: '采购负责人工号', key: 'purchaseLeaderId', width: 15 },
{ header: '采购负责人姓名', key: 'purchaseLeaderName', width: 15 },
{ header: '采购部门', key: 'purchaseDepartment', width: 18 }
];
// 添加数据
worksheet.addRows(testData);
// 设置表头样式
const headerRow = worksheet.getRow(1);
headerRow.font = { bold: true };
headerRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FFE6E6FA' }
};
// 保存文件
console.log('正在写入Excel文件...');
await workbook.xlsx.writeFile(OUTPUT_FILE);
console.log(`✓ 文件已保存: ${OUTPUT_FILE}`);
// 显示统计信息
console.log('\n========================================');
console.log('数据统计');
console.log('========================================');
console.log(`总记录数: ${testData.length}`);
console.log(`采购数量范围: ${Math.min(...testData.map(d => d.purchaseQty))} - ${Math.max(...testData.map(d => d.purchaseQty))}`);
console.log(`预算金额范围: ${Math.min(...testData.map(d => d.budgetAmount))} - ${Math.max(...testData.map(d => d.budgetAmount))}`);
console.log('\n前3条记录预览:');
testData.slice(0, 3).forEach((record, index) => {
console.log(`\n记录 ${index + 1}:`);
console.log(` 采购事项ID: ${record.purchaseId}`);
console.log(` 项目名称: ${record.projectName}`);
console.log(` 采购数量: ${record.purchaseQty}`);
console.log(` 预算金额: ${record.budgetAmount}`);
console.log(` 申请人: ${record.applicantName} (${record.applicantId})`);
console.log(` 申请部门: ${record.applyDepartment}`);
console.log(` 申请日期: ${record.applyDate}`);
});
}
// 运行
createExcelFile().catch(console.error);

View File

@@ -1,382 +0,0 @@
/**
* 采购交易Excel字段类型验证脚本
*
* 此脚本用于生成包含正确格式的数值和日期字段的测试数据
* 可以验证修复后的字段类型是否能正确导入
*/
const fs = require('fs');
const path = require('path');
/**
* 生成测试数据
*/
function generateTestData() {
const testData = [
{
purchaseId: 'PT202602090001',
purchaseCategory: '货物采购',
projectName: '办公设备采购项目',
subjectName: '笔记本电脑',
subjectDesc: '高性能办公用笔记本,配置要求:i7处理器,16G内存,512G固态硬盘',
purchaseQty: 50,
budgetAmount: 350000.00,
bidAmount: 320000.00,
actualAmount: 315000.00,
contractAmount: 320000.00,
settlementAmount: 315000.00,
purchaseMethod: '公开招标',
supplierName: '某某科技有限公司',
contactPerson: '张三',
contactPhone: '13800138000',
supplierUscc: '91110000123456789X',
supplierBankAccount: '1234567890123456789',
applyDate: '2026-01-15',
planApproveDate: '2026-01-20',
announceDate: '2026-01-25',
bidOpenDate: '2026-02-01',
contractSignDate: '2026-02-05',
expectedDeliveryDate: '2026-02-20',
actualDeliveryDate: '2026-02-18',
acceptanceDate: '2026-02-19',
settlementDate: '2026-02-25',
applicantId: '1234567',
applicantName: '李四',
applyDepartment: '行政部',
purchaseLeaderId: '7654321',
purchaseLeaderName: '王五',
purchaseDepartment: '采购部'
},
{
purchaseId: 'PT202602090002',
purchaseCategory: '服务采购',
projectName: 'IT运维服务项目',
subjectName: '系统运维服务',
subjectDesc: '为期一年的信息系统运维服务,包括日常维护、故障排除、系统升级等',
purchaseQty: 1,
budgetAmount: 120000.00,
bidAmount: 0,
actualAmount: 0,
contractAmount: 0,
settlementAmount: 0,
purchaseMethod: '竞争性谈判',
supplierName: '某某信息技术有限公司',
contactPerson: '赵六',
contactPhone: '13900139000',
supplierUscc: '91110000987654321Y',
supplierBankAccount: '9876543210987654321',
applyDate: '2026-02-01',
planApproveDate: '2026-02-05',
announceDate: '2026-02-08',
bidOpenDate: '2026-02-10',
contractSignDate: '2026-02-12',
expectedDeliveryDate: '2027-02-12',
actualDeliveryDate: '2027-02-10',
acceptanceDate: '2027-02-11',
settlementDate: '2027-02-15',
applicantId: '2345678',
applicantName: '孙七',
applyDepartment: '信息技术部',
purchaseLeaderId: '8765432',
purchaseLeaderName: '周八',
purchaseDepartment: '采购部'
},
// 测试数据:缺少必填字段(用于测试导入失败记录)
{
purchaseId: 'PT202602090003',
purchaseCategory: '',
projectName: '测试错误数据1',
subjectName: '测试标的',
subjectDesc: '测试描述',
purchaseQty: 0, // 错误:数量必须大于0
budgetAmount: -100, // 错误:金额必须大于0
bidAmount: 0,
actualAmount: 0,
contractAmount: 0,
settlementAmount: 0,
purchaseMethod: '',
supplierName: '测试供应商',
contactPerson: '测试联系人',
contactPhone: '13000000000',
supplierUscc: '91110000123456789X',
supplierBankAccount: '1234567890123456789',
applyDate: '2026-02-09',
planApproveDate: '',
announceDate: '',
bidOpenDate: '',
contractSignDate: '',
expectedDeliveryDate: '',
actualDeliveryDate: '',
acceptanceDate: '',
settlementDate: '',
applicantId: '123456', // 错误:工号必须7位
applicantName: '',
applyDepartment: '',
purchaseLeaderId: '',
purchaseLeaderName: '',
purchaseDepartment: ''
},
// 测试数据:工号格式错误
{
purchaseId: 'PT202602090004',
purchaseCategory: '工程采购',
projectName: '测试错误数据2',
subjectName: '测试标的2',
subjectDesc: '测试描述2',
purchaseQty: 10,
budgetAmount: 50000,
bidAmount: 0,
actualAmount: 0,
contractAmount: 0,
settlementAmount: 0,
purchaseMethod: '询价',
supplierName: '测试供应商2',
contactPerson: '测试联系人2',
contactPhone: '13100000000',
supplierUscc: '91110000987654321Y',
supplierBankAccount: '9876543210987654321',
applyDate: '2026-02-09',
planApproveDate: '',
announceDate: '',
bidOpenDate: '',
contractSignDate: '',
expectedDeliveryDate: '',
actualDeliveryDate: '',
acceptanceDate: '',
settlementDate: '',
applicantId: 'abcdefgh', // 错误:工号必须为数字
applicantName: '测试申请人',
applyDepartment: '测试部门',
purchaseLeaderId: 'abcdefg', // 错误:工号必须为数字
purchaseLeaderName: '测试负责人',
purchaseDepartment: '采购部'
}
];
return testData;
}
/**
* 生成CSV格式的测试文件
*/
function generateCSV() {
const data = generateTestData();
// CSV表头
const headers = [
'采购事项ID', '采购类别', '项目名称', '标的物名称', '标的物描述',
'采购数量', '预算金额', '中标金额', '实际采购金额', '合同金额', '结算金额',
'采购方式', '中标供应商名称', '供应商联系人', '供应商联系电话',
'供应商统一信用代码', '供应商银行账户',
'采购申请日期', '采购计划批准日期', '采购公告发布日期', '开标日期',
'合同签订日期', '预计交货日期', '实际交货日期', '验收日期', '结算日期',
'申请人工号', '申请人姓名', '申请部门',
'采购负责人工号', '采购负责人姓名', '采购部门'
];
// 生成CSV内容
let csvContent = headers.join(',') + '\n';
data.forEach(row => {
const values = [
row.purchaseId,
row.purchaseCategory,
row.projectName,
row.subjectName,
row.subjectDesc,
row.purchaseQty,
row.budgetAmount,
row.bidAmount,
row.actualAmount,
row.contractAmount,
row.settlementAmount,
row.purchaseMethod,
row.supplierName,
row.contactPerson,
row.contactPhone,
row.supplierUscc,
row.supplierBankAccount,
row.applyDate,
row.planApproveDate,
row.announceDate,
row.bidOpenDate,
row.contractSignDate,
row.expectedDeliveryDate,
row.actualDeliveryDate,
row.acceptanceDate,
row.settlementDate,
row.applicantId,
row.applicantName,
row.applyDepartment,
row.purchaseLeaderId,
row.purchaseLeaderName,
row.purchaseDepartment
];
csvContent += values.join(',') + '\n';
});
return csvContent;
}
/**
* 生成JSON格式的测试文件
*/
function generateJSON() {
const data = generateTestData();
return JSON.stringify(data, null, 2);
}
/**
* 生成数据说明文档
*/
function generateReadme() {
return `# 采购交易测试数据说明
## 测试数据文件
本项目包含3类测试数据:
### 1. 正确数据 (2条)
- **PT202602090001**: 货物采购 - 办公设备采购项目
- 包含完整的数值和日期字段
- 所有必填字段都已填写
- 用于验证正常导入功能
- **PT202602090002**: 服务采购 - IT运维服务项目
- 部分金额字段为0(可选字段)
- 用于验证可选字段为空的情况
### 2. 错误数据 (2条)
- **PT202602090003**: 测试错误数据1
- 采购类别为空 (必填)
- 采购数量为0 (必须大于0)
- 预算金额为负数 (必须大于0)
- 申请人工号不是7位 (必须7位数字)
- 申请人姓名为空 (必填)
- 申请部门为空 (必填)
- 用于验证必填字段和数值范围校验
- **PT202602090004**: 测试错误数据2
- 申请人工号为字母 (必须为数字)
- 采购负责人工号为字母 (必须为数字)
- 用于验证工号格式校验
## 字段类型说明
### 数值字段 (BigDecimal)
- 采购数量 (purchaseQty)
- 预算金额 (budgetAmount)
- 中标金额 (bidAmount)
- 实际采购金额 (actualAmount)
- 合同金额 (contractAmount)
- 结算金额 (settlementAmount)
**Excel格式要求**: 单元格格式设置为"数值"类型
### 日期字段 (Date)
- 采购申请日期 (applyDate)
- 采购计划批准日期 (planApproveDate)
- 采购公告发布日期 (announceDate)
- 开标日期 (bidOpenDate)
- 合同签订日期 (contractSignDate)
- 预计交货日期 (expectedDeliveryDate)
- 实际交货日期 (actualDeliveryDate)
- 验收日期 (acceptanceDate)
- 结算日期 (settlementDate)
**Excel格式要求**:
- 推荐格式: yyyy-MM-dd (例如: 2026-02-09)
- 或使用Excel日期格式
### 必填字段
- 采购事项ID (purchaseId)
- 采购类别 (purchaseCategory)
- 标的物名称 (subjectName)
- 采购数量 (purchaseQty) - 必须>0
- 预算金额 (budgetAmount) - 必须>0
- 采购方式 (purchaseMethod)
- 采购申请日期 (applyDate)
- 申请人工号 (applicantId) - 必须为7位数字
- 申请人姓名 (applicantName)
- 申请部门 (applyDepartment)
## 使用方法
### 方法1: 使用CSV文件
1. 将 \`purchase_transaction_test_data.csv\` 导入Excel
2. 保存为 .xlsx 格式
3. 通过系统界面上传导入
### 方法2: 使用JSON文件
1. 使用JSON文件作为API测试数据
2. 通过接口测试工具调用导入接口
## 预期结果
### 成功导入
- 前两条数据应该成功导入
- 导入成功通知: "成功2条,失败2条"
### 失败记录
- 后两条数据应该在失败记录中显示
- 失败原因包括:
- "采购类别不能为空"
- "采购数量必须大于0"
- "预算金额必须大于0"
- "申请人工号必须为7位数字"
- "申请人姓名不能为空"
- "申请部门不能为空"
- "采购方式不能为空"
## 验证字段类型修复
导入成功后,验证数据库中的数据类型:
- 数值字段应该存储为 DECIMAL 类型
- 日期字段应该存储为 DATETIME 类型
- 不应该出现类型转换错误
---
生成时间: ${new Date().toISOString()}
`;
}
/**
* 主函数
*/
function main() {
console.log('========================================');
console.log('采购交易测试数据生成工具');
console.log('========================================\n');
const outputDir = path.join(__dirname, 'generated');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// 生成CSV文件
const csvPath = path.join(outputDir, 'purchase_transaction_test_data.csv');
fs.writeFileSync(csvPath, generateCSV(), 'utf-8');
console.log('✅ CSV文件已生成:', csvPath);
// 生成JSON文件
const jsonPath = path.join(outputDir, 'purchase_transaction_test_data.json');
fs.writeFileSync(jsonPath, generateJSON(), 'utf-8');
console.log('✅ JSON文件已生成:', jsonPath);
// 生成说明文档
const readmePath = path.join(outputDir, 'README.md');
fs.writeFileSync(readmePath, generateReadme(), 'utf-8');
console.log('✅ 说明文档已生成:', readmePath);
console.log('\n========================================');
console.log('✅ 测试数据生成完成!');
console.log('========================================\n');
console.log('📝 使用说明:');
console.log('1. CSV文件可用于导入Excel后生成xlsx文件');
console.log('2. JSON文件可用于API测试');
console.log('3. 查看 README.md 了解详细说明\n');
}
// 运行
main();

View File

@@ -1,107 +0,0 @@
# 采购交易测试数据说明
## 测试数据文件
本项目包含3类测试数据:
### 1. 正确数据 (2条)
- **PT202602090001**: 货物采购 - 办公设备采购项目
- 包含完整的数值和日期字段
- 所有必填字段都已填写
- 用于验证正常导入功能
- **PT202602090002**: 服务采购 - IT运维服务项目
- 部分金额字段为0(可选字段)
- 用于验证可选字段为空的情况
### 2. 错误数据 (2条)
- **PT202602090003**: 测试错误数据1
- 采购类别为空 (必填)
- 采购数量为0 (必须大于0)
- 预算金额为负数 (必须大于0)
- 申请人工号不是7位 (必须7位数字)
- 申请人姓名为空 (必填)
- 申请部门为空 (必填)
- 用于验证必填字段和数值范围校验
- **PT202602090004**: 测试错误数据2
- 申请人工号为字母 (必须为数字)
- 采购负责人工号为字母 (必须为数字)
- 用于验证工号格式校验
## 字段类型说明
### 数值字段 (BigDecimal)
- 采购数量 (purchaseQty)
- 预算金额 (budgetAmount)
- 中标金额 (bidAmount)
- 实际采购金额 (actualAmount)
- 合同金额 (contractAmount)
- 结算金额 (settlementAmount)
**Excel格式要求**: 单元格格式设置为"数值"类型
### 日期字段 (Date)
- 采购申请日期 (applyDate)
- 采购计划批准日期 (planApproveDate)
- 采购公告发布日期 (announceDate)
- 开标日期 (bidOpenDate)
- 合同签订日期 (contractSignDate)
- 预计交货日期 (expectedDeliveryDate)
- 实际交货日期 (actualDeliveryDate)
- 验收日期 (acceptanceDate)
- 结算日期 (settlementDate)
**Excel格式要求**:
- 推荐格式: yyyy-MM-dd (例如: 2026-02-09)
- 或使用Excel日期格式
### 必填字段
- 采购事项ID (purchaseId)
- 采购类别 (purchaseCategory)
- 标的物名称 (subjectName)
- 采购数量 (purchaseQty) - 必须>0
- 预算金额 (budgetAmount) - 必须>0
- 采购方式 (purchaseMethod)
- 采购申请日期 (applyDate)
- 申请人工号 (applicantId) - 必须为7位数字
- 申请人姓名 (applicantName)
- 申请部门 (applyDepartment)
## 使用方法
### 方法1: 使用CSV文件
1.`purchase_transaction_test_data.csv` 导入Excel
2. 保存为 .xlsx 格式
3. 通过系统界面上传导入
### 方法2: 使用JSON文件
1. 使用JSON文件作为API测试数据
2. 通过接口测试工具调用导入接口
## 预期结果
### 成功导入
- 前两条数据应该成功导入
- 导入成功通知: "成功2条,失败2条"
### 失败记录
- 后两条数据应该在失败记录中显示
- 失败原因包括:
- "采购类别不能为空"
- "采购数量必须大于0"
- "预算金额必须大于0"
- "申请人工号必须为7位数字"
- "申请人姓名不能为空"
- "申请部门不能为空"
- "采购方式不能为空"
## 验证字段类型修复
导入成功后,验证数据库中的数据类型:
- 数值字段应该存储为 DECIMAL 类型
- 日期字段应该存储为 DATETIME 类型
- 不应该出现类型转换错误
---
生成时间: 2026-02-08T16:09:52.655Z

View File

@@ -1,5 +0,0 @@
采购事项ID,采购类别,项目名称,标的物名称,标的物描述,采购数量,预算金额,中标金额,实际采购金额,合同金额,结算金额,采购方式,中标供应商名称,供应商联系人,供应商联系电话,供应商统一信用代码,供应商银行账户,采购申请日期,采购计划批准日期,采购公告发布日期,开标日期,合同签订日期,预计交货日期,实际交货日期,验收日期,结算日期,申请人工号,申请人姓名,申请部门,采购负责人工号,采购负责人姓名,采购部门
PT202602090001,货物采购,办公设备采购项目,笔记本电脑,高性能办公用笔记本,配置要求:i7处理器,16G内存,512G固态硬盘,50,350000,320000,315000,320000,315000,公开招标,某某科技有限公司,张三,13800138000,91110000123456789X,1234567890123456789,2026-01-15,2026-01-20,2026-01-25,2026-02-01,2026-02-05,2026-02-20,2026-02-18,2026-02-19,2026-02-25,1234567,李四,行政部,7654321,王五,采购部
PT202602090002,服务采购,IT运维服务项目,系统运维服务,为期一年的信息系统运维服务,包括日常维护、故障排除、系统升级等,1,120000,0,0,0,0,竞争性谈判,某某信息技术有限公司,赵六,13900139000,91110000987654321Y,9876543210987654321,2026-02-01,2026-02-05,2026-02-08,2026-02-10,2026-02-12,2027-02-12,2027-02-10,2027-02-11,2027-02-15,2345678,孙七,信息技术部,8765432,周八,采购部
PT202602090003,,测试错误数据1,测试标的,测试描述,0,-100,0,0,0,0,,测试供应商,测试联系人,13000000000,91110000123456789X,1234567890123456789,2026-02-09,,,,,,,,,123456,,,,,
PT202602090004,工程采购,测试错误数据2,测试标的2,测试描述2,10,50000,0,0,0,0,询价,测试供应商2,测试联系人2,13100000000,91110000987654321Y,9876543210987654321,2026-02-09,,,,,,,,,abcdefgh,测试申请人,测试部门,abcdefg,测试负责人,采购部
1 采购事项ID,采购类别,项目名称,标的物名称,标的物描述,采购数量,预算金额,中标金额,实际采购金额,合同金额,结算金额,采购方式,中标供应商名称,供应商联系人,供应商联系电话,供应商统一信用代码,供应商银行账户,采购申请日期,采购计划批准日期,采购公告发布日期,开标日期,合同签订日期,预计交货日期,实际交货日期,验收日期,结算日期,申请人工号,申请人姓名,申请部门,采购负责人工号,采购负责人姓名,采购部门
2 PT202602090001,货物采购,办公设备采购项目,笔记本电脑,高性能办公用笔记本,配置要求:i7处理器,16G内存,512G固态硬盘,50,350000,320000,315000,320000,315000,公开招标,某某科技有限公司,张三,13800138000,91110000123456789X,1234567890123456789,2026-01-15,2026-01-20,2026-01-25,2026-02-01,2026-02-05,2026-02-20,2026-02-18,2026-02-19,2026-02-25,1234567,李四,行政部,7654321,王五,采购部
3 PT202602090002,服务采购,IT运维服务项目,系统运维服务,为期一年的信息系统运维服务,包括日常维护、故障排除、系统升级等,1,120000,0,0,0,0,竞争性谈判,某某信息技术有限公司,赵六,13900139000,91110000987654321Y,9876543210987654321,2026-02-01,2026-02-05,2026-02-08,2026-02-10,2026-02-12,2027-02-12,2027-02-10,2027-02-11,2027-02-15,2345678,孙七,信息技术部,8765432,周八,采购部
4 PT202602090003,,测试错误数据1,测试标的,测试描述,0,-100,0,0,0,0,,测试供应商,测试联系人,13000000000,91110000123456789X,1234567890123456789,2026-02-09,,,,,,,,,123456,,,,,
5 PT202602090004,工程采购,测试错误数据2,测试标的2,测试描述2,10,50000,0,0,0,0,询价,测试供应商2,测试联系人2,13100000000,91110000987654321Y,9876543210987654321,2026-02-09,,,,,,,,,abcdefgh,测试申请人,测试部门,abcdefg,测试负责人,采购部

View File

@@ -1,138 +0,0 @@
[
{
"purchaseId": "PT202602090001",
"purchaseCategory": "货物采购",
"projectName": "办公设备采购项目",
"subjectName": "笔记本电脑",
"subjectDesc": "高性能办公用笔记本,配置要求:i7处理器,16G内存,512G固态硬盘",
"purchaseQty": 50,
"budgetAmount": 350000,
"bidAmount": 320000,
"actualAmount": 315000,
"contractAmount": 320000,
"settlementAmount": 315000,
"purchaseMethod": "公开招标",
"supplierName": "某某科技有限公司",
"contactPerson": "张三",
"contactPhone": "13800138000",
"supplierUscc": "91110000123456789X",
"supplierBankAccount": "1234567890123456789",
"applyDate": "2026-01-15",
"planApproveDate": "2026-01-20",
"announceDate": "2026-01-25",
"bidOpenDate": "2026-02-01",
"contractSignDate": "2026-02-05",
"expectedDeliveryDate": "2026-02-20",
"actualDeliveryDate": "2026-02-18",
"acceptanceDate": "2026-02-19",
"settlementDate": "2026-02-25",
"applicantId": "1234567",
"applicantName": "李四",
"applyDepartment": "行政部",
"purchaseLeaderId": "7654321",
"purchaseLeaderName": "王五",
"purchaseDepartment": "采购部"
},
{
"purchaseId": "PT202602090002",
"purchaseCategory": "服务采购",
"projectName": "IT运维服务项目",
"subjectName": "系统运维服务",
"subjectDesc": "为期一年的信息系统运维服务,包括日常维护、故障排除、系统升级等",
"purchaseQty": 1,
"budgetAmount": 120000,
"bidAmount": 0,
"actualAmount": 0,
"contractAmount": 0,
"settlementAmount": 0,
"purchaseMethod": "竞争性谈判",
"supplierName": "某某信息技术有限公司",
"contactPerson": "赵六",
"contactPhone": "13900139000",
"supplierUscc": "91110000987654321Y",
"supplierBankAccount": "9876543210987654321",
"applyDate": "2026-02-01",
"planApproveDate": "2026-02-05",
"announceDate": "2026-02-08",
"bidOpenDate": "2026-02-10",
"contractSignDate": "2026-02-12",
"expectedDeliveryDate": "2027-02-12",
"actualDeliveryDate": "2027-02-10",
"acceptanceDate": "2027-02-11",
"settlementDate": "2027-02-15",
"applicantId": "2345678",
"applicantName": "孙七",
"applyDepartment": "信息技术部",
"purchaseLeaderId": "8765432",
"purchaseLeaderName": "周八",
"purchaseDepartment": "采购部"
},
{
"purchaseId": "PT202602090003",
"purchaseCategory": "",
"projectName": "测试错误数据1",
"subjectName": "测试标的",
"subjectDesc": "测试描述",
"purchaseQty": 0,
"budgetAmount": -100,
"bidAmount": 0,
"actualAmount": 0,
"contractAmount": 0,
"settlementAmount": 0,
"purchaseMethod": "",
"supplierName": "测试供应商",
"contactPerson": "测试联系人",
"contactPhone": "13000000000",
"supplierUscc": "91110000123456789X",
"supplierBankAccount": "1234567890123456789",
"applyDate": "2026-02-09",
"planApproveDate": "",
"announceDate": "",
"bidOpenDate": "",
"contractSignDate": "",
"expectedDeliveryDate": "",
"actualDeliveryDate": "",
"acceptanceDate": "",
"settlementDate": "",
"applicantId": "123456",
"applicantName": "",
"applyDepartment": "",
"purchaseLeaderId": "",
"purchaseLeaderName": "",
"purchaseDepartment": ""
},
{
"purchaseId": "PT202602090004",
"purchaseCategory": "工程采购",
"projectName": "测试错误数据2",
"subjectName": "测试标的2",
"subjectDesc": "测试描述2",
"purchaseQty": 10,
"budgetAmount": 50000,
"bidAmount": 0,
"actualAmount": 0,
"contractAmount": 0,
"settlementAmount": 0,
"purchaseMethod": "询价",
"supplierName": "测试供应商2",
"contactPerson": "测试联系人2",
"contactPhone": "13100000000",
"supplierUscc": "91110000987654321Y",
"supplierBankAccount": "9876543210987654321",
"applyDate": "2026-02-09",
"planApproveDate": "",
"announceDate": "",
"bidOpenDate": "",
"contractSignDate": "",
"expectedDeliveryDate": "",
"actualDeliveryDate": "",
"acceptanceDate": "",
"settlementDate": "",
"applicantId": "abcdefgh",
"applicantName": "测试申请人",
"applyDepartment": "测试部门",
"purchaseLeaderId": "abcdefg",
"purchaseLeaderName": "测试负责人",
"purchaseDepartment": "采购部"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"name": "purchase_transaction",
"version": "1.0.0",
"description": "- **操作系统**: Windows/Linux\r - **Java版本**: JDK 17\r - **数据库**: MySQL 8.2.0\r - **后端框架**: Spring Boot 3.5.8\r - **前端框架**: Vue 2.6.12 + Element UI 2.15.14",
"main": "test-import-debug.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"axios": "^1.13.4",
"exceljs": "^4.4.0",
"form-data": "^4.0.5"
}
}

View File

@@ -1,278 +0,0 @@
/**
* 采购交易申请日期查询功能测试脚本
*
* 测试目的: 验证申请日期查询条件修复后能正常工作
* 问题描述: 之前申请日期查询条件未生效,原因是 Mapper XML 中存在两套参数名导致混乱
* 修复方案: 统一使用 applyDateStart 和 applyDateEnd 作为日期查询参数
*/
const axios = require('axios');
const fs = require('fs');
const BASE_URL = 'http://localhost:8080';
// 测试配置
const TEST_CONFIG = {
// 使用固定的测试账号
username: 'admin',
password: 'admin123',
};
/**
* 登录获取 token
*/
async function login() {
try {
console.log('📝 正在登录...');
const response = await axios.post(`${BASE_URL}/login/test`, {
username: TEST_CONFIG.username,
password: TEST_CONFIG.password
});
if (response.data.code === 200) {
const token = response.data.data.token;
console.log('✅ 登录成功!');
console.log(` Token: ${token.substring(0, 20)}...`);
return token;
} else {
throw new Error(`登录失败: ${response.data.msg}`);
}
} catch (error) {
console.error('❌ 登录失败:', error.message);
throw error;
}
}
/**
* 测试申请日期查询功能
*/
async function testDateQuery(token) {
const testResults = [];
const config = {
headers: {
'Authorization': `Bearer ${token}`
}
};
try {
console.log('\n📊 开始测试申请日期查询功能...\n');
// 测试1: 不带日期查询条件(获取所有数据)
console.log('测试1: 不带日期查询条件');
const response1 = await axios.get(`${BASE_URL}/ccdi/purchaseTransaction/list`, {
...config,
params: {
pageNum: 1,
pageSize: 10
}
});
const totalRecords = response1.data.total;
console.log(` 总记录数: ${totalRecords}`);
testResults.push({
test: '无日期条件查询',
status: response1.data.code === 200 ? '✅ 通过' : '❌ 失败',
total: totalRecords
});
if (totalRecords === 0) {
console.log('⚠️ 数据库中没有数据,无法继续测试日期查询功能');
return testResults;
}
// 测试2: 查询2024年的申请日期
console.log('\n测试2: 查询2024-01-01到2024-12-31的申请日期');
const response2 = await axios.get(`${BASE_URL}/ccdi/purchaseTransaction/list`, {
...config,
params: {
pageNum: 1,
pageSize: 10,
applyDateStart: '2024-01-01',
applyDateEnd: '2024-12-31'
}
});
const records2024 = response2.data.total;
console.log(` 2024年记录数: ${records2024}`);
testResults.push({
test: '2024年日期查询',
status: response2.data.code === 200 ? '✅ 通过' : '❌ 失败',
total: records2024,
params: 'applyDateStart=2024-01-01, applyDateEnd=2024-12-31'
});
// 测试3: 查询2025年的申请日期
console.log('\n测试3: 查询2025-01-01到2025-12-31的申请日期');
const response3 = await axios.get(`${BASE_URL}/ccdi/purchaseTransaction/list`, {
...config,
params: {
pageNum: 1,
pageSize: 10,
applyDateStart: '2025-01-01',
applyDateEnd: '2025-12-31'
}
});
const records2025 = response3.data.total;
console.log(` 2025年记录数: ${records2025}`);
testResults.push({
test: '2025年日期查询',
status: response3.data.code === 200 ? '✅ 通过' : '❌ 失败',
total: records2025,
params: 'applyDateStart=2025-01-01, applyDateEnd=2025-12-31'
});
// 测试4: 查询2026年2月的申请日期
console.log('\n测试4: 查询2026-02-01到2026-02-28的申请日期');
const response4 = await axios.get(`${BASE_URL}/ccdi/purchaseTransaction/list`, {
...config,
params: {
pageNum: 1,
pageSize: 10,
applyDateStart: '2026-02-01',
applyDateEnd: '2026-02-28'
}
});
const recordsFeb2026 = response4.data.total;
console.log(` 2026年2月记录数: ${recordsFeb2026}`);
testResults.push({
test: '2026年2月日期查询',
status: response4.data.code === 200 ? '✅ 通过' : '❌ 失败',
total: recordsFeb2026,
params: 'applyDateStart=2026-02-01, applyDateEnd=2026-02-28'
});
// 测试5: 只传入开始日期
console.log('\n测试5: 只传入开始日期(2024-01-01)');
const response5 = await axios.get(`${BASE_URL}/ccdi/purchaseTransaction/list`, {
...config,
params: {
pageNum: 1,
pageSize: 10,
applyDateStart: '2024-01-01'
}
});
const recordsFrom2024 = response5.data.total;
console.log(` 2024-01-01之后记录数: ${recordsFrom2024}`);
testResults.push({
test: '只有开始日期查询',
status: response5.data.code === 200 ? '✅ 通过' : '❌ 失败',
total: recordsFrom2024,
params: 'applyDateStart=2024-01-01'
});
// 测试6: 只传入结束日期
console.log('\n测试6: 只传入结束日期(2024-12-31)');
const response6 = await axios.get(`${BASE_URL}/ccdi/purchaseTransaction/list`, {
...config,
params: {
pageNum: 1,
pageSize: 10,
applyDateEnd: '2024-12-31'
}
});
const recordsUntil2024 = response6.data.total;
console.log(` 2024-12-31之前记录数: ${recordsUntil2024}`);
testResults.push({
test: '只有结束日期查询',
status: response6.data.code === 200 ? '✅ 通过' : '❌ 失败',
total: recordsUntil2024,
params: 'applyDateEnd=2024-12-31'
});
// 验证: 日期查询是否生效
console.log('\n🔍 验证结果:');
console.log(` 总记录数: ${totalRecords}`);
console.log(` 2024年: ${records2024}`);
console.log(` 2025年: ${records2025}`);
console.log(` 2026年2月: ${recordsFeb2026}`);
const dateQueryWorks = (records2024 !== totalRecords) ||
(records2025 !== totalRecords) ||
(recordsFeb2026 !== totalRecords);
if (dateQueryWorks) {
console.log(' ✅ 日期查询功能正常!不同日期范围返回不同的记录数');
} else {
console.log(' ⚠️ 日期查询可能未生效,所有日期范围返回相同记录数');
console.log(' 提示: 如果数据库中所有记录的申请日期都在同一个范围内,这是正常现象');
}
} catch (error) {
console.error('❌ 测试失败:', error.message);
if (error.response) {
console.error(' 响应数据:', error.response.data);
}
testResults.push({
test: '异常',
status: '❌ 失败',
error: error.message
});
}
return testResults;
}
/**
* 生成测试报告
*/
function generateReport(testResults, testResultsPath) {
const report = {
testDate: new Date().toISOString(),
description: '采购交易申请日期查询功能测试报告',
issue: '申请日期查询条件未生效',
fix: '统一使用 applyDateStart 和 applyDateEnd 作为日期查询参数',
results: testResults
};
fs.writeFileSync(testResultsPath, JSON.stringify(report, null, 2));
console.log(`\n📄 测试报告已保存: ${testResultsPath}`);
}
/**
* 主函数
*/
async function main() {
console.log('=================================');
console.log('采购交易申请日期查询功能测试');
console.log('=================================\n');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const testResultsPath = `doc/test-results/purchase-transaction-date-query-${timestamp}.json`;
// 确保测试结果目录存在
const testResultsDir = 'doc/test-results';
if (!fs.existsSync(testResultsDir)) {
fs.mkdirSync(testResultsDir, { recursive: true });
}
try {
// 登录获取token
const token = await login();
// 测试日期查询功能
const testResults = await testDateQuery(token);
// 生成测试报告
generateReport(testResults, testResultsPath);
console.log('\n=================================');
console.log('✅ 测试完成!');
console.log('=================================\n');
// 显示汇总
const passedTests = testResults.filter(r => r.status.includes('通过')).length;
const totalTests = testResults.length;
console.log(`测试结果: ${passedTests}/${totalTests} 通过`);
} catch (error) {
console.error('\n❌ 测试失败:', error.message);
process.exit(1);
}
}
// 运行测试
main();

View File

@@ -1,269 +0,0 @@
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const FormData = require('form-data');
// 配置
const BASE_URL = 'http://localhost:8080';
const LOGIN_URL = `${BASE_URL}/login/test`;
const IMPORT_URL = `${BASE_URL}/ccdi/purchaseTransaction/importData`;
const STATUS_URL_TEMPLATE = `${BASE_URL}/ccdi/purchaseTransaction/importStatus`;
const FAILURES_URL_TEMPLATE = `${BASE_URL}/ccdi/purchaseTransaction/importFailures`;
// 测试账号
const USERNAME = 'admin';
const PASSWORD = 'admin123';
// 测试文件
const TEST_FILE = path.join(__dirname, 'purchase_test_data_2000_v2.xlsx');
/**
* 登录获取token
*/
async function login() {
try {
console.log('正在登录...');
const response = await axios.post(LOGIN_URL, {
username: USERNAME,
password: PASSWORD
});
if (response.data.code === 200) {
console.log('✓ 登录成功');
return response.data.token;
} else {
throw new Error(`登录失败: ${response.data.msg}`);
}
} catch (error) {
console.error('✗ 登录异常:', error.message);
throw error;
}
}
/**
* 导入数据
*/
async function importData(token, updateSupport = false) {
try {
console.log('\n========================================');
console.log('开始导入测试');
console.log('========================================');
console.log(`文件: ${TEST_FILE}`);
console.log(`更新支持: ${updateSupport}`);
// 检查文件是否存在
if (!fs.existsSync(TEST_FILE)) {
throw new Error(`测试文件不存在: ${TEST_FILE}`);
}
// 创建form-data
const formData = new FormData();
formData.append('file', fs.createReadStream(TEST_FILE));
console.log('\n正在上传文件...');
const response = await axios.post(
`${IMPORT_URL}?updateSupport=${updateSupport}`,
formData,
{
headers: {
...formData.getHeaders(),
'Authorization': `Bearer ${token}`
}
}
);
console.log('\n响应状态:', response.status);
console.log('响应数据:', JSON.stringify(response.data, null, 2));
if (response.data.code === 200) {
console.log('\n✓ 导入任务已提交');
return response.data.data.taskId;
} else {
throw new Error(`导入失败: ${response.data.msg}`);
}
} catch (error) {
console.error('\n✗ 导入异常:', error.message);
if (error.response) {
console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
}
throw error;
}
}
/**
* 查询导入状态
*/
async function getImportStatus(token, taskId) {
try {
console.log(`\n查询导入状态 (taskId: ${taskId})...`);
const response = await axios.get(
`${STATUS_URL_TEMPLATE}/${taskId}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
console.log('导入状态:', JSON.stringify(response.data, null, 2));
if (response.data.code === 200) {
return response.data.data;
} else {
throw new Error(`查询状态失败: ${response.data.msg}`);
}
} catch (error) {
console.error('✗ 查询状态异常:', error.message);
if (error.response) {
console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
}
throw error;
}
}
/**
* 查询失败记录
*/
async function getImportFailures(token, taskId) {
try {
console.log(`\n查询失败记录 (taskId: ${taskId})...`);
const response = await axios.get(
`${FAILURES_URL_TEMPLATE}/${taskId}`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
console.log('失败记录数量:', response.data.total || response.data.data?.length);
console.log('失败记录:', JSON.stringify(response.data, null, 2));
if (response.data.code === 200) {
return response.data.data || response.data.rows;
} else {
throw new Error(`查询失败记录失败: ${response.data.msg}`);
}
} catch (error) {
console.error('✗ 查询失败记录异常:', error.message);
if (error.response) {
console.error('响应数据:', JSON.stringify(error.response.data, null, 2));
}
throw error;
}
}
/**
* 轮询导入状态
*/
async function pollImportStatus(token, taskId, maxPolls = 30) {
let pollCount = 0;
const interval = 2000; // 2秒
console.log(`\n开始轮询导入状态 (最多${maxPolls}次, 间隔${interval}ms)...`);
return new Promise((resolve, reject) => {
const timer = setInterval(async () => {
pollCount++;
try {
const status = await getImportStatus(token, taskId);
console.log(`\n[轮询 ${pollCount}/${maxPolls}] 状态: ${status.status}`);
if (status.status !== 'PROCESSING' && status.status !== 'PENDING' && status.status !== 'RUNNING') {
clearInterval(timer);
console.log('\n✓ 导入完成!');
resolve(status);
} else if (pollCount >= maxPolls) {
clearInterval(timer);
reject(new Error('轮询超时'));
}
} catch (error) {
clearInterval(timer);
reject(error);
}
}, interval);
});
}
/**
* 主函数
*/
async function main() {
let token;
let taskId;
try {
// 登录
token = await login();
// 导入数据
taskId = await importData(token, false);
// 轮询状态
const finalStatus = await pollImportStatus(token, taskId);
console.log('\n========================================');
console.log('最终导入结果');
console.log('========================================');
console.log('状态:', finalStatus.status);
console.log('总数:', finalStatus.totalCount);
console.log('成功:', finalStatus.successCount);
console.log('失败:', finalStatus.failureCount);
console.log('消息:', finalStatus.message);
// 如果有失败记录,查询失败记录
if (finalStatus.failureCount > 0) {
console.log('\n有失败记录,正在查询...');
const failures = await getImportFailures(token, taskId);
console.log('\n========================================');
console.log('失败记录详情');
console.log('========================================');
console.log(`失败记录数: ${failures.length}`);
// 显示前10条失败记录
const displayCount = Math.min(10, failures.length);
console.log(`\n${displayCount}条失败记录:`);
for (let i = 0; i < displayCount; i++) {
const failure = failures[i];
console.log(`\n[${i + 1}] 采购事项ID: ${failure.purchaseId}`);
console.log(` 项目名称: ${failure.projectName || '(空)'}`);
console.log(` 标的物名称: ${failure.subjectName || '(空)'}`);
console.log(` 失败原因: ${failure.errorMessage}`);
}
if (failures.length > displayCount) {
console.log(`\n... 还有 ${failures.length - displayCount} 条失败记录`);
}
// 统计失败原因
const errorReasons = {};
failures.forEach(f => {
const reason = f.errorMessage;
errorReasons[reason] = (errorReasons[reason] || 0) + 1;
});
console.log('\n失败原因统计:');
Object.entries(errorReasons)
.sort((a, b) => b[1] - a[1])
.forEach(([reason, count]) => {
console.log(` ${reason}: ${count}`);
});
} else {
console.log('\n✓ 全部导入成功,无失败记录');
}
} catch (error) {
console.error('\n✗ 测试失败:', error.message);
process.exit(1);
}
}
// 运行测试
main().catch(console.error);

View File

@@ -1,246 +0,0 @@
/**
* 采购交易导入失败记录接口测试脚本
*
* 测试目标: 验证修复后的 /importFailures/{taskId} 接口返回正确的分页数据
*
* 使用方法:
* 1. 确保后端服务已启动
* 2. 先执行一次导入操作(包含失败数据)
* 3. 获取返回的taskId
* 4. 运行此脚本: node test-purchase-import-failures-api.js <taskId>
*/
const http = require('http');
const BASE_URL = 'localhost';
const PORT = 8080;
const USERNAME = 'admin';
const PASSWORD = 'admin123';
let authToken = null;
/**
* 登录获取token
*/
async function login() {
return new Promise((resolve, reject) => {
const postData = JSON.stringify({
username: USERNAME,
password: PASSWORD
});
const options = {
hostname: BASE_URL,
port: PORT,
path: '/login/test',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
if (response.code === 200 && response.token) {
authToken = response.token;
console.log('✅ 登录成功,获取到token');
resolve();
} else {
reject(new Error('登录失败:' + JSON.stringify(response)));
}
} catch (error) {
reject(new Error('解析响应失败:' + error.message));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.write(postData);
req.end();
});
}
/**
* 测试导入失败记录接口
*/
async function testImportFailuresAPI(taskId) {
return new Promise((resolve, reject) => {
const path = `/ccdi/purchaseTransaction/importFailures/${taskId}?pageNum=1&pageSize=10`;
const options = {
hostname: BASE_URL,
port: PORT,
path: path,
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`
}
};
console.log(`\n📡 测试接口: GET ${path}`);
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
console.log('\n📥 响应状态码:', res.statusCode);
console.log('📦 响应数据:', JSON.stringify(response, null, 2));
// 验证响应结构
console.log('\n🔍 验证响应结构:');
if (response.code === 200) {
console.log(' ✅ code 字段正确: 200');
} else {
console.log(' ❌ code 字段错误:', response.code);
}
if (response.rows !== undefined) {
console.log(' ✅ rows 字段存在, 类型:', Array.isArray(response.rows) ? 'Array' : typeof response.rows);
console.log(' ✅ rows 长度:', response.rows ? response.rows.length : 0);
if (response.rows && response.rows.length > 0) {
console.log('\n📄 第一条失败记录示例:');
console.log(JSON.stringify(response.rows[0], null, 2));
}
} else {
console.log(' ❌ rows 字段缺失');
}
if (response.total !== undefined) {
console.log(' ✅ total 字段存在:', response.total);
} else {
console.log(' ❌ total 字段缺失');
}
// 测试分页参数
console.log('\n📄 测试不同分页参数:');
testPagination(taskId, 1, 5).then(() => resolve(response));
} catch (error) {
reject(new Error('解析响应失败:' + error.message));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.end();
});
}
/**
* 测试分页功能
*/
async function testPagination(taskId, pageNum, pageSize) {
return new Promise((resolve, reject) => {
const path = `/ccdi/purchaseTransaction/importFailures/${taskId}?pageNum=${pageNum}&pageSize=${pageSize}`;
const options = {
hostname: BASE_URL,
port: PORT,
path: path,
method: 'GET',
headers: {
'Authorization': `Bearer ${authToken}`
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
console.log(`\n 📌 分页测试 (pageNum=${pageNum}, pageSize=${pageSize}):`);
console.log(` 返回记录数: ${response.rows ? response.rows.length : 0}`);
console.log(` 总记录数: ${response.total || 0}`);
if (response.rows && response.rows.length <= pageSize) {
console.log(` ✅ 分页大小正确`);
} else {
console.log(` ❌ 分页大小错误,期望最多${pageSize}`);
}
resolve();
} catch (error) {
reject(new Error('解析响应失败:' + error.message));
}
});
});
req.on('error', (error) => {
reject(error);
});
req.end();
});
}
/**
* 主测试函数
*/
async function main() {
console.log('========================================');
console.log('采购交易导入失败记录接口测试');
console.log('========================================');
// 获取命令行参数
const taskId = process.argv[2];
if (!taskId) {
console.error('\n❌ 错误: 请提供任务ID');
console.error('\n使用方法: node test-purchase-import-failures-api.js <taskId>');
console.error('示例: node test-purchase-import-failures-api.js 1234567890\n');
process.exit(1);
}
console.log(`\n🎯 测试任务ID: ${taskId}`);
try {
// 登录
await login();
// 测试接口
const result = await testImportFailuresAPI(taskId);
console.log('\n========================================');
console.log('✅ 测试完成!');
console.log('========================================\n');
} catch (error) {
console.error('\n❌ 测试失败:', error.message);
console.error('\n请检查:');
console.error('1. 后端服务是否已启动');
console.error('2. 任务ID是否正确');
console.error('3. 是否已执行过导入操作(包含失败数据)');
console.error('');
process.exit(1);
}
}
// 运行测试
main();

View File

@@ -1,38 +0,0 @@
const fs = require('fs');
const path = require('path');
// 测试配置
const CONFIG = {
baseUrl: 'http://localhost:8080',
username: 'admin',
password: 'admin123',
testFile: path.join(__dirname, 'purchase_test_data_2000.xlsx')
};
// 日志函数
function log(message, level = 'INFO') {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] ${message}`);
}
// 主测试流程
async function runTests() {
log('=== 采购交易导入功能测试 ===');
log('开始时间:', new Date().toLocaleString('zh-CN'));
log('提示: 此脚本需要配合实际后端服务运行');
log('请手动在浏览器中测试导入功能');
log('\n验证:');
log(' - 对话框已关闭 ✓');
log(' - 显示导入通知 ✓');
log(' - 如有失败,显示查看失败记录按钮 ✓');
log('\n=== 测试完成 ===');
}
if (require.main === module) {
runTests();
}
module.exports = { runTests };

View File

@@ -1,379 +0,0 @@
# 中介库导入失败记录清除功能测试报告
**测试日期:** 2026-02-08
**测试人员:** 待指定
**测试环境:** 开发环境 (localhost)
**功能版本:** v1.0
---
## 一、测试概述
### 1.1 测试目标
验证在用户重新提交导入时,系统能够自动清除上一次导入失败记录的 localStorage 数据和页面按钮显示状态。
### 1.2 测试范围
- ✅ Task 1: ImportDialog.vue 触发清除历史记录事件
- ✅ Task 2: index.vue 添加事件监听
- ✅ Task 3: index.vue 添加事件处理方法
### 1.3 涉及文件
- `ruoyi-ui/src/views/ccdiIntermediary/components/ImportDialog.vue`
- `ruoyi-ui/src/views/ccdiIntermediary/index.vue`
---
## 二、测试环境准备
### 2.1 启动前端开发服务器
```bash
cd ruoyi-ui
npm run dev
```
**预期结果:** 服务器正常运行在 `http://localhost`
### 2.2 登录系统
- 访问: `http://localhost`
- 用户名: `admin`
- 密码: `admin123`
### 2.3 导航到中介库管理页面
点击菜单: **中介库管理****中介黑名单**
---
## 三、详细测试步骤
### 测试场景 1: 个人中介导入失败记录清除
**目的:** 验证重新导入个人中介时能够清除上一次的失败记录
**步骤:**
1. 准备一份包含错误数据的个人中介导入文件
- 文件格式: `.xlsx``.xls`
- 确保至少有 1-2 条数据存在错误(如身份证号格式错误、必填字段缺失等)
2. 点击"导入"按钮
3. 确认导入类型为"个人中介"(默认)
4. 上传准备好的文件
5. 点击"开始导入"按钮
6. 等待导入完成(会有通知提示导入完成)
7. **验证点 1:** 确认页面上显示"查看个人导入失败记录"按钮
- 预期: 按钮显示在工具栏中
8. 点击"查看个人导入失败记录"按钮
9. **验证点 2:** 确认能看到失败记录列表
- 预期: 弹出对话框,显示失败的记录和失败原因
10. 关闭失败记录对话框
11. 再次点击"导入"按钮
12. 选择任意文件(可以是正确的文件,也可以是包含错误的文件)
13. **关键步骤:** 点击"开始导入"按钮
14. **验证点 3:** "查看个人导入失败记录"按钮应该立即消失
- 预期: 按钮在点击"开始导入"后立即从页面上消失
- 验证时机: 在新导入完成前就能看到效果
15. 等待新导入完成
16. **验证点 4:** 如果新导入有失败,确认显示的是新的失败记录
- 预期: 失败记录列表中显示的是新导入的失败数据
**测试结果:** ⬜ 通过 ⬜ 失败
**备注:**
---
### 测试场景 2: 实体中介导入失败记录清除
**目的:** 验证重新导入实体中介时能够清除上一次的失败记录
**步骤:**
1. 准备一份包含错误数据的实体中介导入文件
- 文件格式: `.xlsx``.xls`
- 确保至少有 1-2 条数据存在错误(如统一社会信用代码格式错误、必填字段缺失等)
2. 点击"导入"按钮
3. 切换到"机构中介"标签
4. 上传准备好的文件
5. 点击"开始导入"按钮
6. 等待导入完成
7. **验证点 1:** 确认页面上显示"查看实体导入失败记录"按钮
8. 点击"查看实体导入失败记录"按钮
9. **验证点 2:** 确认能看到失败记录列表
10. 关闭失败记录对话框
11. 再次点击"导入"按钮,选择任意文件
12. **关键步骤:** 点击"开始导入"按钮
13. **验证点 3:** "查看实体导入失败记录"按钮应该立即消失
**测试结果:** ⬜ 通过 ⬜ 失败
**备注:**
---
### 测试场景 3: 两种类型互不影响
**目的:** 验证个人和实体中介的导入记录清除操作互不干扰
**步骤:**
1. 导入个人中介数据(确保有失败记录)
- 点击"导入" → 选择"个人中介" → 上传文件 → 点击"开始导入"
- 等待导入完成
2. **验证点 1:** 确认显示"查看个人导入失败记录"按钮
3. 导入实体中介数据(确保有失败记录)
- 点击"导入" → 选择"机构中介" → 上传文件 → 点击"开始导入"
- 等待导入完成
4. **验证点 2:** 确认两个按钮都显示
- 预期: "查看个人导入失败记录"和"查看实体导入失败记录"按钮同时显示
5. 重新导入个人中介
- 点击"导入" → 选择"个人中介" → 选择文件 → 点击"开始导入"
6. **验证点 3:** 只清除个人中介的失败记录按钮
- 预期: "查看个人导入失败记录"按钮消失
- 预期: "查看实体导入失败记录"按钮仍然显示
7. 重新导入实体中介
- 点击"导入" → 选择"机构中介" → 选择文件 → 点击"开始导入"
8. **验证点 4:** 只清除实体中介的失败记录按钮
- 预期: "查看实体导入失败记录"按钮消失
- 预期: "查看个人导入失败记录"按钮不会重新出现(因为已在步骤5中清除)
**测试结果:** ⬜ 通过 ⬜ 失败
**备注:**
---
### 测试场景 4: 边界情况测试
**目的:** 验证特殊情况下功能的稳定性
**步骤:**
1. **子场景 4.1: 导入全部成功,无失败记录**
- 准备一份完全正确的导入文件
- 执行导入操作
- **验证点:** 确认不显示失败记录按钮
- 再次导入其他数据
- **验证点:** 确认不影响任何状态,页面正常工作
2. **子场景 4.2: localStorage 数据过期**
- 导入数据(有失败),确认按钮显示
- 打开浏览器开发者工具(F12)
- 进入 Application → Local Storage
- 手动修改 `intermediary_person_import_last_task``saveTime` 为过期时间(如7天前)
- 刷新页面
- **验证点:** 确认按钮不显示(数据已过期)
- 重新导入数据
- **验证点:** 导入正常进行,不受localStorage过期影响
3. **子场景 4.3: 浏览器控制台无错误**
- 打开浏览器开发者工具(F12)
- 切换到 Console 标签
- 执行所有导入操作
- **验证点:** 确认 Console 没有错误日志
4. **子场景 4.4: localStorage 数据验证**
- 执行导入操作(有失败)
- 打开开发者工具 → Application → Local Storage
- **验证点 1:** 确认存在 `intermediary_person_import_last_task` 数据
- 重新导入
- **验证点 2:** 确认点击"开始导入"后,localStorage 中的对应数据被清除
- 刷新页面
- **验证点 3:** 确认按钮不再显示
**测试结果:** ⬜ 通过 ⬜ 失败
**备注:**
---
### 测试场景 5: 快速连续点击
**目的:** 验证防止重复提交的机制
**步骤:**
1. 导入数据(有失败),确认按钮显示
2. 打开导入对话框
3. 选择任意文件
4. **关键步骤:** 快速连续多次点击"开始导入"按钮(如双击或三击)
5. **验证点:** 按钮被禁用
- 预期: 按钮变为灰色,显示"导入中..."
- 预期: 不会重复触发多次上传
- 预期: `isUploading` 状态为 `true`,阻止重复提交
6. 等待导入完成
7. **验证点:** 只执行了一次导入操作
- 预期: 只有一个通知提示
- 预期: 失败记录列表只有一组数据
**测试结果:** ⬜ 通过 ⬜ 失败
**备注:**
---
### 测试场景 6: 刷新页面后状态保持
**目的:** 验证 localStorage 的持久化功能
**步骤:**
1. 导入个人中介数据(有失败)
2. **验证点 1:** 确认显示失败记录按钮
3. 刷新浏览器页面(F5)
4. **验证点 2:** 确认按钮仍然显示
- 预期: localStorage 数据持久化,状态保持
5. 打开导入对话框,选择文件,点击"开始导入"
6. **验证点 3:** 按钮立即消失
- 预期: 即使刷新页面后,清除功能仍然正常工作
**测试结果:** ⬜ 通过 ⬜ 失败
**备注:**
---
## 四、测试数据准备
### 4.1 个人中介导入文件模板
**必需字段:**
- 姓名(name)
- 证件号码(personId)
- 人员类型(personType)
- 性别(gender)
- 手机号码(mobile)
**错误数据示例:**
| 姓名 | 证件号码 | 人员类型 | 性别 | 手机号码 |
|------|----------|----------|------|----------|
| 张三 | 12345 | 中介人员 | 男 | 13800138000 |
| 李四 | | 评估人员 | 女 | 13900139000 |
| 王五 | 110101199001011234 | | 男 | 13700137000 |
### 4.2 实体中介导入文件模板
**必需字段:**
- 机构名称(enterpriseName)
- 统一社会信用代码(socialCreditCode)
- 主体类型(enterpriseType)
- 企业性质(enterpriseNature)
- 法定代表人(legalRepresentative)
**错误数据示例:**
| 机构名称 | 统一社会信用代码 | 主体类型 | 企业性质 | 法定代表人 |
|----------|------------------|----------|----------|------------|
| 测试公司1 | ABCDEFGHIJKL | 律师事务所 | 个人独资 | 张三 |
| 测试公司2 | | 会计师事务所 | 合伙 | 李四 |
| 测试公司3 | 91110000123456789X | | | 王五 |
---
## 五、已知问题
**无**
---
## 六、测试总结
### 6.1 测试覆盖率
- [x] 个人中介导入失败记录清除
- [x] 实体中介导入失败记录清除
- [x] 两种类型互不影响
- [x] 边界情况处理
- [x] 快速连续点击防护
- [x] 页面刷新后状态保持
### 6.2 测试结果统计
- 总测试场景: 6 个
- 通过场景: __
- 失败场景: __
- 阻塞问题: __
### 6.3 整体评估
**通过** - 所有测试场景通过,功能符合预期
**有条件通过** - 大部分测试通过,存在非阻塞问题
**不通过** - 存在关键功能缺陷,需要修复
### 6.4 建议
- (根据测试结果填写建议)
---
## 七、附录
### 7.1 相关代码提交
- Task 1: commit 1216ba9 "feat: 导入时触发清除历史记录事件"
- Task 2: commit 51dc466 "feat: 监听清除导入历史记录事件"
- Task 3: commit b35d05a "feat: 实现清除导入历史记录方法"
### 7.2 相关文档
- 实施计划: `doc/plans/2025-02-08-intermediary-import-history-cleanup.md`
- 需求文档: 待补充
### 7.3 联系方式
- 开发人员: Claude (AI Assistant)
- 测试负责人: 待指定
- 项目经理: 待指定
---
**测试报告版本:** v1.0
**最后更新:** 2026-02-08

View File

@@ -1,127 +0,0 @@
# 测试报告目录
本目录用于存放自动化测试生成的测试报告。
## 报告命名规范
```
test_report_YYYYMMDD_HHMMSS.json
```
例如: `test_report_20260209_153045.json`
## 报告内容
每个测试报告包含以下信息:
- test_time: 测试时间
- environment: 测试环境URL
- total_count: 总测试用例数
- passed_count: 通过的用例数
- failed_count: 失败的用例数
- pass_rate: 通过率
- results: 详细测试结果列表
## 查看报告
### 方式1: 文本编辑器
使用任何文本编辑器打开JSON文件即可查看。
### 方式2: JSON格式化工具
使用在线JSON格式化工具或IDE的JSON插件进行格式化查看:
- https://jsoneditoronline.org/
- https://www.json.cn/
### 方式3: Python脚本解析
```python
import json
with open('doc/test-reports/test_report_20260209_153045.json', 'r', encoding='utf-8') as f:
report = json.load(f)
print(f"测试时间: {report['test_time']}")
print(f"通过率: {report['pass_rate']}")
for result in report['results']:
print(f"- {result['name']}: {'通过' if result['passed'] else '失败'}")
```
## 报告分析
### 查看通过率
```json
"pass_rate": "75.0%"
```
通过率 >= 80% 表示测试基本通过
### 查看失败的测试用例
在results数组中查找 "passed": false 的记录
### 查看错误原因
每个测试用例的error_message字段包含失败原因
### 查看详细数据
每个测试用例的details字段包含:
- expected_success/expected_failure: 预期结果
- actual_success/actual_failure: 实际结果
- failures: 失败记录列表
## 历史报告管理
建议定期清理旧的测试报告:
```bash
# 删除7天前的报告
find doc/test-reports -name "test_report_*.json" -mtime +7 -delete
# Windows PowerShell
Get-ChildItem doc/test-reports -Filter "test_report_*.json" |
Where-Object LastWriteTime -lt (Get-Date).AddDays(-7) |
Remove-Item
```
## 测试趋势分析
通过对比不同时间的测试报告,可以分析:
1. 功能稳定性: 通过率是否保持在高水平
2. 回归问题: 之前通过的测试是否开始失败
3. 新增问题: 新功能是否引入了测试失败
## 归档建议
- 每次版本发布前保留一份测试报告
- 重大功能更新后保留测试报告
- 定期(如每月)归档历史报告到单独目录
## 示例报告结构
```json
{
"test_time": "2026-02-09 15:30:45",
"environment": "http://localhost:8080",
"total_count": 4,
"passed_count": 4,
"failed_count": 0,
"pass_rate": "100.0%",
"results": [
{
"name": "采购交易 - Excel内采购事项ID重复",
"description": "测试导入3条采购事项ID相同的记录...",
"passed": true,
"error_message": null,
"details": {
"expected_success": 1,
"expected_failure": 2,
"actual_success": 1,
"actual_failure": 2,
"failures": [
{
"purchaseId": "PURCHASE001",
"errorMessage": "采购事项ID[PURCHASE001]在导入文件中重复,已跳过此条记录"
}
]
},
"duration": "5.23s"
}
]
}
```

View File

@@ -1,257 +0,0 @@
# 导入重复检测测试 - 文件清单
## 本次创建的文件列表
### 核心测试文件
#### 1. Python测试脚本
```
doc/test-scripts/test_import_duplicate_detection.py (600+ 行)
```
- 主测试脚本
- 包含4个完整测试场景
- 自动生成测试数据
- 自动验证结果
- 生成JSON测试报告
#### 2. 测试用例文档
```
doc/test-scripts/test_import_duplicate_detection_cases.md
```
- 详细的测试用例说明
- 4个测试场景的完整描述
- 测试数据和预期结果
#### 3. 使用说明文档
```
doc/test-scripts/README_TEST.md
```
- 完整的使用指南
- 环境准备步骤
- 运行和查看结果说明
- 常见问题解答
#### 4. 文档索引
```
doc/test-scripts/INDEX.md
```
- 所有文档的总索引
- 快速导航指南
- 功能概述
#### 5. 快速开始指南
```
doc/test-scripts/QUICKSTART.md
```
- 一分钟快速开始
- 简化的使用步骤
- 常见问题快速解决
#### 6. 总结文档
```
doc/test-scripts/SUMMARY.md
```
- 完整的工作总结
- 测试覆盖范围
- 验证点说明
#### 7. 测试数据生成工具
```
doc/test-scripts/generate_test_data.py
```
- 独立的数据生成工具
- 可单独运行生成测试数据
### 执行脚本
#### Windows批处理
```
run_duplicate_test.bat
```
- Windows下一键运行
- 自动检查环境
- 自动安装依赖
#### Linux/Mac脚本
```
run_duplicate_test.sh
```
- Linux/Mac下一键运行
- 自动检查环境
- 自动安装依赖
### 说明文档
#### 测试数据说明
```
doc/test-data/README.md
```
- 测试数据目录说明
- 数据结构说明
- 使用方法
#### 测试报告说明
```
doc/test-reports/README.md
```
- 测试报告格式说明
- 报告查看方法
- 报告分析指南
## 目录结构
```
D:\ccdi\ccdi\
├── run_duplicate_test.bat # Windows执行脚本
├── run_duplicate_test.sh # Linux/Mac执行脚本
├── doc/
│ ├── test-scripts/ # 测试脚本目录
│ │ ├── test_import_duplicate_detection.py # 主测试脚本
│ │ ├── test_import_duplicate_detection_cases.md # 测试用例文档
│ │ ├── README_TEST.md # 使用说明
│ │ ├── INDEX.md # 文档索引
│ │ ├── QUICKSTART.md # 快速开始
│ │ ├── SUMMARY.md # 总结文档
│ │ └── generate_test_data.py # 数据生成工具
│ ├── test-data/ # 测试数据目录
│ │ ├── temp/ # 临时测试数据(自动生成)
│ │ ├── employee/ # 员工测试数据
│ │ ├── recruitment/ # 招聘测试数据
│ │ └── README.md # 数据说明
│ └── test-reports/ # 测试报告目录
│ └── README.md # 报告说明
```
## 文件说明
### 测试脚本
| 文件名 | 说明 | 行数 | 用途 |
|--------|------|------|------|
| test_import_duplicate_detection.py | 主测试脚本 | 600+ | 执行所有测试场景 |
| generate_test_data.py | 数据生成工具 | 50+ | 生成测试Excel文件 |
### 文档
| 文件名 | 说明 | 类型 | 用途 |
|--------|------|------|------|
| test_import_duplicate_detection_cases.md | 测试用例文档 | Markdown | 详细的测试用例说明 |
| README_TEST.md | 使用说明 | Markdown | 完整的使用指南 |
| INDEX.md | 文档索引 | Markdown | 快速导航 |
| QUICKSTART.md | 快速开始 | Markdown | 一分钟上手指南 |
| SUMMARY.md | 总结文档 | Markdown | 工作总结 |
### 执行脚本
| 文件名 | 说明 | 类型 | 用途 |
|--------|------|------|------|
| run_duplicate_test.bat | Windows执行脚本 | Batch | Windows下一键运行 |
| run_duplicate_test.sh | Linux/Mac执行脚本 | Shell | Linux/Mac下一键运行 |
### 说明文档
| 文件名 | 说明 | 类型 | 用途 |
|--------|------|------|------|
| doc/test-data/README.md | 数据说明 | Markdown | 测试数据目录说明 |
| doc/test-reports/README.md | 报告说明 | Markdown | 测试报告说明 |
## 测试数据文件(运行时自动生成)
### 临时测试数据
```
doc/test-data/temp/
├── purchase_duplicate.xlsx # 采购重复数据(场景1)
├── employee_employee_id_duplicate.xlsx # 员工柜员号重复(场景2)
├── employee_id_card_duplicate.xlsx # 员工身份证号重复(场景3)
├── purchase_mixed_duplicate.xlsx # 采购混合重复(场景4)
└── employee_mixed_duplicate.xlsx # 员工混合重复(场景4)
```
### 测试报告(运行时自动生成)
```
doc/test-reports/
└── test_report_YYYYMMDD_HHMMSS.json # JSON格式测试报告
```
## 使用方式
### 方式1: 批处理脚本(推荐)
```bash
# Windows
双击 run_duplicate_test.bat
# Linux/Mac
bash run_duplicate_test.sh
```
### 方式2: Python命令
```bash
python doc/test-scripts/test_import_duplicate_detection.py
```
### 方式3: 只生成测试数据
```bash
python doc/test-scripts/generate_test_data.py
```
## 测试场景
| 场景 | 描述 | 数据文件 | 验证点 |
|------|------|----------|--------|
| 场景1 | 采购交易 - Excel内采购事项ID重复 | purchase_duplicate.xlsx | 第1条成功,第2、3条失败 |
| 场景2 | 员工信息 - Excel内柜员号重复 | employee_employee_id_duplicate.xlsx | 第1条成功,第2、3条失败 |
| 场景3 | 员工信息 - Excel内身份证号重复 | employee_id_card_duplicate.xlsx | 第1条成功,第2、3条失败 |
| 场景4 | 混合重复(数据库+Excel) | purchase_mixed_duplicate.xlsx, employee_mixed_duplicate.xlsx | 混合场景验证 |
## 依赖项
### Python依赖
- requests: HTTP请求库
- openpyxl: Excel文件操作库
### 系统要求
- Python 3.7+
- 后端服务运行在 http://localhost:8080
- 测试账号: admin / admin123
## 文件大小
| 文件 | 大小(约) | 说明 |
|------|----------|------|
| test_import_duplicate_detection.py | 25KB | 主测试脚本 |
| test_import_duplicate_detection_cases.md | 15KB | 测试用例文档 |
| README_TEST.md | 12KB | 使用说明 |
| 其他文档 | 5-10KB/个 | 各种说明文档 |
| Excel测试数据 | 10-20KB/个 | 自动生成 |
## 版本信息
- **创建日期**: 2026-02-09
- **版本**: v1.0
- **状态**: ✅ 完成
## 后续维护
### 定期清理
- 删除临时测试数据: `doc/test-data/temp/*.xlsx`
- 归档旧的测试报告: `doc/test-reports/test_report_*.json`
### 更新文档
- 添加新测试场景时更新测试用例文档
- 修改测试逻辑时更新使用说明
- 定期更新常见问题解答
### 代码维护
- 保持代码注释完整
- 遵循现有代码风格
- 添加新功能时保持一致性
## 联系方式
如有问题或建议,请参考:
- 测试用例文档: `doc/test-scripts/test_import_duplicate_detection_cases.md`
- 使用说明文档: `doc/test-scripts/README_TEST.md`
- 快速开始: `doc/test-scripts/QUICKSTART.md`
---
**最后更新**: 2026-02-09
**文件总数**: 12个
**总代码行数**: 约800行
**文档总字数**: 约15000字

View File

@@ -1,227 +0,0 @@
# 导入重复检测功能测试文档索引
## 文档概述
本文档集为"导入文件内部主键重复检测"功能提供完整的测试支持,包括测试用例、测试脚本、使用说明等。
## 文档结构
```
doc/
├── test-scripts/ # 测试脚本和文档
│ ├── test_import_duplicate_detection.py # Python自动化测试脚本
│ ├── test_import_duplicate_detection_cases.md # 详细测试用例文档
│ └── README_TEST.md # 测试使用说明
├── test-data/ # 测试数据
│ ├── temp/ # 临时测试数据(自动生成)
│ ├── employee/ # 员工测试数据
│ ├── recruitment/ # 招聘测试数据
│ └── README.md # 测试数据说明
└── test-reports/ # 测试报告
└── README.md # 测试报告说明
```
## 快速导航
### 1. 测试执行
- **快速开始**: 查看 [测试使用说明](test-scripts/README_TEST.md)
- **运行测试**: 双击 `run_duplicate_test.bat` 或运行Python脚本
- **查看报告**: 查看 `test-reports/` 目录下的JSON报告
### 2. 测试用例
- **详细用例**: 查看 [测试用例文档](test-scripts/test_import_duplicate_detection_cases.md)
- **场景1**: 采购交易 - Excel内采购事项ID重复
- **场景2**: 员工信息 - Excel内柜员号重复
- **场景3**: 员工信息 - Excel内身份证号重复
- **场景4**: 混合重复(数据库+Excel)
### 3. 测试数据
- **数据说明**: 查看 [测试数据说明](test-data/README.md)
- **自动生成**: 运行测试脚本自动生成临时测试数据
- **手动测试**: 使用现有的员工/招聘测试数据
### 4. 测试报告
- **报告说明**: 查看 [测试报告说明](test-reports/README.md)
- **报告格式**: JSON格式,包含详细的测试结果
- **报告位置**: `doc/test-reports/test_report_YYYYMMDD_HHMMSS.json`
## 功能概述
### 测试目标
验证导入功能能够正确检测并处理Excel文件内部的主键重复数据:
1. ✅ 采购交易导入 - 检测采购事项ID重复
2. ✅ 员工信息导入 - 检测柜员号和身份证号重复
### 核心逻辑
- 同一Excel文件内,重复的主键只会导入第一条
- 后续重复记录会被跳过,并记录到失败列表
- 提供清晰的错误提示信息
- 正确区分数据库重复和Excel内重复
### 错误消息格式
- **数据库重复**: "采购事项ID[xxx]已存在,请勿重复导入"
- **Excel内重复**: "采购事项ID[xxx]在导入文件中重复,已跳过此条记录"
- **柜员号重复**: "柜员号[xxx]在导入文件中重复,已跳过此条记录"
- **身份证号重复**: "身份证号[xxx]在导入文件中重复,已跳过此条记录"
## 测试环境要求
### 必需组件
- Python 3.7+
- 后端服务运行在 http://localhost:8080
- 测试账号: admin / admin123
### Python依赖
```bash
pip install requests openpyxl
```
### 数据库准备
- 场景4需要预先在数据库中插入测试数据
- 其他场景不需要预先准备数据
## 测试执行方式
### 方式1: 批处理脚本(推荐)
```bash
# Windows
双击 run_duplicate_test.bat
# Linux/Mac
bash run_duplicate_test.sh
```
### 方式2: Python命令
```bash
python doc/test-scripts/test_import_duplicate_detection.py
```
### 方式3: IDE运行
- 使用PyCharm/VS Code打开测试脚本
- 直接运行
## 测试结果解读
### 成功标准
- ✅ 所有4个测试场景通过
- ✅ 通过率 >= 75% (场景4可能因缺少预置数据而部分失败)
- ✅ 错误消息格式正确
### 失败处理
1. 查看测试报告中的error_message
2. 检查后端日志
3. 确认测试环境是否正确
4. 确认测试账号权限是否正确
### 常见问题
- **连接失败**: 确认后端服务是否启动
- **登录失败**: 确认测试账号密码是否正确
- **权限不足**: 确认admin账号是否有导入权限
- **超时**: 增加等待时间或检查后端性能
## 代码实现
### 后端实现
- **采购交易**: `CcdiPurchaseTransactionImportServiceImpl.java` (第54-82行)
- **员工信息**: `CcdiEmployeeImportServiceImpl.java` (第52-101行)
### 关键代码片段
#### 采购交易重复检测
```java
// 用于跟踪Excel文件内已处理的采购事项ID
Set<String> processedIds = new HashSet<>();
for (int i = 0; i < excelList.size(); i++) {
CcdiPurchaseTransactionExcel excel = excelList.get(i);
if (existingIds.contains(excel.getPurchaseId())) {
// 数据库中已存在
throw new RuntimeException("采购事项ID[" + excel.getPurchaseId() + "]已存在,请勿重复导入");
} else if (processedIds.contains(excel.getPurchaseId())) {
// Excel文件内部重复
throw new RuntimeException("采购事项ID[" + excel.getPurchaseId() + "]在导入文件中重复,已跳过此条记录");
} else {
// 正常导入
newRecords.add(transaction);
processedIds.add(excel.getPurchaseId()); // 标记为已处理
}
}
```
#### 员工信息重复检测
```java
// 用于跟踪Excel文件内已处理的主键
Set<Long> processedEmployeeIds = new HashSet<>();
Set<String> processedIdCards = new HashSet<>();
for (int i = 0; i < excelList.size(); i++) {
CcdiEmployeeExcel excel = excelList.get(i);
// 统一检查Excel内重复
if (processedEmployeeIds.contains(excel.getEmployeeId())) {
throw new RuntimeException("柜员号[" + excel.getEmployeeId() + "]在导入文件中重复,已跳过此条记录");
}
if (StringUtils.isNotEmpty(excel.getIdCard()) &&
processedIdCards.contains(excel.getIdCard())) {
throw new RuntimeException("身份证号[" + excel.getIdCard() + "]在导入文件中重复,已跳过此条记录");
}
// 统一标记为已处理
processedEmployeeIds.add(excel.getEmployeeId());
processedIdCards.add(excel.getIdCard());
}
```
## API接口
### 采购交易导入
- **上传**: `POST /ccdi/purchaseTransaction/importData`
- **状态**: `GET /ccdi/purchaseTransaction/importStatus/{taskId}`
- **失败记录**: `GET /ccdi/purchaseTransaction/importFailures/{taskId}`
### 员工信息导入
- **上传**: `POST /ccdi/employee/importData`
- **状态**: `GET /ccdi/employee/importStatus/{taskId}`
- **失败记录**: `GET /ccdi/employee/importFailures/{taskId}`
### Swagger文档
访问 http://localhost:8080/swagger-ui/index.html 查看完整API文档
## 版本历史
### v1.0 (2026-02-09)
- ✅ 创建测试框架
- ✅ 实现4个测试场景
- ✅ 生成完整测试文档
- ✅ 支持自动化测试和手动测试
## 贡献指南
### 添加新测试场景
1. 在ExcelGenerator中添加数据生成方法
2. 创建新的TestCase子类
3. 更新测试用例文档
4. 运行测试验证
### 修改测试逻辑
1. 修改对应的TestCase类
2. 更新测试用例文档
3. 运行完整测试确保不影响其他场景
### 报告问题
如发现问题,请提供:
- 测试报告JSON文件
- 后端日志
- 复现步骤
- 环境信息
## 联系方式
如有问题或建议,请联系开发团队。
---
**最后更新**: 2026-02-09
**文档版本**: v1.0
**维护者**: 测试团队

View File

@@ -1,146 +0,0 @@
# 导入重复检测测试 - 快速开始
## 一分钟快速开始
### Windows用户
```bash
# 1. 双击运行
双击 run_duplicate_test.bat
# 2. 等待测试完成
测试会自动运行并生成报告
# 3. 查看结果
测试报告保存在: doc\test-reports\test_report_YYYYMMDD_HHMMSS.json
```
### Linux/Mac用户
```bash
# 1. 运行脚本
bash run_duplicate_test.sh
# 2. 等待测试完成
测试会自动运行并生成报告
# 3. 查看结果
测试报告保存在: doc/test-reports/test_report_YYYYMMDD_HHMMSS.json
```
## 测试前提
### 必须满足
- ✅ 后端服务已启动 (http://localhost:8080)
- ✅ 测试账号可用 (admin/admin123)
- ✅ Python 3.7+ 已安装
### 自动安装
测试脚本会自动安装以下Python依赖:
- requests
- openpyxl
## 测试内容
测试会自动验证4个场景:
1. ✅ 采购交易 - Excel内采购事项ID重复
2. ✅ 员工信息 - Excel内柜员号重复
3. ✅ 员工信息 - Excel内身份证号重复
4. ✅ 混合重复(数据库+Excel)
## 预期输出
### 成功的输出
```
================================================================================
导入文件内部主键重复检测功能测试
================================================================================
测试时间: 2026-02-09 15:30:45
测试环境: http://localhost:8080
================================================================================
[1/2] 登录系统...
✓ 登录成功
[2/2] 运行测试用例...
--------------------------------------------------------------------------------
测试用例 1/4: 采购交易 - Excel内采购事项ID重复
✓ 测试通过
测试用例 2/4: 员工信息 - Excel内柜员号重复
✓ 测试通过
测试用例 3/4: 员工信息 - Excel内身份证号重复
✓ 测试通过
测试用例 4/4: 混合重复 - 数据库+Excel重复
✓ 测试通过
================================================================================
测试报告
================================================================================
总测试用例数: 4
通过: 4
失败: 0
通过率: 100.0%
报告已保存到: doc\test-reports\test_report_20260209_153045.json
================================================================================
```
## 常见问题
### Q1: 连接失败
```
[错误] 未检测到后端服务
```
**解决**: 启动后端服务
```bash
mvn spring-boot:run
```
### Q2: 登录失败
```
[错误] 登录失败: 用户名或密码错误
```
**解决**: 确认测试账号是 admin/admin123
### Q3: 权限不足
```
[错误] 上传失败: 没有权限
```
**解决**: 确认admin账号有导入权限
## 手动测试
如果需要手动验证测试场景:
### 1. 生成测试数据
```bash
python doc/test-scripts/generate_test_data.py
```
### 2. 通过前端导入
1. 访问 http://localhost:8080
2. 登录系统
3. 进入"采购交易管理"或"员工信息管理"
4. 点击"导入"
5. 选择测试Excel文件(在 doc/test-data/temp/ 目录)
6. 上传并查看结果
## 详细文档
- **测试用例**: [test_import_duplicate_detection_cases.md](test_import_duplicate_detection_cases.md)
- **使用说明**: [README_TEST.md](README_TEST.md)
- **文档索引**: [INDEX.md](INDEX.md)
## 技术支持
如遇问题:
1. 查看 [常见问题](README_TEST.md#常见问题)
2. 检查后端日志
3. 查看测试报告中的错误消息
---
**准备好了吗? 运行 `run_duplicate_test.bat` 开始测试!** 🚀

View File

@@ -1,320 +0,0 @@
# 导入重复检测测试使用说明
## 概述
本测试套件用于验证"导入文件内部主键重复检测"功能,确保系统能够正确识别并处理Excel文件内部重复的主键数据。
## 文件结构
```
doc/test-scripts/
├── test_import_duplicate_detection.py # Python自动化测试脚本
├── test_import_duplicate_detection_cases.md # 详细测试用例文档
└── README_TEST.md # 本说明文档
```
## 快速开始
### 1. 环境准备
#### 必需组件
- Python 3.7+
- 后端服务运行在 http://localhost:8080
- 测试账号: admin / admin123
#### Python依赖安装
```bash
pip install requests openpyxl
```
或者使用requirements.txt(如果有的话):
```bash
pip install -r requirements.txt
```
### 2. 运行测试
#### 方式1: 命令行运行
```bash
cd D:\ccdi\ccdi
python doc/test-scripts/test_import_duplicate_detection.py
```
#### 方式2: IDE运行
- 使用PyCharm/VS Code打开 `test_import_duplicate_detection.py`
- 直接运行脚本
### 3. 查看结果
测试运行时会实时显示进度,完成后会生成JSON格式的测试报告:
```
doc/test-reports/test_report_20260209_153045.json
```
## 测试场景说明
### 场景1: 采购交易 - Excel内采购事项ID重复
- **目的**: 验证3条相同采购事项ID的记录,只有第1条导入成功
- **预期**: 成功1条,失败2条
- **错误消息**: "采购事项ID[xxx]在导入文件中重复,已跳过此条记录"
### 场景2: 员工信息 - Excel内柜员号重复
- **目的**: 验证3条相同柜员号的记录,只有第1条导入成功
- **预期**: 成功1条,失败2条
- **错误消息**: "柜员号[xxx]在导入文件中重复,已跳过此条记录"
### 场景3: 员工信息 - Excel内身份证号重复
- **目的**: 验证3条相同身份证号的记录,只有第1条导入成功
- **预期**: 成功1条,失败2条
- **错误消息**: "身份证号[xxx]在导入文件中重复,已跳过此条记录"
### 场景4: 混合重复(数据库+Excel)
- **目的**: 验证数据库已存在记录和Excel内重复的混合场景
- **预期**: 第1条失败(数据库重复),第2条成功,第3条失败(Excel内重复),第4条成功
- **注意**: 需要预先在数据库中插入测试数据
## 测试脚本说明
### 核心类
#### 1. APIClient
API客户端封装,负责:
- 登录获取Token
- 上传文件
- 查询导入状态
- 查询失败记录
#### 2. ExcelGenerator
Excel测试数据生成器,提供:
- `create_purchase_duplicate_data()`: 采购重复数据
- `create_employee_employee_id_duplicate()`: 员工柜员号重复数据
- `create_employee_id_card_duplicate()`: 员工身份证号重复数据
- `create_mixed_duplicate_scenario()`: 混合重复数据
#### 3. TestCase
测试用例基类,所有测试用例继承此类:
- `PurchaseDuplicateTestCase`: 场景1
- `EmployeeEmployeeIdDuplicateTestCase`: 场景2
- `EmployeeIdCardDuplicateTestCase`: 场景3
- `MixedDuplicateTestCase`: 场景4
#### 4. TestRunner
测试运行器,负责:
- 初始化API客户端
- 依次执行所有测试用例
- 收集测试结果
- 生成测试报告
### 配置参数
在脚本顶部的配置部分可以修改:
```python
# 服务器地址
BASE_URL = "http://localhost:8080"
# 测试账号
USERNAME = "admin"
PASSWORD = "admin123"
# 报告保存目录
REPORT_DIR = "D:/ccdi/ccdi/doc/test-reports"
EXCEL_DIR = "D:/ccdi/ccdi/doc/test-data/temp"
```
## 测试数据说明
### 自动生成的Excel文件
测试脚本会自动在 `doc/test-data/temp/` 目录下生成测试数据:
1. `purchase_duplicate.xlsx` - 采购重复数据(场景1)
2. `employee_employee_id_duplicate.xlsx` - 员工柜员号重复(场景2)
3. `employee_id_card_duplicate.xlsx` - 员工身份证号重复(场景3)
4. `purchase_mixed_duplicate.xlsx` - 采购混合重复(场景4)
5. `employee_mixed_duplicate.xlsx` - 员工混合重复(场景4)
### 数据字段说明
#### 采购交易测试数据
| 字段 | 说明 | 示例 |
|------|------|------|
| purchaseId | 采购事项ID(主键) | PURCHASE001 |
| purchaseCategory | 采购类别 | 采购类别1 |
| subjectName | 标的物名称 | 标的物名称1 |
| purchaseQty | 采购数量 | 10 |
| budgetAmount | 预算金额 | 10000.00 |
| purchaseMethod | 采购方式 | 公开招标 |
| applyDate | 采购申请日期 | 2024-01-01 |
| applicantId | 申请人工号 | 1000001 |
| applicantName | 申请人姓名 | 张三 |
| applyDepartment | 申请部门 | 技术部 |
#### 员工信息测试数据
| 字段 | 说明 | 示例 |
|------|------|------|
| name | 姓名 | 员工1 |
| employeeId | 柜员号(主键) | 10001 |
| deptId | 所属部门ID | 103 |
| idCard | 身份证号(主键) | 110101199001011234 |
| phone | 电话 | 13800000000 |
| hireDate | 入职时间 | 2024-01-01 |
| status | 状态 | 0 |
## 测试报告说明
### 报告格式
JSON格式,包含以下信息:
```json
{
"test_time": "2026-02-09 15:30:45",
"environment": "http://localhost:8080",
"total_count": 4,
"passed_count": 3,
"failed_count": 1,
"pass_rate": "75.0%",
"results": [
{
"name": "采购交易 - Excel内采购事项ID重复",
"description": "测试导入3条采购事项ID相同的记录...",
"passed": true,
"error_message": null,
"details": {
"expected_success": 1,
"expected_failure": 2,
"actual_success": 1,
"actual_failure": 2,
"failures": [...]
},
"duration": "5.23s"
}
]
}
```
### 查看报告
1. 打开测试报告JSON文件
2. 查看每个测试用例的passed字段
3. 检查details中的实际结果与预期结果是否一致
4. 如果失败,查看error_message了解原因
## 常见问题
### 1. 连接失败
**问题**: `✗ 登录失败: Connection refused`
**解决**:
- 确认后端服务是否启动
- 检查BASE_URL配置是否正确
- 确认端口8080未被占用
### 2. 登录失败
**问题**: `✗ 登录失败: 用户名或密码错误`
**解决**:
- 确认测试账号密码是否正确(admin/admin123)
- 检查数据库中是否存在该账号
- 确认登录接口路径是否为/login/test
### 3. 导入超时
**问题**: 查询导入状态时超时
**解决**:
- 增加等待时间(修改脚本中的time.sleep(3)为更大的值)
- 检查后端异步任务是否正常执行
- 查看后端日志是否有异常
### 4. 权限不足
**问题**: `✗ 上传失败: 没有权限`
**解决**:
- 确认admin账号是否有导入权限
- 检查权限标识: `ccdi:purchaseTransaction:import``ccdi:employee:import`
- 在系统管理->角色管理中配置权限
### 5. 场景4测试失败
**问题**: 混合重复测试结果不符合预期
**解决**:
- 场景4需要预先在数据库中插入测试数据(EXIST001, 柜员号99999)
- 如果数据库中没有这些数据,测试可能部分失败
- 可以手动在数据库中插入,或者跳过该场景
## 手动测试步骤
如果需要手动验证测试场景:
### 1. 准备测试数据
运行Python脚本生成Excel文件(即使不执行测试,也会生成数据):
```python
from doc.test_scripts.test_import_duplicate_detection import ExcelGenerator
import os
# 生成场景1数据
file1 = ExcelGenerator.create_purchase_duplicate_data()
print(f"文件已生成: {file1}")
```
### 2. 通过前端界面导入
1. 访问 http://localhost:8080
2. 登录系统(admin/admin123)
3. 进入"采购交易管理"或"员工信息管理"
4. 点击"导入"按钮
5. 选择生成的Excel文件
6. 点击"确定"上传
7. 等待导入完成
8. 点击"查看失败记录"查看详细信息
### 3. 验证结果
- 检查导入成功的记录数量
- 查看失败记录的错误消息
- 确认数据库中只有第1条重复记录被导入
## 清理测试数据
测试完成后,建议清理测试数据:
### 方式1: 通过前端界面
1. 进入采购交易/员工信息管理页面
2. 搜索测试数据(如采购事项ID为PURCHASE001的记录)
3. 逐条删除
### 方式2: 直接操作数据库
```sql
-- 删除采购交易测试数据
DELETE FROM ccdi_purchase_transaction WHERE purchase_id LIKE 'PURCHASE%' OR purchase_id LIKE 'NEW%';
-- 删除员工测试数据
DELETE FROM ccdi_employee WHERE employee_id BETWEEN 10001 AND 99999;
```
## 扩展测试
如需添加新的测试场景:
1. 在ExcelGenerator中添加新的数据生成方法
2. 创建新的TestCase子类
3. 在main()函数中将新测试用例添加到TestRunner
示例:
```python
class MyNewTestCase(TestCase):
def __init__(self):
super().__init__("我的新测试", "测试描述")
def run(self, client: APIClient):
# 实现测试逻辑
pass
# 在main函数中添加
runner.add_test_case(MyNewTestCase())
```
## 联系支持
如有问题,请联系开发团队或查看相关文档:
- 测试用例详细文档: `test_import_duplicate_detection_cases.md`
- 后端实现代码: `CcdiPurchaseTransactionImportServiceImpl.java`, `CcdiEmployeeImportServiceImpl.java`
- API文档: Swagger UI (http://localhost:8080/swagger-ui/index.html)

View File

@@ -1,287 +0,0 @@
# 导入重复检测功能测试 - 完成总结
## 已创建的文件
### 1. 测试脚本
```
D:\ccdi\ccdi\doc\test-scripts\test_import_duplicate_detection.py
```
- 完整的Python自动化测试脚本
- 包含4个测试场景的完整实现
- 支持自动生成测试数据、执行测试、生成报告
- 约600行代码,注释详细
### 2. 测试用例文档
```
D:\ccdi\ccdi\doc\test-scripts\test_import_duplicate_detection_cases.md
```
- 详细的测试用例说明
- 包含4个测试场景的完整描述
- 每个场景包含:测试目的、测试数据、测试步骤、预期结果
### 3. 使用说明文档
```
D:\ccdi\ccdi\doc\test-scripts\README_TEST.md
```
- 测试使用指南
- 环境准备、运行步骤、结果查看
- 常见问题解答
### 4. 测试文档索引
```
D:\ccdi\ccdi\doc\test-scripts\INDEX.md
```
- 所有测试文档的总索引
- 快速导航指南
- 功能概述和API说明
### 5. 测试数据生成工具
```
D:\ccdi\ccdi\doc\test-scripts\generate_test_data.py
```
- 单独的测试数据生成工具
- 可以只生成测试数据而不运行测试
### 6. Windows批处理脚本
```
D:\ccdi\ccdi\run_duplicate_test.bat
```
- Windows下一键运行测试
- 自动检查环境、安装依赖
### 7. Linux/Mac脚本
```
D:\ccdi\ccdi\run_duplicate_test.sh
```
- Linux/Mac下一键运行测试
- 自动检查环境、安装依赖
### 8. 测试数据说明
```
D:\ccdi\ccdi\doc\test-data\README.md
```
- 测试数据目录说明
- 数据结构和用途说明
### 9. 测试报告说明
```
D:\ccdi\ccdi\doc\test-reports\README.md
```
- 测试报告格式说明
- 报告查看和分析方法
## 测试场景覆盖
### 场景1: 采购交易 - Excel内采购事项ID重复
- **目的**: 验证采购交易导入时Excel内采购事项ID重复的检测
- **数据**: 3条相同采购事项ID的记录
- **预期**: 第1条成功,第2、3条失败
- **验证点**:
- ✅ 成功数量为1
- ✅ 失败数量为2
- ✅ 错误消息包含"在导入文件中重复"
### 场景2: 员工信息 - Excel内柜员号重复
- **目的**: 验证员工信息导入时Excel内柜员号重复的检测
- **数据**: 3条相同柜员号的记录
- **预期**: 第1条成功,第2、3条失败
- **验证点**:
- ✅ 成功数量为1
- ✅ 失败数量为2
- ✅ 错误消息包含"柜员号"和"在导入文件中重复"
### 场景3: 员工信息 - Excel内身份证号重复
- **目的**: 验证员工信息导入时Excel内身份证号重复的检测
- **数据**: 3条相同身份证号的记录
- **预期**: 第1条成功,第2、3条失败
- **验证点**:
- ✅ 成功数量为1
- ✅ 失败数量为2
- ✅ 错误消息包含"身份证号"和"在导入文件中重复"
### 场景4: 混合重复(数据库+Excel)
- **目的**: 验证数据库已存在记录和Excel内重复记录的混合场景
- **数据**: 4条记录,包含数据库重复和Excel内重复
- **预期**: 第1条失败(数据库重复),第2条成功,第3条失败(Excel内重复),第4条成功
- **验证点**:
- ✅ 成功数量为2
- ✅ 失败数量为2
- ✅ 能够区分数据库重复和Excel内重复
## 测试功能特性
### 自动化测试
- ✅ 自动生成测试数据Excel文件
- ✅ 自动上传文件到服务器
- ✅ 自动轮询查询导入状态
- ✅ 自动验证测试结果
- ✅ 自动生成JSON格式测试报告
### 测试报告
- ✅ JSON格式,易于解析
- ✅ 包含详细的测试结果
- ✅ 记录测试耗时
- ✅ 区分预期结果和实际结果
- ✅ 记录失败原因
### 错误处理
- ✅ 网络连接失败处理
- ✅ 登录失败处理
- ✅ 上传失败处理
- ✅ 超时处理
- ✅ 异常捕获和日志记录
## 测试执行方式
### 方式1: 批处理脚本(推荐)
```bash
# Windows
双击 run_duplicate_test.bat
# Linux/Mac
bash run_duplicate_test.sh
```
### 方式2: Python命令
```bash
python doc/test-scripts/test_import_duplicate_detection.py
```
### 方式3: IDE运行
- 使用PyCharm/VS Code打开测试脚本
- 直接运行
## 测试前提条件
### 必需组件
- ✅ Python 3.7+
- ✅ requests库
- ✅ openpyxl库
- ✅ 后端服务运行在 http://localhost:8080
- ✅ 测试账号: admin / admin123
### 数据库准备
- ⚠️ 场景4需要预先在数据库中插入测试数据
- ✅ 其他场景不需要预先准备数据
## 测试输出
### 控制台输出
```
================================================================================
导入文件内部主键重复检测功能测试
================================================================================
测试时间: 2026-02-09 15:30:45
测试环境: http://localhost:8080
================================================================================
[1/2] 登录系统...
✓ 登录成功, Token: eyJhbGciOiJIUzUxMiJ9...
[2/2] 运行测试用例...
--------------------------------------------------------------------------------
测试用例 1/4: 采购交易 - Excel内采购事项ID重复
描述: 测试导入3条采购事项ID相同的记录,预期第1条成功,第2、3条失败
--------------------------------------------------------------------------------
✓ 生成测试数据: D:\ccdi\ccdi\doc\test-data\temp\purchase_duplicate.xlsx
✓ 上传成功, TaskID: purchase-import-1234567890
✓ 导入状态: {...}
✓ 测试通过
...
```
### JSON报告
```json
{
"test_time": "2026-02-09 15:30:45",
"environment": "http://localhost:8080",
"total_count": 4,
"passed_count": 4,
"failed_count": 0,
"pass_rate": "100.0%",
"results": [...]
}
```
## 测试验证点
### 功能验证
- ✅ Excel内重复主键检测正确
- ✅ 只有第1条重复记录被导入
- ✅ 后续重复记录被跳过
- ✅ 错误消息格式正确
- ✅ 能够区分数据库重复和Excel内重复
### 数据验证
- ✅ 成功数量符合预期
- ✅ 失败数量符合预期
- ✅ 失败记录内容正确
- ✅ 错误消息内容正确
### 异常验证
- ✅ 网络异常处理正确
- ✅ 登录失败处理正确
- ✅ 权限不足处理正确
- ✅ 数据格式错误处理正确
## 代码质量
### 代码结构
- ✅ 采用面向对象设计
- ✅ 类职责清晰
- ✅ 代码注释详细
- ✅ 变量命名规范
### 可维护性
- ✅ 易于添加新测试场景
- ✅ 易于修改测试逻辑
- ✅ 易于扩展测试功能
- ✅ 代码复用性好
### 可读性
- ✅ 代码格式统一
- ✅ 注释清晰完整
- ✅ 变量命名语义化
- ✅ 逻辑流程清晰
## 后续工作建议
### 1. 执行测试
- 运行完整的测试套件
- 验证所有测试场景通过
- 生成测试报告
### 2. 数据准备
- 在数据库中插入场景4需要的预置数据
- 确保测试账号有正确的权限
- 清理之前的测试数据
### 3. 测试执行
- 按照测试脚本执行测试
- 记录测试结果
- 分析失败原因
### 4. 问题修复
- 如果测试失败,查看错误消息
- 检查后端实现代码
- 修复问题后重新测试
### 5. 文档完善
- 根据实际测试结果更新文档
- 添加更多测试场景
- 完善错误处理
## 联系方式
如有问题或建议,请参考:
- 测试用例文档: `doc/test-scripts/test_import_duplicate_detection_cases.md`
- 使用说明文档: `doc/test-scripts/README_TEST.md`
- 文档索引: `doc/test-scripts/INDEX.md`
---
**创建时间**: 2026-02-09
**版本**: v1.0
**状态**: ✅ 完成

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
测试数据生成预览工具
用于预览测试数据,无需运行完整测试
"""
import sys
import os
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from doc.test_scripts.test_import_duplicate_detection import ExcelGenerator
def main():
print("=" * 80)
print("测试数据生成预览")
print("=" * 80)
print("\n[1/4] 生成采购交易重复数据...")
file1 = ExcelGenerator.create_purchase_duplicate_data()
print(f"✓ 文件已生成: {file1}")
print(" 包含3条采购事项ID相同的记录(PURCHASE001)")
print("\n[2/4] 生成员工柜员号重复数据...")
file2 = ExcelGenerator.create_employee_employee_id_duplicate()
print(f"✓ 文件已生成: {file2}")
print(" 包含3条柜员号相同的记录(10001)")
print("\n[3/4] 生成员工身份证号重复数据...")
file3 = ExcelGenerator.create_employee_id_card_duplicate()
print(f"✓ 文件已生成: {file3}")
print(" 包含3条身份证号相同的记录(110101199001011234)")
print("\n[4/4] 生成混合重复数据...")
file4, file5 = ExcelGenerator.create_mixed_duplicate_scenario()
print(f"✓ 文件已生成: {file4}")
print(f"✓ 文件已生成: {file5}")
print(" 包含数据库重复+Excel内重复的混合场景")
print("\n" + "=" * 80)
print("所有测试数据已生成完成!")
print("=" * 80)
print("\n数据保存位置: doc/test-data/temp/")
print("\n可以使用以下方式导入测试:")
print("1. 通过前端界面上传")
print("2. 运行完整测试: python doc/test-scripts/test_import_duplicate_detection.py")
print("=" * 80)
if __name__ == "__main__":
main()

View File

@@ -1,94 +0,0 @@
import requests
import json
# 配置
BASE_URL = "http://localhost:8080"
LOGIN_URL = f"{BASE_URL}/login/test"
IMPORT_URL = f"{BASE_URL}/ccdi/employee/importData"
# 测试账号
username = "admin"
password = "admin123"
# 登录获取token
def login():
"""登录获取token"""
print("正在登录...")
response = requests.post(LOGIN_URL, data={
"username": username,
"password": password
})
if response.status_code == 200:
result = response.json()
if result.get("code") == 200:
token = result.get("token")
print(f"登录成功,获取到token: {token[:20]}...")
return token
else:
print(f"登录失败: {result.get('msg')}")
exit(1)
else:
print(f"登录请求失败: {response.status_code}")
exit(1)
# 准备测试Excel文件(需要手动准备)
def test_duplicate_detection():
"""测试Excel内双字段重复检测"""
token = login()
headers = {
"Authorization": f"Bearer {token}"
}
# 测试场景1: 柜员号在Excel内重复
print("\n=== 测试场景1: 柜员号在Excel内重复 ===")
print("准备包含重复柜员号的Excel文件...")
print("期望结果: 第二条记录应该被标记为失败,错误信息包含'柜员号[XXX]在导入文件中重复'")
# 测试场景2: 身份证号在Excel内重复
print("\n=== 测试场景2: 身份证号在Excel内重复 ===")
print("准备包含重复身份证号的Excel文件...")
print("期望结果: 第二条记录应该被标记为失败,错误信息包含'身份证号[XXX]在导入文件中重复'")
# 测试场景3: 柜员号和身份证号同时重复
print("\n=== 测试场景3: 柜员号和身份证号同时重复 ===")
print("准备包含同时重复柜员号和身份证号的Excel文件...")
print("期望结果: 两条记录都应该被标记为失败")
# 测试场景4: 柜员号在数据库中存在
print("\n=== 测试场景4: 柜员号在数据库中存在 ===")
print("准备包含已存在柜员号的Excel文件...")
print("期望结果: 如果启用更新支持,则更新;否则报错'柜员号已存在且未启用更新支持'")
# 测试场景5: 身份证号在数据库中存在
print("\n=== 测试场景5: 身份证号在数据库中存在 ===")
print("准备包含已存在身份证号的Excel文件...")
print("期望结果: 如果是新增(柜员号不存在),则报错'该身份证号已存在'")
# 测试场景6: 正常导入
print("\n=== 测试场景6: 正常导入(无重复) ===")
print("准备无重复的Excel文件...")
print("期望结果: 所有记录都应该成功导入")
print("\n=== 测试说明 ===")
print("请手动准备Excel文件,使用以下接口测试:")
print(f"POST {IMPORT_URL}")
print("Headers:")
print(f" Authorization: Bearer {token[:20]}...")
print("Body (multipart/form-data):")
print(" file: [Excel文件]")
print(" updateSupport: [true/false]")
print("\n=== 查询导入状态 ===")
print("导入后可以使用以下接口查询状态:")
STATUS_URL = f"{BASE_URL}/ccdi/employee/importStatus"
print(f"GET {STATUS_URL}?taskId={{taskId}}")
print("\n=== 查询失败记录 ===")
print("导入失败时可以使用以下接口查询失败记录:")
FAILURES_URL = f"{BASE_URL}/ccdi/employee/importFailures"
print(f"GET {FAILURES_URL}?taskId={{taskId}}")
if __name__ == "__main__":
test_duplicate_detection()

View File

@@ -1,928 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
导入文件内部主键重复检测功能测试脚本
测试目标:
1. 采购交易导入 - Excel内采购事项ID重复检测
2. 员工信息导入 - Excel内柜员号和身份证号重复检测
作者: 测试专家
日期: 2026-02-09
"""
import os
import sys
import time
import json
import requests
from openpyxl import Workbook
from datetime import datetime, timedelta
from typing import List, Dict, Tuple
# ==================== 配置部分 ====================
BASE_URL = "http://localhost:8080"
LOGIN_URL = f"{BASE_URL}/login/test"
# 测试账号
USERNAME = "admin"
PASSWORD = "admin123"
# 测试结果保存目录
REPORT_DIR = "D:/ccdi/ccdi/doc/test-reports"
EXCEL_DIR = "D:/ccdi/ccdi/doc/test-data/temp"
# 创建必要目录
os.makedirs(REPORT_DIR, exist_ok=True)
os.makedirs(EXCEL_DIR, exist_ok=True)
class APIClient:
"""API客户端"""
def __init__(self, base_url: str):
self.base_url = base_url
self.token = None
self.session = requests.Session()
def login(self, username: str, password: str) -> bool:
"""登录获取token"""
try:
response = self.session.post(
LOGIN_URL,
json={"username": username, "password": password},
timeout=10
)
result = response.json()
if result.get("code") == 200:
self.token = result.get("data")
print(f"✓ 登录成功, Token: {self.token[:20]}...")
return True
else:
print(f"✗ 登录失败: {result.get('msg')}")
return False
except Exception as e:
print(f"✗ 登录异常: {str(e)}")
return False
def get_headers(self) -> Dict:
"""获取请求头"""
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
def upload_file(self, url: str, file_path: str) -> Dict:
"""上传文件"""
try:
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')}
headers = {"Authorization": f"Bearer {self.token}"}
response = self.session.post(url, files=files, headers=headers, timeout=30)
return response.json()
except Exception as e:
return {"code": 500, "msg": f"上传失败: {str(e)}"}
def get_import_status(self, url: str) -> Dict:
"""查询导入状态"""
try:
response = self.session.get(url, headers=self.get_headers(), timeout=10)
return response.json()
except Exception as e:
return {"code": 500, "msg": f"查询状态失败: {str(e)}"}
def get_import_failures(self, url: str) -> Dict:
"""查询导入失败记录"""
try:
response = self.session.get(url, headers=self.get_headers(), timeout=10)
return response.json()
except Exception as e:
return {"code": 500, "msg": f"查询失败记录失败: {str(e)}"}
class ExcelGenerator:
"""Excel测试数据生成器"""
@staticmethod
def create_purchase_duplicate_data() -> str:
"""
场景1: Excel内采购事项ID重复
3条记录,采购事项ID都是 PURCHASE001
"""
wb = Workbook()
ws = wb.active
ws.title = "采购交易重复测试"
# 表头
headers = [
"采购事项ID", "采购类别", "项目名称", "标的物名称", "标的物描述",
"采购数量", "预算金额", "采购方式", "采购申请日期",
"申请人工号", "申请人姓名", "申请部门"
]
ws.append(headers)
# 测试数据 - 3条相同采购事项ID
base_date = datetime.now()
for i in range(3):
apply_date = base_date + timedelta(days=i)
ws.append([
f"PURCHASE001", # 相同的采购事项ID
f"采购类别{i+1}",
f"项目名称{i+1}",
f"标的物名称{i+1}",
f"标的物描述{i+1}",
10 + i,
10000.00 + i * 1000,
"公开招标",
apply_date.strftime("%Y-%m-%d"),
"1000001",
"张三",
"技术部"
])
file_path = os.path.join(EXCEL_DIR, "purchase_duplicate.xlsx")
wb.save(file_path)
return file_path
@staticmethod
def create_employee_employee_id_duplicate() -> str:
"""
场景2: Excel内员工柜员号重复
3条记录,柜员号都是 10001,身份证号不同
"""
wb = Workbook()
ws = wb.active
ws.title = "员工柜员号重复测试"
# 表头
headers = ["姓名", "柜员号", "所属部门ID", "身份证号", "电话", "入职时间", "状态"]
ws.append(headers)
# 测试数据 - 3条相同柜员号
for i in range(3):
ws.append([
f"员工{i+1}",
10001, # 相同的柜员号
103,
f"110101199001011{234+i}", # 不同的身份证号
f"1380000000{i}",
"2024-01-01",
"0"
])
file_path = os.path.join(EXCEL_DIR, "employee_employee_id_duplicate.xlsx")
wb.save(file_path)
return file_path
@staticmethod
def create_employee_id_card_duplicate() -> str:
"""
场景3: Excel内员工身份证号重复
3条记录,柜员号不同,身份证号相同
"""
wb = Workbook()
ws = wb.active
ws.title = "员工身份证号重复测试"
# 表头
headers = ["姓名", "柜员号", "所属部门ID", "身份证号", "电话", "入职时间", "状态"]
ws.append(headers)
# 测试数据 - 3条相同身份证号
for i in range(3):
ws.append([
f"员工{i+1}",
10001 + i, # 不同的柜员号
103,
"110101199001011234", # 相同的身份证号
f"1380000000{i}",
"2024-01-01",
"0"
])
file_path = os.path.join(EXCEL_DIR, "employee_id_card_duplicate.xlsx")
wb.save(file_path)
return file_path
@staticmethod
def create_mixed_duplicate_scenario() -> Tuple[str, str]:
"""
场景4: 混合重复(数据库+Excel)
- 第1条: 数据库中已存在
- 第2条: 全新数据
- 第3条: 与第2条Excel内重复
- 第4条: 全新数据
"""
# 采购交易混合重复数据
wb_purchase = Workbook()
ws_purchase = wb_purchase.active
ws_purchase.title = "采购混合重复测试"
headers = [
"采购事项ID", "采购类别", "项目名称", "标的物名称", "标的物描述",
"采购数量", "预算金额", "采购方式", "采购申请日期",
"申请人工号", "申请人姓名", "申请部门"
]
ws_purchase.append(headers)
base_date = datetime.now()
# 第1条: 数据库中已存在(需要先手动插入数据库)
ws_purchase.append([
"EXIST001", # 假设数据库中已存在
"采购类别1",
"项目名称1",
"标的物名称1",
"标的物描述1",
10,
10000.00,
"公开招标",
base_date.strftime("%Y-%m-%d"),
"1000001",
"张三",
"技术部"
])
# 第2条: 全新数据
ws_purchase.append([
"NEW001", # 新的采购事项ID
"采购类别2",
"项目名称2",
"标的物名称2",
"标的物描述2",
20,
20000.00,
"邀请招标",
(base_date + timedelta(days=1)).strftime("%Y-%m-%d"),
"1000002",
"李四",
"市场部"
])
# 第3条: 与第2条Excel内重复
ws_purchase.append([
"NEW001", # 与第2条重复
"采购类别3",
"项目名称3",
"标的物名称3",
"标的物描述3",
30,
30000.00,
"竞争性谈判",
(base_date + timedelta(days=2)).strftime("%Y-%m-%d"),
"1000003",
"王五",
"财务部"
])
# 第4条: 全新数据
ws_purchase.append([
"NEW002", # 新的采购事项ID
"采购类别4",
"项目名称4",
"标的物名称4",
"标的物描述4",
40,
40000.00,
"单一来源",
(base_date + timedelta(days=3)).strftime("%Y-%m-%d"),
"1000004",
"赵六",
"人事部"
])
purchase_file = os.path.join(EXCEL_DIR, "purchase_mixed_duplicate.xlsx")
wb_purchase.save(purchase_file)
# 员工混合重复数据
wb_employee = Workbook()
ws_employee = wb_employee.active
ws_employee.title = "员工混合重复测试"
headers = ["姓名", "柜员号", "所属部门ID", "身份证号", "电话", "入职时间", "状态"]
ws_employee.append(headers)
# 第1条: 数据库中已存在(假设柜员号99999已存在)
ws_employee.append([
"已存在员工",
99999, # 假设数据库中已存在
103,
"110101199001019999",
"13900000000",
"2024-01-01",
"0"
])
# 第2条: 全新数据
ws_employee.append([
"新员工1",
90001, # 新柜员号
103,
"110101199001011111",
"13800000001",
"2024-01-01",
"0"
])
# 第3条: 与第2条Excel内重复(柜员号重复)
ws_employee.append([
"新员工2",
90001, # 与第2条柜员号重复
103,
"110101199001012222",
"13800000002",
"2024-01-01",
"0"
])
# 第4条: 全新数据
ws_employee.append([
"新员工3",
90002, # 新柜员号
103,
"110101199001013333",
"13800000003",
"2024-01-01",
"0"
])
employee_file = os.path.join(EXCEL_DIR, "employee_mixed_duplicate.xlsx")
wb_employee.save(employee_file)
return purchase_file, employee_file
class TestCase:
"""测试用例基类"""
def __init__(self, name: str, description: str):
self.name = name
self.description = description
self.start_time = None
self.end_time = None
self.passed = False
self.error_message = None
self.details = {}
def run(self, client: APIClient):
"""运行测试用例"""
raise NotImplementedError
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"name": self.name,
"description": self.description,
"passed": self.passed,
"error_message": self.error_message,
"details": self.details,
"duration": f"{(self.end_time - self.start_time).total_seconds():.2f}s" if self.start_time and self.end_time else "N/A"
}
class PurchaseDuplicateTestCase(TestCase):
"""场景1: Excel内采购事项ID重复测试"""
def __init__(self):
super().__init__(
"采购交易 - Excel内采购事项ID重复",
"测试导入3条采购事项ID相同的记录,预期第1条成功,第2、3条失败"
)
def run(self, client: APIClient):
self.start_time = datetime.now()
try:
# 生成测试数据
file_path = ExcelGenerator.create_purchase_duplicate_data()
print(f" ✓ 生成测试数据: {file_path}")
# 上传文件
upload_url = f"{BASE_URL}/ccdi/purchaseTransaction/importData"
upload_result = client.upload_file(upload_url, file_path)
if upload_result.get("code") != 200:
self.error_message = f"上传失败: {upload_result.get('msg')}"
self.end_time = datetime.now()
return
task_id = upload_result.get("data", {}).get("taskId")
print(f" ✓ 上传成功, TaskID: {task_id}")
# 等待异步任务完成
time.sleep(3)
# 查询导入状态
status_url = f"{BASE_URL}/ccdi/purchaseTransaction/importStatus/{task_id}"
status_result = client.get_import_status(status_url)
if status_result.get("code") != 200:
self.error_message = f"查询状态失败: {status_result.get('msg')}"
self.end_time = datetime.now()
return
status_data = status_result.get("data", {})
print(f" ✓ 导入状态: {status_data}")
# 查询失败记录
failures_url = f"{BASE_URL}/ccdi/purchaseTransaction/importFailures/{task_id}"
failures_result = client.get_import_failures(failures_url)
if failures_result.get("code") != 200:
self.error_message = f"查询失败记录失败: {failures_result.get('msg')}"
self.end_time = datetime.now()
return
failures = failures_result.get("rows", [])
# 验证结果
# 预期: 成功1条,失败2条
expected_success = 1
expected_failure = 2
actual_success = status_data.get("successCount", 0)
actual_failure = status_data.get("failureCount", 0)
self.details = {
"expected_success": expected_success,
"expected_failure": expected_failure,
"actual_success": actual_success,
"actual_failure": actual_failure,
"failures": failures
}
# 验证成功/失败数量
if actual_success != expected_success or actual_failure != expected_failure:
self.error_message = f"数量不匹配: 预期成功{expected_success}失败{expected_failure}, 实际成功{actual_success}失败{actual_failure}"
self.end_time = datetime.now()
return
# 验证失败消息
if len(failures) < 2:
self.error_message = f"失败记录数量不足: 预期2条, 实际{len(failures)}"
self.end_time = datetime.now()
return
# 检查失败消息是否包含"在导入文件中重复"
error_msg_1 = failures[0].get("errorMessage", "")
error_msg_2 = failures[1].get("errorMessage", "")
if "在导入文件中重复" not in error_msg_1 or "在导入文件中重复" not in error_msg_2:
self.error_message = f"错误消息不正确: {error_msg_1}, {error_msg_2}"
self.end_time = datetime.now()
return
self.passed = True
print(f" ✓ 测试通过")
except Exception as e:
self.error_message = f"测试异常: {str(e)}"
print(f" ✗ 测试异常: {str(e)}")
self.end_time = datetime.now()
class EmployeeEmployeeIdDuplicateTestCase(TestCase):
"""场景2: Excel内员工柜员号重复测试"""
def __init__(self):
super().__init__(
"员工信息 - Excel内柜员号重复",
"测试导入3条柜员号相同的记录,预期第1条成功,第2、3条失败"
)
def run(self, client: APIClient):
self.start_time = datetime.now()
try:
# 生成测试数据
file_path = ExcelGenerator.create_employee_employee_id_duplicate()
print(f" ✓ 生成测试数据: {file_path}")
# 上传文件
upload_url = f"{BASE_URL}/ccdi/employee/importData"
upload_result = client.upload_file(upload_url, file_path)
if upload_result.get("code") != 200:
self.error_message = f"上传失败: {upload_result.get('msg')}"
self.end_time = datetime.now()
return
task_id = upload_result.get("data", {}).get("taskId")
print(f" ✓ 上传成功, TaskID: {task_id}")
# 等待异步任务完成
time.sleep(3)
# 查询导入状态
status_url = f"{BASE_URL}/ccdi/employee/importStatus/{task_id}"
status_result = client.get_import_status(status_url)
if status_result.get("code") != 200:
self.error_message = f"查询状态失败: {status_result.get('msg')}"
self.end_time = datetime.now()
return
status_data = status_result.get("data", {})
print(f" ✓ 导入状态: {status_data}")
# 查询失败记录
failures_url = f"{BASE_URL}/ccdi/employee/importFailures/{task_id}"
failures_result = client.get_import_failures(failures_url)
if failures_result.get("code") != 200:
self.error_message = f"查询失败记录失败: {failures_result.get('msg')}"
self.end_time = datetime.now()
return
failures = failures_result.get("rows", [])
# 验证结果
expected_success = 1
expected_failure = 2
actual_success = status_data.get("successCount", 0)
actual_failure = status_data.get("failureCount", 0)
self.details = {
"expected_success": expected_success,
"expected_failure": expected_failure,
"actual_success": actual_success,
"actual_failure": actual_failure,
"failures": failures
}
if actual_success != expected_success or actual_failure != expected_failure:
self.error_message = f"数量不匹配: 预期成功{expected_success}失败{expected_failure}, 实际成功{actual_success}失败{actual_failure}"
self.end_time = datetime.now()
return
if len(failures) < 2:
self.error_message = f"失败记录数量不足: 预期2条, 实际{len(failures)}"
self.end_time = datetime.now()
return
# 验证失败消息
error_msg_1 = failures[0].get("errorMessage", "")
error_msg_2 = failures[1].get("errorMessage", "")
if "柜员号" not in error_msg_1 or "在导入文件中重复" not in error_msg_1:
self.error_message = f"错误消息不正确(第1条): {error_msg_1}"
self.end_time = datetime.now()
return
if "柜员号" not in error_msg_2 or "在导入文件中重复" not in error_msg_2:
self.error_message = f"错误消息不正确(第2条): {error_msg_2}"
self.end_time = datetime.now()
return
self.passed = True
print(f" ✓ 测试通过")
except Exception as e:
self.error_message = f"测试异常: {str(e)}"
print(f" ✗ 测试异常: {str(e)}")
self.end_time = datetime.now()
class EmployeeIdCardDuplicateTestCase(TestCase):
"""场景3: Excel内员工身份证号重复测试"""
def __init__(self):
super().__init__(
"员工信息 - Excel内身份证号重复",
"测试导入3条身份证号相同的记录,预期第1条成功,第2、3条失败"
)
def run(self, client: APIClient):
self.start_time = datetime.now()
try:
# 生成测试数据
file_path = ExcelGenerator.create_employee_id_card_duplicate()
print(f" ✓ 生成测试数据: {file_path}")
# 上传文件
upload_url = f"{BASE_URL}/ccdi/employee/importData"
upload_result = client.upload_file(upload_url, file_path)
if upload_result.get("code") != 200:
self.error_message = f"上传失败: {upload_result.get('msg')}"
self.end_time = datetime.now()
return
task_id = upload_result.get("data", {}).get("taskId")
print(f" ✓ 上传成功, TaskID: {task_id}")
# 等待异步任务完成
time.sleep(3)
# 查询导入状态
status_url = f"{BASE_URL}/ccdi/employee/importStatus/{task_id}"
status_result = client.get_import_status(status_url)
if status_result.get("code") != 200:
self.error_message = f"查询状态失败: {status_result.get('msg')}"
self.end_time = datetime.now()
return
status_data = status_result.get("data", {})
print(f" ✓ 导入状态: {status_data}")
# 查询失败记录
failures_url = f"{BASE_URL}/ccdi/employee/importFailures/{task_id}"
failures_result = client.get_import_failures(failures_url)
if failures_result.get("code") != 200:
self.error_message = f"查询失败记录失败: {failures_result.get('msg')}"
self.end_time = datetime.now()
return
failures = failures_result.get("rows", [])
# 验证结果
expected_success = 1
expected_failure = 2
actual_success = status_data.get("successCount", 0)
actual_failure = status_data.get("failureCount", 0)
self.details = {
"expected_success": expected_success,
"expected_failure": expected_failure,
"actual_success": actual_success,
"actual_failure": actual_failure,
"failures": failures
}
if actual_success != expected_success or actual_failure != expected_failure:
self.error_message = f"数量不匹配: 预期成功{expected_success}失败{expected_failure}, 实际成功{actual_success}失败{actual_failure}"
self.end_time = datetime.now()
return
if len(failures) < 2:
self.error_message = f"失败记录数量不足: 预期2条, 实际{len(failures)}"
self.end_time = datetime.now()
return
# 验证失败消息
error_msg_1 = failures[0].get("errorMessage", "")
error_msg_2 = failures[1].get("errorMessage", "")
if "身份证号" not in error_msg_1 or "在导入文件中重复" not in error_msg_1:
self.error_message = f"错误消息不正确(第1条): {error_msg_1}"
self.end_time = datetime.now()
return
if "身份证号" not in error_msg_2 or "在导入文件中重复" not in error_msg_2:
self.error_message = f"错误消息不正确(第2条): {error_msg_2}"
self.end_time = datetime.now()
return
self.passed = True
print(f" ✓ 测试通过")
except Exception as e:
self.error_message = f"测试异常: {str(e)}"
print(f" ✗ 测试异常: {str(e)}")
self.end_time = datetime.now()
class MixedDuplicateTestCase(TestCase):
"""场景4: 混合重复(数据库+Excel)测试"""
def __init__(self):
super().__init__(
"混合重复 - 数据库+Excel重复",
"测试数据库已存在+Excel内重复的混合场景"
)
def run(self, client: APIClient):
self.start_time = datetime.now()
try:
# 生成测试数据
purchase_file, employee_file = ExcelGenerator.create_mixed_duplicate_scenario()
print(f" ✓ 生成测试数据: {purchase_file}, {employee_file}")
# 测试采购交易
print("\n >> 测试采购交易混合重复")
purchase_upload_url = f"{BASE_URL}/ccdi/purchaseTransaction/importData"
purchase_upload_result = client.upload_file(purchase_upload_url, purchase_file)
purchase_passed = False
purchase_details = {}
if purchase_upload_result.get("code") == 200:
purchase_task_id = purchase_upload_result.get("data", {}).get("taskId")
print(f" ✓ 采购交易上传成功, TaskID: {purchase_task_id}")
time.sleep(3)
# 查询导入状态
purchase_status_url = f"{BASE_URL}/ccdi/purchaseTransaction/importStatus/{purchase_task_id}"
purchase_status_result = client.get_import_status(purchase_status_url)
if purchase_status_result.get("code") == 200:
purchase_status_data = purchase_status_result.get("data", {})
# 查询失败记录
purchase_failures_url = f"{BASE_URL}/ccdi/purchaseTransaction/importFailures/{purchase_task_id}"
purchase_failures_result = client.get_import_failures(purchase_failures_url)
if purchase_failures_result.get("code") == 200:
purchase_failures = purchase_failures_result.get("rows", [])
purchase_details = {
"success_count": purchase_status_data.get("successCount", 0),
"failure_count": purchase_status_data.get("failureCount", 0),
"failures": purchase_failures
}
# 验证: 第1条失败(数据库重复), 第2条成功, 第3条失败(Excel内重复), 第4条成功
# 预期: 成功2条,失败2条
if purchase_status_data.get("successCount") == 2 and purchase_status_data.get("failureCount") == 2:
purchase_passed = True
print(f" ✓ 采购交易测试通过: 成功2条,失败2条")
else:
print(f" ✗ 采购交易测试失败: 预期成功2失败2, 实际成功{purchase_status_data.get('successCount')}失败{purchase_status_data.get('failureCount')}")
# 测试员工信息
print("\n >> 测试员工信息混合重复")
employee_upload_url = f"{BASE_URL}/ccdi/employee/importData"
employee_upload_result = client.upload_file(employee_upload_url, employee_file)
employee_passed = False
employee_details = {}
if employee_upload_result.get("code") == 200:
employee_task_id = employee_upload_result.get("data", {}).get("taskId")
print(f" ✓ 员工信息上传成功, TaskID: {employee_task_id}")
time.sleep(3)
# 查询导入状态
employee_status_url = f"{BASE_URL}/ccdi/employee/importStatus/{employee_task_id}"
employee_status_result = client.get_import_status(employee_status_url)
if employee_status_result.get("code") == 200:
employee_status_data = employee_status_result.get("data", {})
# 查询失败记录
employee_failures_url = f"{BASE_URL}/ccdi/employee/importFailures/{employee_task_id}"
employee_failures_result = client.get_import_failures(employee_failures_url)
if employee_failures_result.get("code") == 200:
employee_failures = employee_failures_result.get("rows", [])
employee_details = {
"success_count": employee_status_data.get("successCount", 0),
"failure_count": employee_status_data.get("failureCount", 0),
"failures": employee_failures
}
# 验证: 第1条失败(数据库重复), 第2条成功, 第3条失败(Excel内重复), 第4条成功
# 预期: 成功2条,失败2条
if employee_status_data.get("successCount") == 2 and employee_status_data.get("failureCount") == 2:
employee_passed = True
print(f" ✓ 员工信息测试通过: 成功2条,失败2条")
else:
print(f" ✗ 员工信息测试失败: 预期成功2失败2, 实际成功{employee_status_data.get('successCount')}失败{employee_status_data.get('failureCount')}")
self.details = {
"purchase": {
"passed": purchase_passed,
"details": purchase_details
},
"employee": {
"passed": employee_passed,
"details": employee_details
}
}
# 至少一个通过则认为测试通过(因为数据库可能不存在预置数据)
self.passed = purchase_passed or employee_passed
if self.passed:
print(f" ✓ 测试通过")
except Exception as e:
self.error_message = f"测试异常: {str(e)}"
print(f" ✗ 测试异常: {str(e)}")
self.end_time = datetime.now()
class TestRunner:
"""测试运行器"""
def __init__(self):
self.client = APIClient(BASE_URL)
self.test_cases: List[TestCase] = []
self.results = []
def add_test_case(self, test_case: TestCase):
"""添加测试用例"""
self.test_cases.append(test_case)
def run_all(self):
"""运行所有测试用例"""
print("=" * 80)
print("导入文件内部主键重复检测功能测试")
print("=" * 80)
print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"测试环境: {BASE_URL}")
print("=" * 80)
# 登录
print("\n[1/2] 登录系统...")
if not self.client.login(USERNAME, PASSWORD):
print("✗ 登录失败,测试终止")
return
# 运行测试
print("\n[2/2] 运行测试用例...")
print("-" * 80)
for i, test_case in enumerate(self.test_cases, 1):
print(f"\n测试用例 {i}/{len(self.test_cases)}: {test_case.name}")
print(f"描述: {test_case.description}")
print("-" * 80)
test_case.run(self.client)
self.results.append(test_case.to_dict())
# 生成报告
self.generate_report()
def generate_report(self):
"""生成测试报告"""
print("\n" + "=" * 80)
print("测试报告")
print("=" * 80)
passed_count = sum(1 for r in self.results if r["passed"])
failed_count = len(self.results) - passed_count
print(f"\n总测试用例数: {len(self.results)}")
print(f"通过: {passed_count}")
print(f"失败: {failed_count}")
print(f"通过率: {passed_count / len(self.results) * 100:.1f}%")
print("\n详细结果:")
print("-" * 80)
for i, result in enumerate(self.results, 1):
status = "✓ PASS" if result["passed"] else "✗ FAIL"
print(f"\n{i}. {result['name']}")
print(f" 状态: {status}")
print(f" 耗时: {result['duration']}")
if not result["passed"]:
print(f" 错误: {result['error_message']}")
if result["details"]:
print(f" 详情: {json.dumps(result['details'], ensure_ascii=False, indent=6)}")
# 保存报告到文件
report_file = os.path.join(REPORT_DIR, f"test_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json")
with open(report_file, 'w', encoding='utf-8') as f:
json.dump({
"test_time": datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
"environment": BASE_URL,
"total_count": len(self.results),
"passed_count": passed_count,
"failed_count": failed_count,
"pass_rate": f"{passed_count / len(self.results) * 100:.1f}%",
"results": self.results
}, f, ensure_ascii=False, indent=2)
print(f"\n报告已保存到: {report_file}")
print("=" * 80)
def main():
"""主函数"""
runner = TestRunner()
# 添加测试用例
runner.add_test_case(PurchaseDuplicateTestCase())
runner.add_test_case(EmployeeEmployeeIdDuplicateTestCase())
runner.add_test_case(EmployeeIdCardDuplicateTestCase())
runner.add_test_case(MixedDuplicateTestCase())
# 运行所有测试
try:
runner.run_all()
except KeyboardInterrupt:
print("\n\n测试被用户中断")
except Exception as e:
print(f"\n\n测试运行异常: {str(e)}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()

View File

@@ -1,258 +0,0 @@
# 导入文件内部主键重复检测功能测试用例
## 测试目的
验证导入功能能够正确检测并处理Excel文件内部的主键重复数据,确保:
1. 同一Excel文件内重复的主键只会导入第一条,后续重复记录会被跳过
2. 提供清晰的错误提示信息
3. 正确区分数据库重复和Excel内重复
## 测试范围
### 1. 采购交易导入
- **主键字段**: purchaseId (采购事项ID)
- **接口**: POST /ccdi/purchaseTransaction/importData
- **状态查询**: GET /ccdi/purchaseTransaction/importStatus/{taskId}
- **失败记录**: GET /ccdi/purchaseTransaction/importFailures/{taskId}
### 2. 员工信息导入
- **主键字段**:
- employeeId (柜员号)
- idCard (身份证号)
- **接口**: POST /ccdi/employee/importData
- **状态查询**: GET /ccdi/employee/importStatus/{taskId}
- **失败记录**: GET /ccdi/employee/importFailures/{taskId}
## 测试场景
### 场景1: Excel内采购事项ID重复
**测试用例ID**: TEST-PURCHASE-001
**测试目的**: 验证采购交易导入时Excel内采购事项ID重复的检测
**测试数据**:
```
采购事项ID 采购类别 标的物名称 采购数量 预算金额 采购方式 申请人
PURCHASE001 类别1 标的物1 10 10000 公开招标 张三
PURCHASE001 类别2 标的物2 20 20000 邀请招标 李四
PURCHASE001 类别3 标的物3 30 30000 竞争性谈判 王五
```
**测试步骤**:
1. 生成包含3条采购事项ID相同的Excel文件
2. 调用采购交易导入接口上传文件
3. 等待3秒让异步任务完成
4. 查询导入状态
5. 查询导入失败记录
**预期结果**:
- 导入状态: PARTIAL_SUCCESS (部分成功)
- 成功数量: 1 (第1条)
- 失败数量: 2 (第2、3条)
- 失败记录:
- 第1条失败记录: 错误消息包含 "采购事项ID[PURCHASE001]在导入文件中重复,已跳过此条记录"
- 第2条失败记录: 错误消息包含 "采购事项ID[PURCHASE001]在导入文件中重复,已跳过此条记录"
**实际结果**: (待测试)
**测试结论**: (待测试)
---
### 场景2: Excel内员工柜员号重复
**测试用例ID**: TEST-EMPLOYEE-001
**测试目的**: 验证员工信息导入时Excel内柜员号重复的检测
**测试数据**:
```
姓名 柜员号 所属部门ID 身份证号 电话 入职时间 状态
员工1 10001 103 110101199001011234 13800000000 2024-01-01 0
员工2 10001 103 110101199001011235 13800000001 2024-01-01 0
员工3 10001 103 110101199001011236 13800000002 2024-01-01 0
```
**测试步骤**:
1. 生成包含3条柜员号相同的Excel文件
2. 调用员工信息导入接口上传文件
3. 等待3秒让异步任务完成
4. 查询导入状态
5. 查询导入失败记录
**预期结果**:
- 导入状态: PARTIAL_SUCCESS (部分成功)
- 成功数量: 1 (第1条)
- 失败数量: 2 (第2、3条)
- 失败记录:
- 第1条失败记录: 错误消息包含 "柜员号[10001]在导入文件中重复,已跳过此条记录"
- 第2条失败记录: 错误消息包含 "柜员号[10001]在导入文件中重复,已跳过此条记录"
**实际结果**: (待测试)
**测试结论**: (待测试)
---
### 场景3: Excel内员工身份证号重复
**测试用例ID**: TEST-EMPLOYEE-002
**测试目的**: 验证员工信息导入时Excel内身份证号重复的检测
**测试数据**:
```
姓名 柜员号 所属部门ID 身份证号 电话 入职时间 状态
员工1 10001 103 110101199001011234 13800000000 2024-01-01 0
员工2 10002 103 110101199001011234 13800000001 2024-01-01 0
员工3 10003 103 110101199001011234 13800000002 2024-01-01 0
```
**测试步骤**:
1. 生成包含3条身份证号相同的Excel文件
2. 调用员工信息导入接口上传文件
3. 等待3秒让异步任务完成
4. 查询导入状态
5. 查询导入失败记录
**预期结果**:
- 导入状态: PARTIAL_SUCCESS (部分成功)
- 成功数量: 1 (第1条)
- 失败数量: 2 (第2、3条)
- 失败记录:
- 第1条失败记录: 错误消息包含 "身份证号[110101199001011234]在导入文件中重复,已跳过此条记录"
- 第2条失败记录: 错误消息包含 "身份证号[110101199001011234]在导入文件中重复,已跳过此条记录"
**实际结果**: (待测试)
**测试结论**: (待测试)
---
### 场景4: 混合重复(数据库+Excel)
**测试用例ID**: TEST-MIXED-001
**测试目的**: 验证数据库已存在记录和Excel内重复记录的混合场景
**测试数据**:
#### 采购交易
```
采购事项ID 采购类别 标的物名称 采购数量 预算金额 采购方式 申请人
EXIST001 类别1 标的物1 10 10000 公开招标 张三 (数据库已存在)
NEW001 类别2 标的物2 20 20000 邀请招标 李四 (全新数据)
NEW001 类别3 标的物3 30 30000 竞争性谈判 王五 (Excel内与第2条重复)
NEW002 类别4 标的物4 40 40000 单一来源 赵六 (全新数据)
```
#### 员工信息
```
姓名 柜员号 所属部门ID 身份证号 电话 入职时间 状态
已存在员工 99999 103 110101199001019999 13900000000 2024-01-01 0 (数据库已存在)
新员工1 90001 103 110101199001011111 13800000001 2024-01-01 0 (全新数据)
新员工2 90001 103 110101199001012222 13800000002 2024-01-01 0 (Excel内与第2条重复)
新员工3 90002 103 110101199001013333 13800000003 2024-01-01 0 (全新数据)
```
**前置条件**:
- 数据库中已存在采购事项ID为 EXIST001 的记录
- 数据库中已存在柜员号为 99999 的员工记录
**测试步骤**:
1. 生成测试数据Excel文件
2. 分别调用采购交易和员工信息导入接口
3. 等待3秒让异步任务完成
4. 查询导入状态
5. 查询导入失败记录
**预期结果**:
#### 采购交易
- 导入状态: PARTIAL_SUCCESS
- 成功数量: 2 (NEW001, NEW002)
- 失败数量: 2
- 失败记录:
- 第1条: 错误消息包含 "采购事项ID[EXIST001]已存在,请勿重复导入"
- 第2条: 错误消息包含 "采购事项ID[NEW001]在导入文件中重复,已跳过此条记录"
#### 员工信息
- 导入状态: PARTIAL_SUCCESS
- 成功数量: 2 (90001, 90002)
- 失败数量: 2
- 失败记录:
- 第1条: 错误消息包含 "柜员号已存在且未启用更新支持" 或 "该柜员号已存在"
- 第2条: 错误消息包含 "柜员号[90001]在导入文件中重复,已跳过此条记录"
**实际结果**: (待测试)
**测试结论**: (待测试)
---
## 测试注意事项
### 1. 异步处理
- 导入功能采用异步处理,需要等待一段时间(建议3-5秒)后再查询状态
- 导入状态可能经历 PROCESSING -> SUCCESS/PARTIAL_SUCCESS 的变化
### 2. 错误消息格式
- 数据库重复: "采购事项ID[xxx]已存在,请勿重复导入"
- Excel内重复: "采购事项ID[xxx]在导入文件中重复,已跳过此条记录"
- 员工柜员号重复: "柜员号[xxx]在导入文件中重复,已跳过此条记录"
- 员工身份证号重复: "身份证号[xxx]在导入文件中重复,已跳过此条记录"
### 3. 数据准备
- 场景4需要提前在数据库中插入测试数据
- 如果数据库中不存在预置数据,该场景可能不会完全按预期执行
### 4. 清理工作
- 测试完成后需要清理测试数据,避免影响后续测试
- 可以通过删除接口或直接清理数据库
### 5. 权限要求
- 需要登录并有导入权限: `ccdi:purchaseTransaction:import``ccdi:employee:import`
- 测试账号: admin / admin123
## 测试执行
### 自动化测试
使用Python测试脚本:
```bash
python doc/test-scripts/test_import_duplicate_detection.py
```
### 手动测试
1. 登录系统: http://localhost:8080/login
2. 进入采购交易/员工信息管理页面
3. 点击"导入"按钮
4. 选择测试Excel文件
5. 上传并查看导入结果
6. 点击"查看失败记录"查看详细错误信息
## 测试报告
测试报告将保存在: `doc/test-reports/test_report_YYYYMMDD_HHMMSS.json`
报告包含:
- 测试时间
- 测试环境
- 总测试用例数
- 通过/失败数量
- 通过率
- 详细测试结果(包括输入数据、预期结果、实际结果)
## 相关代码
### 后端实现
- 采购交易导入: `CcdiPurchaseTransactionImportServiceImpl.java`
- 员工信息导入: `CcdiEmployeeImportServiceImpl.java`
### Controller接口
- 采购交易: `CcdiPurchaseTransactionController.java`
- 员工信息: `CcdiEmployeeController.java`
### Excel实体
- 采购交易: `CcdiPurchaseTransactionExcel.java`
- 员工信息: `CcdiEmployeeExcel.java`

View File

@@ -1,593 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>员工导入状态持久化功能测试</title>
<style>
body {
font-family: 'Courier New', monospace;
max-width: 1200px;
margin: 20px auto;
padding: 20px;
background-color: #f5f5f5;
}
.test-container {
background: white;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 3px solid #409eff;
padding-bottom: 10px;
}
h2 {
color: #666;
margin-top: 30px;
}
.test-section {
margin: 20px 0;
padding: 15px;
border-left: 4px solid #409eff;
background: #f9f9f9;
}
.status-pass {
color: #67c23a;
font-weight: bold;
}
.status-fail {
color: #f56c6c;
font-weight: bold;
}
.status-info {
color: #909399;
}
.code {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
font-size: 13px;
line-height: 1.5;
}
.summary {
background: #e6f7ff;
border: 2px solid #1890ff;
border-radius: 8px;
padding: 20px;
margin: 30px 0;
}
.summary h3 {
margin-top: 0;
color: #1890ff;
}
button {
background: #409eff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin: 5px;
}
button:hover {
background: #66b1ff;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.log {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 4px;
max-height: 400px;
overflow-y: auto;
font-size: 12px;
line-height: 1.4;
}
.log-entry {
margin: 5px 0;
}
.log-success { color: #67c23a; }
.log-error { color: #f56c6c; }
.log-warning { color: #e6a23c; }
.log-info { color: #909399; }
</style>
</head>
<body>
<div class="test-container">
<h1>员工导入状态持久化功能 - 测试套件</h1>
<div style="margin: 20px 0;">
<button id="runAllTests" onclick="runAllTests()">运行所有测试</button>
<button onclick="clearResults()">清除结果</button>
<button onclick="clearLocalStorage()">清除localStorage</button>
</div>
<div id="log" class="log">
<div class="log-entry log-info">点击"运行所有测试"按钮开始测试...</div>
</div>
<div id="results"></div>
</div>
<script>
const BASE_URL = 'http://localhost:8080';
let authToken = '';
function log(message, type = 'info') {
const logDiv = document.getElementById('log');
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logDiv.appendChild(entry);
logDiv.scrollTop = logDiv.scrollHeight;
}
function clearResults() {
document.getElementById('results').innerHTML = '';
document.getElementById('log').innerHTML = '<div class="log-entry log-info">日志已清除</div>';
}
function clearLocalStorage() {
localStorage.removeItem('employee_import_last_task');
log('localStorage已清除', 'info');
}
function formatJSON(obj) {
return JSON.stringify(obj, null, 2);
}
// 模拟后端ImportStatusVO返回的数据
function simulateImportSuccess() {
log('=== 测试1: 模拟导入成功场景 ===', 'info');
const mockSuccessResult = {
taskId: 'task_' + Date.now(),
status: 'SUCCESS',
totalCount: 100,
successCount: 100,
failureCount: 0,
progress: 100,
message: '导入完成'
};
log('模拟后端返回数据: ' + formatJSON(mockSuccessResult), 'info');
// 模拟前端saveImportTaskToStorage方法
const taskData = {
taskId: mockSuccessResult.taskId,
status: mockSuccessResult.status,
hasFailures: mockSuccessResult.failureCount > 0,
totalCount: mockSuccessResult.totalCount,
successCount: mockSuccessResult.successCount,
failureCount: mockSuccessResult.failureCount,
saveTime: Date.now()
};
localStorage.setItem('employee_import_last_task', JSON.stringify(taskData));
log('✅ 已保存到localStorage', 'success');
log('保存的数据: ' + formatJSON(taskData), 'info');
return mockSuccessResult;
}
function simulateImportWithFailures() {
log('=== 测试2: 模拟导入部分失败场景 ===', 'info');
const mockFailureResult = {
taskId: 'task_' + Date.now(),
status: 'SUCCESS',
totalCount: 100,
successCount: 95,
failureCount: 5,
progress: 100,
message: '导入完成'
};
log('模拟后端返回数据: ' + formatJSON(mockFailureResult), 'info');
const taskData = {
taskId: mockFailureResult.taskId,
status: mockFailureResult.status,
hasFailures: mockFailureResult.failureCount > 0,
totalCount: mockFailureResult.totalCount,
successCount: mockFailureResult.successCount,
failureCount: mockFailureResult.failureCount,
saveTime: Date.now()
};
localStorage.setItem('employee_import_last_task', JSON.stringify(taskData));
log('✅ 已保存到localStorage包含失败记录', 'success');
log('保存的数据: ' + formatJSON(taskData), 'info');
return mockFailureResult;
}
function verifyStorageData() {
log('=== 测试3: 验证localStorage数据 ===', 'info');
try {
const data = localStorage.getItem('employee_import_last_task');
if (!data) {
log('❌ localStorage中没有找到导入任务数据', 'error');
return null;
}
const task = JSON.parse(data);
log('✅ 成功读取localStorage数据', 'success');
log('读取的数据: ' + formatJSON(task), 'info');
// 验证必要字段
const requiredFields = ['taskId', 'status', 'hasFailures', 'totalCount', 'successCount', 'failureCount', 'saveTime'];
const missingFields = requiredFields.filter(field => !(field in task));
if (missingFields.length > 0) {
log('❌ 缺少必要字段: ' + missingFields.join(', '), 'error');
return null;
}
log('✅ 所有必要字段都存在', 'success');
// 验证字段类型
const typeChecks = [
{ field: 'taskId', expected: 'string', actual: typeof task.taskId },
{ field: 'status', expected: 'string', actual: typeof task.status },
{ field: 'hasFailures', expected: 'boolean', actual: typeof task.hasFailures },
{ field: 'saveTime', expected: 'number', actual: typeof task.saveTime }
];
let allTypesCorrect = true;
typeChecks.forEach(check => {
if (check.actual !== check.expected) {
log(`${check.field}字段类型错误,期望${check.expected},实际${check.actual}`, 'error');
allTypesCorrect = false;
}
});
if (allTypesCorrect) {
log('✅ 所有字段类型正确', 'success');
}
// 验证时间戳合理性
const now = Date.now();
const timeDiff = now - task.saveTime;
if (timeDiff < 0 || timeDiff > 60000) {
log('⚠️ saveTime时间戳异常时间差: ' + timeDiff + 'ms', 'warning');
} else {
log('✅ saveTime时间戳正常', 'success');
}
return task;
} catch (error) {
log('❌ 解析localStorage数据失败: ' + error.message, 'error');
return null;
}
}
function testRestoreState() {
log('=== 测试4: 测试状态恢复逻辑 ===', 'info');
const task = verifyStorageData();
if (!task) {
log('❌ 无法恢复状态localStorage数据无效', 'error');
return false;
}
// 模拟restoreImportState()方法的逻辑
const restoredState = {
showFailureButton: false,
currentTaskId: null
};
if (task.hasFailures && task.taskId) {
restoredState.currentTaskId = task.taskId;
restoredState.showFailureButton = true;
log('✅ 检测到失败记录,应该显示"查看导入失败记录"按钮', 'success');
log(' - showFailureButton: ' + restoredState.showFailureButton, 'info');
log(' - currentTaskId: ' + restoredState.currentTaskId, 'info');
} else {
log('✅ 没有失败记录,不显示按钮', 'success');
log(' - showFailureButton: ' + restoredState.showFailureButton, 'info');
log(' - currentTaskId: ' + restoredState.currentTaskId, 'info');
}
return restoredState;
}
function testExpiredData() {
log('=== 测试5: 测试过期数据处理 ===', 'info');
const eightDaysAgo = Date.now() - (8 * 24 * 60 * 60 * 1000);
const expiredTask = {
taskId: 'expired_task',
status: 'SUCCESS',
hasFailures: true,
totalCount: 100,
successCount: 90,
failureCount: 10,
saveTime: eightDaysAgo
};
localStorage.setItem('employee_import_last_task', JSON.stringify(expiredTask));
log('已创建过期数据8天前', 'info');
// 模拟getImportTaskFromStorage()的过期检查逻辑
const sevenDays = 7 * 24 * 60 * 60 * 1000;
const isExpired = Date.now() - expiredTask.saveTime > sevenDays;
if (isExpired) {
localStorage.removeItem('employee_import_last_task');
log('✅ 检测到过期数据,已清除', 'success');
return true;
} else {
log('❌ 过期检查逻辑异常', 'error');
return false;
}
}
function testClearHistory() {
log('=== 测试6: 测试清除导入历史功能 ===', 'info');
const testTask = {
taskId: 'test_clear_task',
status: 'SUCCESS',
hasFailures: true,
totalCount: 50,
successCount: 45,
failureCount: 5,
saveTime: Date.now()
};
localStorage.setItem('employee_import_last_task', JSON.stringify(testTask));
log('已创建测试数据', 'info');
// 模拟clearImportHistory()方法
localStorage.removeItem('employee_import_last_task');
log('✅ 已清除导入历史', 'success');
const data = localStorage.getItem('employee_import_last_task');
if (data === null) {
log('✅ 验证成功:导入历史已完全清除', 'success');
return true;
} else {
log('❌ 清除失败localStorage中仍有数据', 'error');
return false;
}
}
function testFieldConsistency() {
log('=== 测试7: 测试字段名一致性 ===', 'info');
// 模拟后端ImportStatusVO返回的数据
const backendData = {
taskId: 'task_test',
status: 'SUCCESS',
totalCount: 100,
successCount: 95,
failureCount: 5,
progress: 100
};
log('后端ImportStatusVO返回: ' + formatJSON(backendData), 'info');
// 模拟前端saveImportTaskToStorage调用的数据
const frontendSaveData = {
taskId: backendData.taskId,
status: backendData.status,
hasFailures: backendData.failureCount > 0,
totalCount: backendData.totalCount,
successCount: backendData.successCount,
failureCount: backendData.failureCount
};
log('前端保存数据: ' + formatJSON(frontendSaveData), 'info');
// 验证字段映射
const fieldMappings = [
{ backend: 'taskId', frontend: 'taskId' },
{ backend: 'status', frontend: 'status' },
{ backend: 'totalCount', frontend: 'totalCount' },
{ backend: 'successCount', frontend: 'successCount' },
{ backend: 'failureCount', frontend: 'failureCount' }
];
let allMatch = true;
fieldMappings.forEach(mapping => {
const backendValue = backendData[mapping.backend];
const frontendValue = frontendSaveData[mapping.frontend];
if (backendValue === frontendValue) {
log(`${mapping.backend}${mapping.frontend}: 值一致 (${backendValue})`, 'success');
} else {
log(`${mapping.backend}${mapping.frontend}: 值不一致`, 'error');
allMatch = false;
}
});
// 验证saveTime字段会在saveImportTaskToStorage中自动添加
log('✅ saveTime字段在saveImportTaskToStorage方法中自动添加', 'info');
return allMatch;
}
function displayResults(results) {
const resultsDiv = document.getElementById('results');
let html = '<div class="summary">';
html += '<h3>测试结果汇总</h3>';
html += '<table style="width: 100%; border-collapse: collapse;">';
html += '<tr style="border-bottom: 1px solid #ddd;">';
html += '<th style="padding: 10px; text-align: left;">测试项目</th>';
html += '<th style="padding: 10px; text-align: left;">结果</th>';
html += '</tr>';
const testNames = {
importSuccess: '导入成功场景',
importWithFailures: '导入部分失败场景',
restoreState: '状态恢复逻辑',
expiredData: '过期数据处理',
clearHistory: '清除导入历史',
fieldConsistency: '字段名一致性'
};
let passCount = 0;
let failCount = 0;
Object.keys(results).forEach(key => {
const status = results[key] ? '✅ PASS' : '❌ FAIL';
const statusClass = results[key] ? 'status-pass' : 'status-fail';
const testName = testNames[key] || key;
html += '<tr style="border-bottom: 1px solid #eee;">';
html += `<td style="padding: 10px;">${testName}</td>`;
html += `<td style="padding: 10px;" class="${statusClass}">${status}</td>`;
html += '</tr>';
if (results[key]) {
passCount++;
} else {
failCount++;
}
});
html += '</table>';
html += '<p style="margin-top: 20px; font-size: 16px;">';
html += `<strong>总计:</strong> ${passCount + failCount} 个测试 | `;
html += `<span class="status-pass">通过: ${passCount} 个</span> | `;
html += `<span class="status-fail">失败: ${failCount} 个</span>`;
html += '</p>';
if (failCount === 0) {
html += '<p style="margin-top: 15px; font-size: 18px; color: #67c23a;">';
html += '🎉 <strong>所有测试通过!</strong> 导入状态持久化功能正常工作。';
html += '</p>';
} else {
html += '<p style="margin-top: 15px; font-size: 18px; color: #f56c6c;">';
html += '⚠️ <strong>部分测试失败</strong>,请检查相关功能。';
html += '</p>';
}
html += '</div>';
resultsDiv.innerHTML = html;
}
async function runAllTests() {
const btn = document.getElementById('runAllTests');
btn.disabled = true;
btn.textContent = '测试运行中...';
document.getElementById('log').innerHTML = '';
document.getElementById('results').innerHTML = '';
log('╔════════════════════════════════════════════════════════════╗', 'info');
log('║ 员工导入状态持久化功能 - 完整测试套件 ║', 'info');
log('╚════════════════════════════════════════════════════════════╝', 'info');
// 清理环境
localStorage.removeItem('employee_import_last_task');
log('✅ 测试环境已清理', 'success');
const results = {
importSuccess: false,
importWithFailures: false,
restoreState: false,
expiredData: false,
clearHistory: false,
fieldConsistency: false
};
// 测试1: 导入成功场景
try {
localStorage.removeItem('employee_import_last_task');
simulateImportSuccess();
const task = verifyStorageData();
results.importSuccess = (task !== null && !task.hasFailures);
} catch (error) {
log('❌ 导入成功场景测试失败: ' + error.message, 'error');
}
// 测试2: 导入部分失败场景
try {
localStorage.removeItem('employee_import_last_task');
simulateImportWithFailures();
const task = verifyStorageData();
results.importWithFailures = (task !== null && task.hasFailures);
} catch (error) {
log('❌ 导入部分失败场景测试失败: ' + error.message, 'error');
}
// 测试3: 状态恢复
try {
const state = testRestoreState();
results.restoreState = (state !== false && state.showFailureButton === true);
} catch (error) {
log('❌ 状态恢复测试失败: ' + error.message, 'error');
}
// 测试4: 过期数据处理
try {
localStorage.removeItem('employee_import_last_task');
results.expiredData = testExpiredData();
} catch (error) {
log('❌ 过期数据处理测试失败: ' + error.message, 'error');
}
// 测试5: 清除导入历史
try {
results.clearHistory = testClearHistory();
} catch (error) {
log('❌ 清除导入历史测试失败: ' + error.message, 'error');
}
// 测试6: 字段名一致性
try {
localStorage.removeItem('employee_import_last_task');
results.fieldConsistency = testFieldConsistency();
} catch (error) {
log('❌ 字段名一致性测试失败: ' + error.message, 'error');
}
log('╔════════════════════════════════════════════════════════════╗', 'info');
log('║ 测试完成 ║', 'info');
log('╚════════════════════════════════════════════════════════════╝', 'info');
displayResults(results);
// 清理测试数据
localStorage.removeItem('employee_import_last_task');
log('✅ 测试数据已清理', 'success');
btn.disabled = false;
btn.textContent = '运行所有测试';
}
</script>
</body>
</html>

View File

@@ -1,488 +0,0 @@
/**
* 员工导入状态持久化功能测试用例
*
* 测试目标:验证导入状态跨页面持久化功能
*
* 测试场景:
* 1. 导入成功场景(全部成功)
* 2. 导入部分失败场景
* 3. 刷新页面后状态恢复
* 4. localStorage过期处理
* 5. 清除导入历史功能
*/
const BASE_URL = 'http://localhost:8080';
// 测试账号
const TEST_CREDENTIALS = {
username: 'admin',
password: 'admin123'
};
let authToken = '';
/**
* 登录获取token
*/
async function login() {
console.log('\n=== 步骤1: 登录系统 ===');
const response = await fetch(`${BASE_URL}/login/test`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(TEST_CREDENTIALS)
});
const result = await response.json();
if (result.code === 200) {
authToken = result.token;
console.log('✅ 登录成功获取到token');
return true;
} else {
console.error('❌ 登录失败:', result.msg);
return false;
}
}
/**
* 模拟导入场景(不实际上传文件,直接构造数据)
*/
function simulateImportSuccess() {
console.log('\n=== 步骤2: 模拟导入成功场景 ===');
// 模拟后端返回的状态数据
const mockSuccessResult = {
taskId: 'task_' + Date.now(),
status: 'SUCCESS',
totalCount: 100,
successCount: 100,
failureCount: 0,
progress: 100,
message: '导入完成'
};
console.log('模拟数据:', mockSuccessResult);
// 模拟前端保存到localStorage
const taskData = {
taskId: mockSuccessResult.taskId,
status: mockSuccessResult.status,
hasFailures: mockSuccessResult.failureCount > 0,
totalCount: mockSuccessResult.totalCount,
successCount: mockSuccessResult.successCount,
failureCount: mockSuccessResult.failureCount,
saveTime: Date.now()
};
localStorage.setItem('employee_import_last_task', JSON.stringify(taskData));
console.log('✅ 已保存导入任务到localStorage');
console.log('保存的数据:', JSON.stringify(taskData, null, 2));
return mockSuccessResult;
}
/**
* 模拟导入部分失败场景
*/
function simulateImportWithFailures() {
console.log('\n=== 步骤3: 模拟导入部分失败场景 ===');
// 模拟后端返回的状态数据
const mockFailureResult = {
taskId: 'task_' + Date.now(),
status: 'SUCCESS',
totalCount: 100,
successCount: 95,
failureCount: 5,
progress: 100,
message: '导入完成'
};
console.log('模拟数据:', mockFailureResult);
// 模拟前端保存到localStorage
const taskData = {
taskId: mockFailureResult.taskId,
status: mockFailureResult.status,
hasFailures: mockFailureResult.failureCount > 0,
totalCount: mockFailureResult.totalCount,
successCount: mockFailureResult.successCount,
failureCount: mockFailureResult.failureCount,
saveTime: Date.now()
};
localStorage.setItem('employee_import_last_task', JSON.stringify(taskData));
console.log('✅ 已保存导入任务到localStorage包含失败记录');
console.log('保存的数据:', JSON.stringify(taskData, null, 2));
return mockFailureResult;
}
/**
* 验证localStorage中的数据
*/
function verifyStorageData() {
console.log('\n=== 步骤4: 验证localStorage数据 ===');
try {
const data = localStorage.getItem('employee_import_last_task');
if (!data) {
console.log('❌ localStorage中没有找到导入任务数据');
return null;
}
const task = JSON.parse(data);
console.log('✅ 成功读取localStorage中的数据');
console.log('读取的数据:', JSON.stringify(task, null, 2));
// 验证必要字段
const requiredFields = ['taskId', 'status', 'hasFailures', 'totalCount', 'successCount', 'failureCount', 'saveTime'];
const missingFields = requiredFields.filter(field => !(field in task));
if (missingFields.length > 0) {
console.error('❌ 缺少必要字段:', missingFields);
return null;
}
console.log('✅ 所有必要字段都存在');
// 验证字段类型
if (typeof task.taskId !== 'string') {
console.error('❌ taskId字段类型错误期望string实际:', typeof task.taskId);
return null;
}
if (typeof task.status !== 'string') {
console.error('❌ status字段类型错误期望string实际:', typeof task.status);
return null;
}
if (typeof task.hasFailures !== 'boolean') {
console.error('❌ hasFailures字段类型错误期望boolean实际:', typeof task.hasFailures);
return null;
}
if (typeof task.saveTime !== 'number') {
console.error('❌ saveTime字段类型错误期望number实际:', typeof task.saveTime);
return null;
}
console.log('✅ 所有字段类型正确');
// 验证时间戳合理性
const now = Date.now();
const timeDiff = now - task.saveTime;
if (timeDiff < 0 || timeDiff > 60000) { // 超过1分钟认为不合理
console.warn('⚠️ saveTime时间戳可能异常当前时间:', now, 'saveTime:', task.saveTime);
} else {
console.log('✅ saveTime时间戳正常');
}
return task;
} catch (error) {
console.error('❌ 解析localStorage数据失败:', error);
return null;
}
}
/**
* 测试状态恢复逻辑
*/
function testRestoreState() {
console.log('\n=== 步骤5: 测试状态恢复逻辑 ===');
const task = verifyStorageData();
if (!task) {
console.log('❌ 无法恢复状态localStorage数据无效');
return false;
}
// 模拟restoreImportState()方法的逻辑
const restoredState = {
showFailureButton: false,
currentTaskId: null
};
if (task.hasFailures && task.taskId) {
restoredState.currentTaskId = task.taskId;
restoredState.showFailureButton = true;
console.log('✅ 检测到失败记录,应该显示"查看导入失败记录"按钮');
console.log(' - showFailureButton:', restoredState.showFailureButton);
console.log(' - currentTaskId:', restoredState.currentTaskId);
} else {
console.log('✅ 没有失败记录,不显示按钮');
console.log(' - showFailureButton:', restoredState.showFailureButton);
console.log(' - currentTaskId:', restoredState.currentTaskId);
}
return restoredState;
}
/**
* 测试过期数据处理
*/
function testExpiredData() {
console.log('\n=== 步骤6: 测试过期数据处理 ===');
// 创建一个8天前的过期数据
const eightDaysAgo = Date.now() - (8 * 24 * 60 * 60 * 1000);
const expiredTask = {
taskId: 'expired_task',
status: 'SUCCESS',
hasFailures: true,
totalCount: 100,
successCount: 90,
failureCount: 10,
saveTime: eightDaysAgo
};
localStorage.setItem('employee_import_last_task', JSON.stringify(expiredTask));
console.log('已创建过期数据8天前');
// 模拟getImportTaskFromStorage()的过期检查逻辑
const sevenDays = 7 * 24 * 60 * 60 * 1000;
const isExpired = Date.now() - expiredTask.saveTime > sevenDays;
if (isExpired) {
localStorage.removeItem('employee_import_last_task');
console.log('✅ 检测到过期数据,已清除');
return true;
} else {
console.log('❌ 过期检查逻辑异常');
return false;
}
}
/**
* 测试清除导入历史功能
*/
function testClearHistory() {
console.log('\n=== 步骤7: 测试清除导入历史功能 ===');
// 先保存一些测试数据
const testTask = {
taskId: 'test_clear_task',
status: 'SUCCESS',
hasFailures: true,
totalCount: 50,
successCount: 45,
failureCount: 5,
saveTime: Date.now()
};
localStorage.setItem('employee_import_last_task', JSON.stringify(testTask));
console.log('已创建测试数据');
// 模拟clearImportHistory()方法
localStorage.removeItem('employee_import_last_task');
console.log('✅ 已清除导入历史');
// 验证是否真的清除了
const data = localStorage.getItem('employee_import_last_task');
if (data === null) {
console.log('✅ 验证成功:导入历史已完全清除');
return true;
} else {
console.error('❌ 清除失败localStorage中仍有数据');
return false;
}
}
/**
* 测试字段名一致性
*/
function testFieldConsistency() {
console.log('\n=== 步骤8: 测试字段名一致性 ===');
// 模拟ImportStatusVO返回的数据后端
const backendData = {
taskId: 'task_test',
status: 'SUCCESS',
totalCount: 100,
successCount: 95,
failureCount: 5,
progress: 100
};
console.log('后端返回的数据:', backendData);
// 模拟saveImportTaskToStorage()调用的数据(前端)
const frontendSaveData = {
taskId: backendData.taskId,
status: backendData.status,
hasFailures: backendData.failureCount > 0,
totalCount: backendData.totalCount,
successCount: backendData.successCount,
failureCount: backendData.failureCount
};
console.log('前端保存的数据:', frontendSaveData);
// 验证字段映射
const fieldMappings = [
{ backend: 'taskId', frontend: 'taskId' },
{ backend: 'status', frontend: 'status' },
{ backend: 'totalCount', frontend: 'totalCount' },
{ backend: 'successCount', frontend: 'successCount' },
{ backend: 'failureCount', frontend: 'failureCount' }
];
let allMatch = true;
fieldMappings.forEach(mapping => {
const backendValue = backendData[mapping.backend];
const frontendValue = frontendSaveData[mapping.frontend];
if (backendValue === frontendValue) {
console.log(`${mapping.backend}${mapping.frontend}: 值一致 (${backendValue})`);
} else {
console.error(`${mapping.backend}${mapping.frontend}: 值不一致`);
allMatch = false;
}
});
// 验证saveTime字段
if (frontendSaveData.saveTime || typeof frontendSaveData.saveTime === 'number') {
console.log('✅ saveTime字段存在且为number类型');
} else {
console.error('❌ saveTime字段缺失或类型错误');
allMatch = false;
}
return allMatch;
}
/**
* 运行所有测试
*/
async function runAllTests() {
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ 员工导入状态持久化功能 - 完整测试套件 ║');
console.log('╚════════════════════════════════════════════════════════════╝');
// 清理环境
localStorage.removeItem('employee_import_last_task');
console.log('✅ 测试环境已清理');
// 登录
const loginSuccess = await login();
if (!loginSuccess) {
console.error('\n❌ 测试终止:登录失败');
return;
}
const results = {
login: true,
importSuccess: false,
importWithFailures: false,
verifyStorage: false,
restoreState: false,
expiredData: false,
clearHistory: false,
fieldConsistency: false
};
// 测试1: 导入成功场景
try {
simulateImportSuccess();
const task = verifyStorageData();
results.importSuccess = (task !== null && !task.hasFailures);
} catch (error) {
console.error('❌ 导入成功场景测试失败:', error);
}
// 测试2: 导入部分失败场景
try {
localStorage.removeItem('employee_import_last_task'); // 清理
simulateImportWithFailures();
const task = verifyStorageData();
results.importWithFailures = (task !== null && task.hasFailures);
} catch (error) {
console.error('❌ 导入部分失败场景测试失败:', error);
}
// 测试3: 状态恢复
try {
const state = testRestoreState();
results.restoreState = (state !== false && state.showFailureButton === true);
} catch (error) {
console.error('❌ 状态恢复测试失败:', error);
}
// 测试4: 过期数据处理
try {
localStorage.removeItem('employee_import_last_task'); // 清理
results.expiredData = testExpiredData();
} catch (error) {
console.error('❌ 过期数据处理测试失败:', error);
}
// 测试5: 清除导入历史
try {
results.clearHistory = testClearHistory();
} catch (error) {
console.error('❌ 清除导入历史测试失败:', error);
}
// 测试6: 字段名一致性
try {
results.fieldConsistency = testFieldConsistency();
} catch (error) {
console.error('❌ 字段名一致性测试失败:', error);
}
// 输出测试报告
console.log('\n╔════════════════════════════════════════════════════════════╗');
console.log('║ 测试结果汇总 ║');
console.log('╚════════════════════════════════════════════════════════════╝\n');
const testNames = {
login: '用户登录',
importSuccess: '导入成功场景',
importWithFailures: '导入部分失败场景',
restoreState: '状态恢复逻辑',
expiredData: '过期数据处理',
clearHistory: '清除导入历史',
fieldConsistency: '字段名一致性'
};
let passCount = 0;
let failCount = 0;
Object.keys(results).forEach(key => {
const status = results[key] ? '✅ PASS' : '❌ FAIL';
const testName = testNames[key] || key;
console.log(`${status} - ${testName}`);
if (results[key]) {
passCount++;
} else {
failCount++;
}
});
console.log('\n--------------------------------------------------------');
console.log(`总计: ${passCount + failCount} 个测试`);
console.log(`通过: ${passCount}`);
console.log(`失败: ${failCount}`);
console.log('--------------------------------------------------------\n');
if (failCount === 0) {
console.log('🎉 所有测试通过!导入状态持久化功能正常工作。');
} else {
console.log('⚠️ 部分测试失败,请检查相关功能。');
}
// 清理测试数据
localStorage.removeItem('employee_import_last_task');
console.log('✅ 测试数据已清理\n');
}
// 运行测试
runAllTests().catch(error => {
console.error('❌ 测试执行异常:', error);
process.exit(1);
});