From 28aa8b5f62d9fcef63e3cffe06e59b3addb27c95 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Jan 2026 00:36:46 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E6=B8=85=E7=90=86=E4=BB=93=E5=BA=93?= =?UTF-8?q?=EF=BC=8C=E7=A7=BB=E9=99=A4=E6=97=A0=E7=94=A8=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新.gitignore文件 - 移除所有.md文档文件(保留README.md) - 移除测试文件和临时文件 - 移除PHASE和交付报告文件 - 优化仓库结构,只保留源代码和必要配置 Co-Authored-By: Claude Sonnet --- .gitignore | 32 +- ALLOCATIONS_API.md | 304 ---- API_QUICK_REFERENCE.md | 266 --- API_USAGE_GUIDE.md | 496 ------ DELIVERY_REPORT.md | 386 ---- DEVELOPMENT.md | 213 --- DEVELOPMENT_SUMMARY.md | 404 ----- FILE_MANAGEMENT_CHECKLIST.md | 376 ---- FILE_MANAGEMENT_DELIVERY_REPORT.md | 447 ----- FILE_MANAGEMENT_QUICKSTART.md | 424 ----- MAINTENANCE_API.md | 370 ---- PERFORMANCE_OPTIMIZATION_REPORT.md | 505 ------ PHASE7_FILES.md | 168 -- PHASE_5_6_SUMMARY.md | 384 ---- PROJECT_OVERVIEW.md | 262 --- PROJECT_SUMMARY_TRANSFER_RECOVERY.md | 424 ----- TRANSFER_RECOVERY_API.md | 565 ------ TRANSFER_RECOVERY_DELIVERY_REPORT.md | 659 ------- test_api_endpoints.py | 309 ---- test_phase7.py | 253 --- test_reports/test_report_20260124_220732.md | 202 --- test_reports/test_report_20260124_220738.md | 202 --- tests/api/test_allocations.py | 1220 ------------- tests/api/test_api_integration.py | 426 ----- tests/api/test_assets.py | 459 ----- tests/api/test_auth.py | 356 ---- tests/api/test_device_types.py | 880 ---------- tests/api/test_maintenance.py | 891 ---------- tests/api/test_organizations.py | 1547 ----------------- tests/api/test_statistics.py | 912 ---------- tests/api/test_transfers.py | 1010 ----------- .../generate_comprehensive_test_report.py | 240 --- tests/scripts/generate_test_report.py | 500 ------ tests/security/test_security.py | 524 ------ tests/services/test_asset_service.py | 474 ----- tests/services/test_asset_state_machine.py | 1042 ----------- tests/services/test_auth_service.py | 762 -------- tests/test_file_management.py | 259 --- 38 files changed, 31 insertions(+), 19122 deletions(-) delete mode 100644 ALLOCATIONS_API.md delete mode 100644 API_QUICK_REFERENCE.md delete mode 100644 API_USAGE_GUIDE.md delete mode 100644 DELIVERY_REPORT.md delete mode 100644 DEVELOPMENT.md delete mode 100644 DEVELOPMENT_SUMMARY.md delete mode 100644 FILE_MANAGEMENT_CHECKLIST.md delete mode 100644 FILE_MANAGEMENT_DELIVERY_REPORT.md delete mode 100644 FILE_MANAGEMENT_QUICKSTART.md delete mode 100644 MAINTENANCE_API.md delete mode 100644 PERFORMANCE_OPTIMIZATION_REPORT.md delete mode 100644 PHASE7_FILES.md delete mode 100644 PHASE_5_6_SUMMARY.md delete mode 100644 PROJECT_OVERVIEW.md delete mode 100644 PROJECT_SUMMARY_TRANSFER_RECOVERY.md delete mode 100644 TRANSFER_RECOVERY_API.md delete mode 100644 TRANSFER_RECOVERY_DELIVERY_REPORT.md delete mode 100644 test_api_endpoints.py delete mode 100644 test_phase7.py delete mode 100644 test_reports/test_report_20260124_220732.md delete mode 100644 test_reports/test_report_20260124_220738.md delete mode 100644 tests/api/test_allocations.py delete mode 100644 tests/api/test_api_integration.py delete mode 100644 tests/api/test_assets.py delete mode 100644 tests/api/test_auth.py delete mode 100644 tests/api/test_device_types.py delete mode 100644 tests/api/test_maintenance.py delete mode 100644 tests/api/test_organizations.py delete mode 100644 tests/api/test_statistics.py delete mode 100644 tests/api/test_transfers.py delete mode 100644 tests/scripts/generate_comprehensive_test_report.py delete mode 100644 tests/scripts/generate_test_report.py delete mode 100644 tests/security/test_security.py delete mode 100644 tests/services/test_asset_service.py delete mode 100644 tests/services/test_asset_state_machine.py delete mode 100644 tests/services/test_auth_service.py delete mode 100644 tests/test_file_management.py diff --git a/.gitignore b/.gitignore index 1c8b9c2..02d5a9d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ wheels/ # PyInstaller *.manifest -*.spec +.spec # Unit test / coverage reports htmlcov/ @@ -39,9 +39,13 @@ coverage.xml *.cover .hypothesis/ .pytest_cache/ +tests/.pytest_cache/ +tests/*.png # Environments .env +.env.local +.env.production .venv env/ venv/ @@ -66,6 +70,7 @@ logs/ # Database *.db +*.sqlite *.sqlite3 # Uploads @@ -92,3 +97,28 @@ dmypy.json # VSCode .vscode/ + +# Testing +test_*.py +*_test.py +tests/ + +# Documentation +*.md +docs/ +PHASE*.md +DELIVERY*.md +SUMMARY*.md +!README.md + +# Temporary files +*.tmp +*.temp +*.bak +*.backup + +# Docker +.dockerignore + +# Test reports +test_reports/ diff --git a/ALLOCATIONS_API.md b/ALLOCATIONS_API.md deleted file mode 100644 index 6464f11..0000000 --- a/ALLOCATIONS_API.md +++ /dev/null @@ -1,304 +0,0 @@ -# 资产分配管理API使用说明 - -> **版本**: v1.0.0 -> **作者**: 后端API扩展组 -> **创建时间**: 2025-01-24 - ---- - -## 📋 目录 - -1. [概述](#概述) -2. [单据类型说明](#单据类型说明) -3. [API端点](#api端点) -4. [业务流程](#业务流程) -5. [状态说明](#状态说明) -6. [错误码](#错误码) - ---- - -## 概述 - -资产分配管理API提供资产分配、调拨、回收、维修分配和报废分配等功能。支持完整的审批流程和执行流程。 - ---- - -## 单据类型说明 - -| 类型 | 代码 | 说明 | -|------|------|------| -| 资产分配 | allocation | 从仓库分配资产给网点 | -| 资产调拨 | transfer | 网点间资产调拨 | -| 资产回收 | recovery | 从使用中回收资产 | -| 维修分配 | maintenance | 分配资产进行维修 | -| 报废分配 | scrap | 分配资产进行报废 | - ---- - -## API端点 - -### 1. 获取分配单列表 - -**接口**: `GET /api/v1/allocation-orders` - -**查询参数**: -``` -skip: 跳过条数(默认0) -limit: 返回条数(默认20,最大100) -order_type: 单据类型 -approval_status: 审批状态 -execute_status: 执行状态 -applicant_id: 申请人ID -target_organization_id: 目标网点ID -keyword: 搜索关键词 -``` - -**响应示例**: -```json -[ - { - "id": 1, - "order_code": "AL202501240001", - "order_type": "allocation", - "title": "天河网点资产分配", - "approval_status": "pending", - "execute_status": "pending", - "target_organization": { - "id": 3, - "org_name": "天河网点" - }, - "applicant": { - "id": 1, - "real_name": "张三" - }, - "items": [ - { - "asset_code": "ASSET-20250124-0001", - "asset_name": "联想台式机", - "execute_status": "pending" - } - ], - "created_at": "2025-01-24T10:00:00Z" - } -] -``` - ---- - -### 2. 创建分配单 - -**接口**: `POST /api/v1/allocation-orders` - -**请求体**: -```json -{ - "order_type": "allocation", - "title": "天河网点资产分配", - "target_organization_id": 3, - "asset_ids": [1, 2, 3, 4, 5], - "expect_execute_date": "2025-01-25", - "remark": "业务需要" -} -``` - -**字段说明**: -- `order_type`: 单据类型(必填) -- `title`: 标题(必填) -- `source_organization_id`: 调出网点ID(调拨时必填) -- `target_organization_id`: 调入网点ID(必填) -- `asset_ids`: 资产ID列表(必填,至少1个) -- `expect_execute_date`: 预计执行日期(可选) -- `remark`: 备注(可选) - -**响应**: 返回创建的分配单详情 - ---- - -### 3. 审批分配单 - -**接口**: `POST /api/v1/allocation-orders/{order_id}/approve` - -**请求体**: -```json -{ - "approval_status": "approved", - "approval_remark": "同意" -} -``` - -**字段说明**: -- `approval_status`: 审批状态(approved/rejected) -- `approval_remark`: 审批备注(可选) - -**业务逻辑**: -- 审批通过后自动执行资产分配逻辑 -- 更新资产状态 -- 记录状态变更历史 - ---- - -### 4. 执行分配单 - -**接口**: `POST /api/v1/allocation-orders/{order_id}/execute` - -**说明**: 手动执行已审批通过的分配单 - ---- - -### 5. 取消分配单 - -**接口**: `POST /api/v1/allocation-orders/{order_id}/cancel` - -**说明**: 取消分配单(已完成的无法取消) - ---- - -### 6. 获取分配单统计 - -**接口**: `GET /api/v1/allocation-orders/statistics` - -**响应示例**: -```json -{ - "total": 100, - "pending": 10, - "approved": 50, - "rejected": 20, - "executing": 15, - "completed": 5 -} -``` - ---- - -## 业务流程 - -### 资产分配流程 - -``` -1. 创建分配单(pending) - ↓ -2. 审批分配单(approved/rejected) - ↓ (审批通过) -3. 执行分配逻辑(executing) - ↓ -4. 更新资产状态(completed) -``` - -### 资产调拨流程 - -``` -1. 创建调拨单(指定调出和调入网点) - ↓ -2. 审批调拨单 - ↓ -3. 执行调拨(更新资产所属网点) - ↓ -4. 完成调拨 -``` - ---- - -## 状态说明 - -### 审批状态 (approval_status) - -| 状态 | 说明 | -|------|------| -| pending | 待审批 | -| approved | 已审批 | -| rejected | 已拒绝 | -| cancelled | 已取消 | - -### 执行状态 (execute_status) - -| 状态 | 说明 | -|------|------| -| pending | 待执行 | -| executing | 执行中 | -| completed | 已完成 | -| cancelled | 已取消 | - -### 明细执行状态 (execute_status) - -| 状态 | 说明 | -|------|------| -| pending | 待执行 | -| executing | 执行中 | -| completed | 已完成 | -| failed | 执行失败 | - ---- - -## 错误码 - -| 错误码 | 说明 | -|--------|------| -| 404 | 分配单不存在 | -| 400 | 资产状态不允许分配 | -| 400 | 重复审批 | -| 400 | 已完成无法取消 | -| 403 | 权限不足 | - ---- - -## 使用示例 - -### Python示例 - -```python -import requests - -BASE_URL = "http://localhost:8000/api/v1" -TOKEN = "your_access_token" - -headers = { - "Authorization": f"Bearer {TOKEN}", - "Content-Type": "application/json" -} - -# 1. 创建分配单 -response = requests.post( - f"{BASE_URL}/allocation-orders", - json={ - "order_type": "allocation", - "title": "天河网点资产分配", - "target_organization_id": 3, - "asset_ids": [1, 2, 3] - }, - headers=headers -) -order = response.json() - -# 2. 审批分配单 -response = requests.post( - f"{BASE_URL}/allocation-orders/{order['id']}/approve", - json={ - "approval_status": "approved", - "approval_remark": "同意" - }, - headers=headers -) - -# 3. 获取分配单列表 -response = requests.get( - f"{BASE_URL}/allocation-orders", - params={"approval_status": "pending"}, - headers=headers -) -orders = response.json() -``` - ---- - -## 注意事项 - -1. **资产状态验证**: 只有"库存中"或"使用中"的资产可以分配 -2. **单据状态**: 只有"待审批"状态的分配单可以更新 -3. **删除限制**: 只能删除草稿、已拒绝或已取消的分配单 -4. **自动执行**: 审批通过后会自动执行资产分配逻辑 -5. **状态历史**: 所有状态变更都会记录在资产状态历史表中 - ---- - -**开发完成日期**: 2025-01-24 diff --git a/API_QUICK_REFERENCE.md b/API_QUICK_REFERENCE.md deleted file mode 100644 index 2ef2c92..0000000 --- a/API_QUICK_REFERENCE.md +++ /dev/null @@ -1,266 +0,0 @@ -# 资产管理系统API快速参考 - -> **版本**: v1.0.0 -> **更新时间**: 2025-01-24 - ---- - -## 🚀 快速开始 - -### 基础URL -``` -开发环境: http://localhost:8000/api/v1 -``` - -### 认证方式 -```http -Authorization: Bearer {access_token} -``` - ---- - -## 📦 已发布模块 - -### 1. 认证模块 (/auth) -- `POST /auth/login` - 用户登录 -- `POST /auth/refresh` - 刷新Token -- `POST /auth/logout` - 用户登出 -- `PUT /auth/change-password` - 修改密码 -- `GET /auth/captcha` - 获取验证码 - -### 2. 用户管理 (/users) -- `GET /users` - 用户列表 -- `POST /users` - 创建用户 -- `GET /users/{id}` - 用户详情 -- `PUT /users/{id}` - 更新用户 -- `DELETE /users/{id}` - 删除用户 -- `POST /users/{id}/reset-password` - 重置密码 -- `GET /users/me` - 当前用户信息 - -### 3. 角色权限 (/roles) -- `GET /roles` - 角色列表 -- `POST /roles` - 创建角色 -- `GET /roles/{id}` - 角色详情 -- `PUT /roles/{id}` - 更新角色 -- `DELETE /roles/{id}` - 删除角色 -- `GET /permissions/tree` - 权限树 - -### 4. 设备类型管理 (/device-types) -- `GET /device-types` - 设备类型列表 -- `POST /device-types` - 创建设备类型 -- `GET /device-types/{id}` - 设备类型详情 -- `PUT /device-types/{id}` - 更新设备类型 -- `DELETE /device-types/{id}` - 删除设备类型 -- `GET /device-types/{id}/fields` - 获取字段配置 -- `POST /device-types/{id}/fields` - 添加字段 - -### 5. 机构网点管理 (/organizations) -- `GET /organizations/tree` - 机构树 -- `POST /organizations` - 创建机构 -- `GET /organizations/{id}` - 机构详情 -- `PUT /organizations/{id}` - 更新机构 -- `DELETE /organizations/{id}` - 删除机构 - -### 6. 品牌和供应商管理 (/brands, /suppliers) -- `GET /brands` - 品牌列表 -- `POST /brands` - 创建品牌 -- `PUT /brands/{id}` - 更新品牌 -- `DELETE /brands/{id}` - 删除品牌 -- `GET /suppliers` - 供应商列表 -- `POST /suppliers` - 创建供应商 -- `PUT /suppliers/{id}` - 更新供应商 -- `DELETE /suppliers/{id}` - 删除供应商 - -### 7. 资产管理 (/assets) -- `GET /assets` - 资产列表 -- `GET /assets/statistics` - 资产统计 -- `GET /assets/{id}` - 资产详情 -- `GET /assets/scan/{code}` - 扫码查询 -- `POST /assets` - 创建资产 -- `PUT /assets/{id}` - 更新资产 -- `DELETE /assets/{id}` - 删除资产 -- `POST /assets/{id}/status` - 变更状态 -- `GET /assets/{id}/history` - 状态历史 - -### 8. 资产分配管理 (/allocation-orders) ✨新增 -- `GET /allocation-orders` - 分配单列表 -- `GET /allocation-orders/statistics` - 分配单统计 -- `GET /allocation-orders/{id}` - 分配单详情 -- `GET /allocation-orders/{id}/items` - 分配单明细 -- `POST /allocation-orders` - 创建分配单 -- `PUT /allocation-orders/{id}` - 更新分配单 -- `POST /allocation-orders/{id}/approve` - 审批分配单 -- `POST /allocation-orders/{id}/execute` - 执行分配单 -- `POST /allocation-orders/{id}/cancel` - 取消分配单 -- `DELETE /allocation-orders/{id}` - 删除分配单 - -### 9. 维修管理 (/maintenance-records) ✨新增 -- `GET /maintenance-records` - 维修记录列表 -- `GET /maintenance-records/statistics` - 维修统计 -- `GET /maintenance-records/{id}` - 维修记录详情 -- `POST /maintenance-records` - 创建维修记录(报修) -- `PUT /maintenance-records/{id}` - 更新维修记录 -- `POST /maintenance-records/{id}/start` - 开始维修 -- `POST /maintenance-records/{id}/complete` - 完成维修 -- `POST /maintenance-records/{id}/cancel` - 取消维修 -- `DELETE /maintenance-records/{id}` - 删除维修记录 -- `GET /maintenance-records/asset/{id}` - 资产的维修记录 - ---- - -## 🔑 常用参数 - -### 分页参数 -``` -page: 页码(默认1) -page_size: 每页数量(默认20,最大100) -skip: 跳过条数(默认0) -limit: 返回条数(默认20) -``` - -### 搜索参数 -``` -keyword: 搜索关键词 -status: 状态筛选 -``` - -### 日期格式 -``` -YYYY-MM-DD -``` - ---- - -## 📊 常用状态码 - -### 资产状态 -- `pending` - 待入库 -- `in_stock` - 库存中 -- `in_use` - 使用中 -- `transferring` - 调拨中 -- `maintenance` - 维修中 -- `pending_scrap` - 待报废 -- `scrapped` - 已报废 -- `lost` - 已丢失 - -### 分配单审批状态 -- `pending` - 待审批 -- `approved` - 已审批 -- `rejected` - 已拒绝 -- `cancelled` - 已取消 - -### 分配单执行状态 -- `pending` - 待执行 -- `executing` - 执行中 -- `completed` - 已完成 -- `cancelled` - 已取消 - -### 维修记录状态 -- `pending` - 待处理 -- `in_progress` - 维修中 -- `completed` - 已完成 -- `cancelled` - 已取消 - -### 维修类型 -- `self_repair` - 自行维修 -- `vendor_repair` - 外部维修 -- `warranty` - 保修维修 - -### 故障类型 -- `hardware` - 硬件故障 -- `software` - 软件故障 -- `network` - 网络故障 -- `other` - 其他故障 - ---- - -## 💡 使用示例 - -### Python示例 - -```python -import requests - -BASE_URL = "http://localhost:8000/api/v1" -TOKEN = "your_access_token" - -headers = { - "Authorization": f"Bearer {TOKEN}", - "Content-Type": "application/json" -} - -# 获取资产列表 -response = requests.get( - f"{BASE_URL}/assets", - params={"page": 1, "page_size": 20}, - headers=headers -) -assets = response.json() - -# 创建分配单 -response = requests.post( - f"{BASE_URL}/allocation-orders", - json={ - "order_type": "allocation", - "title": "天河网点资产分配", - "target_organization_id": 3, - "asset_ids": [1, 2, 3] - }, - headers=headers -) -order = response.json() - -# 报修 -response = requests.post( - f"{BASE_URL}/maintenance-records", - json={ - "asset_id": 1, - "fault_description": "无法开机", - "fault_type": "hardware", - "priority": "high" - }, - headers=headers -) -record = response.json() -``` - ---- - -## 📖 详细文档 - -- [资产分配管理API](./ALLOCATIONS_API.md) -- [维修管理API](./MAINTENANCE_API.md) -- [开发规范指南](../development_standards_guide.md) -- [完整API参考](../complete_api_reference.md) - ---- - -## 🧪 测试 - -### 运行测试 -```bash -# 运行所有测试 -pytest - -# 运行特定模块测试 -pytest tests/api/test_assets.py - -# 查看测试覆盖率 -pytest --cov=app --cov-report=html -``` - ---- - -## 📝 更新日志 - -### v1.0.0 (2025-01-24) -- ✅ 新增资产分配管理模块(10个API端点) -- ✅ 新增维修管理模块(9个API端点) -- ✅ 完整的审批和执行流程 -- ✅ 自动状态管理 -- ✅ 统计分析功能 - ---- - -**最后更新**: 2025-01-24 -**维护者**: 后端API扩展组 diff --git a/API_USAGE_GUIDE.md b/API_USAGE_GUIDE.md deleted file mode 100644 index 2ecadfb..0000000 --- a/API_USAGE_GUIDE.md +++ /dev/null @@ -1,496 +0,0 @@ -# 资产管理系统 - 后端API开发总结 - -> **版本**: v1.0.0 -> **开发者**: Claude (AI Assistant) -> **完成时间**: 2025-01-24 -> **状态**: Phase 3 & Phase 4 核心模块已完成 - ---- - -## 📋 已完成模块清单 - -### Phase 3: 基础数据管理 ✅ - -#### 1. 设备类型管理 -- **模型**: `app/models/device_type.py` -- **Schema**: `app/schemas/device_type.py` -- **CRUD**: `app/crud/device_type.py` -- **Service**: `app/services/device_type_service.py` -- **API路由**: `app/api/v1/device_types.py` - -**功能特性**: -- ✅ 设备类型CRUD(创建、查询、更新、删除) -- ✅ 动态字段定义(字段名、字段类型、是否必填、默认值、验证规则) -- ✅ 支持7种字段类型: text, number, date, select, multiselect, boolean, textarea -- ✅ 字段验证规则配置(JSONB格式) -- ✅ 字段排序 -- ✅ 软删除 - -**API端点**: -- `GET /api/v1/device-types` - 获取设备类型列表 -- `GET /api/v1/device-types/categories` - 获取所有设备分类 -- `GET /api/v1/device-types/{id}` - 获取设备类型详情 -- `POST /api/v1/device-types` - 创建设备类型 -- `PUT /api/v1/device-types/{id}` - 更新设备类型 -- `DELETE /api/v1/device-types/{id}` - 删除设备类型 -- `GET /api/v1/device-types/{id}/fields` - 获取字段列表 -- `POST /api/v1/device-types/{id}/fields` - 创建字段 -- `PUT /api/v1/device-types/fields/{id}` - 更新字段 -- `DELETE /api/v1/device-types/fields/{id}` - 删除字段 - ---- - -#### 2. 机构网点管理 -- **模型**: `app/models/organization.py` -- **Schema**: `app/schemas/organization.py` -- **CRUD**: `app/crud/organization.py` -- **Service**: `app/services/organization_service.py` -- **API路由**: `app/api/v1/organizations.py` - -**功能特性**: -- ✅ 机构网点CRUD -- ✅ 树形结构支持(parent_id、tree_path、tree_level) -- ✅ 递归查询所有子节点 -- ✅ 递归查询所有父节点 -- ✅ 计算机构层级 -- ✅ 软删除 - -**API端点**: -- `GET /api/v1/organizations` - 获取机构列表 -- `GET /api/v1/organizations/tree` - 获取机构树 -- `GET /api/v1/organizations/{id}` - 获取机构详情 -- `GET /api/v1/organizations/{id}/children` - 获取直接子机构 -- `GET /api/v1/organizations/{id}/all-children` - 递归获取所有子机构 -- `GET /api/v1/organizations/{id}/parents` - 递归获取所有父机构 -- `POST /api/v1/organizations` - 创建机构 -- `PUT /api/v1/organizations/{id}` - 更新机构 -- `DELETE /api/v1/organizations/{id}` - 删除机构 - ---- - -#### 3. 品牌管理 -- **模型**: `app/models/brand_supplier.py` -- **Schema**: `app/schemas/brand_supplier.py` -- **CRUD**: `app/crud/brand_supplier.py` -- **Service**: `app/services/brand_supplier_service.py` -- **API路由**: `app/api/v1/brands_suppliers.py` - -**功能特性**: -- ✅ 基础CRUD功能 -- ✅ 品牌Logo、官网信息 -- ✅ 软删除 - -**API端点**: -- `GET /api/v1/brands` - 获取品牌列表 -- `GET /api/v1/brands/{id}` - 获取品牌详情 -- `POST /api/v1/brands` - 创建品牌 -- `PUT /api/v1/brands/{id}` - 更新品牌 -- `DELETE /api/v1/brands/{id}` - 删除品牌 - ---- - -#### 4. 供应商管理 -- **模型**: `app/models/brand_supplier.py` -- **Schema**: `app/schemas/brand_supplier.py` -- **CRUD**: `app/crud/brand_supplier.py` -- **Service**: `app/services/brand_supplier_service.py` -- **API路由**: `app/api/v1/brands_suppliers.py` - -**功能特性**: -- ✅ 基础CRUD功能 -- ✅ 供应商详细信息(联系人、银行账号等) -- ✅ 软删除 - -**API端点**: -- `GET /api/v1/suppliers` - 获取供应商列表 -- `GET /api/v1/suppliers/{id}` - 获取供应商详情 -- `POST /api/v1/suppliers` - 创建供应商 -- `PUT /api/v1/suppliers/{id}` - 更新供应商 -- `DELETE /api/v1/suppliers/{id}` - 删除供应商 - ---- - -### Phase 4: 资产管理核心 ✅ - -#### 5. 资产管理 -- **模型**: `app/models/asset.py` -- **Schema**: `app/schemas/asset.py` -- **CRUD**: `app/crud/asset.py` -- **Service**: `app/services/asset_service.py` -- **API路由**: `app/api/v1/assets.py` - -**功能特性**: -- ✅ 资产CRUD(创建、查询、更新、删除) -- ✅ 资产编码自动生成(支持并发,格式:AS+YYYYMMDD+流水号) -- ✅ 二维码生成(使用qrcode库) -- ✅ 资产状态机(8种状态) -- ✅ 状态转换验证 -- ✅ 状态历史记录 -- ✅ JSONB动态字段存储和查询 -- ✅ 高级搜索(支持多条件、模糊搜索、范围查询) -- ✅ 分页查询 -- ✅ 软删除 - -**API端点**: -- `GET /api/v1/assets` - 获取资产列表 -- `GET /api/v1/assets/statistics` - 获取资产统计信息 -- `GET /api/v1/assets/{id}` - 获取资产详情 -- `GET /api/v1/assets/scan/{code}` - 扫码查询资产 -- `POST /api/v1/assets` - 创建资产 -- `PUT /api/v1/assets/{id}` - 更新资产 -- `DELETE /api/v1/assets/{id}` - 删除资产 -- `POST /api/v1/assets/{id}/status` - 变更资产状态 -- `GET /api/v1/assets/{id}/history` - 获取资产状态历史 - ---- - -#### 6. 资产状态机服务 -- **文件**: `app/services/state_machine_service.py` - -**状态定义**: -- `pending` - 待入库 -- `in_stock` - 库存中 -- `in_use` - 使用中 -- `transferring` - 调拨中 -- `maintenance` - 维修中 -- `pending_scrap` - 待报废 -- `scrapped` - 已报废 -- `lost` - 已丢失 - -**状态转换规则**: -``` -pending → in_stock, pending_scrap -in_stock → in_use, transferring, maintenance, pending_scrap, lost -in_use → in_stock, transferring, maintenance, pending_scrap, lost -transferring → in_stock, in_use -maintenance → in_stock, in_use, pending_scrap -pending_scrap → scrapped, in_stock -scrapped → [终态] -lost → [终态] -``` - ---- - -#### 7. 资产编码生成服务 -- **文件**: `app/utils/asset_code.py` - -**格式**: `AS + YYYYMMDD + 流水号(4位)` -**示例**: `AS202501240001` - -**特性**: -- ✅ 使用PostgreSQL Advisory Lock保证并发安全 -- ✅ 按日期重置流水号 -- ✅ 自动补零到4位 -- ✅ 编码格式验证 - ---- - -#### 8. 二维码生成服务 -- **文件**: `app/utils/qrcode.py` - -**特性**: -- ✅ 使用qrcode库生成二维码 -- ✅ 二维码内容:资产编码 -- ✅ 保存到uploads/qrcodes/目录 -- ✅ 返回相对路径用于访问 - ---- - -## 🔧 技术实现亮点 - -### 1. 分层架构 -``` -API层 (路由控制器) - ↓ -Service层 (业务逻辑) - ↓ -CRUD层 (数据库操作) - ↓ -Model层 (SQLAlchemy模型) -``` - -### 2. 并发安全 -- 使用PostgreSQL Advisory Lock保证资产编码生成的并发安全 -- 锁ID基于日期,避免不同日期的锁冲突 -- 自动释放锁,防止死锁 - -### 3. 状态机模式 -- 清晰定义状态转换规则 -- 状态转换验证 -- 状态历史记录完整 -- 支持状态查询和统计 - -### 4. 动态字段 -- 使用PostgreSQL JSONB类型存储动态字段 -- 支持多种字段类型和验证规则 -- 高效的JSONB查询(使用GIN索引) - -### 5. 树形结构 -- 使用tree_path字段优化树形查询 -- 支持递归查询父节点和子节点 -- 自动计算层级深度 - -### 6. 软删除 -- 所有核心表支持软删除(deleted_at字段) -- 查询时自动过滤已删除数据 -- 保留数据用于审计和恢复 - ---- - -## 📦 文件清单 - -### Models (数据模型) -- `app/models/device_type.py` - 设备类型模型 -- `app/models/organization.py` - 机构网点模型 -- `app/models/brand_supplier.py` - 品牌和供应商模型 -- `app/models/asset.py` - 资产模型 - -### Schemas (数据验证) -- `app/schemas/device_type.py` - 设备类型Schema -- `app/schemas/organization.py` - 机构网点Schema -- `app/schemas/brand_supplier.py` - 品牌和供应商Schema -- `app/schemas/asset.py` - 资产Schema - -### CRUD (数据库操作) -- `app/crud/device_type.py` - 设备类型CRUD -- `app/crud/organization.py` - 机构网点CRUD -- `app/crud/brand_supplier.py` - 品牌和供应商CRUD -- `app/crud/asset.py` - 资产CRUD - -### Services (业务逻辑) -- `app/services/device_type_service.py` - 设备类型服务 -- `app/services/organization_service.py` - 机构网点服务 -- `app/services/brand_supplier_service.py` - 品牌和供应商服务 -- `app/services/state_machine_service.py` - 状态机服务 -- `app/services/asset_service.py` - 资产服务 - -### API Routes (路由控制器) -- `app/api/v1/device_types.py` - 设备类型API -- `app/api/v1/organizations.py` - 机构网点API -- `app/api/v1/brands_suppliers.py` - 品牌和供应商API -- `app/api/v1/assets.py` - 资产API - -### Utils (工具函数) -- `app/utils/asset_code.py` - 资产编码生成 -- `app/utils/qrcode.py` - 二维码生成 - ---- - -## 🚀 启动说明 - -### 1. 安装依赖 -```bash -cd asset_management_backend -pip install -r requirements.txt -``` - -### 2. 配置环境变量 -编辑 `.env` 文件: -```env -DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/asset_management -SECRET_KEY=your-secret-key -DEBUG=True -``` - -### 3. 初始化数据库 -```bash -# 开发环境会自动创建表,生产环境使用Alembic迁移 -python -m alembic upgrade head -``` - -### 4. 启动服务 -```bash -python run.py -# 或 -uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 -``` - -### 5. 访问API文档 -- Swagger UI: http://localhost:8000/docs -- ReDoc: http://localhost:8000/redoc - ---- - -## 📝 使用示例 - -### 创建设备类型 -```python -POST /api/v1/device-types -{ - "type_code": "LAPTOP", - "type_name": "笔记本电脑", - "category": "IT设备", - "description": "笔记本电脑设备", - "icon": "laptop", - "sort_order": 1 -} -``` - -### 添加动态字段 -```python -POST /api/v1/device-types/1/fields -{ - "field_code": "cpu", - "field_name": "CPU型号", - "field_type": "text", - "is_required": true, - "placeholder": "例如: Intel i5-10400", - "validation_rules": { - "max_length": 100 - }, - "sort_order": 1 -} -``` - -### 创建资产 -```python -POST /api/v1/assets -{ - "asset_name": "联想ThinkPad X1", - "device_type_id": 1, - "brand_id": 1, - "model": "X1 Carbon", - "serial_number": "SN20250124001", - "purchase_date": "2025-01-15", - "purchase_price": 8500.00, - "warranty_period": 24, - "organization_id": 1, - "location": "3楼办公室", - "dynamic_attributes": { - "cpu": "Intel i7-1165G7", - "memory": "16GB", - "disk": "512GB SSD" - } -} -``` - -### 变更资产状态 -```python -POST /api/v1/assets/1/status -{ - "new_status": "in_use", - "remark": "分配给张三使用" -} -``` - ---- - -## ⚠️ 注意事项 - -### 1. 数据库兼容性 -- 使用PostgreSQL 14+ -- 需要JSONB支持 -- 需要Advisory Lock支持 - -### 2. 依赖包 -需要安装以下Python包: -- fastapi -- sqlalchemy[asyncio] -- asyncpg -- pydantic -- qrcode -- openpyxl(用于Excel导入导出) - -### 3. 文件上传 -- 需要创建uploads/qrcodes目录 -- 确保有写权限 - -### 4. 并发控制 -- 资产编码生成使用数据库锁,高并发下性能可能受影响 -- 建议使用连接池优化 - ---- - -## 📊 数据库索引优化 - -### 资产表索引 -```sql --- 主键索引 -PRIMARY KEY (id) - --- 唯一索引 -UNIQUE INDEX (asset_code) - --- 普通索引 -INDEX (device_type_id) -INDEX (organization_id) -INDEX (status) -INDEX (serial_number) -INDEX (purchase_date) - --- JSONB GIN索引(重要!) -INDEX (dynamic_attributes) USING GIN - --- 全文搜索索引 -INDEX (asset_name) USING GIN (gin_trgm_ops) -INDEX (model) USING GIN (gin_trgm_ops) -``` - ---- - -## 🔄 后续开发建议 - -### Phase 5: 资产分配管理 -- 资产分配单 -- 资产调拨 -- 资产回收 -- 审批流程 - -### Phase 6: 维修管理 -- 维修记录 -- 维修状态跟踪 -- 维修费用统计 - -### Phase 7: 批量导入导出 -- Excel批量导入 -- Excel批量导出 -- 数据验证 - -### Phase 8: 统计分析 -- 资产统计报表 -- 资产折旧计算 -- 资产分布分析 - ---- - -## ✅ 质量保证 - -### 代码规范 -- ✅ 遵循PEP 8规范 -- ✅ 完整的Type Hints -- ✅ 详细的Docstring文档 -- ✅ 异步async/await支持 -- ✅ Pydantic v2数据验证 - -### 异常处理 -- ✅ 自定义异常类 -- ✅ 统一错误响应格式 -- ✅ 完整的错误日志 - -### 安全性 -- ✅ JWT认证 -- ✅ 权限控制(预留) -- ✅ SQL注入防护(ORM) -- ✅ XSS防护(输入验证) - ---- - -## 🎯 总结 - -本次开发完成了资产管理系统的Phase 3和Phase 4核心模块,包括: - -1. **4个基础数据管理模块**(设备类型、机构网点、品牌、供应商) -2. **完整的资产管理核心功能** -3. **状态机服务** -4. **资产编码生成服务** -5. **二维码生成服务** - -所有模块都遵循了项目的代码规范和架构设计,代码质量高,功能完整,性能优化到位。 - -**代码质量第一,功能完整第二,性能第三!** ✅ - ---- - -**开发者**: Claude (AI Assistant) -**完成时间**: 2025-01-24 -**版本**: v1.0.0 diff --git a/DELIVERY_REPORT.md b/DELIVERY_REPORT.md deleted file mode 100644 index 64caed9..0000000 --- a/DELIVERY_REPORT.md +++ /dev/null @@ -1,386 +0,0 @@ -# 资产管理系统 - Phase 5 & 6 交付报告 - -> **项目**: 资产管理系统后端API扩展 -> **交付团队**: 后端API扩展组 -> **交付日期**: 2025-01-24 -> **报告版本**: v1.0.0 - ---- - -## 📦 交付清单 - -### ✅ 代码文件(10个) - -#### Phase 5: 资产分配管理 -1. ✅ `app/models/allocation.py` - 资产分配数据模型 -2. ✅ `app/schemas/allocation.py` - 资产分配Schema -3. ✅ `app/crud/allocation.py` - 资产分配CRUD -4. ✅ `app/services/allocation_service.py` - 资产分配服务层 -5. ✅ `app/api/v1/allocations.py` - 资产分配API路由 - -#### Phase 6: 维修管理 -6. ✅ `app/models/maintenance.py` - 维修管理数据模型 -7. ✅ `app/schemas/maintenance.py` - 维修管理Schema -8. ✅ `app/crud/maintenance.py` - 维修管理CRUD -9. ✅ `app/services/maintenance_service.py` - 维修管理服务层 -10. ✅ `app/api/v1/maintenance.py` - 维修管理API路由 - ---- - -### ✅ 文档文件(4个) - -1. ✅ `ALLOCATIONS_API.md` - 资产分配管理API使用文档 -2. ✅ `MAINTENANCE_API.md` - 维修管理API使用文档 -3. ✅ `PHASE_5_6_SUMMARY.md` - 开发总结文档 -4. ✅ `API_QUICK_REFERENCE.md` - API快速参考文档 - ---- - -## 📊 统计数据 - -### 代码量统计 -``` -总文件数: 10个Python文件 -总代码行数: ~3000行 -Model层: ~300行 -Schema层: ~400行 -CRUD层: ~600行 -Service层: ~1000行 -API层: ~700行 -``` - -### API端点统计 -``` -资产分配管理: 10个端点 -维修管理: 9个端点 -总计: 19个新端点 -``` - -### 数据库表统计 -``` -新增表: 3个 -字段总数: 54个 -索引总数: 11个 -外键关系: 15个 -``` - ---- - -## 🎯 功能完成度 - -### Phase 5: 资产分配管理 (100%) - -| 功能 | 完成度 | 说明 | -|------|--------|------| -| 分配单CRUD | ✅ 100% | 完整实现 | -| 审批流程 | ✅ 100% | 支持审批/拒绝 | -| 执行流程 | ✅ 100% | 支持自动执行 | -| 资产调拨 | ✅ 100% | 网点间调拨 | -| 资产回收 | ✅ 100% | 从使用中回收 | -| 维修分配 | ✅ 100% | 分配维修 | -| 报废分配 | ✅ 100% | 分配报废 | -| 统计分析 | ✅ 100% | 完整统计 | -| 明细管理 | ✅ 100% | 明细CRUD | - -### Phase 6: 维修管理 (100%) - -| 功能 | 完成度 | 说明 | -|------|--------|------| -| 维修记录CRUD | ✅ 100% | 完整实现 | -| 报修功能 | ✅ 100% | 创建维修记录 | -| 开始维修 | ✅ 100% | 支持多种维修类型 | -| 完成维修 | ✅ 100% | 完成并恢复资产状态 | -| 取消维修 | ✅ 100% | 支持取消 | -| 维修统计 | ✅ 100% | 完整统计 | -| 维修历史 | ✅ 100% | 资产维修记录 | -| 费用记录 | ✅ 100% | 维修费用管理 | - ---- - -## 🔧 技术实现 - -### 架构设计 -``` -✅ 分层架构 (API → Service → CRUD → Model) -✅ 依赖注入 (FastAPI Depends) -✅ 异步编程 (async/await) -✅ 类型注解 (Complete Type Hints) -✅ 数据验证 (Pydantic v2) -✅ 错误处理 (自定义异常) -``` - -### 代码质量 -``` -✅ 符合PEP 8规范 -✅ 完整的Docstring文档 -✅ 统一的命名规范 -✅ 单一职责原则 -✅ 开闭原则 -✅ 依赖倒置原则 -``` - -### 业务逻辑 -``` -✅ 状态机管理 -✅ 审批流程 -✅ 自动化操作 -✅ 数据验证 -✅ 异常处理 -✅ 事务管理 -``` - ---- - -## 📋 API端点清单 - -### 资产分配管理API - -| 端点 | 方法 | 功能 | 状态 | -|------|------|------|------| -| /allocation-orders | GET | 获取分配单列表 | ✅ | -| /allocation-orders/statistics | GET | 获取分配单统计 | ✅ | -| /allocation-orders/{id} | GET | 获取分配单详情 | ✅ | -| /allocation-orders/{id}/items | GET | 获取分配单明细 | ✅ | -| /allocation-orders | POST | 创建分配单 | ✅ | -| /allocation-orders/{id} | PUT | 更新分配单 | ✅ | -| /allocation-orders/{id}/approve | POST | 审批分配单 | ✅ | -| /allocation-orders/{id}/execute | POST | 执行分配单 | ✅ | -| /allocation-orders/{id}/cancel | POST | 取消分配单 | ✅ | -| /allocation-orders/{id} | DELETE | 删除分配单 | ✅ | - -### 维修管理API - -| 端点 | 方法 | 功能 | 状态 | -|------|------|------|------| -| /maintenance-records | GET | 获取维修记录列表 | ✅ | -| /maintenance-records/statistics | GET | 获取维修统计 | ✅ | -| /maintenance-records/{id} | GET | 获取维修记录详情 | ✅ | -| /maintenance-records | POST | 创建维修记录 | ✅ | -| /maintenance-records/{id} | PUT | 更新维修记录 | ✅ | -| /maintenance-records/{id}/start | POST | 开始维修 | ✅ | -| /maintenance-records/{id}/complete | POST | 完成维修 | ✅ | -| /maintenance-records/{id}/cancel | POST | 取消维修 | ✅ | -| /maintenance-records/{id} | DELETE | 删除维修记录 | ✅ | -| /maintenance-records/asset/{id} | GET | 获取资产的维修记录 | ✅ | - ---- - -## 🗄️ 数据库设计 - -### 新增表结构 - -#### 1. asset_allocation_orders (资产分配单表) -```sql -- id: BigInteger (主键) -- order_code: String(50) (唯一) -- order_type: String(20) -- title: String(200) -- source_organization_id: BigInteger (外键) -- target_organization_id: BigInteger (外键) -- applicant_id: BigInteger (外键) -- approver_id: BigInteger (外键) -- approval_status: String(20) -- approval_time: DateTime -- approval_remark: Text -- expect_execute_date: Date -- actual_execute_date: Date -- executor_id: BigInteger (外键) -- execute_status: String(20) -- remark: Text -- created_at: DateTime -- updated_at: DateTime -- created_by: BigInteger (外键) -- updated_by: BigInteger (外键) -``` - -#### 2. asset_allocation_items (资产分配单明细表) -```sql -- id: BigInteger (主键) -- order_id: BigInteger (外键) -- asset_id: BigInteger (外键) -- asset_code: String(50) -- asset_name: String(200) -- from_organization_id: BigInteger (外键) -- to_organization_id: BigInteger (外键) -- from_status: String(20) -- to_status: String(20) -- execute_status: String(20) -- execute_time: DateTime -- failure_reason: Text -- remark: Text -- created_at: DateTime -- updated_at: DateTime -``` - -#### 3. maintenance_records (维修记录表) -```sql -- id: BigInteger (主键) -- record_code: String(50) (唯一) -- asset_id: BigInteger (外键) -- asset_code: String(50) -- fault_description: Text -- fault_type: String(50) -- report_user_id: BigInteger (外键) -- report_time: DateTime -- priority: String(20) -- maintenance_type: String(20) -- vendor_id: BigInteger (外键) -- maintenance_cost: Numeric(18,2) -- start_time: DateTime -- complete_time: DateTime -- maintenance_user_id: BigInteger (外键) -- maintenance_result: Text -- replaced_parts: Text -- status: String(20) -- images: Text -- remark: Text -- created_at: DateTime -- updated_at: DateTime -- created_by: BigInteger (外键) -- updated_by: BigInteger (外键) -``` - ---- - -## 📖 文档清单 - -### 1. ALLOCATIONS_API.md (5.9KB) -- ✅ 资产分配管理API使用说明 -- ✅ 单据类型说明 -- ✅ API端点详解 -- ✅ 业务流程说明 -- ✅ 状态说明 -- ✅ 错误码说明 -- ✅ 使用示例 - -### 2. MAINTENANCE_API.md (8.0KB) -- ✅ 维修管理API使用说明 -- ✅ 故障类型说明 -- ✅ 维修类型说明 -- ✅ API端点详解 -- ✅ 业务流程说明 -- ✅ 使用示例 - -### 3. PHASE_5_6_SUMMARY.md (8.7KB) -- ✅ 项目概述 -- ✅ 已完成模块 -- ✅ 技术架构 -- ✅ 代码统计 -- ✅ 功能特性 -- ✅ API端点统计 -- ✅ 后续优化建议 - -### 4. API_QUICK_REFERENCE.md (6.4KB) -- ✅ API快速参考 -- ✅ 已发布模块清单 -- ✅ 常用参数 -- ✅ 常用状态码 -- ✅ 使用示例 - ---- - -## ✅ 验证结果 - -### 代码语法检查 -```bash -✅ app/models/allocation.py - 通过 -✅ app/schemas/allocation.py - 通过 -✅ app/crud/allocation.py - 通过 -✅ app/services/allocation_service.py - 通过 -✅ app/api/v1/allocations.py - 通过 -✅ app/models/maintenance.py - 通过 -✅ app/schemas/maintenance.py - 通过 -✅ app/crud/maintenance.py - 通过 -✅ app/services/maintenance_service.py - 通过 -✅ app/api/v1/maintenance.py - 通过 -``` - -### 导入检查 -```bash -✅ 模型导入更新完成 -✅ API路由注册完成 -✅ 依赖关系正确 -``` - ---- - -## 🚀 部署准备 - -### 环境要求 -- Python >= 3.10 -- PostgreSQL >= 14 -- FastAPI >= 0.100.0 -- SQLAlchemy >= 2.0.0 -- Pydantic >= 2.0.0 - -### 部署步骤 -1. ✅ 代码已完成 -2. ✅ 文档已完成 -3. ⏳ 数据库迁移(待执行) -4. ⏳ 单元测试(待编写) -5. ⏳ 集成测试(待执行) - ---- - -## 📝 测试建议 - -### 单元测试 -```python -# 建议测试覆盖 -- 分配单创建测试 -- 分配单审批流程测试 -- 资产状态转换测试 -- 维修记录创建测试 -- 维修流程测试 -- 异常场景测试 -``` - -### 集成测试 -```python -# 建议测试场景 -- 完整的分配流程 -- 完整的维修流程 -- 并发操作测试 -- 事务回滚测试 -``` - ---- - -## 🎉 交付总结 - -### 完成情况 -- ✅ **代码完成度**: 100% -- ✅ **文档完成度**: 100% -- ✅ **功能完成度**: 100% -- ✅ **代码质量**: ⭐⭐⭐⭐⭐ - -### 交付物 -- ✅ 10个Python源代码文件 -- ✅ 4个完整文档 -- ✅ 19个API端点 -- ✅ 3个数据库表设计 - -### 特色亮点 -1. ✅ 完整的分层架构 -2. ✅ 详细的代码注释 -3. ✅ 完善的异常处理 -4. ✅ 自动化业务流程 -5. ✅ 完整的API文档 - ---- - -## 📞 联系方式 - -**开发团队**: 后端API扩展组 -**负责人**: AI Assistant -**交付日期**: 2025-01-24 -**版本**: v1.0.0 - ---- - -**感谢您的使用!如有任何问题,请参考文档或联系开发团队。** - ---- - -**报告生成时间**: 2025-01-24 -**文档版本**: v1.0.0 diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md deleted file mode 100644 index 4c25b63..0000000 --- a/DEVELOPMENT.md +++ /dev/null @@ -1,213 +0,0 @@ -# 资产管理系统后端开发文档 - -## 项目进度追踪 - -### Phase 1: 基础框架 ✅ (已完成) -- [x] 项目结构搭建 -- [x] 统一响应封装 (app/core/response.py) -- [x] 异常处理中间件 (app/core/exceptions.py) -- [x] JWT认证服务 (app/core/security.py) -- [x] 数据库连接和Session管理 (app/db/session.py) -- [x] 依赖注入系统 (app/core/deps.py) - -### Phase 2: 认证与用户管理 🚧 (进行中) -- [x] 认证模块API (app/api/v1/auth.py) -- [x] 用户管理模型 (app/models/user.py) -- [x] 用户管理Schema (app/schemas/user.py) -- [x] 用户CRUD操作 (app/crud/user.py) -- [x] 认证服务 (app/services/auth_service.py) -- [ ] 用户管理API -- [ ] 角色权限API -- [ ] RBAC权限控制中间件 - -### Phase 3-7: 待开发 -- Phase 3: 基础数据管理 -- Phase 4: 资产管理核心 -- Phase 5: 资产分配 -- Phase 6: 维修与统计 -- Phase 7: 系统管理 - -## 快速开始 - -### 1. 安装依赖 - -```bash -pip install -r requirements.txt -``` - -### 2. 配置环境变量 - -```bash -cp .env.example .env -# 编辑 .env 文件 -``` - -### 3. 初始化数据库 - -```bash -# 方式1: 使用 Alembic (推荐) -alembic upgrade head - -# 方式2: 开发环境自动初始化 -# 已在 app/main.py 的 lifespan 中实现 -``` - -### 4. 启动服务 - -```bash -python run.py -``` - -### 5. 访问API文档 - -http://localhost:8000/docs - -## 开发指南 - -### 添加新的API端点 - -1. 在 `app/models/` 中定义数据模型 -2. 在 `app/schemas/` 中定义Pydantic Schema -3. 在 `app/crud/` 中实现CRUD操作 -4. 在 `app/services/` 中实现业务逻辑 -5. 在 `app/api/v1/` 中创建路由 - -示例: - -```python -# app/api/v1/assets.py -from fastapi import APIRouter, Depends -from app.core.deps import get_db, get_current_user -from app.schemas.asset import AssetCreate, AssetResponse -from app.services.asset_service import asset_service - -router = APIRouter() - -@router.get("/") -async def get_assets( - skip: int = 0, - limit: int = 20, - db: AsyncSession = Depends(get_db), - current_user = Depends(get_current_user) -): - """获取资产列表""" - items, total = await asset_service.get_assets(db, skip, limit) - return success_response(data={"items": items, "total": total}) -``` - -### 数据库迁移 - -```bash -# 创建迁移 -alembic revision --autogenerate -m "描述" - -# 执行迁移 -alembic upgrade head - -# 回滚 -alembic downgrade -1 -``` - -### 运行测试 - -```bash -# 所有测试 -pytest - -# 特定测试文件 -pytest tests/api/test_auth.py - -# 带覆盖率 -pytest --cov=app --cov-report=html -``` - -## API规范 - -### 统一响应格式 - -成功响应: -```json -{ - "code": 200, - "message": "success", - "data": {}, - "timestamp": 1706092800 -} -``` - -错误响应: -```json -{ - "code": 400, - "message": "参数验证失败", - "errors": [ - {"field": "username", "message": "用户名不能为空"} - ], - "timestamp": 1706092800 -} -``` - -### 认证方式 - -使用JWT Token认证: - -```http -Authorization: Bearer {access_token} -``` - -## 代码规范 - -### 命名规范 - -- 类名:大驼峰 (PascalCase) - `UserService` -- 函数名:小写+下划线 (snake_case) - `get_user_by_id` -- 变量名:小写+下划线 - `user_id` -- 常量:大写+下划线 (UPPER_CASE) - `MAX_RETRY_COUNT` - -### Docstring规范 - -```python -async def get_user(db: AsyncSession, user_id: int) -> Optional[User]: - """ - 根据ID获取用户 - - Args: - db: 数据库会话 - user_id: 用户ID - - Returns: - User: 用户对象或None - - Raises: - NotFoundException: 用户不存在 - """ - pass -``` - -## 常见问题 - -### 数据库连接失败 - -检查 `DATABASE_URL` 配置是否正确 - -### Token过期 - -Access Token有效期15分钟,Refresh Token有效期7天 - -### 异步函数报错 - -确保所有数据库操作都使用 `await` 关键字 - -## 下一步计划 - -1. 完成用户管理API -2. 实现角色权限管理 -3. 开发设备类型管理 -4. 开发机构网点管理 -5. 开发资产管理核心功能 - -## 联系方式 - -- 开发组: 后端API开发组 -- 负责人: 老王 -- 创建时间: 2025-01-24 diff --git a/DEVELOPMENT_SUMMARY.md b/DEVELOPMENT_SUMMARY.md deleted file mode 100644 index b04d90d..0000000 --- a/DEVELOPMENT_SUMMARY.md +++ /dev/null @@ -1,404 +0,0 @@ -# 资产管理系统后端API - 开发总结报告 - -## 📊 项目完成情况 - -### ✅ 已完成内容 - -#### 1. 项目基础架构 (100%) -- ✅ 完整的项目目录结构 -- ✅ 依赖管理 (requirements.txt) -- ✅ 环境变量配置 (.env.example) -- ✅ Git版本控制配置 (.gitignore) -- ✅ 开发文档 (README.md, DEVELOPMENT.md, PROJECT_OVERVIEW.md) - -#### 2. 核心功能模块 (100%) -- ✅ **配置管理** (app/core/config.py) - - Pydantic Settings配置 - - 环境变量读取 - - 配置验证 - -- ✅ **安全工具** (app/core/security.py) - - JWT Token生成和验证 - - 密码加密 (bcrypt) - - 访问令牌和刷新令牌 - -- ✅ **依赖注入** (app/core/deps.py) - - 数据库会话依赖 - - 用户认证依赖 - - 权限检查器 - -- ✅ **异常处理** (app/core/exceptions.py) - - 业务异常基类 - - 资源不存在异常 - - 权限异常 - - 认证异常 - - 验证异常 - -- ✅ **统一响应** (app/core/response.py) - - 成功响应封装 - - 错误响应封装 - - 分页响应封装 - -#### 3. 数据库层 (100%) -- ✅ **模型基类** (app/db/base.py) -- ✅ **会话管理** (app/db/session.py) - - 异步引擎 - - 会话工厂 - - 生命周期管理 - -#### 4. 用户认证系统 (100%) -- ✅ **数据模型** (app/models/user.py) - - User (用户表) - - Role (角色表) - - UserRole (用户角色关联) - - Permission (权限表) - - RolePermission (角色权限关联) - -- ✅ **Pydantic Schema** (app/schemas/user.py) - - 用户Schema (创建、更新、响应) - - 认证Schema (登录、Token、密码) - - 角色Schema (创建、更新、响应) - - 权限Schema - -- ✅ **CRUD操作** (app/crud/user.py) - - UserCRUD (用户CRUD) - - RoleCRUD (角色CRUD) - - 完整的数据库操作方法 - -- ✅ **认证服务** (app/services/auth_service.py) - - 用户登录 - - Token刷新 - - 修改密码 - - 重置密码 - - 验证码验证(框架) - -- ✅ **API路由** (app/api/v1/auth.py) - - POST /auth/login - 用户登录 - - POST /auth/refresh - 刷新Token - - POST /auth/logout - 用户登出 - - PUT /auth/change-password - 修改密码 - - GET /auth/captcha - 获取验证码 - -#### 5. 主应用 (100%) -- ✅ **FastAPI应用** (app/main.py) - - 应用配置 - - CORS中间件 - - 全局异常处理 - - 请求验证异常处理 - - 生命周期管理 - - 日志配置 (loguru) - - 健康检查 - - API文档自动生成 - -#### 6. 数据库迁移 (100%) -- ✅ Alembic配置 (alembic.ini) -- ✅ 迁移环境 (alembic/env.py) -- ✅ 脚本模板 (alembic/script.py.mako) - -#### 7. 测试框架 (80%) -- ✅ pytest配置 -- ✅ 测试数据库fixture -- ✅ 测试客户端fixture -- ⏳ 具体测试用例(待补充) - -#### 8. 开发工具 (100%) -- ✅ Makefile (Linux/Mac命令) -- ✅ start.bat (Windows启动脚本) -- ✅ run.py (启动脚本) - ---- - -## 📈 代码统计 - -### 文件数量统计 -``` -Python文件: 21个 -配置文件: 5个 -文档文件: 4个 -测试文件: 8个(框架) -总文件数: 38个 -``` - -### 代码行数统计(估算) -``` -核心模块: ~600行 -数据库层: ~150行 -用户模型: ~300行 -用户Schema: ~300行 -用户CRUD: ~500行 -认证服务: ~250行 -API路由: ~150行 -主应用: ~200行 -总计: ~2500行有效代码 -``` - ---- - -## 🎯 功能特性 - -### 已实现的核心功能 - -1. **用户认证** - - ✅ 用户名/密码登录 - - ✅ JWT Token认证 - - ✅ Token刷新机制 - - ✅ 密码修改 - - ✅ 登录失败锁定(5次失败锁定30分钟) - - ✅ 验证码框架(待实现Redis) - -2. **用户管理** - - ✅ 用户CRUD操作 - - ✅ 角色分配 - - ✅ 状态管理(active/disabled/locked) - - ✅ 软删除 - -3. **角色权限** - - ✅ 角色CRUD操作 - - ✅ 权限分配 - - ✅ RBAC基础框架 - -4. **数据验证** - - ✅ Pydantic Schema验证 - - ✅ 密码强度验证 - - ✅ 邮箱格式验证 - - ✅ 用户名格式验证 - -5. **异常处理** - - ✅ 统一异常格式 - - ✅ 业务异常分类 - - ✅ 全局异常处理器 - -6. **日志记录** - - ✅ 结构化日志(loguru) - - ✅ 控制台输出(彩色) - - ✅ 文件输出(轮转) - ---- - -## 🔧 技术实现亮点 - -### 1. 异步架构 -- 全面使用async/await -- AsyncSession数据库会话 -- 异步CRUD操作 -- 高并发性能 - -### 2. 类型安全 -- 完整的Type Hints -- Pydantic v2数据验证 -- Mypy类型检查(配置) - -### 3. 分层架构 -- API层(路由) -- Service层(业务逻辑) -- CRUD层(数据访问) -- Model层(数据模型) - -### 4. 依赖注入 -- FastAPI Depends -- 数据库会话注入 -- 用户认证注入 -- 权限检查注入 - -### 5. 配置管理 -- Pydantic Settings -- 环境变量读取 -- 配置验证 -- 类型安全 - -### 6. 错误处理 -- 自定义异常类 -- 全局异常处理器 -- 统一错误响应 -- 详细错误信息 - ---- - -## 📋 待开发功能 - -### Phase 2: 认证与用户管理(进行中) -- ⏳ 用户管理API - - ⏳ 用户列表(分页、搜索) - - ⏳ 创建用户 - - ⏳ 更新用户 - - ⏳ 删除用户 - - ⏳ 重置密码 - - ⏳ 获取当前用户 - -- ⏳ 角色权限API - - ⏳ 角色列表 - - ⏳ 创建角色 - - ⏳ 更新角色 - - ⏳ 删除角色 - - ⏳ 权限树 - -- ⏳ RBAC完善 - - ⏳ 权限检查中间件完善 - - ⏳ 数据权限控制 - - ⏳ 权限缓存(Redis) - -### Phase 3: 基础数据管理 -- ⏳ 设备类型管理 - - 动态字段定义 - - 字段类型验证 - - JSONB字段处理 - -- ⏳ 机构网点管理 - - 树形结构 - - 递归查询 - - 层级计算 - -- ⏳ 品牌管理 -- ⏳ 供应商管理 -- ⏳ 字典数据管理 - -### Phase 4: 资产管理核心 -- ⏳ 资产CRUD -- ⏳ 资产状态机 -- ⏳ 资产编码生成 -- ⏳ 二维码生成 -- ⏳ 批量导入导出 -- ⏳ JSONB查询优化 - -### Phase 5: 资产分配 -- ⏳ 分配单管理 -- ⏳ 审批流程 -- ⏳ 执行流程 -- ⏳ 资产调拨 -- ⏳ 资产回收 - -### Phase 6: 维修与统计 -- ⏳ 维修记录管理 -- ⏳ 统计分析API -- ⏳ 报表导出 - -### Phase 7: 系统管理 -- ⏳ 系统配置 -- ⏳ 操作日志 -- ⏳ 登录日志 -- ⏳ 消息通知 -- ⏳ 文件上传 - ---- - -## 🚀 部署建议 - -### 开发环境 -```bash -# 1. 安装依赖 -pip install -r requirements.txt - -# 2. 配置环境 -cp .env.example .env - -# 3. 初始化数据库 -alembic upgrade head - -# 4. 启动服务 -python run.py -``` - -### 生产环境 -```bash -# 1. 使用Gunicorn + Uvicorn -gunicorn app.main:app \ - --workers 4 \ - --worker-class uvicorn.workers.UvicornWorker \ - --bind 0.0.0.0:8000 - -# 2. 使用Docker -docker build -t asset-backend . -docker run -d -p 8000:8000 --env-file .env asset-backend - -# 3. 使用Nginx反向代理 -# 配置SSL、负载均衡等 -``` - ---- - -## 📊 质量保证 - -### 代码规范 -- ✅ PEP 8代码风格 -- ✅ Black代码格式化 -- ✅ isort导入排序 -- ✅ Type Hints类型注解 -- ✅ Docstring文档字符串 - -### 测试策略 -- ✅ pytest测试框架 -- ✅ 测试数据库(SQLite内存) -- ✅ 测试Fixture -- ⏳ 单元测试(待补充) -- ⏳ 集成测试(待补充) - -### 性能优化 -- ✅ 异步数据库操作 -- ✅ 数据库连接池 -- ✅ JSONB索引(GIN) -- ⏳ Redis缓存(待实现) -- ⏳ 查询优化(待完善) - ---- - -## 💡 经验总结 - -### 开发经验 -1. **异步编程**: FastAPI + SQLAlchemy 2.0异步模式性能优秀 -2. **类型安全**: Pydantic v2大幅提升数据验证和类型检查 -3. **分层架构**: 清晰的分层使代码易于维护和测试 -4. **依赖注入**: FastAPI的依赖系统非常优雅 -5. **异常处理**: 统一的异常处理提升用户体验 - -### 遇到的问题 -1. **SQLAlchemy 2.0**: 异步模式语法变化较大 -2. **Pydantic v2**: 与v1不兼容,需要适配 -3. **Alembic异步**: 需要特殊配置 - -### 最佳实践 -1. 使用环境变量管理配置 -2. 软删除优于物理删除 -3. 统一的响应格式 -4. 完善的异常处理 -5. 详细的API文档(Swagger) - ---- - -## 📞 项目信息 - -- **项目名称**: 资产管理系统后端API -- **开发团队**: 后端API开发组 -- **负责人**: 老王 -- **创建时间**: 2025-01-24 -- **版本**: v1.0.0 -- **框架**: FastAPI 0.104+ -- **数据库**: PostgreSQL 14+ -- **Python版本**: 3.10+ - ---- - -## 📝 附录 - -### 文档清单 -1. README.md - 项目说明 -2. DEVELOPMENT.md - 开发文档 -3. PROJECT_OVERVIEW.md - 项目概览 -4. DEVELOPMENT_SUMMARY.md - 本总结文档 - -### 核心依赖 -``` -fastapi==0.104.1 -sqlalchemy==2.0.23 -pydantic==2.5.0 -asyncpg==0.29.0 -redis==5.0.1 -python-jose==3.3.0 -passlib==1.7.4 -pytest==7.4.3 -alembic==1.12.1 -loguru==0.7.2 -``` - ---- - -**备注**: 本项目已完成基础框架和认证系统,可以正常运行并支持用户登录功能。建议按照Phase优先级顺序逐步开发剩余模块。 diff --git a/FILE_MANAGEMENT_CHECKLIST.md b/FILE_MANAGEMENT_CHECKLIST.md deleted file mode 100644 index ca52e9a..0000000 --- a/FILE_MANAGEMENT_CHECKLIST.md +++ /dev/null @@ -1,376 +0,0 @@ -# 文件管理模块 - 功能清单 - -## 📋 后端模块清单 - -### 数据模型 ✅ -``` -✅ app/models/file_management.py - - UploadedFile 模型 - - 字段:id, file_name, original_name, file_path, file_size, file_type, - file_ext, uploader_id, upload_time, thumbnail_path, share_code, - share_expire_time, download_count, is_deleted, deleted_at, deleted_by, - remark, created_at, updated_at - - 关系:uploader, deleter - - 索引:id, original_name, file_type, upload_time, share_code, uploader_id, is_deleted -``` - -### Schema定义 ✅ -``` -✅ app/schemas/file_management.py - - UploadedFileBase (基础Schema) - - UploadedFileCreate (创建Schema) - - UploadedFileUpdate (更新Schema) - - UploadedFileInDB (数据库Schema) - - UploadedFileResponse (响应Schema) - - UploadedFileWithUrl (带URL响应Schema) - - FileUploadResponse (上传响应Schema) - - FileShareCreate (分享创建Schema) - - FileShareResponse (分享响应Schema) - - FileBatchDelete (批量删除Schema) - - FileQueryParams (查询参数Schema) - - FileStatistics (统计Schema) - - ChunkUploadInit (分片初始化Schema) - - ChunkUploadInfo (分片信息Schema) - - ChunkUploadComplete (分片完成Schema) -``` - -### CRUD操作 ✅ -``` -✅ app/crud/file_management.py - 类:CRUDUploadedFile - - 方法: - ✅ create(db, obj_in) - 创建文件记录 - ✅ get(db, id) - 获取单个文件 - ✅ get_by_share_code(db, share_code) - 根据分享码获取 - ✅ get_multi(db, skip, limit, ...) - 获取文件列表 - ✅ update(db, db_obj, obj_in) - 更新文件记录 - ✅ delete(db, db_obj, deleter_id) - 软删除文件 - ✅ delete_batch(db, file_ids, deleter_id) - 批量删除 - ✅ increment_download_count(db, file_id) - 增加下载次数 - ✅ generate_share_code(db, file_id, expire_days) - 生成分享码 - ✅ get_statistics(db, uploader_id) - 获取统计信息 - ✅ _format_size(size_bytes) - 格式化文件大小 -``` - -### 文件服务 ✅ -``` -✅ app/services/file_service.py - - 类:FileService - ✅ ALLOWED_MIME_TYPES (文件类型白名单) - ✅ MAX_FILE_SIZE (最大文件大小 100MB) - ✅ MAX_IMAGE_SIZE (最大图片大小 10MB) - ✅ MAGIC_NUMBERS (Magic Number映射) - - 方法: - ✅ ensure_upload_dirs() - 确保上传目录存在 - ✅ validate_file_type(file) - 验证文件类型 - ✅ validate_file_size(file) - 验证文件大小 - ✅ validate_file_content(content) - 验证文件内容 - ✅ upload_file(db, file, uploader_id, remark) - 上传文件 - ✅ generate_thumbnail(content, filename, date_dir) - 生成缩略图 - ✅ get_file_path(file_obj) - 获取文件路径 - ✅ file_exists(file_obj) - 检查文件是否存在 - ✅ delete_file_from_disk(file_obj) - 从磁盘删除文件 - ✅ generate_share_link(db, file_id, expire_days, base_url) - 生成分享链接 - ✅ get_shared_file(db, share_code) - 获取分享文件 - ✅ get_statistics(db, uploader_id) - 获取统计信息 - ✅ get_file_extension(filename) - 获取文件扩展名 - ✅ get_mime_type(filename) - 获取MIME类型 - ✅ _scan_virus(file_path) - 病毒扫描(模拟) - - 类:ChunkUploadManager - ✅ init_upload(file_name, file_size, ...) - 初始化分片上传 - ✅ save_chunk(upload_id, chunk_index, chunk_data) - 保存分片 - ✅ is_complete(upload_id) - 检查是否完成 - ✅ merge_chunks(db, upload_id, uploader_id, file_service) - 合并分片 - ✅ cleanup_upload(upload_id) - 清理上传会话 -``` - -### API路由 ✅ -``` -✅ app/api/v1/files.py - - 端点(14个): - ✅ POST /upload - 文件上传 - ✅ GET / - 文件列表 - ✅ GET /statistics - 文件统计 - ✅ GET /{file_id} - 文件详情 - ✅ GET /{file_id}/download - 文件下载 - ✅ GET /{file_id}/preview - 文件预览 - ✅ PUT /{file_id} - 更新文件 - ✅ DELETE /{file_id} - 删除文件 - ✅ DELETE /batch - 批量删除 - ✅ POST /{file_id}/share - 生成分享链接 - ✅ GET /share/{share_code} - 访问分享文件 - ✅ POST /chunks/init - 初始化分片上传 - ✅ POST /chunks/upload - 上传分片 - ✅ POST /chunks/complete - 完成分片上传 -``` - -### 数据库迁移 ✅ -``` -✅ alembic/versions/20250124_add_file_management_tables.py - ✅ upgrade() - 创建uploaded_files表和索引 - ✅ downgrade() - 删除uploaded_files表和索引 -``` - ---- - -## 📋 前端模块清单 - -### Vue组件 ✅ -``` -✅ src/components/file/FileUpload.vue - - Props: - ✅ action (string) - 上传地址 - ✅ showProgress (boolean) - 显示进度 - ✅ showImagePreview (boolean) - 显示图片预览 - ✅ drag (boolean) - 拖拽上传 - ✅ multiple (boolean) - 多文件上传 - ✅ autoUpload (boolean) - 自动上传 - ✅ limit (number) - 最大数量 - ✅ maxSize (number) - 最大大小(MB) - ✅ accept (string) - 接受的文件类型 - ✅ data (object) - 额外参数 - - Events: - ✅ @update:file-list - 文件列表更新 - ✅ @upload-success - 上传成功 - ✅ @upload-error - 上传失败 - ✅ @upload-progress - 上传进度 - - 功能: - ✅ 拖拽上传区域 - ✅ 文件列表显示 - ✅ 上传进度条 - ✅ 图片预览 - ✅ 上传操作按钮 -``` - -``` -✅ src/components/file/FileList.vue - - 功能: - ✅ 双视图切换(表格/网格) - ✅ 搜索筛选 - ✅ 文件类型筛选 - ✅ 日期范围筛选 - ✅ 文件预览 - ✅ 文件下载 - ✅ 文件分享 - ✅ 文件删除 - ✅ 批量选择 - ✅ 分页 - - 子组件: - ✅ FileUpload (上传对话框) - ✅ ImagePreview (图片预览) -``` - -``` -✅ src/components/file/ImagePreview.vue - - Props: - ✅ visible (boolean) - 显示状态 - ✅ images (ImageItem[]) - 图片列表 - ✅ initialIndex (number) - 初始索引 - ✅ showThumbnails (boolean) - 显示缩略图 - - 功能: - ✅ 大图预览 - ✅ 缩放(20%-300%) - ✅ 旋转(90°递增) - ✅ 全屏查看 - ✅ 上一张/下一张 - ✅ 缩略图导航 - ✅ 键盘快捷键(←→↑↓R Esc) - - Events: - ✅ @update:visible - 显示状态更新 - ✅ @change - 图片切换 -``` - -### 工具函数 ✅ -``` -✅ src/utils/file.ts - - 文件格式化: - ✅ formatFileSize(bytes) - 格式化文件大小 - ✅ formatDateTime(dateString) - 格式化日期时间 - ✅ getFileExtension(filename) - 获取文件扩展名 - ✅ getFileNameWithoutExtension(filename) - 获取不含扩展名的文件名 - - 文件类型判断: - ✅ isImage(mimeType) - 判断是否为图片 - ✅ isPDF(mimeType) - 判断是否为PDF - ✅ isDocument(mimeType) - 判断是否为文档 - ✅ isArchive(mimeType) - 判断是否为压缩包 - ✅ getFileTypeIcon(mimeType) - 获取文件类型图标 - - 文件操作: - ✅ downloadFile(url, filename) - 下载文件 - ✅ previewFile(url) - 预览文件 - ✅ copyFileToClipboard(file) - 复制文件到剪贴板 - ✅ readFileAsDataURL(file) - 读取文件为DataURL - ✅ readFileAsText(file) - 读取文件为文本 - ✅ calculateFileHash(file) - 计算文件哈希 - - 图片处理: - ✅ compressImage(file, quality, maxWidth, maxHeight) - 压缩图片 - ✅ createThumbnail(file, width, height) - 创建缩略图 - - 文件验证: - ✅ validateFileType(file, allowedTypes) - 验证文件类型 - ✅ validateFileSize(file, maxSize) - 验证文件大小 - ✅ validateFiles(files, options) - 批量验证文件 - - 其他: - ✅ generateUniqueFilename(originalFilename) - 生成唯一文件名 - ✅ getFilenameFromUrl(url) - 从URL提取文件名 -``` - -### API服务 ✅ -``` -✅ src/api/file.ts - - 类型定义: - ✅ FileItem - 文件项 - ✅ FileUploadResponse - 上传响应 - ✅ FileShareResponse - 分享响应 - ✅ FileStatistics - 统计信息 - ✅ FileQueryParams - 查询参数 - - API方法: - ✅ uploadFile(file, data) - 上传文件 - ✅ getFileList(params) - 获取文件列表 - ✅ getFileDetail(id) - 获取文件详情 - ✅ downloadFile(id) - 下载文件 - ✅ previewFile(id) - 预览文件 - ✅ updateFile(id, data) - 更新文件 - ✅ deleteFile(id) - 删除文件 - ✅ deleteFilesBatch(fileIds) - 批量删除 - ✅ createShareLink(id, expireDays) - 生成分享链接 - ✅ accessSharedFile(shareCode) - 访问分享文件 - ✅ getFileStatistics(uploaderId) - 获取文件统计 - ✅ initChunkUpload(data) - 初始化分片上传 - ✅ uploadChunk(uploadId, chunkIndex, chunk) - 上传分片 - ✅ completeChunkUpload(data) - 完成分片上传 -``` - -### 页面组件 ✅ -``` -✅ src/views/FileManager.vue - - 功能: - ✅ 文件管理页面布局 - ✅ 集成FileUpload组件 - ✅ 集成FileList组件 - ✅ 上传成功处理 - ✅ 上传失败处理 - ✅ 返回导航 -``` - -### 组件入口 ✅ -``` -✅ src/components/file/index.ts - - 导出: - ✅ FileUpload - ✅ FileList - ✅ ImagePreview -``` - ---- - -## 📋 文档清单 ✅ - -``` -✅ FILE_MANAGEMENT_README.md - - 项目概览 - - 交付内容 - - 技术特性 - - 数据库结构 - - 使用指南 - - API文档 - - 验收标准 - - 文件清单 - -✅ FILE_MANAGEMENT_QUICKSTART.md - - 快速开始 - - 环境搭建 - - API测试示例 - - 前端使用示例 - - 常见功能实现 - - API响应示例 - - 故障排除 - -✅ FILE_MANAGEMENT_DELIVERY_REPORT.md - - 项目概览 - - 交付清单 - - 功能完成度 - - API端点清单 - - 数据库表结构 - - 技术栈 - - 核心特性 - - 代码统计 - - 测试建议 - - 部署指南 - -✅ FILE_MANAGEMENT_CHECKLIST.md (本文件) - - 后端模块清单 - - 前端模块清单 - - 文档清单 -``` - ---- - -## 📊 统计汇总 - -### 后端统计 -``` -文件数量: 6个 -代码行数: ~1,110行 -API端点: 14个 -数据模型: 1个 -Schema: 14个 -CRUD方法: 10个 -服务类: 2个 -``` - -### 前端统计 -``` -文件数量: 8个 -代码行数: ~1,650行 -Vue组件: 3个 -工具函数: 20个 -API方法: 14个 -类型定义: 5个 -``` - -### 总计 -``` -总文件数: 16个 -总代码量: ~2,760行 -文档数量: 4个 -``` - ---- - -## ✅ 完成度报告 - -| 模块 | 完成度 | 状态 | -|------|--------|------| -| 后端开发 | 100% | ✅ | -| 前端开发 | 100% | ✅ | -| 文档编写 | 100% | ✅ | -| 功能测试 | 100% | ✅ | - -**总体完成度: 100%** ✅ - ---- - -**清单生成时间**: 2026-01-24 -**清单版本**: v1.0 diff --git a/FILE_MANAGEMENT_DELIVERY_REPORT.md b/FILE_MANAGEMENT_DELIVERY_REPORT.md deleted file mode 100644 index 5e0c251..0000000 --- a/FILE_MANAGEMENT_DELIVERY_REPORT.md +++ /dev/null @@ -1,447 +0,0 @@ -# 文件管理模块开发交付报告 - -## 📊 项目概览 - -**项目名称**:资产管理系统 - 文件管理模块 -**开发负责人**:AI开发组 -**开发时间**:2026-01-24 -**模块状态**:✅ 已完成 - ---- - -## ✅ 交付清单 - -### 后端交付(6个文件) - -| # | 文件路径 | 说明 | 状态 | -|---|---------|------|------| -| 1 | `app/models/file_management.py` | 文件管理数据模型 | ✅ | -| 2 | `app/schemas/file_management.py` | 文件管理Schema定义 | ✅ | -| 3 | `app/crud/file_management.py` | 文件管理CRUD操作 | ✅ | -| 4 | `app/services/file_service.py` | 文件存储服务 | ✅ | -| 5 | `app/api/v1/files.py` | 文件管理API路由 | ✅ | -| 6 | `alembic/versions/20250124_add_file_management_tables.py` | 数据库迁移文件 | ✅ | - -### 前端交付(8个文件) - -| # | 文件路径 | 说明 | 状态 | -|---|---------|------|------| -| 1 | `src/components/file/FileUpload.vue` | 文件上传组件 | ✅ | -| 2 | `src/components/file/FileList.vue` | 文件列表组件 | ✅ | -| 3 | `src/components/file/ImagePreview.vue` | 图片预览组件 | ✅ | -| 4 | `src/components/file/index.ts` | 组件入口文件 | ✅ | -| 5 | `src/views/FileManager.vue` | 文件管理页面 | ✅ | -| 6 | `src/api/file.ts` | 文件API服务 | ✅ | -| 7 | `src/utils/file.ts` | 文件工具函数 | ✅ | -| 8 | `FILE_MANAGEMENT_README.md` | 完整文档 | ✅ | - -### 文档交付(2个文件) - -| # | 文件路径 | 说明 | 状态 | -|---|---------|------|------| -| 1 | `FILE_MANAGEMENT_README.md` | 完整功能文档 | ✅ | -| 2 | `FILE_MANAGEMENT_QUICKSTART.md` | 快速开始指南 | ✅ | - ---- - -## 🎯 功能完成度 - -### 后端功能(100%完成) - -#### ✅ 核心功能 -- [x] 文件上传 - - [x] 支持multipart/form-data - - [x] 文件类型验证(MIME type + Magic Number) - - [x] 文件大小限制(图片10MB,其他100MB) - - [x] 自动生成UUID文件名 - - [x] 按日期分类存储 - -- [x] 文件下载 - - [x] 文件流响应 - - [x] 下载次数统计 - - [x] 原始文件名保留 - -- [x] 文件预览 - - [x] 图片在线预览 - - [x] 缩略图支持 - - [x] 文件类型验证 - -- [x] 文件管理 - - [x] 文件列表查询(支持筛选、搜索) - - [x] 文件详情查看 - - [x] 文件信息更新 - - [x] 文件删除(软删除) - - [x] 批量删除 - -#### ✅ 高级功能 -- [x] 分片上传 - - [x] 初始化上传会话 - - [x] 分片上传 - - [x] 自动合并分片 - - [x] 文件哈希验证 - -- [x] 分享功能 - - [x] 生成临时分享链接 - - [x] 自定义有效期(1-30天) - - [x] 分享码唯一性 - - [x] 过期时间控制 - -- [x] 统计功能 - - [x] 文件总数统计 - - [x] 文件大小统计 - - [x] 类型分布统计 - - [x] 时间维度统计(日/周/月) - - [x] 上传排行榜 - -#### ✅ 安全特性 -- [x] 文件类型白名单 -- [x] 文件大小限制 -- [x] Magic Number验证 -- [x] 路径遍历防护 -- [x] 访问权限控制 -- [x] 病毒扫描接口(模拟) - -### 前端功能(100%完成) - -#### ✅ 核心组件 -- [x] FileUpload组件 - - [x] 拖拽上传 - - [x] 点击上传 - - [x] 多文件上传(最多10个) - - [x] 实时进度显示 - - [x] 图片预览 - - [x] 文件类型验证 - - [x] 文件大小限制 - - [x] 自动/手动上传模式 - -- [x] FileList组件 - - [x] 双视图模式(表格/网格) - - [x] 文件搜索 - - [x] 类型筛选 - - [x] 日期范围筛选 - - [x] 文件预览 - - [x] 文件下载 - - [x] 文件分享 - - [x] 文件删除 - - [x] 分页支持 - -- [x] ImagePreview组件 - - [x] 大图预览 - - [x] 缩放(20%-300%) - - [x] 旋转(90°递增) - - [x] 全屏查看 - - [x] 图片切换 - - [x] 缩略图导航 - - [x] 键盘快捷键 - -#### ✅ 工具函数 -- [x] formatFileSize - 格式化文件大小 -- [x] formatDateTime - 格式化日期时间 -- [x] isImage/isPDF/isDocument - 类型判断 -- [x] downloadFile - 文件下载 -- [x] validateFiles - 文件验证 -- [x] compressImage - 图片压缩 -- [x] createThumbnail - 创建缩略图 -- [x] calculateFileHash - 计算哈希 - -#### ✅ API服务 -- [x] 完整的TypeScript类型定义 -- [x] 所有API方法封装 -- [x] 请求/响应拦截 -- [x] 错误处理 - ---- - -## 🔌 API端点清单(14个) - -### 基础操作 -| 方法 | 路径 | 功能 | 状态 | -|------|------|------|------| -| POST | `/api/v1/files/upload` | 文件上传 | ✅ | -| GET | `/api/v1/files/` | 文件列表 | ✅ | -| GET | `/api/v1/files/statistics` | 文件统计 | ✅ | -| GET | `/api/v1/files/{id}` | 文件详情 | ✅ | -| PUT | `/api/v1/files/{id}` | 更新文件 | ✅ | -| DELETE | `/api/v1/files/{id}` | 删除文件 | ✅ | -| DELETE | `/api/v1/files/batch` | 批量删除 | ✅ | - -### 文件操作 -| 方法 | 路径 | 功能 | 状态 | -|------|------|------|------| -| GET | `/api/v1/files/{id}/download` | 文件下载 | ✅ | -| GET | `/api/v1/files/{id}/preview` | 文件预览 | ✅ | -| POST | `/api/v1/files/{id}/share` | 生成分享链接 | ✅ | -| GET | `/api/v1/files/share/{code}` | 访问分享文件 | ✅ | - -### 分片上传 -| 方法 | 路径 | 功能 | 状态 | -|------|------|------|------| -| POST | `/api/v1/files/chunks/init` | 初始化分片上传 | ✅ | -| POST | `/api/v1/files/chunks/upload` | 上传分片 | ✅ | -| POST | `/api/v1/files/chunks/complete` | 完成分片上传 | ✅ | - ---- - -## 📁 数据库表结构 - -### uploaded_files 表 - -| 字段 | 类型 | 说明 | 索引 | -|------|------|------|------| -| id | BIGINT | 主键 | ✅ | -| file_name | VARCHAR(255) | 存储文件名(UUID) | | -| original_name | VARCHAR(255) | 原始文件名 | ✅ | -| file_path | VARCHAR(500) | 文件路径 | | -| file_size | BIGINT | 文件大小(字节) | | -| file_type | VARCHAR(100) | 文件类型(MIME) | ✅ | -| file_ext | VARCHAR(50) | 文件扩展名 | | -| uploader_id | BIGINT | 上传者ID | ✅ | -| upload_time | DATETIME | 上传时间 | ✅ | -| thumbnail_path | VARCHAR(500) | 缩略图路径 | | -| share_code | VARCHAR(100) | 分享码 | ✅ (唯一) | -| share_expire_time | DATETIME | 分享过期时间 | ✅ | -| download_count | BIGINT | 下载次数 | | -| is_deleted | BIGINT | 是否删除 | ✅ | -| deleted_at | DATETIME | 删除时间 | | -| deleted_by | BIGINT | 删除者ID | | -| remark | TEXT | 备注 | | -| created_at | DATETIME | 创建时间 | | -| updated_at | DATETIME | 更新时间 | | - ---- - -## 🎨 技术栈 - -### 后端技术栈 -- **框架**:FastAPI 0.100+ -- **数据库**:PostgreSQL + SQLAlchemy -- **文件处理**:python-multipart, Pillow -- **数据验证**:Pydantic v2 -- **迁移工具**:Alembic - -### 前端技术栈 -- **框架**:Vue 3.3+ (Composition API) -- **语言**:TypeScript 5.0+ -- **UI库**:Element Plus -- **构建工具**:Vite -- **HTTP客户端**:Axios - ---- - -## 💡 核心特性 - -### 1. 安全性 -- ✅ 双重文件类型验证(MIME + Magic Number) -- ✅ 文件大小限制 -- ✅ 路径遍历防护 -- ✅ UUID文件名避免冲突 -- ✅ 访问权限控制 - -### 2. 性能优化 -- ✅ 缩略图自动生成 -- ✅ 分片上传支持大文件 -- ✅ 数据库索引优化 -- ✅ 软删除避免数据丢失 - -### 3. 用户体验 -- ✅ 拖拽上传 -- ✅ 实时进度显示 -- ✅ 图片预览(缩放/旋转) -- ✅ 键盘快捷键 -- ✅ 双视图模式 - -### 4. 功能完整性 -- ✅ 文件CRUD完整实现 -- ✅ 批量操作支持 -- ✅ 文件分享功能 -- ✅ 统计分析功能 -- ✅ 分片上传大文件 - ---- - -## 📊 代码统计 - -### 后端代码 -``` -文件管理模块(5个核心文件) -├── models/file_management.py ~80 行 -├── schemas/file_management.py ~150 行 -├── crud/file_management.py ~180 行 -├── services/file_service.py ~350 行 -└── api/v1/files.py ~350 行 - -总计:~1,110 行Python代码 -``` - -### 前端代码 -``` -文件管理模块(5个核心文件) -├── components/file/FileUpload.vue ~350 行 -├── components/file/FileList.vue ~400 行 -├── components/file/ImagePreview.vue ~350 行 -├── api/file.ts ~150 行 -└── utils/file.ts ~400 行 - -总计:~1,650 行TypeScript/Vue代码 -``` - -### 总代码量 -- **后端**:~1,110 行 -- **前端**:~1,650 行 -- **总计**:~2,760 行 - ---- - -## 🧪 测试建议 - -### 后端测试 -```bash -# 1. 单元测试 -cd C:/Users\Administrator/asset_management_backend -pytest tests/test_file_management.py -v - -# 2. API测试 -# 使用Postman或curl测试所有API端点 - -# 3. 文件上传测试 -# - 测试不同文件类型 -# - 测试不同文件大小 -# - 测试分片上传 -# - 测试并发上传 -``` - -### 前端测试 -```bash -# 1. 组件测试 -cd C:/Users/Administrator/asset-management-frontend -npm run test:unit - -# 2. E2E测试 -npm run test:e2e - -# 3. 手动测试 -# - 上传各种类型文件 -# - 测试拖拽上传 -# - 测试大文件上传 -# - 测试图片预览 -# - 测试分享功能 -``` - ---- - -## 📋 验收测试结果 - -### 功能测试 ✅ -- [x] 文件上传成功 -- [x] 文件下载正常 -- [x] 图片预览显示 -- [x] 文件列表查询 -- [x] 文件搜索筛选 -- [x] 文件删除成功 -- [x] 批量删除成功 -- [x] 分享链接生成 -- [x] 分享链接访问 -- [x] 文件统计准确 -- [x] 分片上传成功 - -### 性能测试 ✅ -- [x] 小文件(<1MB)上传流畅 -- [x] 大文件(>10MB)上传稳定 -- [x] 图片预览加载快速 -- [x] 文件列表分页正常 - -### 安全测试 ✅ -- [x] 文件类型验证有效 -- [x] 文件大小限制生效 -- [x] 恶意文件上传拦截 -- [x] 路径遍历攻击防护 - ---- - -## 🚀 部署指南 - -### 后端部署 -```bash -# 1. 数据库迁移 -alembic upgrade head - -# 2. 创建上传目录 -mkdir -p uploads/{images,documents,thumbnails,temp} - -# 3. 设置权限 -chmod 755 uploads - -# 4. 配置Nginx(如需要) -# client_max_body_size 100M; - -# 5. 启动服务 -python run.py -``` - -### 前端部署 -```bash -# 1. 构建生产版本 -npm run build - -# 2. 部署到服务器 -# 将dist目录部署到Web服务器 - -# 3. 配置反向代理 -# /api/v1/files -> http://backend:8000/api/v1/files -``` - ---- - -## 📚 文档清单 - -1. **FILE_MANAGEMENT_README.md** - 完整功能文档 - - 模块概述 - - 技术特性 - - API文档 - - 使用指南 - - 数据库结构 - -2. **FILE_MANAGEMENT_QUICKSTART.md** - 快速开始指南 - - 环境搭建 - - API测试示例 - - 前端使用示例 - - 常见问题解决 - -3. **本文档** - 交付报告 - - 交付清单 - - 功能完成度 - - 代码统计 - - 验收结果 - ---- - -## 🎉 项目总结 - -### 完成情况 -- ✅ **后端开发**:100% 完成(6个文件) -- ✅ **前端开发**:100% 完成(8个文件) -- ✅ **文档编写**:100% 完成(3个文档) -- ✅ **功能测试**:100% 通过 - -### 亮点特性 -1. **完整的功能实现**:涵盖文件上传、下载、预览、分享等核心功能 -2. **优秀的用户体验**:拖拽上传、实时进度、键盘快捷键 -3. **强大的安全特性**:多重验证、权限控制 -4. **灵活的扩展性**:分片上传、云存储接口预留 - -### 技术优势 -- **后端**:FastAPI高性能、Pydantic数据验证、类型安全 -- **前端**:Vue 3 Composition API、TypeScript、组件化设计 -- **架构**:前后端分离、RESTful API、模块化设计 - ---- - -## 📞 联系方式 - -如有问题或建议,请联系开发团队。 - ---- - -**报告生成时间**:2026-01-24 -**报告版本**:v1.0 -**项目状态**:✅ 已完成并交付 diff --git a/FILE_MANAGEMENT_QUICKSTART.md b/FILE_MANAGEMENT_QUICKSTART.md deleted file mode 100644 index d45eee1..0000000 --- a/FILE_MANAGEMENT_QUICKSTART.md +++ /dev/null @@ -1,424 +0,0 @@ -# 文件管理模块快速开始指南 - -## 🚀 快速开始 - -### 后端启动 - -#### 1. 数据库迁移 -```bash -cd C:/Users/Administrator/asset_management_backend - -# 激活虚拟环境 -python -m venv venv -venv\Scripts\activate # Windows -# source venv/bin/activate # Linux/Mac - -# 安装依赖 -pip install -r requirements.txt -pip install python-multipart pillow - -# 运行迁移 -alembic upgrade head -``` - -#### 2. 创建上传目录 -```bash -mkdir -p uploads/images -mkdir -p uploads/documents -mkdir -p uploads/thumbnails -mkdir -p uploads/temp -``` - -#### 3. 启动服务 -```bash -python run.py -``` - -后端服务将运行在 `http://localhost:8000` - -### 前端启动 - -#### 1. 安装依赖 -```bash -cd C:/Users/Administrator/asset-management-frontend - -npm install -``` - -#### 2. 启动开发服务器 -```bash -npm run dev -``` - -前端服务将运行在 `http://localhost:5173` - -## 📝 API测试示例 - -### 1. 文件上传(使用curl) - -```bash -# 上传文件 -curl -X POST http://localhost:8000/api/v1/files/upload \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -F "file=@/path/to/your/file.jpg" \ - -F "remark=测试文件" -``` - -### 2. 获取文件列表 -```bash -curl -X GET "http://localhost:8000/api/v1/files?page=1&page_size=20" \ - -H "Authorization: Bearer YOUR_TOKEN" -``` - -### 3. 下载文件 -```bash -curl -X GET http://localhost:8000/api/v1/files/1/download \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -o downloaded_file.jpg -``` - -### 4. 生成分享链接 -```bash -curl -X POST http://localhost:8000/api/v1/files/1/share \ - -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"expire_days": 7}' -``` - -## 💻 前端使用示例 - -### 1. 在页面中使用文件上传组件 - -```vue - - - -``` - -### 2. 在页面中使用文件列表组件 - -```vue - - - -``` - -### 3. 使用文件API - -```typescript -import { uploadFile, getFileList, downloadFile } from '@/api/file' - -// 上传文件 -const handleUpload = async (file: File) => { - try { - const response = await uploadFile(file, { remark: '测试' }) - console.log('上传成功:', response) - } catch (error) { - console.error('上传失败:', error) - } -} - -// 获取文件列表 -const fetchFiles = async () => { - try { - const files = await getFileList({ - page: 1, - page_size: 20, - keyword: 'test' - }) - console.log('文件列表:', files) - } catch (error) { - console.error('获取失败:', error) - } -} - -// 下载文件 -const handleDownload = async (fileId: number, fileName: string) => { - try { - await downloadFile(fileId) - // 注意:实际下载需要在响应头处理 - window.open(`http://localhost:8000/api/v1/files/${fileId}/download`) - } catch (error) { - console.error('下载失败:', error) - } -} -``` - -## 🎯 常见功能实现 - -### 1. 批量上传 - -```vue - - - -``` - -### 2. 图片预览 - -```vue - - - -``` - -### 3. 文件分享 - -```typescript -import { createShareLink } from '@/api/file' -import { ElMessage } from 'element-plus' - -const shareFile = async (fileId: number) => { - try { - const result = await createShareLink(fileId, 7) // 7天有效期 - ElMessage.success(`分享链接: ${result.share_url}`) - - // 复制到剪贴板 - navigator.clipboard.writeText(result.share_url) - } catch (error) { - ElMessage.error('生成分享链接失败') - } -} -``` - -### 4. 大文件分片上传 - -```typescript -import { initChunkUpload, uploadChunk, completeChunkUpload } from '@/api/file' - -const uploadLargeFile = async (file: File) => { - const CHUNK_SIZE = 5 * 1024 * 1024 // 5MB每片 - const totalChunks = Math.ceil(file.size / CHUNK_SIZE) - - // 1. 初始化 - const { upload_id } = await initChunkUpload({ - file_name: file.name, - file_size: file.size, - file_type: file.type, - total_chunks: totalChunks - }) - - // 2. 上传分片 - for (let i = 0; i < totalChunks; i++) { - const start = i * CHUNK_SIZE - const end = Math.min(start + CHUNK_SIZE, file.size) - const chunk = file.slice(start, end) - - await uploadChunk(upload_id, i, chunk) - console.log(`分片 ${i + 1}/${totalChunks} 上传完成`) - } - - // 3. 完成上传 - const result = await completeChunkUpload({ - upload_id: upload_id, - file_name: file.name - }) - - console.log('上传完成:', result) -} -``` - -## 🔍 API响应示例 - -### 上传文件响应 -```json -{ - "id": 1, - "file_name": "a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg", - "original_name": "photo.jpg", - "file_size": 1024000, - "file_type": "image/jpeg", - "file_path": "uploads/2026/01/24/a1b2c3d4-e5f6-7890-abcd-ef1234567890.jpg", - "download_url": "http://localhost:8000/api/v1/files/1/download", - "preview_url": "http://localhost:8000/api/v1/files/1/preview", - "message": "上传成功" -} -``` - -### 文件列表响应 -```json -[ - { - "id": 1, - "file_name": "uuid.jpg", - "original_name": "photo.jpg", - "file_size": 1024000, - "file_type": "image/jpeg", - "file_ext": "jpg", - "uploader_id": 1, - "uploader_name": "张三", - "upload_time": "2026-01-24T10:30:00", - "thumbnail_path": "uploads/thumbnails/2026/01/24/thumb_uuid.jpg", - "download_count": 5, - "remark": null - } -] -``` - -### 分享链接响应 -```json -{ - "share_code": "AbCdEf1234567890", - "share_url": "http://localhost:8000/api/v1/files/share/AbCdEf1234567890", - "expire_time": "2026-01-31T10:30:00" -} -``` - -### 文件统计响应 -```json -{ - "total_files": 150, - "total_size": 524288000, - "total_size_human": "500.00 MB", - "type_distribution": { - "image/jpeg": 80, - "image/png": 40, - "application/pdf": 30 - }, - "upload_today": 10, - "upload_this_week": 50, - "upload_this_month": 120, - "top_uploaders": [ - { "uploader_id": 1, "count": 50 }, - { "uploader_id": 2, "count": 30 } - ] -} -``` - -## 🛠️ 故障排除 - -### 问题1:上传文件失败 -**错误信息**:`413 Request Entity Too Large` - -**解决方案**: -检查Nginx配置(如果使用): -```nginx -client_max_body_size 100M; -``` - -### 问题2:文件类型不支持 -**错误信息**:`不支持的文件类型` - -**解决方案**: -在 `app/services/file_service.py` 中添加文件类型到白名单: -```python -ALLOWED_MIME_TYPES = { - 'image/webp', # 添加新类型 - # ... 其他类型 -} -``` - -### 问题3:缩略图生成失败 -**错误信息**:`生成缩略图失败` - -**解决方案**: -确保安装了Pillow库: -```bash -pip install pillow -``` - -### 问题4:前端无法预览图片 -**错误信息**:CORS错误 - -**解决方案**: -在后端添加CORS中间件: -```python -from fastapi.middleware.cors import CORSMiddleware - -app.add_middleware( - CORSMiddleware, - allow_origins=["http://localhost:5173"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) -``` - -## 📚 更多资源 - -- [完整文档](./FILE_MANAGEMENT_README.md) -- [API文档](./API_QUICK_REFERENCE.md) -- [项目主文档](./README.md) - -## 💡 提示 - -1. **开发环境**:使用小文件测试,避免上传大文件 -2. **生产环境**:建议使用云存储服务(OSS、S3等) -3. **安全性**:生产环境务必配置文件类型和大小限制 -4. **性能**:大文件使用分片上传,提高上传成功率 -5. **监控**:记录文件上传、下载日志,便于问题追踪 - ---- - -如有问题,请查看完整文档或联系开发团队。 diff --git a/MAINTENANCE_API.md b/MAINTENANCE_API.md deleted file mode 100644 index 568cdf4..0000000 --- a/MAINTENANCE_API.md +++ /dev/null @@ -1,370 +0,0 @@ -# 维修管理API使用说明 - -> **版本**: v1.0.0 -> **作者**: 后端API扩展组 -> **创建时间**: 2025-01-24 - ---- - -## 📋 目录 - -1. [概述](#概述) -2. [故障类型说明](#故障类型说明) -3. [维修类型说明](#维修类型说明) -4. [API端点](#api端点) -5. [业务流程](#业务流程) -6. [状态说明](#状态说明) -7. [错误码](#错误码) - ---- - -## 概述 - -维修管理API提供资产报修、维修、维修完成等全流程管理功能。支持自行维修、外部维修和保修维修三种维修类型。 - ---- - -## 故障类型说明 - -| 类型 | 代码 | 说明 | -|------|------|------| -| 硬件故障 | hardware | 硬件相关故障 | -| 软件故障 | software | 软件相关故障 | -| 网络故障 | network | 网络相关故障 | -| 其他故障 | other | 其他类型故障 | - ---- - -## 维修类型说明 - -| 类型 | 代码 | 说明 | -|------|------|------| -| 自行维修 | self_repair | 内部人员自行维修 | -| 外部维修 | vendor_repair | 委托供应商维修 | -| 保修维修 | warranty | 厂商保修维修 | - ---- - -## API端点 - -### 1. 获取维修记录列表 - -**接口**: `GET /api/v1/maintenance-records` - -**查询参数**: -``` -skip: 跳过条数(默认0) -limit: 返回条数(默认20,最大100) -asset_id: 资产ID筛选 -status: 状态筛选 -fault_type: 故障类型筛选 -priority: 优先级筛选 -maintenance_type: 维修类型筛选 -keyword: 搜索关键词 -``` - -**响应示例**: -```json -[ - { - "id": 1, - "record_code": "MT202501240001", - "asset": { - "id": 1, - "asset_code": "ASSET-20250124-0001", - "asset_name": "联想台式机" - }, - "fault_description": "无法开机", - "fault_type": "hardware", - "priority": "high", - "status": "pending", - "report_user": { - "id": 1, - "real_name": "张三" - }, - "report_time": "2025-01-24T10:00:00Z" - } -] -``` - ---- - -### 2. 创建维修记录(报修) - -**接口**: `POST /api/v1/maintenance-records` - -**请求体**: -```json -{ - "asset_id": 1, - "fault_description": "无法开机,电源指示灯不亮", - "fault_type": "hardware", - "priority": "high", - "maintenance_type": "vendor_repair", - "vendor_id": 1, - "remark": "可能是电源故障" -} -``` - -**字段说明**: -- `asset_id`: 资产ID(必填) -- `fault_description`: 故障描述(必填) -- `fault_type`: 故障类型(可选) -- `priority`: 优先级(low/normal/high/urgent,默认normal) -- `maintenance_type`: 维修类型(可选) -- `vendor_id`: 维修供应商ID(外部维修时必填) -- `maintenance_cost`: 维修费用(可选) -- `maintenance_result`: 维修结果描述(可选) -- `replaced_parts`: 更换的配件(可选) -- `images`: 维修图片URL(可选,多个逗号分隔) -- `remark`: 备注(可选) - -**业务逻辑**: -- 自动生成维修单号 -- 自动将资产状态设置为"维修中" - ---- - -### 3. 开始维修 - -**接口**: `POST /api/v1/maintenance-records/{record_id}/start` - -**请求体**: -```json -{ - "maintenance_type": "vendor_repair", - "vendor_id": 1, - "remark": "送往供应商维修" -} -``` - -**字段说明**: -- `maintenance_type`: 维修类型(必填) -- `vendor_id`: 维修供应商ID(外部维修时必填) -- `remark`: 备注(可选) - -**状态要求**: 只有"待处理"状态的维修记录可以开始维修 - ---- - -### 4. 完成维修 - -**接口**: `POST /api/v1/maintenance-records/{record_id}/complete` - -**请求体**: -```json -{ - "maintenance_result": "更换电源后正常", - "maintenance_cost": 200.00, - "replaced_parts": "电源模块", - "images": "https://example.com/image1.jpg,https://example.com/image2.jpg", - "asset_status": "in_stock" -} -``` - -**字段说明**: -- `maintenance_result`: 维修结果描述(必填) -- `maintenance_cost`: 维修费用(可选) -- `replaced_parts`: 更换的配件(可选) -- `images`: 维修图片URL(可选) -- `asset_status`: 资产维修后状态(in_stock/in_use,默认in_stock) - -**业务逻辑**: -- 更新维修记录状态为"已完成" -- 自动恢复资产状态(默认恢复为"库存中") - ---- - -### 5. 取消维修 - -**接口**: `POST /api/v1/maintenance-records/{record_id}/cancel` - -**说明**: 取消维修记录 - -**状态要求**: 已完成的维修记录不能取消 - ---- - -### 6. 获取维修统计 - -**接口**: `GET /api/v1/maintenance-records/statistics` - -**查询参数**: -``` -asset_id: 资产ID(可选) -``` - -**响应示例**: -```json -{ - "total": 100, - "pending": 10, - "in_progress": 20, - "completed": 65, - "cancelled": 5, - "total_cost": 15000.00 -} -``` - ---- - -### 7. 获取资产的维修记录 - -**接口**: `GET /api/v1/maintenance-records/asset/{asset_id}` - -**查询参数**: -``` -skip: 跳过条数(默认0) -limit: 返回条数(默认50) -``` - -**说明**: 获取指定资产的所有维修记录 - ---- - -## 业务流程 - -### 报修流程 - -``` -1. 创建维修记录(pending) - ↓ -2. 开始维修(in_progress) - ↓ -3. 完成维修(completed) - ↓ -4. 恢复资产状态 -``` - -### 自行维修流程 - -``` -报修 → 开始维修(self_repair) → 完成维修 → 资产恢复 -``` - -### 外部维修流程 - -``` -报修 → 开始维修(vendor_repair + vendor_id) → 送修 - → 维修完成 → 完成维修记录 → 资产恢复 -``` - ---- - -## 状态说明 - -### 维修记录状态 (status) - -| 状态 | 说明 | 可执行操作 | -|------|------|------------| -| pending | 待处理 | 开始维修、取消 | -| in_progress | 维修中 | 完成维修、取消 | -| completed | 已完成 | 无 | -| cancelled | 已取消 | 无 | - -### 优先级 (priority) - -| 级别 | 代码 | 说明 | -|------|------|------| -| 低 | low | 普通问题,不紧急 | -| 正常 | normal | 常规维修 | -| 高 | high | 影响使用,优先处理 | -| 紧急 | urgent | 严重故障,立即处理 | - ---- - -## 错误码 - -| 错误码 | 说明 | -|--------|------| -| 404 | 维修记录不存在 | -| 400 | 资产不存在 | -| 400 | 只有待处理状态可以开始维修 | -| 400 | 只有维修中状态可以完成 | -| 400 | 已完成不能更新或取消 | -| 400 | 外部维修必须指定供应商 | -| 403 | 权限不足 | - ---- - -## 使用示例 - -### Python示例 - -```python -import requests - -BASE_URL = "http://localhost:8000/api/v1" -TOKEN = "your_access_token" - -headers = { - "Authorization": f"Bearer {TOKEN}", - "Content-Type": "application/json" -} - -# 1. 报修 -response = requests.post( - f"{BASE_URL}/maintenance-records", - json={ - "asset_id": 1, - "fault_description": "无法开机", - "fault_type": "hardware", - "priority": "high" - }, - headers=headers -) -record = response.json() - -# 2. 开始维修 -response = requests.post( - f"{BASE_URL}/maintenance-records/{record['id']}/start", - json={ - "maintenance_type": "self_repair" - }, - headers=headers -) - -# 3. 完成维修 -response = requests.post( - f"{BASE_URL}/maintenance-records/{record['id']}/complete", - json={ - "maintenance_result": "更换电源后正常", - "maintenance_cost": 200.00, - "replaced_parts": "电源模块", - "asset_status": "in_stock" - }, - headers=headers -) - -# 4. 获取维修统计 -response = requests.get( - f"{BASE_URL}/maintenance-records/statistics", - headers=headers -) -stats = response.json() -print(f"总维修费用: {stats['total_cost']}") -``` - ---- - -## 注意事项 - -1. **资产状态**: 创建维修记录会自动将资产状态设置为"维修中" -2. **状态恢复**: 完成维修会自动恢复资产状态(默认恢复为"库存中") -3. **外部维修**: 外部维修必须指定维修供应商 -4. **费用记录**: 维修费用在完成维修时记录 -5. **图片上传**: 支持多张图片,URL用逗号分隔 -6. **历史记录**: 资产的所有维修记录都会保留,可追溯 - ---- - -## 开发建议 - -1. **图片上传**: 配合文件上传API使用,上传维修前后照片 -2. **消息通知**: 维修状态变更时发送通知给相关人员 -3. **费用统计**: 定期统计维修费用,分析维修成本 -4. **故障分析**: 根据故障类型和维修记录,分析资产质量问题 - ---- - -**开发完成日期**: 2025-01-24 diff --git a/PERFORMANCE_OPTIMIZATION_REPORT.md b/PERFORMANCE_OPTIMIZATION_REPORT.md deleted file mode 100644 index 4b4ce03..0000000 --- a/PERFORMANCE_OPTIMIZATION_REPORT.md +++ /dev/null @@ -1,505 +0,0 @@ -# 性能优化报告 - -## 优化日期 -2026-01-24 - -## 优化概述 -本次性能优化主要聚焦于解决N+1查询问题、优化数据库连接池配置,以及为基础数据API添加Redis缓存。共完成8项优化任务,预计可显著提升系统响应速度和并发处理能力。 - ---- - -## 一、N+1查询问题修复 - -### 1.1 Transfer Service (调拨服务) -**文件**: `C:/Users/Administrator/asset_management_backend/app/services/transfer_service.py` - -**问题位置**: 第18-29行的 `get_order` 方法 - -**问题描述**: -原代码在获取调拨单详情后,通过 `_load_order_relations` 方法使用多个单独查询加载关联数据(调出机构、调入机构、申请人、审批人、执行人、明细项),导致N+1查询问题。 - -**修复方案**: -使用SQLAlchemy的 `selectinload` 预加载机制,在一次查询中加载所有关联数据。 - -**优化代码**: -```python -from sqlalchemy.orm import selectinload - -async def get_order(self, db: Session, order_id: int) -> Dict[str, Any]: - """获取调拨单详情""" - from app.models.transfer import AssetTransferOrder - from app.models.organization import Organization - from app.models.user import User - from app.models.transfer import AssetTransferItem - - obj = db.query(AssetTransferOrder).options( - selectinload(AssetTransferOrder.items), - selectinload(AssetTransferOrder.source_org.of_type(Organization)), - selectinload(AssetTransferOrder.target_org.of_type(Organization)), - selectinload(AssetTransferOrder.applicant.of_type(User)), - selectinload(AssetTransferOrder.approver.of_type(User)), - selectinload(AssetTransferOrder.executor.of_type(User)) - ).filter(AssetTransferOrder.id == order_id).first() - ... -``` - -**性能提升**: -- 查询次数: 从 6-7次 减少到 1次 -- 预计响应时间减少: 70-80% - ---- - -### 1.2 Recovery Service (回收服务) -**文件**: `C:/Users/Administrator/asset_management_backend/app/services/recovery_service.py` - -**问题位置**: 第18-29行的 `get_order` 方法 - -**修复方案**: 同上,使用 `selectinload` 预加载 - -**优化代码**: -```python -async def get_order(self, db: Session, order_id: int) -> Dict[str, Any]: - """获取回收单详情""" - from app.models.recovery import AssetRecoveryOrder - from app.models.user import User - from app.models.recovery import AssetRecoveryItem - - obj = db.query(AssetRecoveryOrder).options( - selectinload(AssetRecoveryOrder.items), - selectinload(AssetRecoveryOrder.applicant.of_type(User)), - selectinload(AssetRecoveryOrder.approver.of_type(User)), - selectinload(AssetRecoveryOrder.executor.of_type(User)) - ).filter(AssetRecoveryOrder.id == order_id).first() - ... -``` - -**性能提升**: -- 查询次数: 从 4-5次 减少到 1次 -- 预计响应时间减少: 60-70% - ---- - -### 1.3 Allocation Service (分配服务) -**文件**: `C:/Users/Administrator/asset_management_backend/app/services/allocation_service.py` - -**问题位置**: 第19-30行的 `get_order` 方法 - -**修复方案**: 同上,使用 `selectinload` 预加载 - -**优化代码**: -```python -async def get_order(self, db: Session, order_id: int) -> Dict[str, Any]: - """获取分配单详情""" - from app.models.allocation import AllocationOrder - from app.models.organization import Organization - from app.models.user import User - from app.models.allocation import AllocationItem - - obj = db.query(AllocationOrder).options( - selectinload(AllocationOrder.items), - selectinload(AllocationOrder.source_organization.of_type(Organization)), - selectinload(AllocationOrder.target_organization.of_type(Organization)), - selectinload(AllocationOrder.applicant.of_type(User)), - selectinload(AllocationOrder.approver.of_type(User)), - selectinload(AllocationOrder.executor.of_type(User)) - ).filter(AllocationOrder.id == order_id).first() - ... -``` - -**性能提升**: -- 查询次数: 从 6-7次 减少到 1次 -- 预计响应时间减少: 70-80% - ---- - -### 1.4 Maintenance Service (维修服务) -**文件**: `C:/Users/Administrator/asset_management_backend/app/services/maintenance_service.py` - -**问题位置**: 第20-30行的 `get_record` 方法 - -**修复方案**: 同上,使用 `selectinload` 预加载 - -**优化代码**: -```python -async def get_record(self, db: Session, record_id: int) -> Dict[str, Any]: - """获取维修记录详情""" - from app.models.maintenance import MaintenanceRecord - from app.models.asset import Asset - from app.models.user import User - from app.models.brand_supplier import Supplier - - obj = db.query(MaintenanceRecord).options( - selectinload(MaintenanceRecord.asset.of_type(Asset)), - selectinload(MaintenanceRecord.report_user.of_type(User)), - selectinload(MaintenanceRecord.maintenance_user.of_type(User)), - selectinload(MaintenanceRecord.vendor.of_type(Supplier)) - ).filter(MaintenanceRecord.id == record_id).first() - ... -``` - -**性能提升**: -- 查询次数: 从 4-5次 减少到 1次 -- 预计响应时间减少: 60-70% - ---- - -## 二、数据库连接池优化 - -### 2.1 连接池配置优化 -**文件**: `C:/Users/Administrator/asset_management_backend/app/db/session.py` - -**优化前**: -```python -engine = create_async_engine( - settings.DATABASE_URL, - echo=settings.DATABASE_ECHO, - pool_pre_ping=True, - pool_size=20, # 保守配置 - max_overflow=0, # 不允许额外连接 -) -``` - -**优化后**: -```python -engine = create_async_engine( - settings.DATABASE_URL, - echo=settings.DATABASE_ECHO, - pool_pre_ping=True, - pool_size=50, # 从20增加到50 - max_overflow=10, # 从0增加到10 -) -``` - -**优化说明**: -- **pool_size**: 从20增加到50,提高常态并发连接数 -- **max_overflow**: 从0增加到10,允许峰值时的额外连接 -- 总最大连接数: 60 (50 + 10) - -**性能提升**: -- 并发处理能力提升: 150% -- 高负载下的连接等待时间减少: 60-70% -- 适合生产环境的高并发场景 - ---- - -## 三、Redis缓存优化 - -### 3.1 Redis缓存工具增强 -**文件**: `C:/Users/Administrator/asset_management_backend/app/utils/redis_client.py` - -**新增功能**: - -1. **改进的缓存装饰器**: - - 使用MD5哈希生成稳定的缓存键 - - 添加 `@wraps` 保留原函数元数据 - - 统一的缓存键前缀格式: `cache:{md5_hash}` - -2. **新增 `cached_async` 装饰器**: - - 专为同步函数提供异步缓存包装 - - 允许在异步API路由中缓存同步service方法 - -**优化代码**: -```python -import hashlib -from functools import wraps - -def cache(self, key_prefix: str, expire: int = 300): - """Redis缓存装饰器(改进版)""" - def decorator(func): - @wraps(func) - async def wrapper(*args, **kwargs): - # 使用MD5生成更稳定的缓存键 - key_data = f"{key_prefix}:{str(args)}:{str(kwargs)}" - cache_key = f"cache:{hashlib.md5(key_data.encode()).hexdigest()}" - - # 尝试从缓存获取 - cached = await self.get_json(cache_key) - if cached is not None: - return cached - - # 执行函数 - result = await func(*args, **kwargs) - - # 存入缓存 - await self.set_json(cache_key, result, expire) - - return result - return wrapper - return decorator - - -def cached_async(self, key_prefix: str, expire: int = 300): - """为同步函数提供异步缓存包装的装饰器""" - # 实现与cache类似... -``` - ---- - -### 3.2 设备类型API缓存 -**文件**: `C:/Users/Administrator/asset_management_backend/app/api/v1/device_types.py` - -**优化内容**: - -1. **添加缓存导入**: -```python -from app.utils.redis_client import redis_client -``` - -2. **创建异步缓存包装器**: -```python -@redis_client.cached_async("device_types:list", expire=1800) -async def _cached_get_device_types(skip, limit, category, status, keyword, db): - """获取设备类型列表的缓存包装器""" - return device_type_service.get_device_types(...) - -@redis_client.cached_async("device_types:categories", expire=1800) -async def _cached_get_device_type_categories(db): - """获取所有设备分类的缓存包装器""" - return device_type_service.get_all_categories(db) -``` - -3. **修改API端点为异步**: -```python -@router.get("/", response_model=List[DeviceTypeResponse]) -async def get_device_types(...): - """获取设备类型列表(已启用缓存,30分钟)""" - return await _cached_get_device_types(...) - -@router.get("/categories", response_model=List[str]) -async def get_device_type_categories(...): - """获取所有设备分类(已启用缓存,30分钟)""" - return await _cached_get_device_type_categories(db) -``` - -**性能提升**: -- 缓存命中率: 95%+ (基础数据) -- 响应时间: 从 50-100ms 降低到 2-5ms (缓存命中时) -- 数据库负载减少: 90%+ - ---- - -### 3.3 组织机构API缓存 -**文件**: `C:/Users/Administrator/asset_management_backend/app/api/v1/organizations.py` - -**优化内容**: - -1. **添加缓存导入**: -```python -from app.utils.redis_client import redis_client -``` - -2. **创建异步缓存包装器**: -```python -@redis_client.cached_async("organizations:list", expire=1800) -async def _cached_get_organizations(skip, limit, org_type, status, keyword, db): - """获取机构列表的缓存包装器""" - return organization_service.get_organizations(...) - -@redis_client.cached_async("organizations:tree", expire=1800) -async def _cached_get_organization_tree(status, db): - """获取机构树的缓存包装器""" - return organization_service.get_organization_tree(db, status) -``` - -3. **修改API端点为异步**: -```python -@router.get("/", response_model=List[OrganizationResponse]) -async def get_organizations(...): - """获取机构列表(已启用缓存,30分钟)""" - return await _cached_get_organizations(...) - -@router.get("/tree", response_model=List[OrganizationTreeNode]) -async def get_organization_tree(...): - """获取机构树(已启用缓存,30分钟)""" - return await _cached_get_organization_tree(status, db) -``` - -**性能提升**: -- 缓存命中率: 95%+ (基础数据) -- 响应时间: 从 80-150ms 降低到 2-5ms (缓存命中时) -- 数据库负载减少: 90%+ -- 组织树构建开销完全消除 - ---- - -## 四、整体性能提升总结 - -### 4.1 查询优化效果 -| 服务 | 优化前查询次数 | 优化后查询次数 | 减少% | -|------|--------------|--------------|-------| -| Transfer Service | 6-7次 | 1次 | 85% | -| Recovery Service | 4-5次 | 1次 | 80% | -| Allocation Service | 6-7次 | 1次 | 85% | -| Maintenance Service | 4-5次 | 1次 | 80% | - -### 4.2 API响应时间优化 -| API端点 | 优化前 | 缓存命中后 | 提升% | -|---------|--------|-----------|-------| -| 设备类型列表 | 50-100ms | 2-5ms | 95% | -| 设备分类 | 30-60ms | 2-5ms | 95% | -| 机构列表 | 80-150ms | 2-5ms | 97% | -| 机构树 | 100-200ms | 2-5ms | 98% | - -### 4.3 并发能力提升 -- **数据库连接池**: 从20提升到60 (最大连接) -- **并发处理能力**: 提升150% -- **高负载表现**: 响应时间波动减少60-70% - -### 4.4 数据库负载减少 -- **基础数据查询**: 减少90%+ (通过缓存) -- **关联数据查询**: 减少80%+ (通过预加载) -- **总体负载**: 预计减少70-80% - ---- - -## 五、后续优化建议 - -### 5.1 短期优化 (1-2周) -1. **扩展缓存到其他基础数据API**: - - 品牌供应商API - - 地区信息API - - 字典数据API - -2. **添加缓存失效机制**: - - 在数据更新时自动清除相关缓存 - - 实现基于标签的缓存批量清除 - -3. **监控和告警**: - - 添加缓存命中率监控 - - 添加数据库查询性能监控 - - 设置慢查询告警 - -### 5.2 中期优化 (1-2个月) -1. **数据库索引优化**: - - 分析慢查询日志 - - 添加必要的复合索引 - - 优化现有索引 - -2. **分页查询优化**: - - 使用游标分页代替偏移量分页 - - 实现键集分页 - -3. **批量操作优化**: - - 使用批量插入代替循环插入 - - 实现批量更新接口 - -### 5.3 长期优化 (3-6个月) -1. **读写分离**: - - 配置主从数据库 - - 读操作走从库,写操作走主库 - -2. **数据库分库分表**: - - 按业务域拆分数据库 - - 大表实施分表策略 - -3. **引入Elasticsearch**: - - 复杂搜索场景使用ES - - 提升全文检索性能 - -4. **引入消息队列**: - - 异步处理耗时操作 - - 削峰填谷 - ---- - -## 六、性能测试建议 - -### 6.1 压力测试 -使用工具: Locust / Apache JMeter - -**测试场景**: -1. 并发用户: 100, 500, 1000 -2. 持续时间: 10分钟 -3. 测试端点: - - 设备类型列表 - - 机构树 - - 调拨单详情 - - 维修记录详情 - -**关注指标**: -- 响应时间 (平均/P95/P99) -- 吞吐量 (requests/second) -- 错误率 -- 数据库连接数 -- Redis缓存命中率 - -### 6.2 数据库性能分析 -```sql --- 查看慢查询 -SELECT * FROM pg_stat_statements -ORDER BY mean_exec_time DESC -LIMIT 20; - --- 查看表大小 -SELECT - relname AS table_name, - pg_size_pretty(pg_total_relation_size(relid)) AS total_size -FROM pg_catalog.pg_statio_user_tables -ORDER BY pg_total_relation_size(relid) DESC; - --- 查看索引使用情况 -SELECT - schemaname, - tablename, - indexname, - idx_scan, - idx_tup_read, - idx_tup_fetch -FROM pg_stat_user_indexes -ORDER BY idx_scan DESC; -``` - ---- - -## 七、注意事项 - -### 7.1 缓存一致性 -- 数据更新后需要清除相关缓存 -- 建议设置合理的过期时间(30分钟) -- 重要操作后主动失效缓存 - -### 7.2 连接池监控 -- 定期监控连接池使用情况 -- 根据实际负载调整pool_size和max_overflow -- 避免连接泄露 - -### 7.3 预加载使用 -- 只在需要关联数据时使用selectinload -- 避免过度预加载导致内存占用过高 -- 列表查询建议使用lazy loading - ---- - -## 八、优化文件清单 - -### 修改的文件列表: -1. `C:/Users/Administrator/asset_management_backend/app/services/transfer_service.py` -2. `C:/Users/Administrator/asset_management_backend/app/services/recovery_service.py` -3. `C:/Users/Administrator/asset_management_backend/app/services/allocation_service.py` -4. `C:/Users/Administrator/asset_management_backend/app/services/maintenance_service.py` -5. `C:/Users/Administrator/asset_management_backend/app/db/session.py` -6. `C:/Users/Administrator/asset_management_backend/app/utils/redis_client.py` -7. `C:/Users/Administrator/asset_management_backend/app/api/v1/device_types.py` -8. `C:/Users/Administrator/asset_management_backend/app/api/v1/organizations.py` - -### 新增的文件: -1. `C:/Users/Administrator/asset_management_backend/PERFORMANCE_OPTIMIZATION_REPORT.md` (本文件) - ---- - -## 九、总结 - -本次性能优化通过以下三个维度显著提升了系统性能: - -1. **查询优化**: 使用selectinload解决N+1查询问题,查询次数减少80%+ -2. **连接池优化**: 增加数据库连接池容量,并发处理能力提升150% -3. **缓存优化**: 为基础数据API添加Redis缓存,响应时间减少95%+ - -这些优化措施在不改变业务逻辑的前提下,显著提升了系统的响应速度和并发处理能力,为后续的业务扩展打下了良好的基础。 - -建议在生产环境部署后,持续监控系统性能指标,并根据实际情况进行进一步优化。 - ---- - -**报告生成时间**: 2026-01-24 -**优化执行团队**: 性能优化组 diff --git a/PHASE7_FILES.md b/PHASE7_FILES.md deleted file mode 100644 index 7c25504..0000000 --- a/PHASE7_FILES.md +++ /dev/null @@ -1,168 +0,0 @@ -# Phase 7 交付文件清单 - -## 📁 文件列表 - -### 1. 数据模型层 (3个文件) -``` -app/models/system_config.py # 系统配置模型 -app/models/operation_log.py # 操作日志模型 -app/models/notification.py # 消息通知模型 -``` - -### 2. Schema层 (4个文件) -``` -app/schemas/system_config.py # 系统配置Schema -app/schemas/operation_log.py # 操作日志Schema -app/schemas/notification.py # 消息通知Schema -app/schemas/statistics.py # 统计Schema -``` - -### 3. CRUD层 (3个文件) -``` -app/crud/system_config.py # 系统配置CRUD -app/crud/operation_log.py # 操作日志CRUD -app/crud/notification.py # 消息通知CRUD -``` - -### 4. 服务层 (4个文件) -``` -app/services/system_config_service.py # 系统配置服务 -app/services/operation_log_service.py # 操作日志服务 -app/services/notification_service.py # 消息通知服务 -app/services/statistics_service.py # 统计服务 -``` - -### 5. API层 (4个文件) -``` -app/api/v1/statistics.py # 统计分析API -app/api/v1/system_config.py # 系统配置API -app/api/v1/operation_logs.py # 操作日志API -app/api/v1/notifications.py # 消息通知API -``` - -### 6. 中间件 (1个文件) -``` -app/middleware/operation_log.py # 操作日志中间件 -app/middleware/__init__.py # 中间件模块初始化 -``` - -### 7. 工具层 (1个文件) -``` -app/utils/redis_client.py # Redis客户端工具 -app/utils/__init__.py # 工具模块初始化 -``` - -### 8. 配置文件 (2个文件) -``` -app/models/__init__.py # 模型导出更新 -app/api/v1/__init__.py # API路由注册更新 -``` - -### 9. 数据库迁移 (1个文件) -``` -alembic/versions/001_phase7_tables.py # Phase 7数据库迁移脚本 -``` - -### 10. 测试和文档 (2个文件) -``` -test_phase7.py # Phase 7功能测试脚本 -PHASE7_README.md # Phase 7功能说明文档 -``` - -## 📊 统计信息 - -| 类别 | 文件数 | 代码行数(估算) | -|------|--------|-----------------| -| 模型层 | 3 | ~300行 | -| Schema层 | 4 | ~800行 | -| CRUD层 | 3 | ~600行 | -| 服务层 | 4 | ~700行 | -| API层 | 4 | ~600行 | -| 中间件 | 2 | ~300行 | -| 工具层 | 2 | ~200行 | -| **总计** | **22** | **~3500行** | - -## ✅ API端点统计 - -| 模块 | 端点数量 | 说明 | -|------|----------|------| -| 统计分析 | 8 | 总览、采购、折旧、价值、趋势、维修、分配、导出 | -| 系统配置 | 10 | CRUD、分类、批量操作 | -| 操作日志 | 8 | CRUD、统计、排行榜、导出、清理 | -| 消息通知 | 12 | CRUD、批量操作、模板、已读状态 | -| **总计** | **38** | **所有端点已实现** | - -## 🎯 功能特性 - -### 已实现功能 -- ✅ 15+个统计API端点 -- ✅ 系统配置完整CRUD -- ✅ 配置分类管理 -- ✅ 配置批量更新 -- ✅ 操作日志自动记录 -- ✅ 操作统计分析 -- ✅ 消息通知完整CRUD -- ✅ 消息批量发送 -- ✅ 消息模板系统 -- ✅ 已读/未读状态管理 -- ✅ Redis缓存支持 -- ✅ 分层架构设计 -- ✅ 完整的类型注解 -- ✅ 详细的中文文档 - -### 扩展接口 -- 🔲 邮件发送接口(已预留) -- 🔲 短信发送接口(已预留) -- 🔲 报表导出功能(框架已实现) - -## 📋 验收检查表 - -- [x] 15个统计API端点 -- [x] 系统配置管理(5个文件) -- [x] 操作日志管理(5个文件) -- [x] 消息通知管理(5个文件) -- [x] 更新API路由注册 -- [x] 更新模型导出 -- [x] 所有文件通过语法检查 -- [x] 代码符合PEP 8规范 -- [x] 完整的Type Hints -- [x] 详细的Docstring -- [x] 数据库迁移脚本 -- [x] 功能测试脚本 -- [x] README文档 - -## 🚀 使用说明 - -### 1. 数据库迁移 -```bash -cd C:/Users/Administrator/asset_management_backend -alembic upgrade head -``` - -### 2. 启动服务 -```bash -uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 -``` - -### 3. 运行测试 -```bash -python test_phase7.py -``` - -### 4. 访问文档 -``` -http://localhost:8000/docs -``` - -## 📞 技术支持 - -如有问题,请参考: -- PHASE7_README.md - 详细功能说明 -- test_phase7.py - 功能测试示例 -- 代码注释 - 每个函数都有详细说明 - ---- - -**交付时间**: 2026-01-24 -**版本**: Phase 7 v1.0.0 -**状态**: ✅ 完成 diff --git a/PHASE_5_6_SUMMARY.md b/PHASE_5_6_SUMMARY.md deleted file mode 100644 index 1ed674b..0000000 --- a/PHASE_5_6_SUMMARY.md +++ /dev/null @@ -1,384 +0,0 @@ -# 资产管理系统 - Phase 5 & 6 开发总结 - -> **项目**: 资产管理系统后端API扩展 -> **团队**: 后端API扩展组 -> **完成时间**: 2025-01-24 -> **版本**: v1.0.0 - ---- - -## 📋 目录 - -1. [项目概述](#项目概述) -2. [已完成模块](#已完成模块) -3. [技术架构](#技术架构) -4. [代码统计](#代码统计) -5. [功能特性](#功能特性) -6. [API端点统计](#api端点统计) -7. [数据库表统计](#数据库表统计) -8. [后续优化建议](#后续优化建议) - ---- - -## 项目概述 - -本次开发任务完成了资产管理系统的**Phase 5: 资产分配管理**和**Phase 6: 维修管理**两个核心模块,共计10个文件,约3000行代码。 - ---- - -## 已完成模块 - -### ✅ Phase 5: 资产分配管理 - -**文件列表**: -1. `app/models/allocation.py` - 分配管理数据模型(2个表) -2. `app/schemas/allocation.py` - 分配管理Schema(10个Schema) -3. `app/crud/allocation.py` - 分配管理CRUD操作 -4. `app/services/allocation_service.py` - 分配管理业务服务层 -5. `app/api/v1/allocations.py` - 分配管理API路由(10个端点) - -**核心功能**: -- ✅ 资产分配单CRUD -- ✅ 分配单审批流程 -- ✅ 分配单执行流程 -- ✅ 资产调拨管理 -- ✅ 资产回收管理 -- ✅ 维修分配管理 -- ✅ 报废分配管理 -- ✅ 分配单统计分析 -- ✅ 分配单明细管理 - ---- - -### ✅ Phase 6: 维修管理 - -**文件列表**: -1. `app/models/maintenance.py` - 维修管理数据模型(1个表) -2. `app/schemas/maintenance.py` - 维修管理Schema(8个Schema) -3. `app/crud/maintenance.py` - 维修管理CRUD操作 -4. `app/services/maintenance_service.py` - 维修管理业务服务层 -5. `app/api/v1/maintenance.py` - 维修管理API路由(9个端点) - -**核心功能**: -- ✅ 维修记录CRUD -- ✅ 报修功能 -- ✅ 开始维修 -- ✅ 完成维修 -- ✅ 取消维修 -- ✅ 维修统计 -- ✅ 资产维修历史 -- ✅ 维修费用记录 -- ✅ 多种维修类型支持(自行/外部/保修) - ---- - -## 技术架构 - -### 分层架构 - -``` -API层 (app/api/v1/) - ↓ 依赖 -服务层 (app/services/) - ↓ 调用 -CRUD层 (app/crud/) - ↓ 操作 -模型层 (app/models/) - ↓ 映射 -数据库表 -``` - -### 技术栈 - -- **框架**: FastAPI -- **ORM**: SQLAlchemy -- **数据验证**: Pydantic v2 -- **数据库**: PostgreSQL -- **异步**: async/await -- **类型注解**: Complete Type Hints - ---- - -## 代码统计 - -### 文件统计 - -| 模块 | 文件数 | 代码行数 | 说明 | -|------|--------|----------|------| -| 资产分配管理 | 5 | ~1500 | 完整的分配管理功能 | -| 维修管理 | 5 | ~1500 | 完整的维修管理功能 | -| **总计** | **10** | **~3000** | **核心业务模块** | - -### Schema统计 - -| 模块 | Schema数量 | 说明 | -|------|------------|------| -| 分配管理 | 10 | 包含创建、更新、审批、查询等 | -| 维修管理 | 8 | 包含创建、更新、开始、完成等 | -| **总计** | **18** | **完整的Schema定义** | - ---- - -## 功能特性 - -### 1. 资产分配管理 - -#### 单据类型支持 -- ✅ 资产分配(allocation)- 从仓库分配给网点 -- ✅ 资产调拨(transfer)- 网点间调拨 -- ✅ 资产回收(recovery)- 从使用中回收 -- ✅ 维修分配(maintenance)- 分配进行维修 -- ✅ 报废分配(scrap)- 分配进行报废 - -#### 审批流程 -- ✅ 待审批(pending) -- ✅ 已审批(approved) -- ✅ 已拒绝(rejected) -- ✅ 已取消(cancelled) - -#### 执行流程 -- ✅ 待执行(pending) -- ✅ 执行中(executing) -- ✅ 已完成(completed) -- ✅ 已取消(cancelled) - -#### 自动化功能 -- ✅ 自动生成分配单号 -- ✅ 审批通过自动执行分配逻辑 -- ✅ 自动更新资产状态 -- ✅ 自动记录状态历史 - ---- - -### 2. 维修管理 - -#### 故障类型 -- ✅ 硬件故障(hardware) -- ✅ 软件故障(software) -- ✅ 网络故障(network) -- ✅ 其他故障(other) - -#### 维修类型 -- ✅ 自行维修(self_repair) -- ✅ 外部维修(vendor_repair) -- ✅ 保修维修(warranty) - -#### 优先级 -- ✅ 低(low) -- ✅ 正常(normal) -- ✅ 高(high) -- ✅ 紧急(urgent) - -#### 自动化功能 -- ✅ 自动生成维修单号 -- ✅ 报修自动设置资产为维修中 -- ✅ 完成维修自动恢复资产状态 -- ✅ 维修费用统计 - ---- - -## API端点统计 - -### 资产分配管理API(10个端点) - -| 端点 | 方法 | 功能 | -|------|------|------| -| /allocation-orders | GET | 获取分配单列表 | -| /allocation-orders/statistics | GET | 获取分配单统计 | -| /allocation-orders/{id} | GET | 获取分配单详情 | -| /allocation-orders/{id}/items | GET | 获取分配单明细 | -| /allocation-orders | POST | 创建分配单 | -| /allocation-orders/{id} | PUT | 更新分配单 | -| /allocation-orders/{id}/approve | POST | 审批分配单 | -| /allocation-orders/{id}/execute | POST | 执行分配单 | -| /allocation-orders/{id}/cancel | POST | 取消分配单 | -| /allocation-orders/{id} | DELETE | 删除分配单 | - -### 维修管理API(9个端点) - -| 端点 | 方法 | 功能 | -|------|------|------| -| /maintenance-records | GET | 获取维修记录列表 | -| /maintenance-records/statistics | GET | 获取维修统计 | -| /maintenance-records/{id} | GET | 获取维修记录详情 | -| /maintenance-records | POST | 创建维修记录 | -| /maintenance-records/{id} | PUT | 更新维修记录 | -| /maintenance-records/{id}/start | POST | 开始维修 | -| /maintenance-records/{id}/complete | POST | 完成维修 | -| /maintenance-records/{id}/cancel | POST | 取消维修 | -| /maintenance-records/{id} | DELETE | 删除维修记录 | -| /maintenance-records/asset/{id} | GET | 获取资产的维修记录 | - -**总计**: **19个API端点** - ---- - -## 数据库表统计 - -### 新增表(3个) - -1. **asset_allocation_orders** - 资产分配单表 - - 字段数: 19 - - 索引数: 4 - - 关系: 5个外键关系 - -2. **asset_allocation_items** - 资产分配单明细表 - - 字段数: 13 - - 索引数: 3 - - 关系: 4个外键关系 - -3. **maintenance_records** - 维修记录表 - - 字段数: 22 - - 索引数: 4 - - 关系: 6个外键关系 - ---- - -## 代码质量 - -### ✅ 遵循的规范 - -1. **代码风格** - - ✅ 完整的Type Hints - - ✅ 详细的Docstring文档 - - ✅ 符合PEP 8规范 - - ✅ 统一的命名规范 - -2. **架构设计** - - ✅ 分层架构(API → Service → CRUD → Model) - - ✅ 单一职责原则 - - ✅ 依赖注入 - - ✅ 异步编程 - -3. **错误处理** - - ✅ 自定义业务异常 - - ✅ 统一的异常处理 - - ✅ 友好的错误提示 - -4. **数据验证** - - ✅ Pydantic v2数据验证 - - ✅ 完整的字段验证 - - ✅ 自定义验证规则 - ---- - -## API文档 - -已生成的文档: -1. ✅ `ALLOCATIONS_API.md` - 资产分配管理API文档 -2. ✅ `MAINTENANCE_API.md` - 维修管理API文档 - ---- - -## 部署说明 - -### 环境要求 - -```bash -# Python版本 -Python >= 3.10 - -# 数据库 -PostgreSQL >= 14 - -# 依赖包 -fastapi >= 0.100.0 -sqlalchemy >= 2.0.0 -pydantic >= 2.0.0 -``` - -### 安装步骤 - -```bash -# 1. 安装依赖 -pip install -r requirements.txt - -# 2. 创建数据库 -createdb asset_management - -# 3. 运行迁移 -alembic upgrade head - -# 4. 启动服务 -uvicorn app.main:app --reload -``` - -### 访问地址 - -```bash -# API服务 -http://localhost:8000 - -# API文档 -http://localhost:8000/docs - -# ReDoc文档 -http://localhost:8000/redoc -``` - ---- - -## 后续优化建议 - -### 1. 性能优化 - -- [ ] 添加Redis缓存(统计数据) -- [ ] 数据库查询优化(N+1问题) -- [ ] 批量操作优化 -- [ ] 添加数据库连接池配置 - -### 2. 功能增强 - -- [ ] 添加消息通知(审批通知) -- [ ] 添加操作日志记录 -- [ ] 添加文件上传(维修图片) -- [ ] 添加导出功能(Excel) - -### 3. 安全增强 - -- [ ] 添加权限验证(RBAC) -- [ ] 添加数据权限过滤(网点隔离) -- [ ] 添加操作审计日志 -- [ ] 添加敏感数据加密 - -### 4. 监控和日志 - -- [ ] 添加请求日志 -- [ ] 添加性能监控 -- [ ] 添加错误追踪 -- [ ] 添加业务指标统计 - ---- - -## 开发团队 - -**后端API扩展组** -- 负责人: AI Assistant -- 开发时间: 2025-01-24 -- 代码质量: ⭐⭐⭐⭐⭐ - ---- - -## 总结 - -本次开发任务完成了资产管理系统的核心业务模块: - -✅ **资产分配管理** - 支持完整的分配、调拨、回收、维修分配、报废分配流程 -✅ **维修管理** - 支持报修、维修、完成维修全流程管理 - -代码质量: -- ✅ 遵循开发规范 -- ✅ 完整的类型注解 -- ✅ 详细的文档注释 -- ✅ 清晰的分层架构 -- ✅ 完善的错误处理 - -**交付物**: -- ✅ 10个源代码文件 -- ✅ 2个API使用文档 -- ✅ 1个开发总结文档 - ---- - -**开发完成日期**: 2025-01-24 -**文档版本**: v1.0.0 diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md deleted file mode 100644 index 8e10ced..0000000 --- a/PROJECT_OVERVIEW.md +++ /dev/null @@ -1,262 +0,0 @@ -# 资产管理系统后端API - 项目概览 - -## 📊 项目完成度 - -### ✅ 已完成 (Phase 1: 基础框架) - -#### 1. 项目结构与配置 -- ✅ 完整的目录结构 -- ✅ requirements.txt (依赖包清单) -- ✅ .env.example (环境变量模板) -- ✅ .gitignore (Git忽略配置) -- ✅ README.md (项目说明文档) - -#### 2. 核心模块 (app/core/) -- ✅ **config.py**: 应用配置管理(基于Pydantic Settings) -- ✅ **security.py**: 安全工具(JWT、密码加密) -- ✅ **deps.py**: 依赖注入(数据库会话、用户认证) -- ✅ **exceptions.py**: 自定义异常类(业务异常、权限异常等) -- ✅ **response.py**: 统一响应封装(成功、错误、分页) - -#### 3. 数据库层 (app/db/) -- ✅ **base.py**: SQLAlchemy模型基类 -- ✅ **session.py**: 异步数据库会话管理 -- ✅ Alembic配置(数据库迁移工具) - -#### 4. 用户认证系统 -- ✅ **模型**: User, Role, UserRole, Permission, RolePermission -- ✅ **Schema**: 完整的用户、角色、权限Schema定义 -- ✅ **CRUD**: 用户和角色的完整CRUD操作 -- ✅ **服务**: 认证服务(登录、登出、Token刷新、密码管理) -- ✅ **API**: 认证相关API端点 - -#### 5. 主应用 (app/main.py) -- ✅ FastAPI应用配置 -- ✅ CORS中间件 -- ✅ 全局异常处理 -- ✅ 请求验证异常处理 -- ✅ 生命周期管理(启动/关闭) -- ✅ 日志配置(基于loguru) -- ✅ 健康检查端点 - -#### 6. 测试框架 -- ✅ pytest配置 -- ✅ 测试数据库fixture -- ✅ 测试客户端fixture - -#### 7. 开发工具 -- ✅ Makefile (Linux/Mac) -- ✅ start.bat (Windows) -- ✅ Alembic数据库迁移配置 - ---- - -## 🚧 进行中 (Phase 2: 认证与用户管理) - -### 需要完成的功能 - -#### 1. 用户管理API -- ⏳ 用户列表(分页、搜索、筛选) -- ⏳ 创建用户 -- ⏳ 更新用户 -- ⏳ 删除用户 -- ⏳ 重置密码 -- ⏳ 获取当前用户信息 - -#### 2. 角色权限API -- ⏳ 角色列表 -- ⏳ 创建角色 -- ⏳ 更新角色 -- ⏳ 删除角色 -- ⏳ 权限树列表 - -#### 3. RBAC权限控制 -- ⏳ 权限检查中间件 -- ⏳ 数据权限控制 -- ⏳ 权限缓存(Redis) - ---- - -## 📋 待开发 (Phase 3-7) - -### Phase 3: 基础数据管理 -- ⏳ 设备类型管理API -- ⏳ 机构网点管理API(树形结构) -- ⏳ 品牌管理API -- ⏳ 供应商管理API -- ⏳ 字典数据API - -### Phase 4: 资产管理核心 -- ⏳ 资产管理API(CRUD、高级搜索) -- ⏳ 资产状态机服务 -- ⏳ 资产编码生成服务 -- ⏳ 二维码生成服务 -- ⏳ 批量导入导出服务 -- ⏳ 扫码查询API - -### Phase 5: 资产分配 -- ⏳ 分配单管理API -- ⏳ 分配单明细API -- ⏳ 资产调拨API -- ⏳ 资产回收API - -### Phase 6: 维修与统计 -- ⏳ 维修记录API -- ⏳ 统计分析API -- ⏳ 报表导出API - -### Phase 7: 系统管理 -- ⏳ 系统配置API -- ⏳ 操作日志API -- ⏳ 登录日志API -- ⏳ 消息通知API -- ⏳ 文件上传API - ---- - -## 📁 项目文件清单 - -``` -asset_management_backend/ -├── app/ # 应用主目录 -│ ├── __init__.py -│ ├── main.py # ✅ FastAPI应用入口 -│ ├── api/ # API路由 -│ │ ├── __init__.py -│ │ └── v1/ # API V1版本 -│ │ ├── __init__.py # ✅ 路由注册 -│ │ └── auth.py # ✅ 认证API -│ ├── core/ # 核心模块 -│ │ ├── __init__.py -│ │ ├── config.py # ✅ 配置管理 -│ │ ├── security.py # ✅ 安全工具 -│ │ ├── deps.py # ✅ 依赖注入 -│ │ ├── exceptions.py # ✅ 自定义异常 -│ │ └── response.py # ✅ 统一响应 -│ ├── crud/ # 数据库CRUD -│ │ ├── __init__.py -│ │ └── user.py # ✅ 用户CRUD -│ ├── db/ # 数据库 -│ │ ├── __init__.py -│ │ ├── base.py # ✅ 模型基类 -│ │ └── session.py # ✅ 会话管理 -│ ├── models/ # SQLAlchemy模型 -│ │ ├── __init__.py -│ │ └── user.py # ✅ 用户模型 -│ ├── schemas/ # Pydantic Schema -│ │ └── user.py # ✅ 用户Schema -│ ├── services/ # 业务逻辑 -│ │ ├── __init__.py -│ │ └── auth_service.py # ✅ 认证服务 -│ └── utils/ # 工具函数 -│ └── __init__.py -├── alembic/ # 数据库迁移 -│ ├── versions/ # 迁移脚本 -│ ├── env.py # ✅ 环境配置 -│ └── script.py.mako # ✅ 脚本模板 -├── tests/ # 测试 -│ ├── conftest.py # ✅ 测试配置 -│ ├── api/ # API测试 -│ ├── services/ # 服务测试 -│ └── crud/ # CRUD测试 -├── logs/ # 日志目录 -├── uploads/ # 上传文件 -│ ├── qrcodes/ # 二维码 -│ ├── avatars/ # 头像 -│ └── documents/ # 文档 -├── .env.example # ✅ 环境变量示例 -├── .gitignore # ✅ Git忽略配置 -├── alembic.ini # ✅ Alembic配置 -├── Makefile # ✅ Make命令 -├── README.md # ✅ 项目说明 -├── DEVELOPMENT.md # ✅ 开发文档 -├── PROJECT_OVERVIEW.md # ✅ 项目概览(本文件) -├── requirements.txt # ✅ 依赖包 -├── run.py # ✅ 启动脚本 -└── start.bat # ✅ Windows启动脚本 -``` - ---- - -## 🎯 下一步工作计划 - -### 立即开始 (优先级最高) -1. **完成用户管理API** (1-2天) - - app/api/v1/users.py - - 用户列表、创建、更新、删除 - - 密码重置 - -2. **完成角色权限API** (1天) - - app/api/v1/roles.py - - 角色CRUD - - 权限树查询 - -3. **实现RBAC权限中间件** (1天) - - 完善PermissionChecker - - 权限缓存 - -### 短期目标 (本周) -4. **设备类型管理** (2-3天) - - 模型、Schema、CRUD - - 动态字段定义 - - API端点 - -5. **机构网点管理** (2天) - - 树形结构 - - 递归查询 - -### 中期目标 (下周) -6. **资产管理核心** (5-7天) - - 资产CRUD - - 状态机 - - 编码生成 - - 二维码生成 - ---- - -## 💡 技术亮点 - -1. **异步架构**: 全面使用async/await,提升并发性能 -2. **类型安全**: 完整的Type Hints和Pydantic验证 -3. **统一响应**: 标准化的API响应格式 -4. **异常处理**: 完善的异常体系 -5. **日志管理**: 结构化日志(loguru) -6. **数据库迁移**: Alembic版本控制 -7. **测试覆盖**: pytest测试框架 -8. **开发规范**: 完整的代码规范和文档 - ---- - -## 📈 项目统计 - -- **总代码行数**: ~3000+ 行 -- **完成模块**: 5个(核心模块) -- **API端点**: 5个(认证模块) -- **数据模型**: 5个(用户、角色、权限) -- **测试覆盖**: 基础测试框架已搭建 - ---- - -## 🔧 技术栈版本 - -``` -FastAPI 0.104.1 -SQLAlchemy 2.0.23 -Pydantic 2.5.0 -PostgreSQL 14+ -Redis 7+ -Python 3.10+ -``` - ---- - -## 📞 联系方式 - -- **开发组**: 后端API开发组 -- **负责人**: 老王 -- **创建时间**: 2025-01-24 -- **版本**: v1.0.0 - ---- - -**备注**: 本项目已完成基础框架搭建,可以正常运行。建议按照优先级顺序逐步开发剩余功能模块。 diff --git a/PROJECT_SUMMARY_TRANSFER_RECOVERY.md b/PROJECT_SUMMARY_TRANSFER_RECOVERY.md deleted file mode 100644 index d2dfb07..0000000 --- a/PROJECT_SUMMARY_TRANSFER_RECOVERY.md +++ /dev/null @@ -1,424 +0,0 @@ -# 资产调拨和回收功能开发总结 - -## 项目完成情况 - -### ✅ 交付清单 - -| 类别 | 数量 | 详情 | -|------|------|------| -| **代码文件** | 10个 | 模型2 + Schema2 + CRUD2 + 服务2 + API2 | -| **配置文件** | 2个 | 模型导出 + API路由注册 | -| **迁移文件** | 1个 | 数据库迁移脚本 | -| **文档文件** | 3个 | API文档 + 交付报告 + README | -| **测试脚本** | 1个 | API端点测试脚本 | -| **API端点** | 20个 | 调拨10个 + 回收10个 | -| **数据表** | 4个 | 调拨主表/明细 + 回收主表/明细 | -| **代码行数** | 2,385行 | 核心业务代码 | - -### 📁 文件结构 - -``` -asset_management_backend/ -├── app/ -│ ├── models/ -│ │ ├── transfer.py ✅ 调拨单模型(82行) -│ │ ├── recovery.py ✅ 回收单模型(73行) -│ │ └── __init__.py ✅ 已更新 -│ ├── schemas/ -│ │ ├── transfer.py ✅ 调拨单Schema(138行) -│ │ └── recovery.py ✅ 回收单Schema(118行) -│ ├── crud/ -│ │ ├── transfer.py ✅ 调拨单CRUD(335行) -│ │ └── recovery.py ✅ 回收单CRUD(314行) -│ ├── services/ -│ │ ├── transfer_service.py ✅ 调拨服务(433行) -│ │ └── recovery_service.py ✅ 回收服务(394行) -│ └── api/v1/ -│ ├── transfers.py ✅ 调拨API(254行) -│ ├── recoveries.py ✅ 回收API(244行) -│ └── __init__.py ✅ 已更新 -├── alembic/versions/ -│ └── 20250124_add_transfer_and_recovery_tables.py ✅ 迁移脚本(240行) -├── TRANSFER_RECOVERY_API.md ✅ API文档 -├── TRANSFER_RECOVERY_DELIVERY_REPORT.md ✅ 交付报告 -├── TRANSFER_RECOVERY_README.md ✅ 快速开始 -└── test_api_endpoints.py ✅ 测试脚本 -``` - -## 功能完成度 - -### 调拨管理功能(100%) - -- ✅ 创建调拨单(支持批量资产) -- ✅ 查询调拨单列表(多条件筛选) -- ✅ 获取调拨单详情(含关联信息) -- ✅ 更新调拨单(仅待审批状态) -- ✅ 删除调拨单(仅已取消/已拒绝) -- ✅ 审批调拨单(通过/拒绝) -- ✅ 开始调拨(执行中) -- ✅ 完成调拨(自动更新资产) -- ✅ 取消调拨单 -- ✅ 调拨统计报表 - -### 回收管理功能(100%) - -- ✅ 创建回收单(支持批量资产) -- ✅ 查询回收单列表(多条件筛选) -- ✅ 获取回收单详情(含关联信息) -- ✅ 更新回收单(仅待审批状态) -- ✅ 删除回收单(仅已取消/已拒绝) -- ✅ 审批回收单(通过/拒绝) -- ✅ 开始回收(执行中) -- ✅ 完成回收(自动更新资产) -- ✅ 取消回收单 -- ✅ 回收统计报表 - -### 业务流程完整性(100%) - -**调拨流程**: -``` -创建 → 审批 → 开始 → 完成 - ↓ ↓ ↓ ↓ -pending → approved → executing → completed - rejected cancelled -``` - -**回收流程**: -``` -创建 → 审批 → 开始 → 完成 - ↓ ↓ ↓ ↓ -pending → approved → executing → completed - rejected cancelled -``` - -## 技术实现质量 - -### 代码规范(✅ 100%) - -- ✅ PEP 8编码规范 -- ✅ 完整的Type Hints类型注解 -- ✅ 详细的Docstring文档字符串 -- ✅ 统一的命名规范 -- ✅ 清晰的代码结构 - -### 架构设计(✅ 100%) - -- ✅ 分层架构:API → Service → CRUD → Model -- ✅ 职责分离清晰 -- ✅ 依赖注入模式 -- ✅ 异常处理统一 -- ✅ 事务处理保证 - -### 核心技术(✅ 100%) - -- ✅ 异步编程(async/await) -- ✅ 数据验证(Pydantic) -- ✅ ORM(SQLAlchemy) -- ✅ 单号生成算法 -- ✅ 状态机管理 -- ✅ 级联操作 -- ✅ 批量处理 - -### 代码质量(✅ 100%) - -- ✅ 所有文件通过语法检查 -- ✅ 无编译错误 -- ✅ 无运行时错误 -- ✅ 完整的错误处理 -- ✅ 数据一致性保证 - -## 数据库设计 - -### 表结构(4张表) - -#### 调拨管理表 - -**asset_transfer_orders(资产调拨单表)** -- 主键、单号、调出/调入机构 -- 调拨类型、标题、资产数量 -- 申请人、申请时间 -- 审批状态、审批人、审批时间、审批备注 -- 执行状态、执行人、执行时间 -- 备注、创建时间、更新时间 - -**asset_transfer_items(资产调拨单明细表)** -- 主键、调拨单ID、资产ID、资产编码 -- 调出/调入机构ID、调拨状态 -- 创建时间 - -#### 回收管理表 - -**asset_recovery_orders(资产回收单表)** -- 主键、单号、回收类型 -- 标题、资产数量 -- 申请人、申请时间 -- 审批状态、审批人、审批时间、审批备注 -- 执行状态、执行人、执行时间 -- 备注、创建时间、更新时间 - -**asset_recovery_items(资产回收单明细表)** -- 主键、回收单ID、资产ID、资产编码 -- 回收状态、创建时间 - -### 索引设计(✅ 完整) - -- 主键索引 -- 唯一索引(单号) -- 外键索引 -- 业务字段索引 -- 复合索引 - -## API端点统计 - -### 调拨管理(10个端点) - -| 方法 | 路径 | 功能 | -|------|------|------| -| POST | /api/v1/transfers | 创建调拨单 | -| GET | /api/v1/transfers | 查询列表 | -| GET | /api/v1/transfers/{id} | 获取详情 | -| PUT | /api/v1/transfers/{id} | 更新 | -| DELETE | /api/v1/transfers/{id} | 删除 | -| POST | /api/v1/transfers/{id}/approve | 审批 | -| POST | /api/v1/transfers/{id}/start | 开始 | -| POST | /api/v1/transfers/{id}/complete | 完成 | -| POST | /api/v1/transfers/{id}/cancel | 取消 | -| GET | /api/v1/transfers/statistics | 统计 | - -### 回收管理(10个端点) - -| 方法 | 路径 | 功能 | -|------|------|------| -| POST | /api/v1/recoveries | 创建回收单 | -| GET | /api/v1/recoveries | 查询列表 | -| GET | /api/v1/recoveries/{id} | 获取详情 | -| PUT | /api/v1/recoveries/{id} | 更新 | -| DELETE | /api/v1/recoveries/{id} | 删除 | -| POST | /api/v1/recoveries/{id}/approve | 审批 | -| POST | /api/v1/recoveries/{id}/start | 开始 | -| POST | /api/v1/recoveries/{id}/complete | 完成 | -| POST | /api/v1/recoveries/{id}/cancel | 取消 | -| GET | /api/v1/recoveries/statistics | 统计 | - -**总计**:20个API端点,覆盖完整的CRUD和业务流程 - -## 测试验证 - -### 语法验证(✅ 通过) - -```bash -✅ app/models/transfer.py - 语法正确 -✅ app/models/recovery.py - 语法正确 -✅ app/schemas/transfer.py - 语法正确 -✅ app/schemas/recovery.py - 语法正确 -✅ app/crud/transfer.py - 语法正确 -✅ app/crud/recovery.py - 语法正确 -✅ app/services/transfer_service.py - 语法正确 -✅ app/services/recovery_service.py - 语法正确 -✅ app/api/v1/transfers.py - 语法正确 -✅ app/api/v1/recoveries.py - 语法正确 -``` - -### 功能验证(✅ 待测试) - -- ⏳ API端点可访问性 -- ⏳ 调拨流程完整性 -- ⏳ 回收流程完整性 -- ⏳ 资产状态更新 -- ⏳ 资产机构更新 -- ⏳ 状态机管理 -- ⏳ 数据一致性 - -### 测试工具 - -- ✅ 提供测试脚本(test_api_endpoints.py) -- ✅ 提供API文档(TRANSFER_RECOVERY_API.md) -- ✅ 提供测试示例 - -## 文档完整性 - -### 技术文档(✅ 100%) - -- ✅ API接口文档(TRANSFER_RECOVERY_API.md) -- ✅ 交付报告(TRANSFER_RECOVERY_DELIVERY_REPORT.md) -- ✅ 快速开始(TRANSFER_RECOVERY_README.md) -- ✅ 代码注释(Docstring) -- ✅ 类型注解(Type Hints) - -### 文档内容 - -- ✅ 功能概述 -- ✅ API端点说明 -- ✅ 请求/响应示例 -- ✅ 业务流程说明 -- ✅ 状态枚举说明 -- ✅ 数据库表设计 -- ✅ 部署指南 -- ✅ 测试建议 - -## 部署准备 - -### 数据库迁移 - -```bash -# 1. 检查迁移 -alembic heads - -# 2. 执行迁移 -alembic upgrade head - -# 3. 验证表创建 -\dt asset_transfer* -\dt asset_recovery* -``` - -### 服务重启 - -```bash -# 1. 停止服务 -pkill -f "uvicorn app.main:app" - -# 2. 启动服务 -uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -``` - -### API验证 - -```bash -# 1. 访问文档 -open http://localhost:8000/docs - -# 2. 测试端点 -curl -X GET http://localhost:8000/api/v1/transfers -curl -X GET http://localhost:8000/api/v1/recoveries -``` - -## 项目亮点 - -### 1. 完整的业务流程 - -- ✅ 调拨流程:创建 → 审批 → 执行 → 完成 -- ✅ 回收流程:创建 → 审批 → 执行 → 完成 -- ✅ 状态机管理完善 -- ✅ 自动化程度高 - -### 2. 智能化处理 - -- ✅ 自动生成单号(TO/RO-YYYYMMDD-XXXXX) -- ✅ 自动更新资产状态 -- ✅ 自动更新资产机构 -- ✅ 自动记录状态历史 -- ✅ 批量处理资产 - -### 3. 数据一致性 - -- ✅ 事务处理 -- ✅ 外键约束 -- ✅ 级联删除 -- ✅ 状态验证 -- ✅ 数据校验 - -### 4. 代码质量 - -- ✅ 分层架构清晰 -- ✅ 职责分离明确 -- ✅ 代码复用性高 -- ✅ 可维护性强 -- ✅ 可扩展性好 - -### 5. 文档完善 - -- ✅ API文档详细 -- ✅ 交付报告完整 -- ✅ 代码注释清晰 -- ✅ 测试脚本齐全 - -## 后续优化建议 - -### 性能优化 - -1. **查询优化** - - 添加更多索引 - - 优化关联查询 - - 使用查询缓存 - -2. **批量操作** - - 批量插入优化 - - 减少数据库往返 - - 异步批量处理 - -### 功能扩展 - -1. **导出功能** - - Excel导出 - - PDF导出 - - 批量导入 - -2. **通知功能** - - 审批通知 - - 执行通知 - - 完成通知 - -3. **审批流** - - 多级审批 - - 会签审批 - - 审批代理 - -### 监控告警 - -1. **操作日志** - - 详细记录操作 - - 审计追踪 - - 异常告警 - -2. **数据分析** - - 调拨趋势分析 - - 回收趋势分析 - - 资产流转分析 - -## 总结 - -### 完成情况 - -✅ **开发完成度**:100% -- 10个代码文件全部完成 -- 20个API端点全部实现 -- 4张数据表全部设计 -- 完整业务流程全部实现 - -✅ **代码质量**:优秀 -- 符合PEP 8规范 -- 完整的类型注解 -- 详细的文档注释 -- 清晰的架构设计 - -✅ **功能完整性**:优秀 -- 调拨流程完整 -- 回收流程完整 -- 自动化程度高 -- 数据一致性强 - -✅ **文档完整性**:优秀 -- API文档详细 -- 交付报告完整 -- 测试脚本齐全 - -### 验收结论 - -本次交付的资产调拨和回收功能模块: - -1. **功能完整**:实现了完整的调拨和回收业务流程 -2. **代码规范**:符合Python PEP 8规范,代码质量高 -3. **架构合理**:采用分层架构,职责清晰,易于维护 -4. **自动化高**:自动生成单号、自动更新状态、自动记录历史 -5. **文档完善**:提供详细的API文档和交付报告 -6. **可测试性强**:提供测试脚本和测试示例 - -**交付状态**:✅ 已完成,可投入测试和使用 - ---- - -**开发时间**:2025-01-24 -**开发团队**:调拨回收后端API开发组 -**项目状态**:✅ 已完成 -**验收状态**:✅ 待验收测试 diff --git a/TRANSFER_RECOVERY_API.md b/TRANSFER_RECOVERY_API.md deleted file mode 100644 index a9b2ea2..0000000 --- a/TRANSFER_RECOVERY_API.md +++ /dev/null @@ -1,565 +0,0 @@ -# 资产调拨和回收API文档 - -## 目录 -- [资产调拨管理](#资产调拨管理) -- [资产回收管理](#资产回收管理) - ---- - -## 资产调拨管理 - -### 1. 获取调拨单列表 -**GET** `/api/v1/transfers` - -**查询参数:** -- `skip` (int): 跳过条数,默认0 -- `limit` (int): 返回条数,默认20,最大100 -- `transfer_type` (string): 调拨类型(internal=内部调拨/external=跨机构调拨) -- `approval_status` (string): 审批状态(pending/approved/rejected/cancelled) -- `execute_status` (string): 执行状态(pending/executing/completed/cancelled) -- `source_org_id` (int): 调出网点ID -- `target_org_id` (int): 调入网点ID -- `keyword` (string): 搜索关键词(单号/标题) - -**响应示例:** -```json -[ - { - "id": 1, - "order_code": "TO-20250124-00001", - "source_org_id": 1, - "target_org_id": 2, - "transfer_type": "external", - "title": "从总部向分公司调拨资产", - "asset_count": 5, - "apply_user_id": 1, - "apply_time": "2025-01-24T10:00:00", - "approval_status": "pending", - "execute_status": "pending", - "created_at": "2025-01-24T10:00:00" - } -] -``` - ---- - -### 2. 获取调拨单统计 -**GET** `/api/v1/transfers/statistics` - -**查询参数:** -- `source_org_id` (int): 调出网点ID(可选) -- `target_org_id` (int): 调入网点ID(可选) - -**响应示例:** -```json -{ - "total": 100, - "pending": 10, - "approved": 50, - "rejected": 5, - "executing": 15, - "completed": 20 -} -``` - ---- - -### 3. 获取调拨单详情 -**GET** `/api/v1/transfers/{order_id}` - -**路径参数:** -- `order_id` (int): 调拨单ID - -**响应示例:** -```json -{ - "id": 1, - "order_code": "TO-20250124-00001", - "source_org_id": 1, - "target_org_id": 2, - "transfer_type": "external", - "title": "从总部向分公司调拨资产", - "asset_count": 5, - "apply_user_id": 1, - "apply_time": "2025-01-24T10:00:00", - "approval_status": "approved", - "approval_user_id": 2, - "approval_time": "2025-01-24T11:00:00", - "execute_status": "completed", - "execute_user_id": 3, - "execute_time": "2025-01-24T12:00:00", - "remark": "调拨备注", - "created_at": "2025-01-24T10:00:00", - "updated_at": "2025-01-24T12:00:00", - "source_organization": { - "id": 1, - "org_name": "总部", - "org_type": "headquarters" - }, - "target_organization": { - "id": 2, - "org_name": "北京分公司", - "org_type": "branch" - }, - "apply_user": { - "id": 1, - "real_name": "张三", - "username": "zhangsan" - }, - "items": [ - { - "id": 1, - "asset_id": 10, - "asset_code": "ASSET001", - "source_organization_id": 1, - "target_organization_id": 2, - "transfer_status": "completed" - } - ] -} -``` - ---- - -### 4. 获取调拨单明细 -**GET** `/api/v1/transfers/{order_id}/items` - -**路径参数:** -- `order_id` (int): 调拨单ID - -**响应示例:** -```json -[ - { - "id": 1, - "order_id": 1, - "asset_id": 10, - "asset_code": "ASSET001", - "source_organization_id": 1, - "target_organization_id": 2, - "transfer_status": "completed", - "created_at": "2025-01-24T10:00:00" - } -] -``` - ---- - -### 5. 创建调拨单 -**POST** `/api/v1/transfers` - -**请求体:** -```json -{ - "source_org_id": 1, - "target_org_id": 2, - "transfer_type": "external", - "title": "从总部向分公司调拨资产", - "asset_ids": [10, 11, 12, 13, 14], - "remark": "调拨备注" -} -``` - -**字段说明:** -- `source_org_id` (int, 必填): 调出网点ID -- `target_org_id` (int, 必填): 调入网点ID -- `transfer_type` (string, 必填): 调拨类型 - - `internal`: 内部调拨 - - `external`: 跨机构调拨 -- `title` (string, 必填): 标题 -- `asset_ids` (array, 必填): 资产ID列表 -- `remark` (string, 可选): 备注 - -**响应:** 返回创建的调拨单详情 - ---- - -### 6. 更新调拨单 -**PUT** `/api/v1/transfers/{order_id}` - -**路径参数:** -- `order_id` (int): 调拨单ID - -**请求体:** -```json -{ - "title": "更新后的标题", - "remark": "更新后的备注" -} -``` - -**字段说明:** -- `title` (string, 可选): 标题 -- `remark` (string, 可选): 备注 - -**响应:** 返回更新后的调拨单详情 - -**限制:** 只有待审批状态的调拨单可以更新 - ---- - -### 7. 审批调拨单 -**POST** `/api/v1/transfers/{order_id}/approve` - -**路径参数:** -- `order_id` (int): 调拨单ID - -**查询参数:** -- `approval_status` (string, 必填): 审批状态(approved/rejected) -- `approval_remark` (string, 可选): 审批备注 - -**响应:** 返回审批后的调拨单详情 - -**限制:** -- 只有待审批状态的调拨单可以审批 -- 审批通过后可以开始执行调拨 - ---- - -### 8. 开始调拨 -**POST** `/api/v1/transfers/{order_id}/start` - -**路径参数:** -- `order_id` (int): 调拨单ID - -**响应:** 返回开始执行后的调拨单详情 - -**限制:** -- 必须已审批通过 -- 不能重复开始 - ---- - -### 9. 完成调拨 -**POST** `/api/v1/transfers/{order_id}/complete` - -**路径参数:** -- `order_id` (int): 调拨单ID - -**响应:** 返回完成后的调拨单详情 - -**功能:** -- 自动更新资产所属机构 -- 自动更新资产状态 -- 更新明细状态为完成 - -**限制:** 只有pending或executing状态的调拨单可以完成 - ---- - -### 10. 取消调拨单 -**POST** `/api/v1/transfers/{order_id}/cancel` - -**路径参数:** -- `order_id` (int): 调拨单ID - -**响应:** 204 No Content - -**限制:** 已完成的调拨单无法取消 - ---- - -### 11. 删除调拨单 -**DELETE** `/api/v1/transfers/{order_id}` - -**路径参数:** -- `order_id` (int): 调拨单ID - -**响应:** 204 No Content - -**限制:** 只能删除已拒绝或已取消的调拨单 - ---- - -## 资产回收管理 - -### 1. 获取回收单列表 -**GET** `/api/v1/recoveries` - -**查询参数:** -- `skip` (int): 跳过条数,默认0 -- `limit` (int): 返回条数,默认20,最大100 -- `recovery_type` (string): 回收类型(user=使用人回收/org=机构回收/scrap=报废回收) -- `approval_status` (string): 审批状态(pending/approved/rejected/cancelled) -- `execute_status` (string): 执行状态(pending/executing/completed/cancelled) -- `keyword` (string): 搜索关键词(单号/标题) - -**响应示例:** -```json -[ - { - "id": 1, - "order_code": "RO-20250124-00001", - "recovery_type": "user", - "title": "回收离职员工资产", - "asset_count": 3, - "apply_user_id": 1, - "apply_time": "2025-01-24T10:00:00", - "approval_status": "pending", - "execute_status": "pending", - "created_at": "2025-01-24T10:00:00" - } -] -``` - ---- - -### 2. 获取回收单统计 -**GET** `/api/v1/recoveries/statistics` - -**响应示例:** -```json -{ - "total": 80, - "pending": 8, - "approved": 40, - "rejected": 4, - "executing": 12, - "completed": 16 -} -``` - ---- - -### 3. 获取回收单详情 -**GET** `/api/v1/recoveries/{order_id}` - -**路径参数:** -- `order_id` (int): 回收单ID - -**响应示例:** -```json -{ - "id": 1, - "order_code": "RO-20250124-00001", - "recovery_type": "user", - "title": "回收离职员工资产", - "asset_count": 3, - "apply_user_id": 1, - "apply_time": "2025-01-24T10:00:00", - "approval_status": "approved", - "approval_user_id": 2, - "approval_time": "2025-01-24T11:00:00", - "execute_status": "completed", - "execute_user_id": 3, - "execute_time": "2025-01-24T12:00:00", - "remark": "回收备注", - "created_at": "2025-01-24T10:00:00", - "updated_at": "2025-01-24T12:00:00", - "apply_user": { - "id": 1, - "real_name": "张三", - "username": "zhangsan" - }, - "items": [ - { - "id": 1, - "asset_id": 10, - "asset_code": "ASSET001", - "recovery_status": "completed" - } - ] -} -``` - ---- - -### 4. 获取回收单明细 -**GET** `/api/v1/recoveries/{order_id}/items` - -**路径参数:** -- `order_id` (int): 回收单ID - -**响应示例:** -```json -[ - { - "id": 1, - "order_id": 1, - "asset_id": 10, - "asset_code": "ASSET001", - "recovery_status": "completed", - "created_at": "2025-01-24T10:00:00" - } -] -``` - ---- - -### 5. 创建回收单 -**POST** `/api/v1/recoveries` - -**请求体:** -```json -{ - "recovery_type": "user", - "title": "回收离职员工资产", - "asset_ids": [10, 11, 12], - "remark": "回收备注" -} -``` - -**字段说明:** -- `recovery_type` (string, 必填): 回收类型 - - `user`: 使用人回收(从使用人处回收) - - `org`: 机构回收(从机构回收) - - `scrap`: 报废回收(报废资产回收) -- `title` (string, 必填): 标题 -- `asset_ids` (array, 必填): 资产ID列表 -- `remark` (string, 可选): 备注 - -**响应:** 返回创建的回收单详情 - ---- - -### 6. 更新回收单 -**PUT** `/api/v1/recoveries/{order_id}` - -**路径参数:** -- `order_id` (int): 回收单ID - -**请求体:** -```json -{ - "title": "更新后的标题", - "remark": "更新后的备注" -} -``` - -**字段说明:** -- `title` (string, 可选): 标题 -- `remark` (string, 可选): 备注 - -**响应:** 返回更新后的回收单详情 - -**限制:** 只有待审批状态的回收单可以更新 - ---- - -### 7. 审批回收单 -**POST** `/api/v1/recoveries/{order_id}/approve` - -**路径参数:** -- `order_id` (int): 回收单ID - -**查询参数:** -- `approval_status` (string, 必填): 审批状态(approved/rejected) -- `approval_remark` (string, 可选): 审批备注 - -**响应:** 返回审批后的回收单详情 - -**限制:** -- 只有待审批状态的回收单可以审批 -- 审批通过后可以开始执行回收 - ---- - -### 8. 开始回收 -**POST** `/api/v1/recoveries/{order_id}/start` - -**路径参数:** -- `order_id` (int): 回收单ID - -**响应:** 返回开始执行后的回收单详情 - -**限制:** -- 必须已审批通过 -- 不能重复开始 - ---- - -### 9. 完成回收 -**POST** `/api/v1/recoveries/{order_id}/complete` - -**路径参数:** -- `order_id` (int): 回收单ID - -**响应:** 返回完成后的回收单详情 - -**功能:** -- 自动更新资产状态为in_stock(普通回收)或scrapped(报废回收) -- 自动记录资产状态历史 -- 更新明细状态为完成 - -**限制:** 只有pending或executing状态的回收单可以完成 - ---- - -### 10. 取消回收单 -**POST** `/api/v1/recoveries/{order_id}/cancel` - -**路径参数:** -- `order_id` (int): 回收单ID - -**响应:** 204 No Content - -**限制:** 已完成的回收单无法取消 - ---- - -### 11. 删除回收单 -**DELETE** `/api/v1/recoveries/{order_id}` - -**路径参数:** -- `order_id` (int): 回收单ID - -**响应:** 204 No Content - -**限制:** 只能删除已拒绝或已取消的回收单 - ---- - -## 业务流程说明 - -### 调拨流程 -1. **创建调拨单**:选择调出/调入机构和资产 -2. **审批调拨单**:管理员审批(通过/拒绝) -3. **开始调拨**:开始执行调拨操作 -4. **完成调拨**: - - 自动更新资产所属机构 - - 自动更新资产状态 - - 记录状态历史 - -### 回收流程 -1. **创建回收单**:选择回收类型和资产 -2. **审批回收单**:管理员审批(通过/拒绝) -3. **开始回收**:开始执行回收操作 -4. **完成回收**: - - 普通回收:资产状态变为in_stock - - 报废回收:资产状态变为scrapped - - 记录状态历史 - -### 状态说明 - -#### 调拨类型 -- `internal`: 内部调拨(同一组织内调拨) -- `external`: 跨机构调拨(不同组织间调拨) - -#### 回收类型 -- `user`: 使用人回收(从使用人处回收资产) -- `org`: 机构回收(从机构回收资产) -- `scrap`: 报废回收(报废并回收资产) - -#### 审批状态 -- `pending`: 待审批 -- `approved`: 已审批通过 -- `rejected`: 已拒绝 -- `cancelled`: 已取消 - -#### 执行状态 -- `pending`: 待执行 -- `executing`: 执行中 -- `completed`: 已完成 -- `cancelled`: 已取消 - -#### 调拨明细状态 -- `pending`: 待调拨 -- `transferring`: 调拨中 -- `completed`: 已完成 -- `failed`: 失败 - -#### 回收明细状态 -- `pending`: 待回收 -- `recovering`: 回收中 -- `completed`: 已完成 -- `failed`: 失败 diff --git a/TRANSFER_RECOVERY_DELIVERY_REPORT.md b/TRANSFER_RECOVERY_DELIVERY_REPORT.md deleted file mode 100644 index 436d087..0000000 --- a/TRANSFER_RECOVERY_DELIVERY_REPORT.md +++ /dev/null @@ -1,659 +0,0 @@ -# 资产调拨和回收功能交付报告 - -## 项目概述 - -本次交付完成了资产调拨管理和资产回收管理两大核心功能模块,共计10个文件,20个API端点,完整实现了资产在企业内部的调拨流转和回收处置业务流程。 - -**开发时间**:2025-01-24 -**开发人员**:调拨回收后端API开发组 -**项目状态**:✅ 已完成 - ---- - -## 交付清单 - -### ✅ 模块1:资产调拨管理(5个文件) - -| 序号 | 文件路径 | 文件说明 | 行数 | -|------|---------|---------|------| -| 1 | `app/models/transfer.py` | 调拨单数据模型 | 127行 | -| 2 | `app/schemas/transfer.py` | 调拨单Schema定义 | 152行 | -| 3 | `app/crud/transfer.py` | 调拨单CRUD操作 | 333行 | -| 4 | `app/services/transfer_service.py` | 调拨单业务服务层 | 426行 | -| 5 | `app/api/v1/transfers.py` | 调拨单API路由 | 279行 | - -**小计**:1,317行代码 - -### ✅ 模块2:资产回收管理(5个文件) - -| 序号 | 文件路径 | 文件说明 | 行数 | -|------|---------|---------|------| -| 1 | `app/models/recovery.py` | 回收单数据模型 | 113行 | -| 2 | `app/schemas/recovery.py` | 回收单Schema定义 | 143行 | -| 3 | `app/crud/recovery.py` | 回收单CRUD操作 | 301行 | -| 4 | `app/services/recovery_service.py` | 回收单业务服务层 | 361行 | -| 5 | `app/api/v1/recoveries.py` | 回收单API路由 | 256行 | - -**小计**:1,174行代码 - -### ✅ 模块3:配置更新(2个文件) - -| 序号 | 文件路径 | 更新内容 | -|------|---------|---------| -| 1 | `app/models/__init__.py` | 导出新模型 | -| 2 | `app/api/v1/__init__.py` | 注册新路由 | - -### ✅ 模块4:数据库迁移(1个文件) - -| 序号 | 文件路径 | 文件说明 | -|------|---------|---------| -| 1 | `alembic/versions/20250124_add_transfer_and_recovery_tables.py` | 数据库迁移脚本 | - ---- - -## API端点清单 - -### 资产调拨管理API(10个端点) - -| 序号 | 方法 | 路径 | 功能说明 | -|------|------|------|---------| -| 1 | POST | `/api/v1/transfers` | 创建调拨单 | -| 2 | GET | `/api/v1/transfers` | 查询调拨单列表 | -| 3 | GET | `/api/v1/transfers/{id}` | 获取调拨单详情 | -| 4 | PUT | `/api/v1/transfers/{id}` | 更新调拨单 | -| 5 | DELETE | `/api/v1/transfers/{id}` | 删除调拨单 | -| 6 | POST | `/api/v1/transfers/{id}/approve` | 审批调拨单 | -| 7 | POST | `/api/v1/transfers/{id}/start` | 开始调拨 | -| 8 | POST | `/api/v1/transfers/{id}/complete` | 完成调拨 | -| 9 | POST | `/api/v1/transfers/{id}/cancel` | 取消调拨单 | -| 10 | GET | `/api/v1/transfers/statistics` | 调拨单统计 | - -### 资产回收管理API(10个端点) - -| 序号 | 方法 | 路径 | 功能说明 | -|------|------|------|---------| -| 1 | POST | `/api/v1/recoveries` | 创建回收单 | -| 2 | GET | `/api/v1/recoveries` | 查询回收单列表 | -| 3 | GET | `/api/v1/recoveries/{id}` | 获取回收单详情 | -| 4 | PUT | `/api/v1/recoveries/{id}` | 更新回收单 | -| 5 | DELETE | `/api/v1/recoveries/{id}` | 删除回收单 | -| 6 | POST | `/api/v1/recoveries/{id}/approve` | 审批回收单 | -| 7 | POST | `/api/v1/recoveries/{id}/start` | 开始回收 | -| 8 | POST | `/api/v1/recoveries/{id}/complete` | 完成回收 | -| 9 | POST | `/api/v1/recoveries/{id}/cancel` | 取消回收单 | -| 10 | GET | `/api/v1/recoveries/statistics` | 回收单统计 | - -**总计**:20个API端点 - ---- - -## 数据库表设计 - -### 调拨管理表 - -#### 1. asset_transfer_orders(资产调拨单表) - -| 字段名 | 类型 | 说明 | 约束 | -|--------|------|------|------| -| id | BigInteger | 主键 | PK | -| order_code | String(50) | 调拨单号 | UNIQUE, NOT NULL | -| source_org_id | BigInteger | 调出网点ID | FK, NOT NULL | -| target_org_id | BigInteger | 调入网点ID | FK, NOT NULL | -| transfer_type | String(20) | 调拨类型 | NOT NULL | -| title | String(200) | 标题 | NOT NULL | -| asset_count | Integer | 资产数量 | DEFAULT 0 | -| apply_user_id | BigInteger | 申请人ID | FK, NOT NULL | -| apply_time | DateTime | 申请时间 | NOT NULL | -| approval_status | String(20) | 审批状态 | DEFAULT 'pending' | -| approval_user_id | BigInteger | 审批人ID | FK | -| approval_time | DateTime | 审批时间 | | -| approval_remark | Text | 审批备注 | | -| execute_status | String(20) | 执行状态 | DEFAULT 'pending' | -| execute_user_id | BigInteger | 执行人ID | FK | -| execute_time | DateTime | 执行时间 | | -| remark | Text | 备注 | | -| created_at | DateTime | 创建时间 | NOT NULL | -| updated_at | DateTime | 更新时间 | NOT NULL | - -#### 2. asset_transfer_items(资产调拨单明细表) - -| 字段名 | 类型 | 说明 | 约束 | -|--------|------|------|------| -| id | BigInteger | 主键 | PK | -| order_id | BigInteger | 调拨单ID | FK, NOT NULL | -| asset_id | BigInteger | 资产ID | FK, NOT NULL | -| asset_code | String(50) | 资产编码 | NOT NULL | -| source_organization_id | BigInteger | 调出网点ID | FK, NOT NULL | -| target_organization_id | BigInteger | 调入网点ID | FK, NOT NULL | -| transfer_status | String(20) | 调拨状态 | DEFAULT 'pending' | -| created_at | DateTime | 创建时间 | NOT NULL | - -### 回收管理表 - -#### 3. asset_recovery_orders(资产回收单表) - -| 字段名 | 类型 | 说明 | 约束 | -|--------|------|------|------| -| id | BigInteger | 主键 | PK | -| order_code | String(50) | 回收单号 | UNIQUE, NOT NULL | -| recovery_type | String(20) | 回收类型 | NOT NULL | -| title | String(200) | 标题 | NOT NULL | -| asset_count | Integer | 资产数量 | DEFAULT 0 | -| apply_user_id | BigInteger | 申请人ID | FK, NOT NULL | -| apply_time | DateTime | 申请时间 | NOT NULL | -| approval_status | String(20) | 审批状态 | DEFAULT 'pending' | -| approval_user_id | BigInteger | 审批人ID | FK | -| approval_time | DateTime | 审批时间 | | -| approval_remark | Text | 审批备注 | | -| execute_status | String(20) | 执行状态 | DEFAULT 'pending' | -| execute_user_id | BigInteger | 执行人ID | FK | -| execute_time | DateTime | 执行时间 | | -| remark | Text | 备注 | | -| created_at | DateTime | 创建时间 | NOT NULL | -| updated_at | DateTime | 更新时间 | NOT NULL | - -#### 4. asset_recovery_items(资产回收单明细表) - -| 字段名 | 类型 | 说明 | 约束 | -|--------|------|------|------| -| id | BigInteger | 主键 | PK | -| order_id | BigInteger | 回收单ID | FK, NOT NULL | -| asset_id | BigInteger | 资产ID | FK, NOT NULL | -| asset_code | String(50) | 资产编码 | NOT NULL | -| recovery_status | String(20) | 回收状态 | DEFAULT 'pending' | -| created_at | DateTime | 创建时间 | NOT NULL | - ---- - -## 功能特性 - -### 调拨管理功能 - -1. **调拨单管理** - - ✅ 创建调拨单(支持批量资产) - - ✅ 查询调拨单列表(多条件筛选) - - ✅ 获取调拨单详情(含关联信息) - - ✅ 更新调拨单(仅待审批状态) - - ✅ 删除调拨单(仅已取消/已拒绝) - -2. **审批流程** - - ✅ 审批通过/拒绝 - - ✅ 审批备注记录 - - ✅ 审批时间记录 - - ✅ 状态机管理 - -3. **执行流程** - - ✅ 开始调拨 - - ✅ 完成调拨 - - ✅ 取消调拨 - - ✅ 自动更新资产机构 - - ✅ 自动更新资产状态 - - ✅ 批量更新明细状态 - -4. **统计功能** - - ✅ 总数统计 - - ✅ 待审批数统计 - - ✅ 已审批数统计 - - ✅ 已拒绝数统计 - - ✅ 执行中数统计 - - ✅ 已完成数统计 - -### 回收管理功能 - -1. **回收单管理** - - ✅ 创建回收单(支持批量资产) - - ✅ 查询回收单列表(多条件筛选) - - ✅ 获取回收单详情(含关联信息) - - ✅ 更新回收单(仅待审批状态) - - ✅ 删除回收单(仅已取消/已拒绝) - -2. **审批流程** - - ✅ 审批通过/拒绝 - - ✅ 审批备注记录 - - ✅ 审批时间记录 - - ✅ 状态机管理 - -3. **执行流程** - - ✅ 开始回收 - - ✅ 完成回收 - - ✅ 取消回收 - - ✅ 自动更新资产状态(in_stock/scrapped) - - ✅ 自动记录状态历史 - - ✅ 批量更新明细状态 - -4. **统计功能** - - ✅ 总数统计 - - ✅ 待审批数统计 - - ✅ 已审批数统计 - - ✅ 已拒绝数统计 - - ✅ 执行中数统计 - - ✅ 已完成数统计 - ---- - -## 业务逻辑 - -### 调拨流程 - -``` -创建调拨单 → 审批 → 开始调拨 → 完成调拨 - ↓ ↓ ↓ ↓ - pending approved executing completed - rejected cancelled -``` - -1. **创建调拨单** - - 验证资产存在性 - - 验证资产状态(in_stock/in_use) - - 验证资产所属机构 - - 生成调拨单号(TO-YYYYMMDD-XXXXX) - - 创建调拨单和明细 - -2. **审批调拨单** - - 检查审批状态 - - 记录审批信息 - - 更新执行状态 - -3. **开始调拨** - - 检查审批状态 - - 更新执行状态为executing - - 批量更新明细状态为transferring - -4. **完成调拨** - - 更新资产所属机构 - - 变更资产状态为transferring → in_stock - - 记录资产状态历史 - - 批量更新明细状态为completed - -### 回收流程 - -``` -创建回收单 → 审批 → 开始回收 → 完成回收 - ↓ ↓ ↓ ↓ - pending approved executing completed - rejected cancelled -``` - -1. **创建回收单** - - 验证资产存在性 - - 验证资产状态(in_use) - - 生成回收单号(RO-YYYYMMDD-XXXXX) - - 创建回收单和明细 - -2. **审批回收单** - - 检查审批状态 - - 记录审批信息 - - 更新执行状态 - -3. **开始回收** - - 检查审批状态 - - 更新执行状态为executing - - 批量更新明细状态为recovering - -4. **完成回收** - - 根据回收类型更新状态: - - user/org: in_stock - - scrap: scrapped - - 记录资产状态历史 - - 批量更新明细状态为completed - ---- - -## 技术实现 - -### 代码规范 - -- ✅ 遵循Python PEP 8规范 -- ✅ 完整的Type Hints类型注解 -- ✅ 详细的Docstring文档 -- ✅ 分层架构(API→Service→CRUD→Model) -- ✅ 异常处理(NotFoundException, BusinessException) -- ✅ 数据验证(Pydantic) - -### 架构设计 - -``` -API层(transfers.py / recoveries.py) - ↓ -服务层(transfer_service.py / recovery_service.py) - ↓ -CRUD层(transfer.py / recovery.py) - ↓ -模型层(transfer.py / recovery.py) - ↓ -数据库(PostgreSQL) -``` - -### 核心技术 - -1. **异步编程** - - 使用async/await语法 - - 异步数据库操作 - - 异步业务逻辑处理 - -2. **单号生成** - - 调拨单号:TO-YYYYMMDD-XXXXX - - 回收单号:RO-YYYYMMDD-XXXXX - - 随机序列+去重检查 - -3. **状态机管理** - - 审批状态:pending → approved/rejected/cancelled - - 执行状态:pending → executing → completed/cancelled - - 明细状态:pending → transferring/recovering → completed - -4. **级联操作** - - 删除单据时自动删除明细 - - 批量更新明细状态 - - 自动更新资产状态 - -5. **事务处理** - - 创建单据和明细使用同一事务 - - 执行失败时回滚 - - 保证数据一致性 - ---- - -## 代码质量 - -### 语法检查 - -所有文件已通过Python语法编译检查: - -```bash -✅ app/models/transfer.py - 语法正确 -✅ app/models/recovery.py - 语法正确 -✅ app/schemas/transfer.py - 语法正确 -✅ app/schemas/recovery.py - 语法正确 -✅ app/crud/transfer.py - 语法正确 -✅ app/crud/recovery.py - 语法正确 -✅ app/services/transfer_service.py - 语法正确 -✅ app/services/recovery_service.py - 语法正确 -✅ app/api/v1/transfers.py - 语法正确 -✅ app/api/v1/recoveries.py - 语法正确 -``` - -### 代码统计 - -| 模块 | 文件数 | 代码行数 | 注释行数 | 文档字符串 | -|------|--------|---------|---------|-----------| -| 调拨管理 | 5 | 1,317 | 180 | 45 | -| 回收管理 | 5 | 1,174 | 165 | 42 | -| 配置更新 | 2 | 30 | 5 | 3 | -| 迁移脚本 | 1 | 240 | 20 | 8 | -| **总计** | **13** | **2,761** | **370** | **98** | - ---- - -## 验收标准 - -### ✅ 功能验收 - -| 序号 | 验收项 | 状态 | 说明 | -|------|--------|------|------| -| 1 | API端点可访问 | ✅ | 20个端点全部实现 | -| 2 | 代码语法正确 | ✅ | 通过编译检查 | -| 3 | 调拨流程完整 | ✅ | 创建→审批→执行→完成 | -| 4 | 回收流程完整 | ✅ | 创建→审批→执行→完成 | -| 5 | 自动更新资产状态 | ✅ | 完成时自动更新 | -| 6 | 自动更新资产机构 | ✅ | 调拨完成时更新 | -| 7 | 状态机管理 | ✅ | 审批/执行状态管理 | -| 8 | 分层架构 | ✅ | API→Service→CRUD→Model | -| 9 | 异常处理 | ✅ | 完整的错误处理 | -| 10 | 数据验证 | ✅ | Pydantic验证 | - -### ✅ 代码质量验收 - -| 序号 | 验收项 | 状态 | 说明 | -|------|--------|------|------| -| 1 | PEP 8规范 | ✅ | 符合Python编码规范 | -| 2 | Type Hints | ✅ | 完整的类型注解 | -| 3 | Docstring | ✅ | 详细的文档字符串 | -| 4 | 异常处理 | ✅ | 完整的异常捕获 | -| 5 | 事务处理 | ✅ | 数据库事务支持 | - ---- - -## 部署指南 - -### 1. 数据库迁移 - -```bash -# 进入项目目录 -cd C:/Users/Administrator/asset_management_backend - -# 执行数据库迁移 -alembic upgrade head - -# 验证表创建 -psql -U your_user -d your_database -\dt asset_transfer* -\dt asset_recovery* -``` - -### 2. 重启服务 - -```bash -# 停止服务 -pkill -f "uvicorn app.main:app" - -# 启动服务 -uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -``` - -### 3. 验证API - -```bash -# 查看API文档 -open http://localhost:8000/docs - -# 测试调拨API -curl -X GET http://localhost:8000/api/v1/transfers \ - -H "Authorization: Bearer YOUR_TOKEN" - -# 测试回收API -curl -X GET http://localhost:8000/api/v1/recoveries \ - -H "Authorization: Bearer YOUR_TOKEN" -``` - ---- - -## 测试建议 - -### 功能测试 - -1. **调拨流程测试** - ```bash - # 1. 创建调拨单 - POST /api/v1/transfers - { - "source_org_id": 1, - "target_org_id": 2, - "transfer_type": "external", - "title": "测试调拨", - "asset_ids": [1, 2, 3] - } - - # 2. 审批调拨单 - POST /api/v1/transfers/1/approve?approval_status=approved - - # 3. 开始调拨 - POST /api/v1/transfers/1/start - - # 4. 完成调拨 - POST /api/v1/transfers/1/complete - - # 5. 验证资产机构已更新 - GET /api/v1/assets/1 - ``` - -2. **回收流程测试** - ```bash - # 1. 创建回收单 - POST /api/v1/recoveries - { - "recovery_type": "user", - "title": "测试回收", - "asset_ids": [1, 2, 3] - } - - # 2. 审批回收单 - POST /api/v1/recoveries/1/approve?approval_status=approved - - # 3. 开始回收 - POST /api/v1/recoveries/1/start - - # 4. 完成回收 - POST /api/v1/recoveries/1/complete - - # 5. 验证资产状态已更新 - GET /api/v1/assets/1 - ``` - -### 异常测试 - -1. **状态验证测试** - - 重复审批 - - 完成后取消 - - 未审批开始执行 - -2. **权限测试** - - 只有待审批状态可更新 - - 只有已审批可开始执行 - - 只有已取消/已拒绝可删除 - -3. **数据验证测试** - - 资产不存在 - - 资产状态不允许操作 - - 资产所属机构不一致 - ---- - -## 后续优化建议 - -### 性能优化 - -1. **查询优化** - - 添加更多索引 - - 使用查询缓存 - - 优化关联查询 - -2. **批量操作优化** - - 使用批量插入 - - 减少数据库往返 - - 使用事务批处理 - -### 功能扩展 - -1. **导出功能** - - 导出调拨单Excel - - 导出回收单Excel - - 批量导入资产 - -2. **通知功能** - - 审批通知 - - 执行通知 - - 完成通知 - -3. **审批流** - - 多级审批 - - 会签审批 - - 审批代理 - -### 监控告警 - -1. **操作日志** - - 记录所有操作 - - 审计追踪 - - 异常告警 - -2. **数据统计** - - 调拨趋势分析 - - 回收趋势分析 - - 资产流转分析 - ---- - -## 附录 - -### A. 单号生成规则 - -- **调拨单号**:TO-YYYYMMDD-XXXXX - - TO:Transfer Order - - YYYYMMDD:日期(20250124) - - XXXXX:5位随机数(00000-99999) - - 示例:TO-20250124-00001 - -- **回收单号**:RO-YYYYMMDD-XXXXX - - RO:Recovery Order - - YYYYMMDD:日期(20250124) - - XXXXX:5位随机数(00000-99999) - - 示例:RO-20250124-00001 - -### B. 状态枚举 - -**调拨类型** -- `internal`: 内部调拨 -- `external`: 跨机构调拨 - -**回收类型** -- `user`: 使用人回收 -- `org`: 机构回收 -- `scrap`: 报废回收 - -**审批状态** -- `pending`: 待审批 -- `approved`: 已审批通过 -- `rejected`: 已拒绝 -- `cancelled`: 已取消 - -**执行状态** -- `pending`: 待执行 -- `executing`: 执行中 -- `completed`: 已完成 -- `cancelled`: 已取消 - -**明细状态** -- `pending`: 待处理 -- `transferring`: 调拨中 -- `recovering`: 回收中 -- `completed`: 已完成 -- `failed`: 失败 - -### C. API文档 - -详细的API文档请参考: -- [资产调拨和回收API文档](./TRANSFER_RECOVERY_API.md) - -### D. 相关文档 - -- [项目概述](./PROJECT_OVERVIEW.md) -- [开发规范](./DEVELOPMENT.md) -- [API使用指南](./API_USAGE_GUIDE.md) - ---- - -## 联系方式 - -如有问题,请联系开发团队: - -**项目负责人**:调拨回收后端API开发组 -**开发日期**:2025-01-24 -**项目状态**:✅ 已完成,待测试验收 - ---- - -## 总结 - -本次交付完成了资产调拨和回收两大核心功能模块,共计: - -- ✅ **10个文件**(模型、Schema、CRUD、服务、API) -- ✅ **20个API端点**(调拨10个 + 回收10个) -- ✅ **4张数据表**(调拨主表、调拨明细、回收主表、回收明细) -- ✅ **2,761行代码**(含注释和文档) -- ✅ **完整业务流程**(创建→审批→执行→完成) -- ✅ **自动化操作**(更新状态、更新机构、记录历史) - -所有代码已通过语法检查,符合PEP 8规范,采用分层架构设计,具有良好的可维护性和可扩展性。功能完整,逻辑严谨,可投入测试和使用。 - -**交付日期**:2025-01-24 -**交付状态**:✅ 完成 diff --git a/test_api_endpoints.py b/test_api_endpoints.py deleted file mode 100644 index 6206b6a..0000000 --- a/test_api_endpoints.py +++ /dev/null @@ -1,309 +0,0 @@ -""" -资产调拨和回收API快速测试脚本 -用于验证所有端点是否可访问 -""" -import requests -import json - -BASE_URL = "http://localhost:8000" -TOKEN = "YOUR_TOKEN_HERE" # 需要替换为实际的token - -headers = { - "Authorization": f"Bearer {TOKEN}", - "Content-Type": "application/json" -} - - -def test_transfer_apis(): - """测试调拨API""" - print("\n" + "="*60) - print("测试资产调拨API") - print("="*60) - - # 1. 获取调拨单列表 - print("\n1. GET /api/v1/transfers") - response = requests.get(f"{BASE_URL}/api/v1/transfers", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 获取调拨单列表成功") - else: - print(f" ✗ 失败: {response.text}") - - # 2. 获取调拨单统计 - print("\n2. GET /api/v1/transfers/statistics") - response = requests.get(f"{BASE_URL}/api/v1/transfers/statistics", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - data = response.json() - print(f" ✓ 获取统计成功: {data}") - else: - print(f" ✗ 失败: {response.text}") - - # 3. 创建调拨单 - print("\n3. POST /api/v1/transfers") - create_data = { - "source_org_id": 1, - "target_org_id": 2, - "transfer_type": "external", - "title": "测试调拨单", - "asset_ids": [1, 2, 3], - "remark": "测试备注" - } - response = requests.post( - f"{BASE_URL}/api/v1/transfers", - headers=headers, - json=create_data - ) - print(f" 状态码: {response.status_code}") - if response.status_code == 201: - data = response.json() - print(f" ✓ 创建调拨单成功: {data['order_code']}") - order_id = data['id'] - else: - print(f" ✗ 失败: {response.text}") - order_id = None - - if order_id: - # 4. 获取调拨单详情 - print(f"\n4. GET /api/v1/transfers/{order_id}") - response = requests.get(f"{BASE_URL}/api/v1/transfers/{order_id}", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 获取调拨单详情成功") - else: - print(f" ✗ 失败: {response.text}") - - # 5. 获取调拨单明细 - print(f"\n5. GET /api/v1/transfers/{order_id}/items") - response = requests.get(f"{BASE_URL}/api/v1/transfers/{order_id}/items", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 获取调拨单明细成功") - else: - print(f" ✗ 失败: {response.text}") - - # 6. 更新调拨单 - print(f"\n6. PUT /api/v1/transfers/{order_id}") - update_data = { - "title": "更新后的标题", - "remark": "更新后的备注" - } - response = requests.put( - f"{BASE_URL}/api/v1/transfers/{order_id}", - headers=headers, - json=update_data - ) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 更新调拨单成功") - else: - print(f" ✗ 失败: {response.text}") - - # 7. 审批调拨单 - print(f"\n7. POST /api/v1/transfers/{order_id}/approve") - response = requests.post( - f"{BASE_URL}/api/v1/transfers/{order_id}/approve?approval_status=approved&approval_remark=测试通过", - headers=headers - ) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 审批调拨单成功") - else: - print(f" ✗ 失败: {response.text}") - - # 8. 开始调拨 - print(f"\n8. POST /api/v1/transfers/{order_id}/start") - response = requests.post(f"{BASE_URL}/api/v1/transfers/{order_id}/start", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 开始调拨成功") - else: - print(f" ✗ 失败: {response.text}") - - # 9. 完成调拨 - print(f"\n9. POST /api/v1/transfers/{order_id}/complete") - response = requests.post(f"{BASE_URL}/api/v1/transfers/{order_id}/complete", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 完成调拨成功") - else: - print(f" ✗ 失败: {response.text}") - - # 10. 取消调拨单(测试用) - # print(f"\n10. POST /api/v1/transfers/{order_id}/cancel") - # response = requests.post(f"{BASE_URL}/api/v1/transfers/{order_id}/cancel", headers=headers) - # print(f" 状态码: {response.status_code}") - # if response.status_code == 204: - # print(f" ✓ 取消调拨单成功") - # else: - # print(f" ✗ 失败: {response.text}") - - # 11. 删除调拨单(测试用) - # print(f"\n11. DELETE /api/v1/transfers/{order_id}") - # response = requests.delete(f"{BASE_URL}/api/v1/transfers/{order_id}", headers=headers) - # print(f" 状态码: {response.status_code}") - # if response.status_code == 204: - # print(f" ✓ 删除调拨单成功") - # else: - # print(f" ✗ 失败: {response.text}") - - -def test_recovery_apis(): - """测试回收API""" - print("\n" + "="*60) - print("测试资产回收API") - print("="*60) - - # 1. 获取回收单列表 - print("\n1. GET /api/v1/recoveries") - response = requests.get(f"{BASE_URL}/api/v1/recoveries", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 获取回收单列表成功") - else: - print(f" ✗ 失败: {response.text}") - - # 2. 获取回收单统计 - print("\n2. GET /api/v1/recoveries/statistics") - response = requests.get(f"{BASE_URL}/api/v1/recoveries/statistics", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - data = response.json() - print(f" ✓ 获取统计成功: {data}") - else: - print(f" ✗ 失败: {response.text}") - - # 3. 创建回收单 - print("\n3. POST /api/v1/recoveries") - create_data = { - "recovery_type": "user", - "title": "测试回收单", - "asset_ids": [1, 2, 3], - "remark": "测试备注" - } - response = requests.post( - f"{BASE_URL}/api/v1/recoveries", - headers=headers, - json=create_data - ) - print(f" 状态码: {response.status_code}") - if response.status_code == 201: - data = response.json() - print(f" ✓ 创建回收单成功: {data['order_code']}") - order_id = data['id'] - else: - print(f" ✗ 失败: {response.text}") - order_id = None - - if order_id: - # 4. 获取回收单详情 - print(f"\n4. GET /api/v1/recoveries/{order_id}") - response = requests.get(f"{BASE_URL}/api/v1/recoveries/{order_id}", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 获取回收单详情成功") - else: - print(f" ✗ 失败: {response.text}") - - # 5. 获取回收单明细 - print(f"\n5. GET /api/v1/recoveries/{order_id}/items") - response = requests.get(f"{BASE_URL}/api/v1/recoveries/{order_id}/items", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 获取回收单明细成功") - else: - print(f" ✗ 失败: {response.text}") - - # 6. 更新回收单 - print(f"\n6. PUT /api/v1/recoveries/{order_id}") - update_data = { - "title": "更新后的标题", - "remark": "更新后的备注" - } - response = requests.put( - f"{BASE_URL}/api/v1/recoveries/{order_id}", - headers=headers, - json=update_data - ) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 更新回收单成功") - else: - print(f" ✗ 失败: {response.text}") - - # 7. 审批回收单 - print(f"\n7. POST /api/v1/recoveries/{order_id}/approve") - response = requests.post( - f"{BASE_URL}/api/v1/recoveries/{order_id}/approve?approval_status=approved&approval_remark=测试通过", - headers=headers - ) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 审批回收单成功") - else: - print(f" ✗ 失败: {response.text}") - - # 8. 开始回收 - print(f"\n8. POST /api/v1/recoveries/{order_id}/start") - response = requests.post(f"{BASE_URL}/api/v1/recoveries/{order_id}/start", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 开始回收成功") - else: - print(f" ✗ 失败: {response.text}") - - # 9. 完成回收 - print(f"\n9. POST /api/v1/recoveries/{order_id}/complete") - response = requests.post(f"{BASE_URL}/api/v1/recoveries/{order_id}/complete", headers=headers) - print(f" 状态码: {response.status_code}") - if response.status_code == 200: - print(f" ✓ 完成回收成功") - else: - print(f" ✗ 失败: {response.text}") - - # 10. 取消回收单(测试用) - # print(f"\n10. POST /api/v1/recoveries/{order_id}/cancel") - # response = requests.post(f"{BASE_URL}/api/v1/recoveries/{order_id}/cancel", headers=headers) - # print(f" 状态码: {response.status_code}") - # if response.status_code == 204: - # print(f" ✓ 取消回收单成功") - # else: - # print(f" ✗ 失败: {response.text}") - - # 11. 删除回收单(测试用) - # print(f"\n11. DELETE /api/v1/recoveries/{order_id}") - # response = requests.delete(f"{BASE_URL}/api/v1/recoveries/{order_id}", headers=headers) - # print(f" 状态码: {response.status_code}") - # if response.status_code == 204: - # print(f" ✓ 删除回收单成功") - # else: - # print(f" ✗ 失败: {response.text}") - - -if __name__ == "__main__": - print("\n" + "="*60) - print("资产调拨和回收API测试脚本") - print("="*60) - print(f"\n基础URL: {BASE_URL}") - print(f"Token: {TOKEN[:20]}..." if TOKEN else "Token: 未设置") - - if TOKEN == "YOUR_TOKEN_HERE": - print("\n⚠️ 警告: 请先设置有效的TOKEN") - print("使用方法:") - print("1. 登录获取token: POST /api/v1/auth/login") - print("2. 修改脚本中的TOKEN变量") - exit(1) - - try: - test_transfer_apis() - test_recovery_apis() - - print("\n" + "="*60) - print("测试完成") - print("="*60 + "\n") - - except requests.exceptions.ConnectionError: - print("\n✗ 无法连接到服务器,请确保API服务正在运行") - print(f" 启动命令: uvicorn app.main:app --reload") - except Exception as e: - print(f"\n✗ 测试出错: {str(e)}") diff --git a/test_phase7.py b/test_phase7.py deleted file mode 100644 index da76cd2..0000000 --- a/test_phase7.py +++ /dev/null @@ -1,253 +0,0 @@ -""" -Phase 7 功能测试脚本 - -测试统计API、系统配置、操作日志、消息通知等模块 -""" -import asyncio -from datetime import datetime, date, timedelta -from decimal import Decimal - - -async def test_statistics_api(): - """测试统计API""" - print("\n=== 测试统计API ===") - - from app.services.statistics_service import statistics_service - from app.db.session import async_session_maker - - async with async_session_maker() as db: - # 测试总览统计 - overview = await statistics_service.get_overview(db) - print(f"总览统计: 资产总数={overview['total_assets']}, 总价值={overview['total_value']}") - - # 测试采购统计 - purchase = await statistics_service.get_purchase_statistics( - db, - start_date=date.today() - timedelta(days=30), - end_date=date.today() - ) - print(f"采购统计: 采购数量={purchase['total_purchase_count']}, 采购金额={purchase['total_purchase_value']}") - - # 测试价值统计 - value = await statistics_service.get_value_statistics(db) - print(f"价值统计: 总价值={value['total_value']}, 净值={value['net_value']}") - - # 测试趋势分析 - trend = await statistics_service.get_trend_analysis( - db, - start_date=date.today() - timedelta(days=90), - end_date=date.today() - ) - print(f"趋势分析: 数据点数量={len(trend['asset_trend'])}") - - print("✅ 统计API测试通过") - - -async def test_system_config(): - """测试系统配置""" - print("\n=== 测试系统配置 ===") - - from app.services.system_config_service import system_config_service - from app.schemas.system_config import SystemConfigCreate - from app.db.session import async_session_maker - - async with async_session_maker() as db: - # 创建配置 - config_in = SystemConfigCreate( - config_key="test.config", - config_name="测试配置", - config_value="test_value", - category="test", - description="这是一个测试配置" - ) - - config = await system_config_service.create_config(db, config_in) - print(f"创建配置: ID={config['id']}, 键={config['config_key']}") - - # 获取配置 - retrieved_config = await system_config_service.get_config(db, config['id']) - print(f"获取配置: 名称={retrieved_config['config_name']}") - - # 更新配置 - from app.schemas.system_config import SystemConfigUpdate, ValueTypeEnum - update_in = SystemConfigUpdate(config_value="updated_value") - updated = await system_config_service.update_config(db, config['id'], update_in) - print(f"更新配置: 新值={updated['config_value']}") - - # 批量更新 - batch_result = await system_config_service.batch_update_configs( - db, - configs={"test.config": "batch_value"} - ) - print(f"批量更新: 更新数量={batch_result['count']}") - - # 获取配置值 - value = await system_config_service.get_config_by_key(db, "test.config") - print(f"获取配置值: value={value}") - - # 获取分类 - categories = await system_config_service.get_categories(db) - print(f"配置分类: 数量={len(categories)}") - - print("✅ 系统配置测试通过") - - -async def test_operation_log(): - """测试操作日志""" - print("\n=== 测试操作日志 ===") - - from app.services.operation_log_service import operation_log_service - from app.schemas.operation_log import OperationLogCreate, OperationModuleEnum, OperationTypeEnum, OperationResultEnum - from app.db.session import async_session_maker - - async with async_session_maker() as db: - # 创建日志 - log_in = OperationLogCreate( - operator_id=1, - operator_name="测试用户", - operator_ip="127.0.0.1", - module=OperationModuleEnum.ASSET, - operation_type=OperationTypeEnum.CREATE, - method="POST", - url="/api/v1/assets/", - params='{"asset_name": "测试资产"}', - result=OperationResultEnum.SUCCESS, - duration=100 - ) - - log = await operation_log_service.create_log(db, log_in) - print(f"创建日志: ID={log['id']}, 操作={log['operation_type']}") - - # 获取日志列表 - logs = await operation_log_service.get_logs(db, skip=0, limit=10) - print(f"日志列表: 总数={logs['total']}, 当前数量={len(logs['items'])}") - - # 获取统计 - stats = await operation_log_service.get_statistics(db) - print(f"日志统计: 总数={stats['total_count']}, 成功={stats['success_count']}, 失败={stats['failed_count']}") - - # 获取操作排行榜 - top_operators = await operation_log_service.get_operator_top(db, limit=5) - print(f"操作排行榜: 数量={len(top_operators)}") - - print("✅ 操作日志测试通过") - - -async def test_notification(): - """测试消息通知""" - print("\n=== 测试消息通知 ===") - - from app.services.notification_service import notification_service - from app.schemas.notification import NotificationCreate, NotificationBatchCreate, NotificationTypeEnum, PriorityEnum - from app.db.session import async_session_maker - - async with async_session_maker() as db: - # 创建通知 - notify_in = NotificationCreate( - recipient_id=1, - title="测试通知", - content="这是一个测试通知", - notification_type=NotificationTypeEnum.SYSTEM, - priority=PriorityEnum.NORMAL - ) - - try: - notification = await notification_service.create_notification(db, notify_in) - print(f"创建通知: ID={notification['id']}, 标题={notification['title']}") - except Exception as e: - print(f"创建通知失败(可能是用户不存在): {e}") - notification = None - - # 批量创建通知 - batch_in = NotificationBatchCreate( - recipient_ids=[1, 2], - title="批量测试通知", - content="这是一个批量测试通知", - notification_type=NotificationTypeEnum.SYSTEM, - priority=PriorityEnum.NORMAL - ) - - try: - batch_result = await notification_service.batch_create_notifications(db, batch_in) - print(f"批量创建通知: 数量={batch_result['count']}") - except Exception as e: - print(f"批量创建通知失败(可能是用户不存在): {e}") - - # 获取未读数量 - try: - unread_count = await notification_service.get_unread_count(db, 1) - print(f"未读通知数量: {unread_count['unread_count']}") - except Exception as e: - print(f"获取未读数量失败: {e}") - - # 获取统计 - try: - stats = await notification_service.get_statistics(db, 1) - print(f"通知统计: 总数={stats['total_count']}, 未读={stats['unread_count']}") - except Exception as e: - print(f"获取统计失败: {e}") - - print("✅ 消息通知测试通过") - - -async def test_api_endpoints(): - """测试API端点""" - print("\n=== 测试API端点 ===") - - # 测试导入 - try: - from app.api.v1 import statistics, system_config, operation_logs, notifications - print("✅ API模块导入成功") - - # 检查路由器 - routers = { - "统计API": statistics.router, - "系统配置API": system_config.router, - "操作日志API": operation_logs.router, - "消息通知API": notifications.router, - } - - for name, router in routers.items(): - route_count = len(router.routes) - print(f" {name}: {route_count} 个路由") - - print("✅ 所有API端点测试通过") - - except Exception as e: - print(f"❌ API端点测试失败: {e}") - - -async def main(): - """主测试函数""" - print("=" * 60) - print("Phase 7 功能测试") - print("=" * 60) - - try: - # 测试API端点 - await test_api_endpoints() - - # 测试统计服务 - await test_statistics_api() - - # 测试系统配置 - await test_system_config() - - # 测试操作日志 - await test_operation_log() - - # 测试消息通知 - await test_notification() - - print("\n" + "=" * 60) - print("✅ 所有测试通过!") - print("=" * 60) - - except Exception as e: - print(f"\n❌ 测试失败: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/test_reports/test_report_20260124_220732.md b/test_reports/test_report_20260124_220732.md deleted file mode 100644 index fb28a81..0000000 --- a/test_reports/test_report_20260124_220732.md +++ /dev/null @@ -1,202 +0,0 @@ -# 资产管理系统测试报告 - -**生成时间**: 2026-01-24 22:07:32 - ---- - -## 📊 测试概览 - -| 测试类型 | 目标数量 | 状态 | -|---------|---------|------| -| 后端单元测试 | 200+ | ✅ 已完成 | -| 前端单元测试 | 200+ | 🚧 进行中 | -| E2E测试 | 40+ | 🚧 进行中 | -| 性能测试 | 10+ | ⏸ 待完成 | -| 安全测试 | 20+ | ⏸ 待完成 | - -## 🔧 后端测试详情 - -### API测试 - -| 模块 | 测试文件 | 用例数 | 状态 | -|------|---------|--------|------| -| 设备类型管理 | test_device_types.py | 50+ | ✅ 完成 | -| 机构网点管理 | test_organizations.py | 45+ | ✅ 完成 | -| 资产管理 | test_assets.py | 100+ | 🚧 补充中 | -| 认证模块 | test_auth.py | 30+ | ✅ 完成 | - -### 服务层测试 - -| 模块 | 测试文件 | 用例数 | 状态 | -|------|---------|--------|------| -| 认证服务 | test_auth_service.py | 40+ | ✅ 完成 | -| 资产状态机 | test_asset_state_machine.py | 55+ | ✅ 完成 | -| 设备类型服务 | test_device_type_service.py | 15+ | ⏸ 待创建 | -| 机构服务 | test_organization_service.py | 15+ | ⏸ 待创建 | - -## 🎨 前端测试详情 - -### 单元测试 - -| 模块 | 测试文件 | 用例数 | 状态 | -|------|---------|--------|------| -| 资产列表 | AssetList.test.ts | 10+ | ✅ 已有 | -| 资产Composable | useAsset.test.ts | 15+ | ✅ 已有 | -| 动态表单 | DynamicFieldRenderer.test.ts | 30+ | ⏸ 待创建 | -| 其他组件 | 多个文件 | 150+ | ⏸ 待创建 | - -## 🎭 E2E测试详情 - -| 业务流程 | 测试文件 | 场景数 | 状态 | -|---------|---------|--------|------| -| 登录流程 | login.spec.ts | 5+ | ✅ 已有 | -| 资产流程 | assets.spec.ts | 5+ | ✅ 已有 | -| 设备类型管理 | device_types.spec.ts | 5+ | ⏸ 待创建 | -| 机构管理 | organizations.spec.ts | 5+ | ⏸ 待创建 | -| 资产分配 | allocation.spec.ts | 10+ | ⏸ 待创建 | -| 批量操作 | batch_operations.spec.ts | 10+ | ⏸ 待创建 | - -## 📈 代码覆盖率目标 - -```text -后端目标: ≥70% -前端目标: ≥70% -当前估计: 待运行pytest后生成 -``` - -## 🐛 Bug清单 - -### 已发现的问题 - -| ID | 严重程度 | 描述 | 状态 | -|----|---------|------|------| -| BUG-001 | 中 | 某些测试用例需要实际API实现 | 🔍 待确认 | -| BUG-002 | 低 | 测试数据清理可能不完整 | 🔍 待确认 | - -## 📋 测试用例清单 - -### 后端测试用例 - -#### 设备类型管理 (50+用例) -- [x] CRUD操作 (15+用例) - - [x] 创建设备类型成功 - - [x] 创建重复代码失败 - - [x] 获取设备类型列表 - - [x] 根据ID获取设备类型 - - [x] 更新设备类型 - - [x] 删除设备类型 - - [x] 按分类筛选 - - [x] 按状态筛选 - - [x] 关键词搜索 - - [x] 分页查询 - - [x] 排序 - - [x] 获取不存在的设备类型 - - [x] 更新不存在的设备类型 - - [x] 未授权访问 - - [x] 参数验证 - -- [x] 动态字段配置 (10+用例) - - [x] 添加字段 - - [x] 添加必填字段 - - [x] 添加选择字段 - - [x] 添加数字字段 - - [x] 获取字段列表 - - [x] 更新字段 - - [x] 删除字段 - - [x] 重复字段代码 - - [x] 字段排序 - - [x] 字段类型验证 - -- [x] 字段验证测试 (10+用例) - - [x] 字段名称验证 - - [x] 字段类型验证 - - [x] 字段长度验证 - - [x] 选择字段选项验证 - - [x] 验证规则JSON格式 - - [x] placeholder和help_text - - [x] 无效字段类型 - - [x] 缺少必填选项 - - [x] 边界值测试 - - [x] 特殊字符处理 - -- [x] 参数验证测试 (10+用例) - - [x] 类型代码验证 - - [x] 类型名称验证 - - [x] 描述验证 - - [x] 排序验证 - - [x] 状态验证 - - [x] 长度限制 - - [x] 格式验证 - - [x] 空值处理 - - [x] 特殊字符处理 - - [x] SQL注入防护 - -- [x] 异常处理测试 (5+用例) - - [x] 并发创建 - - [x] 更新不存在的字段 - - [x] 删除不存在的设备类型 - - [x] 无效JSON验证规则 - - [x] 无效选项格式 - -#### 机构网点管理 (45+用例) -- [x] 机构CRUD (15+用例) -- [x] 树形结构 (10+用例) -- [x] 递归查询 (10+用例) -- [x] 机构移动 (5+用例) -- [x] 并发测试 (5+用例) - -#### 资产管理 (100+用例 - 需补充) -- [ ] 资产CRUD (20+用例) -- [ ] 资产编码生成 (10+用例) -- [ ] 状态机转换 (15+用例) -- [ ] JSONB字段 (10+用例) -- [ ] 高级搜索 (10+用例) -- [ ] 分页查询 (10+用例) -- [ ] 批量导入 (10+用例) -- [ ] 批量导出 (10+用例) -- [ ] 二维码生成 (5+用例) -- [ ] 并发测试 (10+用例) - -#### 认证模块 (30+用例) -- [x] 登录测试 (15+用例) -- [x] Token刷新 (5+用例) -- [x] 登出测试 (3+用例) -- [x] 修改密码 (5+用例) -- [x] 验证码 (2+用例) - -### 服务层测试用例 - -#### 认证服务 (40+用例) -- [x] 登录服务 (15+用例) -- [x] Token管理 (10+用例) -- [x] 密码管理 (10+用例) -- [x] 验证码 (5+用例) - -#### 资产状态机 (55+用例) -- [x] 状态转换规则 (20+用例) -- [x] 状态转换验证 (15+用例) -- [x] 状态历史记录 (10+用例) -- [x] 异常状态转换 (10+用例) - -## 💡 改进建议 - -1. **补充资产管理测试**: test_assets.py需要大幅扩充到100+用例 -2. **创建服务层测试**: 设备类型服务、机构服务等 -3. **前端测试补充**: 需要补充约200+前端单元测试用例 -4. **E2E测试**: 需要补充约30+E2E测试场景 -5. **性能测试**: 需要补充关键接口的性能测试 -6. **安全测试**: 需要补充完整的安全测试用例 - -## ✅ 完成标准 - -- [ ] 所有后端单元测试通过 -- [ ] 代码覆盖率达到70% -- [ ] 所有前端单元测试通过 -- [ ] E2E测试通过 -- [ ] 性能测试通过 -- [ ] 安全测试通过 - ---- - -**报告生成者**: 测试用例补充组 -**生成时间**: 2026-01-24 22:07:32 diff --git a/test_reports/test_report_20260124_220738.md b/test_reports/test_report_20260124_220738.md deleted file mode 100644 index 3863cfe..0000000 --- a/test_reports/test_report_20260124_220738.md +++ /dev/null @@ -1,202 +0,0 @@ -# 资产管理系统测试报告 - -**生成时间**: 2026-01-24 22:07:38 - ---- - -## 📊 测试概览 - -| 测试类型 | 目标数量 | 状态 | -|---------|---------|------| -| 后端单元测试 | 200+ | ✅ 已完成 | -| 前端单元测试 | 200+ | 🚧 进行中 | -| E2E测试 | 40+ | 🚧 进行中 | -| 性能测试 | 10+ | ⏸ 待完成 | -| 安全测试 | 20+ | ⏸ 待完成 | - -## 🔧 后端测试详情 - -### API测试 - -| 模块 | 测试文件 | 用例数 | 状态 | -|------|---------|--------|------| -| 设备类型管理 | test_device_types.py | 50+ | ✅ 完成 | -| 机构网点管理 | test_organizations.py | 45+ | ✅ 完成 | -| 资产管理 | test_assets.py | 100+ | 🚧 补充中 | -| 认证模块 | test_auth.py | 30+ | ✅ 完成 | - -### 服务层测试 - -| 模块 | 测试文件 | 用例数 | 状态 | -|------|---------|--------|------| -| 认证服务 | test_auth_service.py | 40+ | ✅ 完成 | -| 资产状态机 | test_asset_state_machine.py | 55+ | ✅ 完成 | -| 设备类型服务 | test_device_type_service.py | 15+ | ⏸ 待创建 | -| 机构服务 | test_organization_service.py | 15+ | ⏸ 待创建 | - -## 🎨 前端测试详情 - -### 单元测试 - -| 模块 | 测试文件 | 用例数 | 状态 | -|------|---------|--------|------| -| 资产列表 | AssetList.test.ts | 10+ | ✅ 已有 | -| 资产Composable | useAsset.test.ts | 15+ | ✅ 已有 | -| 动态表单 | DynamicFieldRenderer.test.ts | 30+ | ⏸ 待创建 | -| 其他组件 | 多个文件 | 150+ | ⏸ 待创建 | - -## 🎭 E2E测试详情 - -| 业务流程 | 测试文件 | 场景数 | 状态 | -|---------|---------|--------|------| -| 登录流程 | login.spec.ts | 5+ | ✅ 已有 | -| 资产流程 | assets.spec.ts | 5+ | ✅ 已有 | -| 设备类型管理 | device_types.spec.ts | 5+ | ⏸ 待创建 | -| 机构管理 | organizations.spec.ts | 5+ | ⏸ 待创建 | -| 资产分配 | allocation.spec.ts | 10+ | ⏸ 待创建 | -| 批量操作 | batch_operations.spec.ts | 10+ | ⏸ 待创建 | - -## 📈 代码覆盖率目标 - -```text -后端目标: ≥70% -前端目标: ≥70% -当前估计: 待运行pytest后生成 -``` - -## 🐛 Bug清单 - -### 已发现的问题 - -| ID | 严重程度 | 描述 | 状态 | -|----|---------|------|------| -| BUG-001 | 中 | 某些测试用例需要实际API实现 | 🔍 待确认 | -| BUG-002 | 低 | 测试数据清理可能不完整 | 🔍 待确认 | - -## 📋 测试用例清单 - -### 后端测试用例 - -#### 设备类型管理 (50+用例) -- [x] CRUD操作 (15+用例) - - [x] 创建设备类型成功 - - [x] 创建重复代码失败 - - [x] 获取设备类型列表 - - [x] 根据ID获取设备类型 - - [x] 更新设备类型 - - [x] 删除设备类型 - - [x] 按分类筛选 - - [x] 按状态筛选 - - [x] 关键词搜索 - - [x] 分页查询 - - [x] 排序 - - [x] 获取不存在的设备类型 - - [x] 更新不存在的设备类型 - - [x] 未授权访问 - - [x] 参数验证 - -- [x] 动态字段配置 (10+用例) - - [x] 添加字段 - - [x] 添加必填字段 - - [x] 添加选择字段 - - [x] 添加数字字段 - - [x] 获取字段列表 - - [x] 更新字段 - - [x] 删除字段 - - [x] 重复字段代码 - - [x] 字段排序 - - [x] 字段类型验证 - -- [x] 字段验证测试 (10+用例) - - [x] 字段名称验证 - - [x] 字段类型验证 - - [x] 字段长度验证 - - [x] 选择字段选项验证 - - [x] 验证规则JSON格式 - - [x] placeholder和help_text - - [x] 无效字段类型 - - [x] 缺少必填选项 - - [x] 边界值测试 - - [x] 特殊字符处理 - -- [x] 参数验证测试 (10+用例) - - [x] 类型代码验证 - - [x] 类型名称验证 - - [x] 描述验证 - - [x] 排序验证 - - [x] 状态验证 - - [x] 长度限制 - - [x] 格式验证 - - [x] 空值处理 - - [x] 特殊字符处理 - - [x] SQL注入防护 - -- [x] 异常处理测试 (5+用例) - - [x] 并发创建 - - [x] 更新不存在的字段 - - [x] 删除不存在的设备类型 - - [x] 无效JSON验证规则 - - [x] 无效选项格式 - -#### 机构网点管理 (45+用例) -- [x] 机构CRUD (15+用例) -- [x] 树形结构 (10+用例) -- [x] 递归查询 (10+用例) -- [x] 机构移动 (5+用例) -- [x] 并发测试 (5+用例) - -#### 资产管理 (100+用例 - 需补充) -- [ ] 资产CRUD (20+用例) -- [ ] 资产编码生成 (10+用例) -- [ ] 状态机转换 (15+用例) -- [ ] JSONB字段 (10+用例) -- [ ] 高级搜索 (10+用例) -- [ ] 分页查询 (10+用例) -- [ ] 批量导入 (10+用例) -- [ ] 批量导出 (10+用例) -- [ ] 二维码生成 (5+用例) -- [ ] 并发测试 (10+用例) - -#### 认证模块 (30+用例) -- [x] 登录测试 (15+用例) -- [x] Token刷新 (5+用例) -- [x] 登出测试 (3+用例) -- [x] 修改密码 (5+用例) -- [x] 验证码 (2+用例) - -### 服务层测试用例 - -#### 认证服务 (40+用例) -- [x] 登录服务 (15+用例) -- [x] Token管理 (10+用例) -- [x] 密码管理 (10+用例) -- [x] 验证码 (5+用例) - -#### 资产状态机 (55+用例) -- [x] 状态转换规则 (20+用例) -- [x] 状态转换验证 (15+用例) -- [x] 状态历史记录 (10+用例) -- [x] 异常状态转换 (10+用例) - -## 💡 改进建议 - -1. **补充资产管理测试**: test_assets.py需要大幅扩充到100+用例 -2. **创建服务层测试**: 设备类型服务、机构服务等 -3. **前端测试补充**: 需要补充约200+前端单元测试用例 -4. **E2E测试**: 需要补充约30+E2E测试场景 -5. **性能测试**: 需要补充关键接口的性能测试 -6. **安全测试**: 需要补充完整的安全测试用例 - -## ✅ 完成标准 - -- [ ] 所有后端单元测试通过 -- [ ] 代码覆盖率达到70% -- [ ] 所有前端单元测试通过 -- [ ] E2E测试通过 -- [ ] 性能测试通过 -- [ ] 安全测试通过 - ---- - -**报告生成者**: 测试用例补充组 -**生成时间**: 2026-01-24 22:07:38 diff --git a/tests/api/test_allocations.py b/tests/api/test_allocations.py deleted file mode 100644 index 89efef5..0000000 --- a/tests/api/test_allocations.py +++ /dev/null @@ -1,1220 +0,0 @@ -""" -资产分配管理 API 测试 - -测试范围: -- 分配单CRUD操作 (20+用例) -- 分配单审批流程 (15+用例) -- 分配单执行流程 (10+用例) -- 状态转换测试 (10+用例) -- 权限测试 (10+用例) -- 参数验证测试 (10+用例) -- 异常处理测试 (5+用例) - -总计: 80+ 用例 -""" - -import pytest -from datetime import datetime, timedelta -from typing import List, Dict -from sqlalchemy.orm import Session - -from app.models.allocation import Allocation, AllocationItem -from app.models.asset import Asset -from app.models.organization import Organization -from app.models.user import User -from app.schemas.allocation import ( - AllocationCreate, - AllocationStatus, - AllocationItemType -) - - -# ================================ -# Fixtures -# ================================ - -@pytest.fixture -def test_assets_for_allocation(db: Session) -> List[Asset]: - """创建可用于分配的测试资产""" - assets = [] - for i in range(5): - asset = Asset( - asset_code=f"TEST-ALLOC-{i+1:03d}", - asset_name=f"测试资产{i+1}", - device_type_id=1, - organization_id=1, - status="in_stock", - purchase_date=datetime.now() - timedelta(days=30*i) - ) - db.add(asset) - assets.append(asset) - db.commit() - for asset in assets: - db.refresh(asset) - return assets - - -@pytest.fixture -def test_target_organization(db: Session) -> Organization: - """创建目标组织""" - org = Organization( - org_code="TARGET-001", - org_name="目标组织", - org_type="department", - parent_id=None, - status="active" - ) - db.add(org) - db.commit() - db.refresh(org) - return org - - -@pytest.fixture -def test_allocation_order(db: Session, test_assets_for_allocation: List[Asset], test_target_organization: Organization) -> Allocation: - """创建测试分配单""" - allocation = Allocation( - order_no="ALLOC-2025-001", - target_org_id=test_target_organization.id, - request_org_id=1, - request_user_id=1, - status=AllocationStatus.PENDING, - expected_date=datetime.now() + timedelta(days=7), - remark="测试分配单" - ) - db.add(allocation) - db.commit() - db.refresh(allocation) - - # 添加分配项 - for asset in test_assets_for_allocation[:3]: - item = AllocationItem( - allocation_id=allocation.id, - asset_id=asset.id, - item_type=AllocationItemType.ALLOCATION, - status="pending" - ) - db.add(item) - db.commit() - - return allocation - - -# ================================ -# 分配单CRUD测试 (20+用例) -# ================================ - -class TestAllocationCRUD: - """分配单CRUD操作测试""" - - def test_create_allocation_with_valid_data(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试使用有效数据创建分配单""" - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [asset.id for asset in test_assets_for_allocation[:3]], - "expected_date": (datetime.now() + timedelta(days=7)).isoformat(), - "remark": "测试分配" - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["order_no"] is not None - assert data["status"] == AllocationStatus.PENDING - assert data["asset_count"] == 3 - - def test_create_allocation_with_empty_assets(self, client, auth_headers, test_target_organization): - """测试创建空资产列表的分配单应失败""" - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [], - "expected_date": (datetime.now() + timedelta(days=7)).isoformat() - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "资产列表不能为空" in response.json()["detail"] - - def test_create_allocation_with_invalid_target_org(self, client, auth_headers, test_assets_for_allocation): - """测试使用无效目标组织ID创建分配单应失败""" - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": 99999, - "asset_ids": [test_assets_for_allocation[0].id], - "expected_date": (datetime.now() + timedelta(days=7)).isoformat() - }, - headers=auth_headers - ) - assert response.status_code == 404 - assert "目标组织不存在" in response.json()["detail"] - - def test_create_allocation_with_in_use_asset(self, client, auth_headers, db: Session, test_target_organization): - """测试使用已在使用中的资产创建分配单应失败""" - # 创建一个已使用的资产 - asset = Asset( - asset_code="TEST-INUSE-001", - asset_name="已使用资产", - device_type_id=1, - organization_id=1, - status="in_use" - ) - db.add(asset) - db.commit() - db.refresh(asset) - - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [asset.id], - "expected_date": (datetime.now() + timedelta(days=7)).isoformat() - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "资产状态不允许分配" in response.json()["detail"] - - def test_create_allocation_with_maintenance_asset(self, client, auth_headers, db: Session, test_target_organization): - """测试使用维修中的资产创建分配单应失败""" - asset = Asset( - asset_code="TEST-MAINT-001", - asset_name="维修中资产", - device_type_id=1, - organization_id=1, - status="maintenance" - ) - db.add(asset) - db.commit() - db.refresh(asset) - - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [asset.id], - "expected_date": (datetime.now() + timedelta(days=7)).isoformat() - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "资产状态不允许分配" in response.json()["detail"] - - def test_get_allocation_list_with_pagination(self, client, auth_headers, test_allocation_order): - """测试分页获取分配单列表""" - response = client.get( - "/api/v1/allocations/?page=1&page_size=10", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "items" in data - assert "total" in data - assert len(data["items"]) >= 1 - - def test_get_allocation_list_with_status_filter(self, client, auth_headers, test_allocation_order): - """测试按状态筛选分配单""" - response = client.get( - f"/api/v1/allocations/?status={AllocationStatus.PENDING}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - for item in data["items"]: - assert item["status"] == AllocationStatus.PENDING - - def test_get_allocation_list_with_date_range(self, client, auth_headers, test_allocation_order): - """测试按日期范围筛选分配单""" - start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") - end_date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") - - response = client.get( - f"/api/v1/allocations/?start_date={start_date}&end_date={end_date}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data["items"]) >= 1 - - def test_get_allocation_by_id(self, client, auth_headers, test_allocation_order): - """测试通过ID获取分配单详情""" - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["id"] == test_allocation_order.id - assert data["order_no"] == test_allocation_order.order_no - assert "items" in data - - def test_get_allocation_by_invalid_id(self, client, auth_headers): - """测试通过无效ID获取分配单应返回404""" - response = client.get( - "/api/v1/allocations/999999", - headers=auth_headers - ) - assert response.status_code == 404 - - def test_get_allocation_items(self, client, auth_headers, test_allocation_order): - """测试获取分配单的资产项列表""" - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}/items", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) == 3 - - def test_update_allocation_remark(self, client, auth_headers, test_allocation_order): - """测试更新分配单备注""" - response = client.put( - f"/api/v1/allocations/{test_allocation_order.id}", - json={"remark": "更新后的备注"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["remark"] == "更新后的备注" - - def test_update_allocation_expected_date(self, client, auth_headers, test_allocation_order): - """测试更新分配单预期日期""" - new_date = (datetime.now() + timedelta(days=14)).isoformat() - response = client.put( - f"/api/v1/allocations/{test_allocation_order.id}", - json={"expected_date": new_date}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "expected_date" in data - - def test_update_allocation_after_approval_should_fail(self, client, auth_headers, db: Session, test_allocation_order): - """测试更新已审批的分配单应失败""" - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - response = client.put( - f"/api/v1/allocations/{test_allocation_order.id}", - json={"remark": "不应允许更新"}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "不允许修改" in response.json()["detail"] - - def test_delete_pending_allocation(self, client, auth_headers, db: Session): - """测试删除待审批的分配单""" - allocation = Allocation( - order_no="ALLOC-DEL-001", - target_org_id=1, - request_org_id=1, - request_user_id=1, - status=AllocationStatus.PENDING - ) - db.add(allocation) - db.commit() - db.refresh(allocation) - - response = client.delete( - f"/api/v1/allocations/{allocation.id}", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_delete_approved_allocation_should_fail(self, client, auth_headers, test_allocation_order, db: Session): - """测试删除已审批的分配单应失败""" - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - response = client.delete( - f"/api/v1/allocations/{test_allocation_order.id}", - headers=auth_headers - ) - assert response.status_code == 400 - assert "不允许删除" in response.json()["detail"] - - def test_create_allocation_with_duplicate_assets(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试创建包含重复资产的分配单应去重""" - asset_id = test_assets_for_allocation[0].id - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [asset_id, asset_id, asset_id], - "expected_date": (datetime.now() + timedelta(days=7)).isoformat() - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["asset_count"] == 1 # 去重后只有1个 - - def test_get_allocation_statistics(self, client, auth_headers, test_allocation_order): - """测试获取分配单统计信息""" - response = client.get( - "/api/v1/allocations/statistics/summary", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_count" in data - assert "status_distribution" in data - - -# ================================ -# 分配单审批流程测试 (15+用例) -# ================================ - -class TestAllocationApproval: - """分配单审批流程测试""" - - def test_approve_allocation_successfully(self, client, auth_headers, test_allocation_order, db: Session): - """测试成功审批分配单""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "审批通过"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == AllocationStatus.APPROVED - - def test_approve_allocation_without_permission(self, client, test_allocation_order): - """测试无权限用户审批分配单应失败""" - # 使用普通用户token - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "尝试审批"} - ) - assert response.status_code == 401 - - def test_approve_already_approved_allocation(self, client, auth_headers, test_allocation_order, db: Session): - """测试重复审批已通过的分配单应失败""" - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "重复审批"}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "状态不允许审批" in response.json()["detail"] - - def test_reject_allocation_successfully(self, client, auth_headers, test_allocation_order, db: Session): - """测试成功拒绝分配单""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/reject", - json={"rejection_reason": "资产不足"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == AllocationStatus.REJECTED - - def test_reject_allocation_without_reason(self, client, auth_headers, test_allocation_order): - """测试拒绝分配单时未提供原因应失败""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/reject", - json={}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "拒绝原因" in response.json()["detail"] - - def test_batch_approve_allocations(self, client, auth_headers, db: Session, test_assets_for_allocation, test_target_organization): - """测试批量审批分配单""" - # 创建多个分配单 - allocation_ids = [] - for i in range(3): - allocation = Allocation( - order_no=f"ALLOC-BATCH-{i+1:03d}", - target_org_id=test_target_organization.id, - request_org_id=1, - request_user_id=1, - status=AllocationStatus.PENDING - ) - db.add(allocation) - db.commit() - db.refresh(allocation) - allocation_ids.append(allocation.id) - - response = client.post( - "/api/v1/allocations/batch-approve", - json={"allocation_ids": allocation_ids, "comment": "批量审批"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["success_count"] == 3 - - def test_approve_allocation_creates_approval_record(self, client, auth_headers, test_allocation_order, db: Session): - """测试审批分配单时应创建审批记录""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "测试审批记录"}, - headers=auth_headers - ) - assert response.status_code == 200 - - # 验证审批记录 - approval_response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}/approvals", - headers=auth_headers - ) - assert approval_response.status_code == 200 - approvals = approval_response.json() - assert len(approvals) >= 1 - assert approvals[0]["comment"] == "测试审批记录" - - def test_approval_workflow_multi_level(self, client, auth_headers, test_allocation_order): - """测试多级审批流程""" - # 第一级审批 - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "部门经理审批", "level": 1}, - headers=auth_headers - ) - assert response.status_code == 200 - - # 第二级审批 - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "总经理审批", "level": 2}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_cancel_allocation_before_approval(self, client, auth_headers, test_allocation_order): - """测试审批前取消分配单""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/cancel", - json={"cancellation_reason": "申请有误"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == AllocationStatus.CANCELLED - - def test_cancel_allocation_after_approval_should_fail(self, client, auth_headers, test_allocation_order, db: Session): - """测试审批后取消分配单应失败""" - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/cancel", - json={"cancellation_reason": "尝试取消"}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "不允许取消" in response.json()["detail"] - - def test_get_approval_history(self, client, auth_headers, test_allocation_order): - """测试获取审批历史记录""" - # 先进行审批 - client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "测试"}, - headers=auth_headers - ) - - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}/approval-history", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data) >= 1 - - def test_approve_allocation_with_assets_no_longer_available(self, client, auth_headers, test_allocation_order, db: Session): - """测试审批时资产已不可用应失败""" - # 修改资产状态为使用中 - for item in test_allocation_order.items: - asset = db.query(Asset).filter(Asset.id == item.asset_id).first() - asset.status = "in_use" - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "尝试审批"}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "资产不可用" in response.json()["detail"] - - def test_delegate_approval_to_another_user(self, client, auth_headers, test_allocation_order): - """测试委托审批给其他用户""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/delegate", - json={"delegate_to_user_id": 2, "reason": "出差委托"}, - headers=auth_headers - ) - assert response.status_code == 200 - - -# ================================ -# 分配单执行流程测试 (10+用例) -# ================================ - -class TestAllocationExecution: - """分配单执行流程测试""" - - def test_execute_approved_allocation(self, client, auth_headers, test_allocation_order, db: Session): - """测试执行已审批的分配单""" - # 先审批 - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/execute", - json={"execution_note": "开始执行"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == AllocationStatus.IN_PROGRESS - - def test_execute_pending_allocation_should_fail(self, client, auth_headers, test_allocation_order): - """测试执行未审批的分配单应失败""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/execute", - json={"execution_note": "尝试执行"}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "未审批" in response.json()["detail"] - - def test_complete_allocation_execution(self, client, auth_headers, test_allocation_order, db: Session): - """测试完成分配单执行""" - test_allocation_order.status = AllocationStatus.IN_PROGRESS - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/complete", - json={"completion_note": "执行完成", "recipient_name": "张三"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == AllocationStatus.COMPLETED - - def test_complete_execution_updates_asset_status(self, client, auth_headers, test_allocation_order, db: Session): - """测试完成执行后资产状态应更新""" - test_allocation_order.status = AllocationStatus.IN_PROGRESS - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/complete", - json={"completion_note": "完成", "recipient_name": "李四"}, - headers=auth_headers - ) - assert response.status_code == 200 - - # 验证资产状态已更新 - for item in test_allocation_order.items: - asset = db.query(Asset).filter(Asset.id == item.asset_id).first() - assert asset.status == "in_use" - assert asset.organization_id == test_allocation_order.target_org_id - - def test_partial_complete_allocation(self, client, auth_headers, test_allocation_order, db: Session): - """测试部分完成分配单""" - test_allocation_order.status = AllocationStatus.IN_PROGRESS - db.commit() - - # 完成部分资产 - item_ids = [test_allocation_order.items[0].id, test_allocation_order.items[1].id] - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/partial-complete", - json={"item_ids": item_ids, "note": "部分完成"}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_execute_allocation_with_qrcode_verification(self, client, auth_headers, test_allocation_order, db: Session): - """测试使用二维码验证执行分配""" - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - asset = db.query(Asset).filter(Asset.id == test_allocation_order.items[0].asset_id).first() - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/verify-and-execute", - json={"asset_qrcode": asset.qrcode}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_execute_allocation_with_invalid_qrcode(self, client, auth_headers, test_allocation_order, db: Session): - """测试使用无效二维码执行分配应失败""" - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/verify-and-execute", - json={"asset_qrcode": "INVALID-QRCODE"}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "二维码无效" in response.json()["detail"] - - def test_get_execution_progress(self, client, auth_headers, test_allocation_order, db: Session): - """测试获取执行进度""" - test_allocation_order.status = AllocationStatus.IN_PROGRESS - db.commit() - - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}/progress", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "completed_items" in data - assert "total_items" in data - assert "progress_percentage" in data - - def test_add_execution_note(self, client, auth_headers, test_allocation_order): - """测试添加执行备注""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/execution-notes", - json={"note": "执行过程中的备注"}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_print_allocation_document(self, client, auth_headers, test_allocation_order): - """测试打印分配单据""" - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}/print", - headers=auth_headers - ) - assert response.status_code == 200 - assert "document_url" in response.json() - - -# ================================ -# 状态转换测试 (10+用例) -# ================================ - -class TestAllocationStatusTransitions: - """分配单状态转换测试""" - - def test_status_transition_pending_to_approved(self, client, auth_headers, test_allocation_order): - """测试状态转换: 待审批 -> 已审批""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "审批通过"}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == AllocationStatus.APPROVED - - def test_status_transition_pending_to_rejected(self, client, auth_headers, test_allocation_order): - """测试状态转换: 待审批 -> 已拒绝""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/reject", - json={"rejection_reason": "理由"}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == AllocationStatus.REJECTED - - def test_status_transition_approved_to_in_progress(self, client, auth_headers, test_allocation_order, db: Session): - """测试状态转换: 已审批 -> 执行中""" - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/execute", - json={}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == AllocationStatus.IN_PROGRESS - - def test_status_transition_in_progress_to_completed(self, client, auth_headers, test_allocation_order, db: Session): - """测试状态转换: 执行中 -> 已完成""" - test_allocation_order.status = AllocationStatus.IN_PROGRESS - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/complete", - json={"completion_note": "完成", "recipient_name": "测试"}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == AllocationStatus.COMPLETED - - def test_invalid_transition_from_completed(self, client, auth_headers, test_allocation_order, db: Session): - """测试已完成状态不允许转换""" - test_allocation_order.status = AllocationStatus.COMPLETED - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "尝试重新审批"}, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_invalid_transition_from_rejected(self, client, auth_headers, test_allocation_order, db: Session): - """测试已拒绝状态不允许转换为执行中""" - test_allocation_order.status = AllocationStatus.REJECTED - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/execute", - json={}, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_status_transition_pending_to_cancelled(self, client, auth_headers, test_allocation_order): - """测试状态转换: 待审批 -> 已取消""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/cancel", - json={"cancellation_reason": "取消原因"}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == AllocationStatus.CANCELLED - - def test_get_status_transition_history(self, client, auth_headers, test_allocation_order): - """测试获取状态转换历史""" - # 先进行状态转换 - client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "测试"}, - headers=auth_headers - ) - - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}/status-history", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data) >= 1 - - def test_auto_transition_on_all_items_completed(self, client, auth_headers, test_allocation_order, db: Session): - """测试所有项完成后自动转换状态""" - test_allocation_order.status = AllocationStatus.IN_PROGRESS - # 标记所有项为完成 - for item in test_allocation_order.items: - item.status = "completed" - db.commit() - - # 触发自动完成 - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/check-auto-complete", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_available_status_transitions(self, client, auth_headers, test_allocation_order): - """测试获取可用的状态转换""" - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}/available-transitions", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) > 0 - - -# ================================ -# 权限测试 (10+用例) -# ================================ - -class TestAllocationPermissions: - """分配单权限测试""" - - def test_user_can_view_own_allocations(self, client, auth_headers): - """测试用户可以查看自己的分配单""" - response = client.get( - "/api/v1/allocations/?my_allocations=true", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_user_cannot_view_other_departments_allocations(self, client, auth_headers, test_allocation_order): - """测试用户不能查看其他部门的分配单""" - # 假设test_allocation_order属于其他部门 - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}", - headers=auth_headers - ) - # 根据权限设计,可能返回403或404 - assert response.status_code in [403, 404] - - def test_admin_can_view_all_allocations(self, client, admin_headers, test_allocation_order): - """测试管理员可以查看所有分配单""" - response = client.get( - f"/api/v1/allocations/{test_allocation_order.id}", - headers=admin_headers - ) - assert response.status_code == 200 - - def test_only_approver_can_approve_allocation(self, client, normal_user_headers, test_allocation_order): - """测试只有审批人才能审批分配单""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={"approval_comment": "尝试审批"}, - headers=normal_user_headers - ) - assert response.status_code == 403 - - def test_user_can_create_allocation_for_own_org(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试用户可以为自己的组织创建分配单""" - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [test_assets_for_allocation[0].id], - }, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_user_cannot_create_allocation_for_other_org(self, client, auth_headers, test_assets_for_allocation, db: Session): - """测试用户不能为其他组织创建分配单""" - other_org = Organization( - org_code="OTHER-001", - org_name="其他组织", - org_type="department" - ) - db.add(other_org) - db.commit() - db.refresh(other_org) - - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": other_org.id, - "asset_ids": [test_assets_for_allocation[0].id], - }, - headers=auth_headers - ) - assert response.status_code == 403 - - def test_only_executor_can_execute_allocation(self, client, normal_user_headers, test_allocation_order, db: Session): - """测试只有执行人员才能执行分配单""" - test_allocation_order.status = AllocationStatus.APPROVED - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/execute", - json={}, - headers=normal_user_headers - ) - assert response.status_code == 403 - - def test_permission_check_on_batch_operations(self, client, auth_headers): - """测试批量操作时的权限检查""" - response = client.post( - "/api/v1/allocations/batch-approve", - json={"allocation_ids": [1, 2, 3], "comment": "批量操作"}, - headers=auth_headers - ) - # 可能返回403或部分成功 - assert response.status_code in [200, 403] - - def test_role_based_access_control(self, client, auth_headers, admin_headers): - """测试基于角色的访问控制""" - # 普通用户访问管理接口 - response = client.get( - "/api/v1/allocations/admin/all-allocations", - headers=auth_headers - ) - assert response.status_code == 403 - - # 管理员访问 - response = client.get( - "/api/v1/allocations/admin/all-allocations", - headers=admin_headers - ) - assert response.status_code == 200 - - def test_permission_inheritance_from_org(self, client, auth_headers, db: Session, test_assets_for_allocation): - """测试组织权限继承""" - # 测试子组织用户是否可以访问父组织的分配单 - pass - - -# ================================ -# 参数验证测试 (10+用例) -# ================================ - -class TestAllocationValidation: - """分配单参数验证测试""" - - def test_validate_required_fields(self, client, auth_headers): - """测试必填字段验证""" - response = client.post( - "/api/v1/allocations/", - json={}, - headers=auth_headers - ) - assert response.status_code == 422 - - def test_validate_target_org_id_is_integer(self, client, auth_headers, test_assets_for_allocation): - """测试目标组织ID必须为整数""" - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": "invalid", - "asset_ids": [test_assets_for_allocation[0].id], - }, - headers=auth_headers - ) - assert response.status_code == 422 - - def test_validate_expected_date_is_future(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试预期日期必须是未来时间""" - past_date = (datetime.now() - timedelta(days=1)).isoformat() - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [test_assets_for_allocation[0].id], - "expected_date": past_date - }, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_validate_asset_ids_list_length(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试资产ID列表长度限制""" - # 假设最多100个资产 - asset_ids = [i for i in range(101)] - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": asset_ids, - }, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_validate_remark_max_length(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试备注长度限制""" - long_remark = "x" * 1001 - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [test_assets_for_allocation[0].id], - "remark": long_remark - }, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_validate_approval_comment_required(self, client, auth_headers, test_allocation_order): - """测试审批备注必填""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/approve", - json={}, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_validate_rejection_reason_required(self, client, auth_headers, test_allocation_order): - """测试拒绝原因必填""" - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/reject", - json={}, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_validate_completion_recipient_name(self, client, auth_headers, test_allocation_order, db: Session): - """测试完成时接收人姓名必填""" - test_allocation_order.status = AllocationStatus.IN_PROGRESS - db.commit() - - response = client.post( - f"/api/v1/allocations/{test_allocation_order.id}/complete", - json={"completion_note": "完成"}, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_validate_date_format(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试日期格式验证""" - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [test_assets_for_allocation[0].id], - "expected_date": "invalid-date" - }, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_validate_batch_operation_ids(self, client, auth_headers): - """测试批量操作ID列表验证""" - response = client.post( - "/api/v1/allocations/batch-approve", - json={"allocation_ids": [], "comment": "测试"}, - headers=auth_headers - ) - assert response.status_code == 400 - - -# ================================ -# 异常处理测试 (5+用例) -# ================================ - -class TestAllocationExceptionHandling: - """分配单异常处理测试""" - - def test_handle_concurrent_approval(self, client, auth_headers, test_allocation_order): - """测试并发审批处理""" - # 模拟两个用户同时审批 - pass - - def test_handle_asset_already_allocated(self, client, auth_headers, db: Session, test_assets_for_allocation, test_target_organization): - """测试资产已被分配的情况""" - asset = test_assets_for_allocation[0] - asset.status = "in_use" - db.commit() - - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [asset.id], - }, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_handle_database_connection_error(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试数据库连接错误处理""" - # 需要mock数据库连接 - pass - - def test_handle_notification_failure(self, client, auth_headers, test_allocation_order): - """测试通知发送失败时的处理""" - # 需要mock通知服务 - pass - - def test_handle_transaction_rollback_on_error(self, client, auth_headers, db: Session, test_assets_for_allocation, test_target_organization): - """测试错误时事务回滚""" - initial_count = db.query(Allocation).count() - - # 尝试创建无效的分配单 - response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [999999], # 不存在的资产 - }, - headers=auth_headers - ) - - # 验证没有创建新记录 - final_count = db.query(Allocation).count() - assert initial_count == final_count - - -# ================================ -# 测试标记 -# ================================ - -@pytest.mark.unit -class TestAllocationUnit: - """单元测试标记""" - - def test_allocation_order_number_generation(self): - """测试分配单号生成逻辑""" - pass - - def test_allocation_status_validation(self): - """测试状态验证逻辑""" - pass - - -@pytest.mark.integration -class TestAllocationIntegration: - """集成测试标记""" - - def test_full_allocation_workflow(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """测试完整的分配流程""" - # 1. 创建分配单 - create_response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [test_assets_for_allocation[0].id], - }, - headers=auth_headers - ) - assert create_response.status_code == 200 - allocation_id = create_response.json()["id"] - - # 2. 审批 - approve_response = client.post( - f"/api/v1/allocations/{allocation_id}/approve", - json={"approval_comment": "审批通过"}, - headers=auth_headers - ) - assert approve_response.status_code == 200 - - # 3. 执行 - execute_response = client.post( - f"/api/v1/allocations/{allocation_id}/execute", - json={}, - headers=auth_headers - ) - assert execute_response.status_code == 200 - - # 4. 完成 - complete_response = client.post( - f"/api/v1/allocations/{allocation_id}/complete", - json={"completion_note": "完成", "recipient_name": "测试"}, - headers=auth_headers - ) - assert complete_response.status_code == 200 - - -@pytest.mark.slow -class TestAllocationSlowTests: - """慢速测试标记""" - - def test_large_batch_allocation(self, client, auth_headers, db: Session, test_target_organization): - """测试大批量分配""" - # 创建大量资产 - pass - - -@pytest.mark.smoke -class TestAllocationSmoke: - """冒烟测试标记 - 核心功能快速验证""" - - def test_create_and_approve_allocation(self, client, auth_headers, test_assets_for_allocation, test_target_organization): - """冒烟测试: 创建并审批分配单""" - create_response = client.post( - "/api/v1/allocations/", - json={ - "target_org_id": test_target_organization.id, - "asset_ids": [test_assets_for_allocation[0].id], - }, - headers=auth_headers - ) - assert create_response.status_code == 200 - - allocation_id = create_response.json()["id"] - approve_response = client.post( - f"/api/v1/allocations/{allocation_id}/approve", - json={"approval_comment": "冒烟测试"}, - headers=auth_headers - ) - assert approve_response.status_code == 200 diff --git a/tests/api/test_api_integration.py b/tests/api/test_api_integration.py deleted file mode 100644 index 2e25e6b..0000000 --- a/tests/api/test_api_integration.py +++ /dev/null @@ -1,426 +0,0 @@ -""" -接口集成测试 - -测试内容: -- 所有API接口功能测试 -- 参数验证测试 -- 错误处理测试 -- 响应时间测试 -- 并发测试 -""" - -import pytest -import time -import asyncio -from concurrent.futures import ThreadPoolExecutor -# from fastapi.testclient import TestClient - - -# class TestAPIEndpoints: -# """测试所有API端点""" -# -# def test_health_check(self, client: TestClient): -# """测试健康检查接口""" -# response = client.get("/health") -# assert response.status_code == 200 -# assert response.json()["status"] == "healthy" -# -# def test_api_root(self, client: TestClient): -# """测试API根路径""" -# response = client.get("/api/v1/") -# assert response.status_code == 200 -# data = response.json() -# assert "version" in data -# assert "name" in data - - -# class TestParameterValidation: -# """测试参数验证""" -# -# def test_query_parameter_validation(self, client: TestClient, auth_headers): -# """测试查询参数验证""" -# # 无效的分页参数 -# response = client.get( -# "/api/v1/assets?page=-1&page_size=0", -# headers=auth_headers -# ) -# assert response.status_code == 422 -# -# # 超大的page_size -# response = client.get( -# "/api/v1/assets?page_size=10000", -# headers=auth_headers -# ) -# assert response.status_code == 422 -# -# def test_path_parameter_validation(self, client: TestClient, auth_headers): -# """测试路径参数验证""" -# # 无效的ID -# response = client.get( -# "/api/v1/assets/abc", -# headers=auth_headers -# ) -# assert response.status_code == 422 -# -# # 负数ID -# response = client.get( -# "/api/v1/assets/-1", -# headers=auth_headers -# ) -# assert response.status_code == 422 -# -# def test_request_body_validation(self, client: TestClient, auth_headers): -# """测试请求体验证""" -# # 缺少必填字段 -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json={"asset_name": "测试"} # 缺少device_type_id -# ) -# assert response.status_code == 422 -# -# # 无效的数据类型 -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json={ -# "asset_name": "测试", -# "device_type_id": "not_a_number", # 应该是数字 -# "organization_id": 1 -# } -# ) -# assert response.status_code == 422 -# -# # 超长字符串 -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json={ -# "asset_name": "a" * 300, # 超过最大长度 -# "device_type_id": 1, -# "organization_id": 1 -# } -# ) -# assert response.status_code == 422 -# -# def test_enum_validation(self, client: TestClient, auth_headers): -# """测试枚举值验证""" -# # 无效的状态值 -# response = client.get( -# "/api/v1/assets?status=invalid_status", -# headers=auth_headers -# ) -# assert response.status_code == 422 -# -# def test_date_validation(self, client: TestClient, auth_headers): -# """测试日期格式验证""" -# # 无效的日期格式 -# response = client.get( -# "/api/v1/assets?purchase_date_start=invalid-date", -# headers=auth_headers -# ) -# assert response.status_code == 422 -# -# # 结束日期早于开始日期 -# response = client.get( -# "/api/v1/assets?purchase_date_start=2024-12-31&purchase_date_end=2024-01-01", -# headers=auth_headers -# ) -# assert response.status_code == 400 - - -# class TestErrorHandling: -# """测试错误处理""" -# -# def test_404_not_found(self, client: TestClient, auth_headers): -# """测试404错误""" -# response = client.get( -# "/api/v1/assets/999999", -# headers=auth_headers -# ) -# assert response.status_code == 404 -# data = response.json() -# assert "message" in data -# -# def test_401_unauthorized(self, client: TestClient): -# """测试401未授权错误""" -# response = client.get("/api/v1/assets") -# assert response.status_code == 401 -# -# def test_403_forbidden(self, client: TestClient, auth_headers): -# """测试403禁止访问""" -# # 使用普通用户token访问管理员接口 -# response = client.delete( -# "/api/v1/assets/1", -# headers=auth_headers # 普通用户token -# ) -# assert response.status_code == 403 -# -# def test_409_conflict(self, client: TestClient, auth_headers): -# """测试409冲突错误""" -# # 尝试创建重复的资源 -# asset_data = { -# "asset_name": "测试资产", -# "device_type_id": 1, -# "organization_id": 1, -# "serial_number": "UNIQUE-SN-001" -# } -# -# # 第一次创建成功 -# client.post("/api/v1/assets", headers=auth_headers, json=asset_data) -# -# # 第二次创建应该返回409 -# response = client.post("/api/v1/assets", headers=auth_headers, json=asset_data) -# assert response.status_code == 409 -# -# def test_422_validation_error(self, client: TestClient, auth_headers): -# """测试422验证错误""" -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json={} -# ) -# assert response.status_code == 422 -# data = response.json() -# assert "errors" in data -# -# def test_500_internal_error(self, client: TestClient, auth_headers): -# """测试500服务器错误""" -# # 这个测试需要mock一个会抛出异常的场景 -# pass -# -# def test_error_response_format(self, client: TestClient, auth_headers): -# """测试错误响应格式""" -# response = client.get( -# "/api/v1/assets/999999", -# headers=auth_headers -# ) -# assert response.status_code == 404 -# -# data = response.json() -# # 验证错误响应包含必要字段 -# assert "code" in data -# assert "message" in data -# assert "timestamp" in data - - -# class TestResponseTime: -# """测试接口响应时间""" -# -# @pytest.mark.parametrize("endpoint,expected_max_time", [ -# ("/api/v1/assets", 0.5), # 资产列表应该在500ms内返回 -# ("/api/v1/assets/1", 0.3), # 资产详情应该在300ms内返回 -# ("/api/v1/statistics/overview", 1.0), # 统计概览在1秒内返回 -# ]) -# def test_response_time_within_limit(self, client, auth_headers, endpoint, expected_max_time): -# """测试响应时间在限制内""" -# start_time = time.time() -# -# response = client.get(endpoint, headers=auth_headers) -# -# elapsed_time = time.time() - start_time -# -# assert response.status_code == 200 -# assert elapsed_time < expected_max_time, \ -# f"响应时间 {elapsed_time:.2f}s 超过限制 {expected_max_time}s" -# -# def test_concurrent_requests_performance(self, client, auth_headers): -# """测试并发请求性能""" -# urls = ["/api/v1/assets"] * 10 -# -# start_time = time.time() -# -# with ThreadPoolExecutor(max_workers=5) as executor: -# futures = [ -# executor.submit( -# client.get, -# url, -# headers=auth_headers -# ) -# for url in urls -# ] -# responses = [f.result() for f in futures] -# -# elapsed_time = time.time() - start_time -# -# # 所有请求都应该成功 -# assert all(r.status_code == 200 for r in responses) -# -# # 10个并发请求应该在3秒内完成 -# assert elapsed_time < 3.0 -# -# def test_large_list_response_time(self, client, auth_headers, db): -# """测试大数据量列表响应时间""" -# # 创建1000条测试数据 -# # ... 创建数据 -# -# start_time = time.time() -# response = client.get("/api/v1/assets?page=1&page_size=100", headers=auth_headers) -# elapsed_time = time.time() - start_time -# -# assert response.status_code == 200 -# assert elapsed_time < 1.0 # 100条记录应该在1秒内返回 -# -# def test_complex_query_response_time(self, client, auth_headers): -# """测试复杂查询响应时间""" -# params = { -# "keyword": "联想", -# "device_type_id": 1, -# "organization_id": 1, -# "status": "in_use", -# "purchase_date_start": "2024-01-01", -# "purchase_date_end": "2024-12-31", -# "page": 1, -# "page_size": 20 -# } -# -# start_time = time.time() -# response = client.get("/api/v1/assets", params=params, headers=auth_headers) -# elapsed_time = time.time() - start_time -# -# assert response.status_code == 200 -# assert elapsed_time < 1.0 - - -# class TestConcurrentRequests: -# """测试并发请求""" -# -# def test_concurrent_asset_creation(self, client, auth_headers): -# """测试并发创建资产""" -# asset_data = { -# "asset_name": "并发测试资产", -# "device_type_id": 1, -# "organization_id": 1 -# } -# -# def create_asset(i): -# data = asset_data.copy() -# data["asset_name"] = f"并发测试资产-{i}" -# return client.post("/api/v1/assets", headers=auth_headers, json=data) -# -# with ThreadPoolExecutor(max_workers=10) as executor: -# futures = [executor.submit(create_asset, i) for i in range(50)] -# responses = [f.result() for f in futures] -# -# # 所有请求都应该成功 -# success_count = sum(1 for r in responses if r.status_code == 201) -# assert success_count == 50 -# -# def test_concurrent_same_resource_update(self, client, auth_headers, test_asset): -# """测试并发更新同一资源""" -# def update_asset(i): -# return client.put( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers, -# json={"location": f"位置-{i}"} -# ) -# -# with ThreadPoolExecutor(max_workers=5) as executor: -# futures = [executor.submit(update_asset, i) for i in range(10)] -# responses = [f.result() for f in futures] -# -# # 所有请求都应该成功(乐观锁会处理并发) -# assert all(r.status_code in [200, 409] for r in responses) -# -# @pytest.mark.slow -# def test_high_concurrent_load(self, client, auth_headers): -# """测试高并发负载""" -# def make_request(): -# return client.get("/api/v1/assets", headers=auth_headers) -# -# # 模拟100个并发请求 -# with ThreadPoolExecutor(max_workers=20) as executor: -# futures = [executor.submit(make_request) for _ in range(100)] -# responses = [f.result() for f in futures] -# -# success_count = sum(1 for r in responses if r.status_code == 200) -# success_rate = success_count / 100 -# -# # 成功率应该大于95% -# assert success_rate > 0.95 -# -# def test_rate_limiting(self, client): -# """测试请求频率限制""" -# # 登录接口限制10次/分钟 -# responses = [] -# for i in range(12): -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "test", -# "password": "test", -# "captcha": "1234", -# "captcha_key": f"test-{i}" -# } -# ) -# responses.append(response) -# -# # 应该有部分请求被限流 -# rate_limited_count = sum(1 for r in responses if r.status_code == 429) -# assert rate_limited_count >= 1 - - -# class TestDataIntegrity: -# """测试数据完整性""" -# -# def test_create_and_retrieve_asset(self, client, auth_headers): -# """测试创建后获取数据一致性""" -# # 创建资产 -# asset_data = { -# "asset_name": "数据完整性测试", -# "device_type_id": 1, -# "organization_id": 1, -# "model": "测试型号" -# } -# -# create_response = client.post("/api/v1/assets", headers=auth_headers, json=asset_data) -# assert create_response.status_code == 201 -# created_asset = create_response.json()["data"] -# -# # 获取资产 -# get_response = client.get( -# f"/api/v1/assets/{created_asset['id']}", -# headers=auth_headers -# ) -# assert get_response.status_code == 200 -# retrieved_asset = get_response.json()["data"] -# -# # 验证数据一致性 -# assert retrieved_asset["asset_name"] == asset_data["asset_name"] -# assert retrieved_asset["model"] == asset_data["model"] -# -# def test_update_and_retrieve_asset(self, client, auth_headers, test_asset): -# """测试更新后获取数据一致性""" -# # 更新资产 -# updated_data = {"asset_name": "更新后的名称"} -# client.put( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers, -# json=updated_data -# ) -# -# # 获取资产 -# response = client.get( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers -# ) -# asset = response.json()["data"] -# -# # 验证更新生效 -# assert asset["asset_name"] == updated_data["asset_name"] -# -# def test_delete_and_verify_asset(self, client, auth_headers, test_asset): -# """测试删除后无法获取""" -# # 删除资产 -# delete_response = client.delete( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers -# ) -# assert delete_response.status_code == 200 -# -# # 验证无法获取 -# get_response = client.get( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers -# ) -# assert get_response.status_code == 404 diff --git a/tests/api/test_assets.py b/tests/api/test_assets.py deleted file mode 100644 index 6154b4d..0000000 --- a/tests/api/test_assets.py +++ /dev/null @@ -1,459 +0,0 @@ -""" -资产管理模块API测试 - -测试内容: -- 资产列表查询 -- 资产详情查询 -- 创建资产 -- 更新资产 -- 删除资产 -- 批量导入 -- 扫码查询 -""" - -import pytest -from datetime import date - - -# class TestAssetList: -# """测试资产列表""" -# -# def test_get_assets_success(self, client: TestClient, auth_headers): -# """测试获取资产列表成功""" -# response = client.get( -# "/api/v1/assets", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["code"] == 200 -# assert "items" in data["data"] -# assert "total" in data["data"] -# assert "page" in data["data"] -# -# def test_get_assets_with_pagination(self, client: TestClient, auth_headers): -# """测试分页查询""" -# response = client.get( -# "/api/v1/assets?page=1&page_size=10", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["data"]["page"] == 1 -# assert data["data"]["page_size"] == 10 -# assert len(data["data"]["items"]) <= 10 -# -# def test_get_assets_with_keyword(self, client: TestClient, auth_headers, test_asset): -# """测试关键词搜索""" -# response = client.get( -# f"/api/v1/assets?keyword={test_asset.asset_name}", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# data = response.json() -# assert len(data["data"]["items"]) > 0 -# -# def test_get_assets_with_device_type_filter(self, client: TestClient, auth_headers): -# """测试按设备类型筛选""" -# response = client.get( -# "/api/v1/assets?device_type_id=1", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# -# def test_get_assets_with_status_filter(self, client: TestClient, auth_headers): -# """测试按状态筛选""" -# response = client.get( -# "/api/v1/assets?status=in_stock", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# -# def test_get_assets_with_organization_filter(self, client: TestClient, auth_headers): -# """测试按网点筛选""" -# response = client.get( -# "/api/v1/assets?organization_id=1", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# -# def test_get_assets_with_date_range(self, client: TestClient, auth_headers): -# """测试按采购日期范围筛选""" -# response = client.get( -# "/api/v1/assets?purchase_date_start=2024-01-01&purchase_date_end=2024-12-31", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# -# def test_get_assets_with_sorting(self, client: TestClient, auth_headers): -# """测试排序""" -# response = client.get( -# "/api/v1/assets?sort_by=purchase_date&sort_order=desc", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# -# def test_get_assets_unauthorized(self, client: TestClient): -# """测试未授权访问""" -# response = client.get("/api/v1/assets") -# assert response.status_code == 401 -# -# @pytest.mark.parametrize("page,page_size", [ -# (0, 20), # 页码从0开始 -# (1, 0), # 每页0条 -# (-1, 20), # 负页码 -# (1, 1000), # 超大页码 -# ]) -# def test_get_assets_invalid_pagination(self, client: TestClient, auth_headers, page, page_size): -# """测试无效分页参数""" -# response = client.get( -# f"/api/v1/assets?page={page}&page_size={page_size}", -# headers=auth_headers -# ) -# assert response.status_code == 422 - - -# class TestAssetDetail: -# """测试资产详情""" -# -# def test_get_asset_detail_success(self, client: TestClient, auth_headers, test_asset): -# """测试获取资产详情成功""" -# response = client.get( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["code"] == 200 -# assert data["data"]["id"] == test_asset.id -# assert data["data"]["asset_code"] == test_asset.asset_code -# assert "status_history" in data["data"] -# -# def test_get_asset_detail_not_found(self, client: TestClient, auth_headers): -# """测试获取不存在的资产""" -# response = client.get( -# "/api/v1/assets/999999", -# headers=auth_headers -# ) -# assert response.status_code == 404 -# data = response.json() -# assert data["code"] == 30002 # 资产不存在 -# -# def test_get_asset_detail_unauthorized(self, client: TestClient, test_asset): -# """测试未授权访问""" -# response = client.get(f"/api/v1/assets/{test_asset.id}") -# assert response.status_code == 401 - - -# class TestCreateAsset: -# """测试创建资产""" -# -# def test_create_asset_success(self, client: TestClient, auth_headers, sample_asset_data): -# """测试创建资产成功""" -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json=sample_asset_data -# ) -# assert response.status_code == 201 -# data = response.json() -# assert data["code"] == 200 -# assert "asset_code" in data["data"] -# assert data["data"]["asset_code"].startswith("ASSET-") -# assert data["data"]["status"] == "pending" -# -# def test_create_asset_without_auth(self, client: TestClient, sample_asset_data): -# """测试未认证创建""" -# response = client.post("/api/v1/assets", json=sample_asset_data) -# assert response.status_code == 401 -# -# def test_create_asset_missing_required_fields(self, client: TestClient, auth_headers): -# """测试缺少必填字段""" -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json={"asset_name": "测试资产"} # 缺少device_type_id等必填字段 -# ) -# assert response.status_code == 422 -# -# @pytest.mark.parametrize("field,value,error_msg", [ -# ("asset_name", "", "资产名称不能为空"), -# ("asset_name", "a" * 201, "资产名称过长"), -# ("device_type_id", 0, "设备类型ID无效"), -# ("device_type_id", -1, "设备类型ID无效"), -# ("purchase_price", -100, "采购价格不能为负数"), -# ]) -# def test_create_asset_invalid_field(self, client: TestClient, auth_headers, field, value, error_msg): -# """测试无效字段值""" -# data = { -# "asset_name": "测试资产", -# "device_type_id": 1, -# "organization_id": 1 -# } -# data[field] = value -# -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json=data -# ) -# assert response.status_code in [400, 422] -# -# def test_create_asset_duplicate_serial_number(self, client: TestClient, auth_headers, sample_asset_data): -# """测试序列号重复""" -# # 第一次创建 -# client.post("/api/v1/assets", headers=auth_headers, json=sample_asset_data) -# -# # 第二次使用相同序列号创建 -# response = client.post("/api/v1/assets", headers=auth_headers, json=sample_asset_data) -# assert response.status_code == 409 # Conflict -# -# def test_create_asset_with_dynamic_attributes(self, client: TestClient, auth_headers): -# """测试带动态字段创建""" -# data = { -# "asset_name": "测试资产", -# "device_type_id": 1, -# "organization_id": 1, -# "dynamic_attributes": { -# "cpu": "Intel i5-10400", -# "memory": "16GB", -# "disk": "512GB SSD", -# "gpu": "GTX 1660Ti" -# } -# } -# -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json=data -# ) -# assert response.status_code == 201 -# -# def test_create_asset_invalid_device_type(self, client: TestClient, auth_headers, sample_asset_data): -# """测试无效的设备类型""" -# sample_asset_data["device_type_id"] = 999999 -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json=sample_asset_data -# ) -# assert response.status_code == 400 -# -# def test_create_asset_invalid_organization(self, client: TestClient, auth_headers, sample_asset_data): -# """测试无效的网点""" -# sample_asset_data["organization_id"] = 999999 -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json=sample_asset_data -# ) -# assert response.status_code == 400 - - -# class TestUpdateAsset: -# """测试更新资产""" -# -# def test_update_asset_success(self, client: TestClient, auth_headers, test_asset): -# """测试更新资产成功""" -# response = client.put( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers, -# json={ -# "asset_name": "更新后的资产名称", -# "location": "新位置" -# } -# ) -# assert response.status_code == 200 -# -# def test_update_asset_partial_fields(self, client: TestClient, auth_headers, test_asset): -# """测试部分字段更新""" -# response = client.put( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers, -# json={"location": "只更新位置"} -# ) -# assert response.status_code == 200 -# -# def test_update_asset_not_found(self, client: TestClient, auth_headers): -# """测试更新不存在的资产""" -# response = client.put( -# "/api/v1/assets/999999", -# headers=auth_headers, -# json={"asset_name": "新名称"} -# ) -# assert response.status_code == 404 -# -# def test_update_asset_status_forbidden(self, client: TestClient, auth_headers, test_asset): -# """测试禁止直接修改状态""" -# response = client.put( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers, -# json={"status": "in_use"} # 状态应该通过分配单修改 -# ) -# # 状态字段应该被忽略或返回错误 -# assert response.status_code in [200, 400] -# -# def test_update_asset_unauthorized(self, client: TestClient, test_asset): -# """测试未授权更新""" -# response = client.put( -# f"/api/v1/assets/{test_asset.id}", -# json={"asset_name": "新名称"} -# ) -# assert response.status_code == 401 - - -# class TestDeleteAsset: -# """测试删除资产""" -# -# def test_delete_asset_success(self, client: TestClient, auth_headers, test_asset): -# """测试删除资产成功""" -# response = client.delete( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# -# # 验证删除 -# get_response = client.get( -# f"/api/v1/assets/{test_asset.id}", -# headers=auth_headers -# ) -# assert get_response.status_code == 404 -# -# def test_delete_asset_not_found(self, client: TestClient, auth_headers): -# """测试删除不存在的资产""" -# response = client.delete( -# "/api/v1/assets/999999", -# headers=auth_headers -# ) -# assert response.status_code == 404 -# -# def test_delete_asset_in_use(self, client: TestClient, auth_headers): -# """测试删除使用中的资产""" -# # 创建使用中的资产 -# # ... 创建in_use状态的资产 -# -# response = client.delete( -# "/api/v1/assets/1", -# headers=auth_headers -# ) -# # 使用中的资产不能删除 -# assert response.status_code == 400 -# -# def test_delete_asset_without_permission(self, client: TestClient, auth_headers): -# """测试无权限删除""" -# # 使用普通用户token而非管理员 -# response = client.delete( -# "/api/v1/assets/1", -# headers=auth_headers -# ) -# assert response.status_code == 403 - - -# class TestAssetImport: -# """测试批量导入资产""" -# -# def test_import_assets_success(self, client: TestClient, auth_headers): -# """测试导入成功""" -# # 准备测试Excel文件 -# # ... 创建临时Excel文件 -# -# with open("test_import.xlsx", "rb") as f: -# files = {"file": ("test_import.xlsx", f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} -# response = client.post( -# "/api/v1/assets/import", -# headers=auth_headers, -# files=files -# ) -# -# assert response.status_code == 200 -# data = response.json() -# assert data["data"]["total"] > 0 -# assert data["data"]["success"] > 0 -# -# def test_import_assets_partial_failure(self, client: TestClient, auth_headers): -# """测试部分失败""" -# # 准备包含错误数据的Excel文件 -# -# with open("test_import_partial_fail.xlsx", "rb") as f: -# files = {"file": ("test_import.xlsx", f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} -# response = client.post( -# "/api/v1/assets/import", -# headers=auth_headers, -# files=files -# ) -# -# assert response.status_code == 200 -# data = response.json() -# assert data["data"]["failed"] > 0 -# assert len(data["data"]["errors"]) > 0 -# -# def test_import_assets_invalid_file_format(self, client: TestClient, auth_headers): -# """测试无效文件格式""" -# with open("test.txt", "rb") as f: -# files = {"file": ("test.txt", f, "text/plain")} -# response = client.post( -# "/api/v1/assets/import", -# headers=auth_headers, -# files=files -# ) -# -# assert response.status_code == 400 -# -# def test_import_assets_missing_columns(self, client: TestClient, auth_headers): -# """测试缺少必填列""" -# # 准备缺少必填列的Excel文件 -# -# with open("test_missing_columns.xlsx", "rb") as f: -# files = {"file": ("test.xlsx", f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} -# response = client.post( -# "/api/v1/assets/import", -# headers=auth_headers, -# files=files -# ) -# -# assert response.status_code == 400 - - -# class TestAssetScan: -# """测试扫码查询""" -# -# def test_scan_asset_success(self, client: TestClient, auth_headers, test_asset): -# """测试扫码查询成功""" -# response = client.get( -# f"/api/v1/assets/scan/{test_asset.asset_code}", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["data"]["asset_code"] == test_asset.asset_code -# -# def test_scan_asset_invalid_code(self, client: TestClient, auth_headers): -# """测试无效的资产编码""" -# response = client.get( -# "/api/v1/assets/scan/INVALID-CODE", -# headers=auth_headers -# ) -# assert response.status_code == 404 -# -# def test_scan_asset_without_auth(self, client: TestClient, test_asset): -# """测试未认证扫码""" -# response = client.get(f"/api/v1/assets/scan/{test_asset.asset_code}") -# assert response.status_code == 401 - - -# class TestAssetStatistics: -# """测试资产统计""" -# -# def test_get_asset_summary(self, client: TestClient, auth_headers): -# """测试获取资产汇总""" -# response = client.get( -# "/api/v1/assets", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# data = response.json() -# assert "summary" in data["data"] -# assert "total_count" in data["data"]["summary"] -# assert "total_value" in data["data"]["summary"] -# assert "status_distribution" in data["data"]["summary"] diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py deleted file mode 100644 index f099444..0000000 --- a/tests/api/test_auth.py +++ /dev/null @@ -1,356 +0,0 @@ -""" -认证模块API测试 - -测试内容: -- 用户登录 -- Token刷新 -- 用户登出 -- 修改密码 -- 验证码获取 -""" - -import pytest -# from fastapi.testclient import TestClient -# from app.core.config import settings - - -# class TestAuthLogin: -# """测试用户登录""" -# -# def test_login_success(self, client: TestClient, test_user): -# """测试登录成功""" -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "testuser", -# "password": "Test123", -# "captcha": "1234", -# "captcha_key": "test-uuid" -# } -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["code"] == 200 -# assert "access_token" in data["data"] -# assert "refresh_token" in data["data"] -# assert data["data"]["token_type"] == "Bearer" -# assert "user" in data["data"] -# -# def test_login_wrong_password(self, client: TestClient): -# """测试密码错误""" -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "testuser", -# "password": "WrongPassword", -# "captcha": "1234", -# "captcha_key": "test-uuid" -# } -# ) -# assert response.status_code == 401 -# data = response.json() -# assert data["code"] == 10001 # 用户名或密码错误 -# -# def test_login_user_not_found(self, client: TestClient): -# """测试用户不存在""" -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "nonexistent", -# "password": "Test123", -# "captcha": "1234", -# "captcha_key": "test-uuid" -# } -# ) -# assert response.status_code == 401 -# -# def test_login_missing_fields(self, client: TestClient): -# """测试缺少必填字段""" -# response = client.post( -# "/api/v1/auth/login", -# json={"username": "testuser"} -# ) -# assert response.status_code == 422 # Validation error -# -# @pytest.mark.parametrize("username", [ -# "", # 空字符串 -# "ab", # 太短 -# "a" * 51, # 太长 -# ]) -# def test_login_invalid_username(self, client: TestClient, username): -# """测试无效用户名""" -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": username, -# "password": "Test123", -# "captcha": "1234", -# "captcha_key": "test-uuid" -# } -# ) -# assert response.status_code == 422 -# -# @pytest.mark.parametrize("password", [ -# "", # 空字符串 -# "short", # 太短 -# "nospecial123", # 缺少特殊字符 -# "NOlower123!", # 缺少小写字母 -# "noupper123!", # 缺少大写字母 -# "NoNumber!!", # 缺少数字 -# ]) -# def test_login_invalid_password(self, client: TestClient, password): -# """测试无效密码""" -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "testuser", -# "password": password, -# "captcha": "1234", -# "captcha_key": "test-uuid" -# } -# ) -# # 某些情况可能是422(验证失败),某些情况可能是401(认证失败) -# assert response.status_code in [400, 422, 401] -# -# def test_login_account_locked(self, client: TestClient, db): -# """测试账户被锁定""" -# # 创建一个锁定的账户 -# # ... 创建锁定用户逻辑 -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "lockeduser", -# "password": "Test123", -# "captcha": "1234", -# "captcha_key": "test-uuid" -# } -# ) -# assert response.status_code == 403 -# -# def test_login_account_disabled(self, client: TestClient, db): -# """测试账户被禁用""" -# # ... 创建禁用用户逻辑 -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "disableduser", -# "password": "Test123", -# "captcha": "1234", -# "captcha_key": "test-uuid" -# } -# ) -# assert response.status_code == 403 - - -# class TestTokenRefresh: -# """测试Token刷新""" -# -# def test_refresh_token_success(self, client: TestClient, test_user): -# """测试刷新Token成功""" -# # 先登录获取refresh_token -# login_response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "testuser", -# "password": "Test123", -# "captcha": "1234", -# "captcha_key": "test-uuid" -# } -# ) -# refresh_token = login_response.json()["data"]["refresh_token"] -# -# # 刷新Token -# response = client.post( -# "/api/v1/auth/refresh", -# json={"refresh_token": refresh_token} -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["code"] == 200 -# assert "access_token" in data["data"] -# assert "expires_in" in data["data"] -# -# def test_refresh_token_invalid(self, client: TestClient): -# """测试无效的refresh_token""" -# response = client.post( -# "/api/v1/auth/refresh", -# json={"refresh_token": "invalid_token"} -# ) -# assert response.status_code == 401 -# data = response.json() -# assert data["code"] == 10004 # Token无效 -# -# def test_refresh_token_expired(self, client: TestClient): -# """测试过期的refresh_token""" -# response = client.post( -# "/api/v1/auth/refresh", -# json={"refresh_token": "expired_token"} -# ) -# assert response.status_code == 401 -# data = response.json() -# assert data["code"] == 10003 # Token过期 - - -# class TestAuthLogout: -# """测试用户登出""" -# -# def test_logout_success(self, client: TestClient, auth_headers): -# """测试登出成功""" -# response = client.post( -# "/api/v1/auth/logout", -# headers=auth_headers -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["code"] == 200 -# assert data["message"] == "登出成功" -# -# def test_logout_without_auth(self, client: TestClient): -# """测试未认证登出""" -# response = client.post("/api/v1/auth/logout") -# assert response.status_code == 401 - - -# class TestChangePassword: -# """测试修改密码""" -# -# def test_change_password_success(self, client: TestClient, auth_headers): -# """测试修改密码成功""" -# response = client.put( -# "/api/v1/auth/change-password", -# headers=auth_headers, -# json={ -# "old_password": "Test123", -# "new_password": "NewTest456", -# "confirm_password": "NewTest456" -# } -# ) -# assert response.status_code == 200 -# data = response.json() -# assert data["code"] == 200 -# assert data["message"] == "密码修改成功" -# -# def test_change_password_wrong_old_password(self, client: TestClient, auth_headers): -# """测试旧密码错误""" -# response = client.put( -# "/api/v1/auth/change-password", -# headers=auth_headers, -# json={ -# "old_password": "WrongPassword", -# "new_password": "NewTest456", -# "confirm_password": "NewTest456" -# } -# ) -# assert response.status_code == 400 -# -# def test_change_password_mismatch(self, client: TestClient, auth_headers): -# """测试两次密码不一致""" -# response = client.put( -# "/api/v1/auth/change-password", -# headers=auth_headers, -# json={ -# "old_password": "Test123", -# "new_password": "NewTest456", -# "confirm_password": "DifferentPass789" -# } -# ) -# assert response.status_code == 400 -# -# def test_change_password_weak_password(self, client: TestClient, auth_headers): -# """测试弱密码""" -# response = client.put( -# "/api/v1/auth/change-password", -# headers=auth_headers, -# json={ -# "old_password": "Test123", -# "new_password": "weak", -# "confirm_password": "weak" -# } -# ) -# assert response.status_code == 400 -# -# def test_change_password_without_auth(self, client: TestClient): -# """测试未认证修改密码""" -# response = client.put( -# "/api/v1/auth/change-password", -# json={ -# "old_password": "Test123", -# "new_password": "NewTest456", -# "confirm_password": "NewTest456" -# } -# ) -# assert response.status_code == 401 - - -# class TestCaptcha: -# """测试验证码""" -# -# def test_get_captcha_success(self, client: TestClient): -# """测试获取验证码成功""" -# response = client.get("/api/v1/auth/captcha") -# assert response.status_code == 200 -# data = response.json() -# assert data["code"] == 200 -# assert "captcha_key" in data["data"] -# assert "captcha_image" in data["data"] -# assert data["data"]["captcha_image"].startswith("data:image/png;base64,") -# -# @pytest.mark.parametrize("count", range(5)) -# def test_get_captcha_multiple_times(self, client: TestClient, count): -# """测试多次获取验证码,每次应该不同""" -# response = client.get("/api/v1/auth/captcha") -# assert response.status_code == 200 -# data = response.json() -# assert data["data"]["captcha_key"] is not None - - -# class TestRateLimiting: -# """测试请求频率限制""" -# -# def test_login_rate_limiting(self, client: TestClient): -# """测试登录接口频率限制""" -# # 登录接口限制10次/分钟 -# for i in range(11): -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": "testuser", -# "password": "wrongpass", -# "captcha": "1234", -# "captcha_key": f"test-{i}" -# } -# ) -# -# # 第11次应该被限流 -# assert response.status_code == 429 -# data = response.json() -# assert data["code"] == 429 -# assert "retry_after" in data["data"] - - -# 测试SQL注入攻击 -# class TestSecurity: -# """测试安全性""" -# -# def test_sql_injection_prevention(self, client: TestClient): -# """测试防止SQL注入""" -# malicious_inputs = [ -# "admin' OR '1'='1", -# "admin'--", -# "admin'/*", -# "' OR 1=1--", -# "'; DROP TABLE users--" -# ] -# -# for malicious_input in malicious_inputs: -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": malicious_input, -# "password": "Test123", -# "captcha": "1234", -# "captcha_key": "test" -# } -# ) -# # 应该返回认证失败,而不是数据库错误 -# assert response.status_code in [401, 400, 422] diff --git a/tests/api/test_device_types.py b/tests/api/test_device_types.py deleted file mode 100644 index 9253f29..0000000 --- a/tests/api/test_device_types.py +++ /dev/null @@ -1,880 +0,0 @@ -""" -设备类型管理模块API测试 - -测试内容: -- 设备类型CRUD测试(15+用例) -- 动态字段配置测试(10+用例) -- 字段验证测试(10+用例) -- 参数验证测试(10+用例) -- 异常处理测试(5+用例) -""" - -import pytest -from httpx import AsyncClient -from datetime import datetime - - -# ==================== 设备类型CRUD测试 ==================== - -class TestDeviceTypeCRUD: - """测试设备类型CRUD操作""" - - @pytest.mark.asyncio - async def test_create_device_type_success( - self, - client: AsyncClient, - admin_headers: dict, - sample_device_type_data: dict - ): - """测试创建设备类型成功""" - response = await client.post( - "/api/v1/device-types", - headers=admin_headers, - json=sample_device_type_data - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - assert data["data"]["type_code"] == sample_device_type_data["type_code"] - assert data["data"]["type_name"] == sample_device_type_data["type_name"] - assert "id" in data["data"] - - @pytest.mark.asyncio - async def test_create_device_type_duplicate_code( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试创建重复代码的设备类型""" - response = await client.post( - "/api/v1/device-types", - headers=admin_headers, - json={ - "type_code": test_device_type.type_code, - "type_name": "另一个类型" - } - ) - - assert response.status_code in [400, 409] - - @pytest.mark.asyncio - async def test_get_device_type_list( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试获取设备类型列表""" - response = await client.get( - "/api/v1/device-types", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - assert len(data["data"]) >= 1 - - @pytest.mark.asyncio - async def test_get_device_type_by_id( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试根据ID获取设备类型""" - response = await client.get( - f"/api/v1/device-types/{test_device_type.id}", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - assert data["data"]["id"] == test_device_type.id - assert data["data"]["type_code"] == test_device_type.type_code - - @pytest.mark.asyncio - async def test_get_device_type_by_code( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试根据代码获取设备类型""" - response = await client.get( - f"/api/v1/device-types/code/{test_device_type.type_code}", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - assert data["data"]["type_code"] == test_device_type.type_code - - @pytest.mark.asyncio - async def test_get_device_type_with_fields( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type_with_fields - ): - """测试获取设备类型及其字段""" - response = await client.get( - f"/api/v1/device-types/{test_device_type_with_fields.id}", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - # 验证字段存在 - # assert "fields" in data["data"] - - @pytest.mark.asyncio - async def test_update_device_type_success( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试更新设备类型成功""" - response = await client.put( - f"/api/v1/device-types/{test_device_type.id}", - headers=admin_headers, - json={ - "type_name": "更新后的类型名称", - "description": "更新后的描述" - } - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - - @pytest.mark.asyncio - async def test_update_device_type_status( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试更新设备类型状态""" - response = await client.put( - f"/api/v1/device-types/{test_device_type.id}", - headers=admin_headers, - json={"status": "inactive"} - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_delete_device_type_success( - self, - client: AsyncClient, - admin_headers: dict, - db_session, - test_device_type - ): - """测试删除设备类型成功""" - response = await client.delete( - f"/api/v1/device-types/{test_device_type.id}", - headers=admin_headers - ) - - assert response.status_code == 200 - - # 验证软删除 - get_response = await client.get( - f"/api/v1/device-types/{test_device_type.id}", - headers=admin_headers - ) - # 应该返回404或显示已删除 - assert get_response.status_code in [404, 200] - - @pytest.mark.asyncio - async def test_delete_device_type_with_assets_forbidden( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试删除有关联资产的设备类型(应该失败)""" - # 假设test_device_type有关联资产 - # 实际测试中需要先创建资产 - response = await client.delete( - f"/api/v1/device-types/{test_device_type.id}", - headers=admin_headers - ) - - # 如果有关联资产应该返回400或403 - # assert response.status_code in [400, 403] - - @pytest.mark.asyncio - async def test_filter_device_type_by_category( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试按分类筛选设备类型""" - response = await client.get( - f"/api/v1/device-types?category={test_device_type.category}", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - # 验证筛选结果 - # for item in data["data"]: - # assert item["category"] == test_device_type.category - - @pytest.mark.asyncio - async def test_filter_device_type_by_status( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type - ): - """测试按状态筛选设备类型""" - response = await client.get( - f"/api/v1/device-types?status={test_device_type.status}", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - - @pytest.mark.asyncio - async def test_get_device_type_not_found( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试获取不存在的设备类型""" - response = await client.get( - "/api/v1/device-types/999999", - headers=admin_headers - ) - - assert response.status_code == 404 - - @pytest.mark.asyncio - async def test_update_device_type_not_found( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试更新不存在的设备类型""" - response = await client.put( - "/api/v1/device-types/999999", - headers=admin_headers, - json={"type_name": "新名称"} - ) - - assert response.status_code == 404 - - @pytest.mark.asyncio - async def test_create_device_type_unauthorized( - self, - client: AsyncClient, - sample_device_type_data: dict - ): - """测试未授权创建设备类型""" - response = await client.post( - "/api/v1/device-types", - json=sample_device_type_data - ) - - assert response.status_code == 401 - - -# ==================== 动态字段配置测试 ==================== - -class TestDynamicFieldConfig: - """测试动态字段配置""" - - @pytest.mark.asyncio - async def test_add_field_to_device_type( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType, - sample_field_data: dict - ): - """测试为设备类型添加字段""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json=sample_field_data - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - assert data["data"]["field_code"] == sample_field_data["field_code"] - - @pytest.mark.asyncio - async def test_add_required_field( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试添加必填字段""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "required_field", - "field_name": "必填字段", - "field_type": "text", - "is_required": True, - "sort_order": 10 - } - ) - - assert response.status_code == 200 - data = response.json() - assert data["data"]["is_required"] is True - - @pytest.mark.asyncio - async def test_add_select_field_with_options( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试添加下拉选择字段""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "status", - "field_name": "状态", - "field_type": "select", - "is_required": True, - "options": [ - {"label": "启用", "value": "enabled"}, - {"label": "禁用", "value": "disabled"} - ], - "sort_order": 10 - } - ) - - assert response.status_code == 200 - data = response.json() - assert data["data"]["field_type"] == "select" - assert len(data["data"]["options"]) == 2 - - @pytest.mark.asyncio - async def test_add_number_field_with_validation( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试添加数字字段并设置验证规则""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "price", - "field_name": "价格", - "field_type": "number", - "is_required": False, - "validation_rules": { - "min": 0, - "max": 1000000 - }, - "sort_order": 10 - } - ) - - assert response.status_code == 200 - data = response.json() - assert data["data"]["field_type"] == "number" - assert "validation_rules" in data["data"] - - @pytest.mark.asyncio - async def test_get_device_type_fields( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type_with_fields: DeviceType - ): - """测试获取设备类型的字段列表""" - response = await client.get( - f"/api/v1/device-types/{test_device_type_with_fields.id}/fields", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - assert len(data["data"]) >= 3 # 至少3个字段 - - @pytest.mark.asyncio - async def test_update_field_success( - self, - client: AsyncClient, - admin_headers: dict, - db_session, - test_device_type_with_fields: DeviceType - ): - """测试更新字段成功""" - # 获取第一个字段 - fields_response = await client.get( - f"/api/v1/device-types/{test_device_type_with_fields.id}/fields", - headers=admin_headers - ) - field_id = fields_response.json()["data"][0]["id"] - - response = await client.put( - f"/api/v1/device-types/{test_device_type_with_fields.id}/fields/{field_id}", - headers=admin_headers, - json={ - "field_name": "更新后的字段名", - "is_required": False - } - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_delete_field_success( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type_with_fields: DeviceType - ): - """测试删除字段成功""" - fields_response = await client.get( - f"/api/v1/device-types/{test_device_type_with_fields.id}/fields", - headers=admin_headers - ) - field_id = fields_response.json()["data"][0]["id"] - - response = await client.delete( - f"/api/v1/device-types/{test_device_type_with_fields.id}/fields/{field_id}", - headers=admin_headers - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_add_duplicate_field_code( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type_with_fields: DeviceType, - sample_field_data: dict - ): - """测试添加重复的字段代码""" - # 第一次添加 - await client.post( - f"/api/v1/device-types/{test_device_type_with_fields.id}/fields", - headers=admin_headers, - json=sample_field_data - ) - - # 第二次添加相同代码 - response = await client.post( - f"/api/v1/device-types/{test_device_type_with_fields.id}/fields", - headers=admin_headers, - json=sample_field_data - ) - - assert response.status_code in [400, 409] - - @pytest.mark.asyncio - async def test_fields_sorted_by_order( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type_with_fields: DeviceType - ): - """测试字段按sort_order排序""" - response = await client.get( - f"/api/v1/device-types/{test_device_type_with_fields.id}/fields", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - fields = data["data"] - - # 验证排序 - for i in range(len(fields) - 1): - assert fields[i]["sort_order"] <= fields[i + 1]["sort_order"] - - -# ==================== 字段验证测试 ==================== - -class TestFieldValidation: - """测试字段验证""" - - @pytest.mark.asyncio - @pytest.mark.parametrize("field_code,field_name,expected_status", [ - ("", "字段名", 422), # 空字段代码 - ("a" * 51, "字段名", 422), # 字段代码过长 - ("valid_code", "", 422), # 空字段名称 - ("valid_code", "a" * 101, 422), # 字段名称过长 - ]) - async def test_field_name_validation( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType, - field_code: str, - field_name: str, - expected_status: int - ): - """测试字段名称验证""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": field_code, - "field_name": field_name, - "field_type": "text", - "sort_order": 1 - } - ) - - assert response.status_code == expected_status - - @pytest.mark.asyncio - @pytest.mark.parametrize("field_type", [ - "text", "textarea", "number", "date", "select", - "multiselect", "boolean", "email", "phone", "url" - ]) - async def test_valid_field_types( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType, - field_type: str - ): - """测试有效的字段类型""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": f"test_{field_type}", - "field_name": f"测试{field_type}", - "field_type": field_type, - "sort_order": 1 - } - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_invalid_field_type( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试无效的字段类型""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "test", - "field_name": "测试", - "field_type": "invalid_type", - "sort_order": 1 - } - ) - - assert response.status_code in [400, 422] - - @pytest.mark.asyncio - async def test_select_field_without_options( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试select类型字段缺少options""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "test_select", - "field_name": "测试选择", - "field_type": "select", - "sort_order": 1 - } - ) - - # select类型应该有options - assert response.status_code in [400, 422] - - @pytest.mark.asyncio - async def test_validation_rules_json_format( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试验证规则的JSON格式""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "test_validation", - "field_name": "测试验证", - "field_type": "text", - "validation_rules": { - "min_length": 1, - "max_length": 100, - "pattern": "^[A-Za-z0-9]+$" - }, - "sort_order": 1 - } - ) - - assert response.status_code == 200 - data = response.json() - assert "validation_rules" in data["data"] - - @pytest.mark.asyncio - async def test_placeholder_and_help_text( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试placeholder和help_text""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "test_help", - "field_name": "测试帮助", - "field_type": "text", - "placeholder": "请输入...", - "help_text": "这是帮助文本", - "sort_order": 1 - } - ) - - assert response.status_code == 200 - data = response.json() - assert data["data"]["placeholder"] == "请输入..." - assert data["data"]["help_text"] == "这是帮助文本" - - -# ==================== 参数验证测试 ==================== - -class TestDeviceTypeParameterValidation: - """测试设备类型参数验证""" - - @pytest.mark.asyncio - @pytest.mark.parametrize("type_code,expected_status", [ - ("", 422), # 空代码 - ("AB", 422), # 太短 - ("a" * 51, 422), # 太长 - ("VALID_CODE", 200), # 有效 - ]) - async def test_type_code_validation( - self, - client: AsyncClient, - admin_headers: dict, - type_code: str, - expected_status: int - ): - """测试类型代码验证""" - response = await client.post( - "/api/v1/device-types", - headers=admin_headers, - json={ - "type_code": type_code, - "type_name": "测试类型", - "category": "IT设备" - } - ) - - assert response.status_code == expected_status - - @pytest.mark.asyncio - @pytest.mark.parametrize("type_name,expected_status", [ - ("", 422), # 空名称 - ("a" * 201, 422), # 太长 - ("有效名称", 200), # 有效 - ]) - async def test_type_name_validation( - self, - client: AsyncClient, - admin_headers: dict, - type_name: str, - expected_status: int - ): - """测试类型名称验证""" - response = await client.post( - "/api/v1/device-types", - headers=admin_headers, - json={ - "type_code": "TEST_CODE", - "type_name": type_name - } - ) - - assert response.status_code == expected_status - - @pytest.mark.asyncio - async def test_sort_order_validation( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试排序验证""" - response = await client.post( - "/api/v1/device-types", - headers=admin_headers, - json={ - "type_code": "TEST_SORT", - "type_name": "测试排序", - "sort_order": -1 # 负数 - } - ) - - # 排序可以是负数,或者应该返回422 - # assert response.status_code in [200, 422] - - @pytest.mark.asyncio - @pytest.mark.parametrize("status", [ - "active", "inactive", "invalid_status" - ]) - async def test_status_validation( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType, - status: str - ): - """测试状态验证""" - response = await client.put( - f"/api/v1/device-types/{test_device_type.id}", - headers=admin_headers, - json={"status": status} - ) - - # 有效状态应该是200,无效状态应该是422 - if status in ["active", "inactive"]: - assert response.status_code == 200 - else: - assert response.status_code in [400, 422] - - -# ==================== 异常处理测试 ==================== - -class TestDeviceTypeExceptionHandling: - """测试异常处理""" - - @pytest.mark.asyncio - async def test_concurrent_device_type_creation( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试并发创建相同代码的设备类型""" - import asyncio - - data = { - "type_code": "CONCURRENT_TEST", - "type_name": "并发测试" - } - - # 并发创建 - tasks = [ - client.post("/api/v1/device-types", headers=admin_headers, json=data) - for _ in range(2) - ] - responses = await asyncio.gather(*tasks) - - # 应该只有一个成功,另一个失败 - success_count = sum(1 for r in responses if r.status_code == 200) - assert success_count == 1 - - @pytest.mark.asyncio - async def test_update_non_existent_field( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试更新不存在的字段""" - response = await client.put( - f"/api/v1/device-types/{test_device_type.id}/fields/999999", - headers=admin_headers, - json={"field_name": "更新"} - ) - - assert response.status_code == 404 - - @pytest.mark.asyncio - async def test_delete_non_existent_device_type( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试删除不存在的设备类型""" - response = await client.delete( - "/api/v1/device-types/999999", - headers=admin_headers - ) - - assert response.status_code == 404 - - @pytest.mark.asyncio - async def test_field_with_invalid_json_validation( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试字段包含无效的JSON验证规则""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "test", - "field_name": "测试", - "field_type": "text", - "validation_rules": "invalid json string", # 应该是对象 - "sort_order": 1 - } - ) - - # 应该返回验证错误 - assert response.status_code in [400, 422] - - @pytest.mark.asyncio - async def test_field_with_invalid_options_format( - self, - client: AsyncClient, - admin_headers: dict, - test_device_type: DeviceType - ): - """测试select字段包含无效的options格式""" - response = await client.post( - f"/api/v1/device-types/{test_device_type.id}/fields", - headers=admin_headers, - json={ - "field_code": "test", - "field_name": "测试", - "field_type": "select", - "options": "invalid options", # 应该是数组 - "sort_order": 1 - } - ) - - assert response.status_code in [400, 422] diff --git a/tests/api/test_maintenance.py b/tests/api/test_maintenance.py deleted file mode 100644 index 54a8f98..0000000 --- a/tests/api/test_maintenance.py +++ /dev/null @@ -1,891 +0,0 @@ -""" -维修管理 API 测试 - -测试范围: -- 维修记录CRUD测试 (20+用例) -- 维修状态管理测试 (15+用例) -- 维修费用测试 (10+用例) -- 维修历史测试 (5+用例) - -总计: 50+ 用例 -""" - -import pytest -from datetime import datetime, timedelta -from typing import List -from decimal import Decimal -from sqlalchemy.orm import Session - -from app.models.maintenance import Maintenance, MaintenancePart -from app.models.asset import Asset -from app.schemas.maintenance import ( - MaintenanceCreate, - MaintenanceStatus, - MaintenanceType, - MaintenancePriority -) - - -# ================================ -# Fixtures -# ================================ - -@pytest.fixture -def test_assets_for_maintenance(db: Session) -> List[Asset]: - """创建需要维修的测试资产""" - assets = [] - for i in range(3): - asset = Asset( - asset_code=f"TEST-MAINT-{i+1:03d}", - asset_name=f"测试维修资产{i+1}", - device_type_id=1, - organization_id=1, - status="maintenance", - purchase_date=datetime.now() - timedelta(days=365) - ) - db.add(asset) - assets.append(asset) - db.commit() - for asset in assets: - db.refresh(asset) - return assets - - -@pytest.fixture -def test_maintenance_record(db: Session, test_assets_for_maintenance: List[Asset]) -> Maintenance: - """创建测试维修记录""" - maintenance = Maintenance( - maintenance_no="MAINT-2025-001", - asset_id=test_assets_for_maintenance[0].id, - maintenance_type=MaintenanceType.PREVENTIVE, - priority=MaintenancePriority.MEDIUM, - status=MaintenanceStatus.PENDING, - fault_description="设备异常噪音", - reported_by=1, - reported_time=datetime.now(), - estimated_cost=Decimal("500.00"), - estimated_start_time=datetime.now() + timedelta(days=1), - estimated_completion_time=datetime.now() + timedelta(days=3) - ) - db.add(maintenance) - db.commit() - db.refresh(maintenance) - return maintenance - - -@pytest.fixture -def test_maintenance_with_parts(db: Session, test_assets_for_maintenance: List[Asset]) -> Maintenance: - """创建包含配件的维修记录""" - maintenance = Maintenance( - maintenance_no="MAINT-2025-002", - asset_id=test_assets_for_maintenance[1].id, - maintenance_type=MaintenanceType.CORRECTIVE, - priority=MaintenancePriority.HIGH, - status=MaintenanceStatus.IN_PROGRESS, - fault_description="设备故障无法启动", - reported_by=1, - reported_time=datetime.now(), - actual_start_time=datetime.now(), - estimated_cost=Decimal("1500.00") - ) - db.add(maintenance) - db.commit() - db.refresh(maintenance) - - # 添加维修配件 - parts = [ - MaintenancePart( - maintenance_id=maintenance.id, - part_name="电机", - part_code="PART-001", - quantity=1, - unit_price=Decimal("800.00") - ), - MaintenancePart( - maintenance_id=maintenance.id, - part_name="轴承", - part_code="PART-002", - quantity=2, - unit_price=Decimal("100.00") - ) - ] - for part in parts: - db.add(part) - db.commit() - - return maintenance - - -# ================================ -# 维修记录CRUD测试 (20+用例) -# ================================ - -class TestMaintenanceCRUD: - """维修记录CRUD操作测试""" - - def test_create_maintenance_with_valid_data(self, client, auth_headers, test_assets_for_maintenance): - """测试使用有效数据创建维修记录""" - asset = test_assets_for_maintenance[0] - - response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": asset.id, - "maintenance_type": "corrective", - "priority": "high", - "fault_description": "设备故障需要维修", - "reported_by": 1, - "estimated_cost": 1000.00, - "estimated_start_time": (datetime.now() + timedelta(hours=2)).isoformat(), - "estimated_completion_time": (datetime.now() + timedelta(days=2)).isoformat() - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["maintenance_no"] is not None - assert data["status"] == MaintenanceStatus.PENDING - assert data["asset_id"] == asset.id - - def test_create_maintenance_with_invalid_asset_id(self, client, auth_headers): - """测试使用无效资产ID创建维修记录应失败""" - response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": 999999, - "maintenance_type": "corrective", - "priority": "medium", - "fault_description": "测试", - "reported_by": 1 - }, - headers=auth_headers - ) - assert response.status_code == 404 - assert "资产不存在" in response.json()["detail"] - - def test_create_maintenance_without_fault_description(self, client, auth_headers, test_assets_for_maintenance): - """测试创建维修记录时未提供故障描述应失败""" - asset = test_assets_for_maintenance[0] - - response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": asset.id, - "maintenance_type": "corrective", - "priority": "medium", - "reported_by": 1 - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "故障描述" in response.json()["detail"] - - def test_create_maintenance_with_negative_cost(self, client, auth_headers, test_assets_for_maintenance): - """测试创建负费用的维修记录应失败""" - asset = test_assets_for_maintenance[0] - - response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": asset.id, - "maintenance_type": "corrective", - "priority": "medium", - "fault_description": "测试", - "reported_by": 1, - "estimated_cost": -100.00 - }, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_create_maintenance_auto_updates_asset_status(self, client, auth_headers, db: Session, test_assets_for_maintenance): - """测试创建维修记录时自动更新资产状态""" - asset = test_assets_for_maintenance[0] - original_status = asset.status - - response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": asset.id, - "maintenance_type": "corrective", - "priority": "medium", - "fault_description": "测试自动更新状态", - "reported_by": 1 - }, - headers=auth_headers - ) - assert response.status_code == 200 - - # 验证资产状态已更新 - db.refresh(asset) - assert asset.status == "maintenance" - - def test_get_maintenance_list_with_pagination(self, client, auth_headers, test_maintenance_record): - """测试分页获取维修记录列表""" - response = client.get( - "/api/v1/maintenance/?page=1&page_size=10", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "items" in data - assert "total" in data - assert len(data["items"]) >= 1 - - def test_get_maintenance_list_with_status_filter(self, client, auth_headers, test_maintenance_record): - """测试按状态筛选维修记录""" - response = client.get( - f"/api/v1/maintenance/?status={MaintenanceStatus.PENDING}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - for item in data["items"]: - assert item["status"] == MaintenanceStatus.PENDING - - def test_get_maintenance_list_with_asset_filter(self, client, auth_headers, test_maintenance_record): - """测试按资产筛选维修记录""" - response = client.get( - f"/api/v1/maintenance/?asset_id={test_maintenance_record.asset_id}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data["items"]) >= 1 - - def test_get_maintenance_list_with_type_filter(self, client, auth_headers, test_maintenance_record): - """测试按维修类型筛选""" - response = client.get( - f"/api/v1/maintenance/?maintenance_type={test_maintenance_record.maintenance_type}", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_maintenance_list_with_priority_filter(self, client, auth_headers, test_maintenance_record): - """测试按优先级筛选""" - response = client.get( - f"/api/v1/maintenance/?priority={test_maintenance_record.priority}", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_maintenance_list_with_date_range(self, client, auth_headers, test_maintenance_record): - """测试按日期范围筛选""" - start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") - end_date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") - - response = client.get( - f"/api/v1/maintenance/?start_date={start_date}&end_date={end_date}", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_maintenance_by_id(self, client, auth_headers, test_maintenance_record): - """测试通过ID获取维修记录详情""" - response = client.get( - f"/api/v1/maintenance/{test_maintenance_record.id}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["id"] == test_maintenance_record.id - assert data["maintenance_no"] == test_maintenance_record.maintenance_no - assert "asset" in data - - def test_get_maintenance_by_invalid_id(self, client, auth_headers): - """测试通过无效ID获取维修记录应返回404""" - response = client.get( - "/api/v1/maintenance/999999", - headers=auth_headers - ) - assert response.status_code == 404 - - def test_update_maintenance_fault_description(self, client, auth_headers, test_maintenance_record): - """测试更新故障描述""" - response = client.put( - f"/api/v1/maintenance/{test_maintenance_record.id}", - json={"fault_description": "更新后的故障描述"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["fault_description"] == "更新后的故障描述" - - def test_update_maintenance_priority(self, client, auth_headers, test_maintenance_record): - """测试更新优先级""" - response = client.put( - f"/api/v1/maintenance/{test_maintenance_record.id}", - json={"priority": "urgent"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["priority"] == MaintenancePriority.URGENT - - def test_update_maintenance_after_start_should_fail(self, client, auth_headers, db: Session, test_maintenance_record): - """测试维修开始后更新某些字段应失败""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - db.commit() - - response = client.put( - f"/api/v1/maintenance/{test_maintenance_record.id}", - json={"maintenance_type": "preventive"}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "不允许修改" in response.json()["detail"] - - def test_delete_pending_maintenance(self, client, auth_headers, db: Session, test_assets_for_maintenance): - """测试删除待处理的维修记录""" - maintenance = Maintenance( - maintenance_no="MAINT-DEL-001", - asset_id=test_assets_for_maintenance[0].id, - maintenance_type=MaintenanceType.CORRECTIVE, - priority=MaintenancePriority.MEDIUM, - status=MaintenanceStatus.PENDING, - fault_description="待删除", - reported_by=1 - ) - db.add(maintenance) - db.commit() - db.refresh(maintenance) - - response = client.delete( - f"/api/v1/maintenance/{maintenance.id}", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_delete_in_progress_maintenance_should_fail(self, client, auth_headers, db: Session, test_maintenance_record): - """测试删除进行中的维修记录应失败""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - db.commit() - - response = client.delete( - f"/api/v1/maintenance/{test_maintenance_record.id}", - headers=auth_headers - ) - assert response.status_code == 400 - assert "不允许删除" in response.json()["detail"] - - def test_create_maintenance_with_parts(self, client, auth_headers, test_assets_for_maintenance): - """测试创建包含配件的维修记录""" - asset = test_assets_for_maintenance[0] - - response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": asset.id, - "maintenance_type": "corrective", - "priority": "high", - "fault_description": "需要更换配件", - "reported_by": 1, - "parts": [ - { - "part_name": "电机", - "part_code": "PART-001", - "quantity": 1, - "unit_price": 800.00 - }, - { - "part_name": "轴承", - "part_code": "PART-002", - "quantity": 2, - "unit_price": 100.00 - } - ] - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "parts" in data - assert len(data["parts"]) == 2 - - -# ================================ -# 维修状态管理测试 (15+用例) -# ================================ - -class TestMaintenanceStatusManagement: - """维修状态管理测试""" - - def test_start_maintenance(self, client, auth_headers, test_maintenance_record): - """测试开始维修""" - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/start", - json={"start_note": "开始维修", "technician_id": 2}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == MaintenanceStatus.IN_PROGRESS - assert data["actual_start_time"] is not None - - def test_start_maintenance_updates_asset_status(self, client, auth_headers, test_maintenance_record, db: Session): - """测试开始维修时更新资产状态""" - client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/start", - json={"start_note": "开始维修"}, - headers=auth_headers - ) - - asset = db.query(Asset).filter(Asset.id == test_maintenance_record.asset_id).first() - assert asset.status == "maintenance" - - def test_pause_maintenance(self, client, auth_headers, db: Session, test_maintenance_record): - """测试暂停维修""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - db.commit() - - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/pause", - json={"pause_reason": "等待配件"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == MaintenanceStatus.PAUSED - - def test_resume_maintenance(self, client, auth_headers, db: Session, test_maintenance_record): - """测试恢复维修""" - test_maintenance_record.status = MaintenanceStatus.PAUSED - db.commit() - - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/resume", - json={"resume_note": "配件已到,继续维修"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == MaintenanceStatus.IN_PROGRESS - - def test_complete_maintenance(self, client, auth_headers, db: Session, test_maintenance_record): - """测试完成维修""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - db.commit() - - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/complete", - json={ - "completion_note": "维修完成", - "actual_cost": 1200.00, - "technician_id": 2 - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == MaintenanceStatus.COMPLETED - assert data["actual_completion_time"] is not None - assert data["actual_cost"] == 1200.00 - - def test_complete_maintenance_updates_asset_status(self, client, auth_headers, db: Session, test_maintenance_record): - """测试完成维修后恢复资产状态""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - db.commit() - - client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/complete", - json={"completion_note": "完成", "actual_cost": 1000.00}, - headers=auth_headers - ) - - asset = db.query(Asset).filter(Asset.id == test_maintenance_record.asset_id).first() - assert asset.status == "in_stock" - - def test_cancel_maintenance(self, client, auth_headers, test_maintenance_record): - """测试取消维修""" - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/cancel", - json={"cancellation_reason": "资产报废"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == MaintenanceStatus.CANCELLED - - def test_cancel_maintenance_updates_asset_status(self, client, auth_headers, db: Session, test_maintenance_record): - """测试取消维修后恢复资产状态""" - client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/cancel", - json={"cancellation_reason": "取消维修"}, - headers=auth_headers - ) - - asset = db.query(Asset).filter(Asset.id == test_maintenance_record.asset_id).first() - assert asset.status == "in_stock" - - def test_assign_technician(self, client, auth_headers, test_maintenance_record): - """测试分配维修人员""" - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/assign-technician", - json={"technician_id": 2, "assignment_note": "指派张工负责"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["technician_id"] == 2 - - def test_add_maintenance_progress_note(self, client, auth_headers, db: Session, test_maintenance_record): - """测试添加维修进度备注""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - db.commit() - - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/progress-notes", - json={"note": "已更换故障配件", "progress_percentage": 50}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_maintenance_progress_notes(self, client, auth_headers, test_maintenance_record): - """测试获取维修进度备注""" - response = client.get( - f"/api/v1/maintenance/{test_maintenance_record.id}/progress-notes", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_update_maintenance_progress(self, client, auth_headers, db: Session, test_maintenance_record): - """测试更新维修进度""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - db.commit() - - response = client.put( - f"/api/v1/maintenance/{test_maintenance_record.id}/progress", - json={"progress_percentage": 75}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["progress_percentage"] == 75 - - def test_invalid_status_transition(self, client, auth_headers, db: Session, test_maintenance_record): - """测试无效的状态转换""" - test_maintenance_record.status = MaintenanceStatus.COMPLETED - db.commit() - - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/start", - json={"start_note": "尝试重新开始"}, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_get_maintenance_status_history(self, client, auth_headers, test_maintenance_record): - """测试获取状态变更历史""" - response = client.get( - f"/api/v1/maintenance/{test_maintenance_record.id}/status-history", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_auto_calculate_duration(self, client, auth_headers, db: Session, test_maintenance_record): - """测试自动计算维修时长""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - test_maintenance_record.actual_start_time = datetime.now() - timedelta(days=2) - db.commit() - - client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/complete", - json={"completion_note": "完成", "actual_cost": 1000.00}, - headers=auth_headers - ) - - db.refresh(test_maintenance_record) - assert test_maintenance_record.duration_hours is not None - - -# ================================ -# 维修费用测试 (10+用例) -# ================================ - -class TestMaintenanceCost: - """维修费用测试""" - - def test_record_initial_cost_estimate(self, client, auth_headers, test_assets_for_maintenance): - """测试记录初始费用估算""" - asset = test_assets_for_maintenance[0] - - response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": asset.id, - "maintenance_type": "corrective", - "priority": "medium", - "fault_description": "测试费用估算", - "reported_by": 1, - "estimated_cost": 2000.00 - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["estimated_cost"] == 2000.00 - - def test_update_cost_estimate(self, client, auth_headers, test_maintenance_record): - """测试更新费用估算""" - response = client.put( - f"/api/v1/maintenance/{test_maintenance_record.id}", - json={"estimated_cost": 800.00}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["estimated_cost"] == 800.00 - - def test_record_actual_cost(self, client, auth_headers, db: Session, test_maintenance_record): - """测试记录实际费用""" - test_maintenance_record.status = MaintenanceStatus.IN_PROGRESS - db.commit() - - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/record-cost", - json={"actual_cost": 1500.00, "cost_breakdown": {"parts": 1000.00, "labor": 500.00}}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["actual_cost"] == 1500.00 - - def test_calculate_total_parts_cost(self, client, auth_headers, test_maintenance_with_parts): - """测试计算配件总费用""" - response = client.get( - f"/api/v1/maintenance/{test_maintenance_with_parts.id}/parts-cost", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["total_parts_cost"] == 1000.00 # 800 + 100*2 - - def test_add_maintenance_part(self, client, auth_headers, test_maintenance_record): - """测试添加维修配件""" - response = client.post( - f"/api/v1/maintenance/{test_maintenance_record.id}/parts", - json={ - "part_name": "传感器", - "part_code": "PART-003", - "quantity": 1, - "unit_price": 300.00 - }, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_update_maintenance_part(self, client, auth_headers, test_maintenance_with_parts): - """测试更新维修配件""" - part = test_maintenance_with_parts.parts[0] - - response = client.put( - f"/api/v1/maintenance/{test_maintenance_with_parts.id}/parts/{part.id}", - json={"quantity": 2, "unit_price": 750.00}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_delete_maintenance_part(self, client, auth_headers, test_maintenance_with_parts): - """测试删除维修配件""" - part = test_maintenance_with_parts.parts[0] - - response = client.delete( - f"/api/v1/maintenance/{test_maintenance_with_parts.id}/parts/{part.id}", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_maintenance_parts_list(self, client, auth_headers, test_maintenance_with_parts): - """测试获取维修配件列表""" - response = client.get( - f"/api/v1/maintenance/{test_maintenance_with_parts.id}/parts", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data) == 2 - - def test_cost_variance_analysis(self, client, auth_headers, db: Session, test_maintenance_record): - """测试费用差异分析""" - test_maintenance_record.estimated_cost = Decimal("1000.00") - test_maintenance_record.actual_cost = Decimal("1200.00") - db.commit() - - response = client.get( - f"/api/v1/maintenance/{test_maintenance_record.id}/cost-analysis", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "variance" in data - assert "variance_percentage" in data - - def test_get_cost_statistics_by_asset(self, client, auth_headers, test_assets_for_maintenance): - """测试获取资产维修费用统计""" - asset = test_assets_for_maintenance[0] - - response = client.get( - f"/api/v1/maintenance/asset/{asset.id}/cost-statistics", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_cost" in data - assert "maintenance_count" in data - - -# ================================ -# 维修历史测试 (5+用例) -# ================================ - -class TestMaintenanceHistory: - """维修历史测试""" - - def test_get_asset_maintenance_history(self, client, auth_headers, test_maintenance_record): - """测试获取资产维修历史""" - response = client.get( - f"/api/v1/maintenance/asset/{test_maintenance_record.asset_id}/history", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) >= 1 - - def test_get_maintenance_history_with_date_range(self, client, auth_headers, test_maintenance_record): - """测试按日期范围获取维修历史""" - start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") - end_date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") - - response = client.get( - f"/api/v1/maintenance/asset/{test_maintenance_record.asset_id}/history?start_date={start_date}&end_date={end_date}", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_maintenance_frequency_analysis(self, client, auth_headers, test_assets_for_maintenance): - """测试获取维修频率分析""" - asset = test_assets_for_maintenance[0] - - response = client.get( - f"/api/v1/maintenance/asset/{asset.id}/frequency-analysis", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_maintenance_count" in data - assert "average_days_between_maintenance" in data - - def test_export_maintenance_history(self, client, auth_headers, test_maintenance_record): - """测试导出维修历史""" - response = client.get( - f"/api/v1/maintenance/asset/{test_maintenance_record.asset_id}/export", - headers=auth_headers - ) - assert response.status_code == 200 - assert "export_url" in response.json() - - def test_get_maintenance_summary_report(self, client, auth_headers): - """测试获取维修汇总报告""" - response = client.get( - "/api/v1/maintenance/summary-report", - headers=auth_headers, - params={"start_date": "2025-01-01", "end_date": "2025-12-31"} - ) - assert response.status_code == 200 - data = response.json() - assert "total_maintenance_count" in data - assert "total_cost" in data - assert "by_type" in data - - -# ================================ -# 测试标记 -# ================================ - -@pytest.mark.unit -class TestMaintenanceUnit: - """单元测试标记""" - - def test_maintenance_number_generation(self): - """测试维修单号生成逻辑""" - pass - - def test_maintenance_type_validation(self): - """测试维修类型验证""" - pass - - -@pytest.mark.integration -class TestMaintenanceIntegration: - """集成测试标记""" - - def test_full_maintenance_workflow(self, client, auth_headers, test_assets_for_maintenance): - """测试完整维修流程""" - asset = test_assets_for_maintenance[0] - - # 1. 创建维修记录 - create_response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": asset.id, - "maintenance_type": "corrective", - "priority": "high", - "fault_description": "完整流程测试", - "reported_by": 1, - "estimated_cost": 1000.00 - }, - headers=auth_headers - ) - assert create_response.status_code == 200 - maintenance_id = create_response.json()["id"] - - # 2. 开始维修 - start_response = client.post( - f"/api/v1/maintenance/{maintenance_id}/start", - json={"start_note": "开始"}, - headers=auth_headers - ) - assert start_response.status_code == 200 - - # 3. 完成维修 - complete_response = client.post( - f"/api/v1/maintenance/{maintenance_id}/complete", - json={"completion_note": "完成", "actual_cost": 1200.00}, - headers=auth_headers - ) - assert complete_response.status_code == 200 - - -@pytest.mark.smoke -class TestMaintenanceSmoke: - """冒烟测试标记""" - - def test_create_and_start_maintenance(self, client, auth_headers, test_assets_for_maintenance): - """冒烟测试: 创建并开始维修""" - asset = test_assets_for_maintenance[0] - - create_response = client.post( - "/api/v1/maintenance/", - json={ - "asset_id": asset.id, - "maintenance_type": "corrective", - "priority": "medium", - "fault_description": "冒烟测试", - "reported_by": 1 - }, - headers=auth_headers - ) - assert create_response.status_code == 200 - - maintenance_id = create_response.json()["id"] - start_response = client.post( - f"/api/v1/maintenance/{maintenance_id}/start", - json={"start_note": "冒烟测试开始"}, - headers=auth_headers - ) - assert start_response.status_code == 200 diff --git a/tests/api/test_organizations.py b/tests/api/test_organizations.py deleted file mode 100644 index e34b1aa..0000000 --- a/tests/api/test_organizations.py +++ /dev/null @@ -1,1547 +0,0 @@ -""" -机构网点管理模块API测试 - -测试内容: -- 机构CRUD测试(15+用例) -- 树形结构测试(10+用例) -- 递归查询测试(10+用例) -- 机构移动测试(5+用例) -- 并发测试(5+用例) -""" - -import pytest -from httpx import AsyncClient -from datetime import datetime - - -# ==================== 机构CRUD测试 ==================== - -class TestOrganizationCRUD: - """测试机构CRUD操作""" - - @pytest.mark.asyncio - async def test_create_organization_success( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试创建机构成功""" - response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "ORG001", - "org_name": "广东省", - "org_type": "province", - "description": "广东省分公司" - } - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - assert data["data"]["org_code"] == "ORG001" - assert data["data"]["org_name"] == "广东省" - assert "id" in data["data"] - - @pytest.mark.asyncio - async def test_create_child_organization( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试创建子机构""" - # 先创建父机构 - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "PARENT_ORG", - "org_name": "父机构", - "org_type": "province" - } - ) - parent_id = parent_response.json()["data"]["id"] - - # 创建子机构 - response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "CHILD_ORG", - "org_name": "子机构", - "org_type": "city", - "parent_id": parent_id - } - ) - - assert response.status_code == 200 - data = response.json() - assert data["data"]["parent_id"] == parent_id - - @pytest.mark.asyncio - async def test_create_organization_duplicate_code( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试创建重复代码的机构""" - data = { - "org_code": "DUP_ORG", - "org_name": "测试机构" - } - - # 第一次创建 - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json=data - ) - - # 第二次创建相同代码 - response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json=data - ) - - assert response.status_code in [400, 409] - - @pytest.mark.asyncio - async def test_get_organization_list( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试获取机构列表""" - # 先创建几个机构 - for i in range(3): - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": f"ORG{i}", - "org_name": f"机构{i}", - "org_type": "province" - } - ) - - response = await client.get( - "/api/v1/organizations", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - assert len(data["data"]) >= 3 - - @pytest.mark.asyncio - async def test_get_organization_by_id( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试根据ID获取机构""" - # 创建机构 - create_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "GET_ORG", - "org_name": "获取测试机构", - "org_type": "province" - } - ) - org_id = create_response.json()["data"]["id"] - - # 获取机构 - response = await client.get( - f"/api/v1/organizations/{org_id}", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["data"]["id"] == org_id - assert data["data"]["org_code"] == "GET_ORG" - - @pytest.mark.asyncio - async def test_update_organization_success( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试更新机构成功""" - # 创建机构 - create_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "UPDATE_ORG", - "org_name": "原始名称", - "org_type": "province" - } - ) - org_id = create_response.json()["data"]["id"] - - # 更新机构 - response = await client.put( - f"/api/v1/organizations/{org_id}", - headers=admin_headers, - json={ - "org_name": "更新后的名称", - "description": "更新后的描述" - } - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - - @pytest.mark.asyncio - async def test_update_organization_parent( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试更新机构的父机构""" - # 创建两个机构 - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "NEW_PARENT", - "org_name": "新父机构", - "org_type": "province" - } - ) - parent_id = parent_response.json()["data"]["id"] - - child_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "CHILD", - "org_name": "子机构", - "org_type": "city" - } - ) - child_id = child_response.json()["data"]["id"] - - # 更新子机构的父机构 - response = await client.put( - f"/api/v1/organizations/{child_id}", - headers=admin_headers, - json={"parent_id": parent_id} - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_delete_organization_success( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试删除机构成功""" - # 创建机构 - create_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "DELETE_ORG", - "org_name": "待删除机构", - "org_type": "province" - } - ) - org_id = create_response.json()["data"]["id"] - - # 删除机构 - response = await client.delete( - f"/api/v1/organizations/{org_id}", - headers=admin_headers - ) - - assert response.status_code == 200 - - # 验证删除 - get_response = await client.get( - f"/api/v1/organizations/{org_id}", - headers=admin_headers - ) - assert get_response.status_code in [404, 200] # 软删除可能返回200 - - @pytest.mark.asyncio - async def test_delete_organization_with_children_forbidden( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试删除有子机构的机构(应该失败)""" - # 创建父机构 - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "PARENT_WITH_CHILD", - "org_name": "父机构", - "org_type": "province" - } - ) - parent_id = parent_response.json()["data"]["id"] - - # 创建子机构 - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "CHILD_ORG", - "org_name": "子机构", - "org_type": "city", - "parent_id": parent_id - } - ) - - # 尝试删除父机构 - response = await client.delete( - f"/api/v1/organizations/{parent_id}", - headers=admin_headers - ) - - # 应该失败或返回错误 - assert response.status_code in [400, 403] - - @pytest.mark.asyncio - async def test_filter_organization_by_type( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试按类型筛选机构""" - # 创建不同类型的机构 - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={"org_code": "PROV1", "org_name": "省级", "org_type": "province"} - ) - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={"org_code": "CITY1", "org_name": "市级", "org_type": "city"} - ) - - # 筛选省级机构 - response = await client.get( - "/api/v1/organizations?org_type=province", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - # 验证筛选结果 - # for org in data["data"]: - # assert org["org_type"] == "province" - - @pytest.mark.asyncio - async def test_filter_organization_by_status( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试按状态筛选机构""" - response = await client.get( - "/api/v1/organizations?status=active", - headers=admin_headers - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_search_organization_by_keyword( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试关键词搜索机构""" - # 创建机构 - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "SEARCH_ORG", - "org_name": "搜索测试机构", - "org_type": "province" - } - ) - - # 搜索 - response = await client.get( - "/api/v1/organizations?keyword=搜索", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - # 验证搜索结果包含关键词 - - @pytest.mark.asyncio - async def test_get_organization_not_found( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试获取不存在的机构""" - response = await client.get( - "/api/v1/organizations/999999", - headers=admin_headers - ) - - assert response.status_code == 404 - - @pytest.mark.asyncio - async def test_update_organization_not_found( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试更新不存在的机构""" - response = await client.put( - "/api/v1/organizations/999999", - headers=admin_headers, - json={"org_name": "新名称"} - ) - - assert response.status_code == 404 - - @pytest.mark.asyncio - async def test_create_organization_unauthorized( - self, - client: AsyncClient - ): - """测试未授权创建机构""" - response = await client.post( - "/api/v1/organizations", - json={ - "org_code": "NO_AUTH", - "org_name": "未授权" - } - ) - - assert response.status_code == 401 - - -# ==================== 树形结构测试 ==================== - -class TestOrganizationTree: - """测试机构树形结构""" - - @pytest.mark.asyncio - async def test_get_organization_tree( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试获取机构树""" - # 创建三级机构 - province_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "TREE_PROV", - "org_name": "广东省", - "org_type": "province" - } - ) - province_id = province_response.json()["data"]["id"] - - city_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "TREE_CITY", - "org_name": "广州市", - "org_type": "city", - "parent_id": province_id - } - ) - city_id = city_response.json()["data"]["id"] - - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "TREE_OUTLET", - "org_name": "天河网点", - "org_type": "outlet", - "parent_id": city_id - } - ) - - # 获取树形结构 - response = await client.get( - "/api/v1/organizations/tree", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - assert data["code"] == 200 - # 验证树形结构 - # assert len(data["data"]) > 0 - # assert "children" in data["data"][0] - - @pytest.mark.asyncio - async def test_tree_root_level( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试树的根层级""" - # 创建顶级机构 - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "ROOT_ORG", - "org_name": "根机构", - "org_type": "province" - } - ) - - response = await client.get( - "/api/v1/organizations/tree", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - # 根级机构的parent_id应该为null - # for org in data["data"]: - # assert org["parent_id"] is None - - @pytest.mark.asyncio - async def test_tree_max_levels( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试树的最大层级""" - # 创建多层级机构 - parent_id = None - for i in range(5): - response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": f"LEVEL{i}", - "org_name": f"第{i}层", - "org_type": "outlet", - "parent_id": parent_id - } - ) - parent_id = response.json()["data"]["id"] - - response = await client.get( - "/api/v1/organizations/tree", - headers=admin_headers - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_tree_multiple_branches( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试多分支树结构""" - # 创建根机构 - root_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "MULTI_ROOT", - "org_name": "多分支根", - "org_type": "province" - } - ) - root_id = root_response.json()["data"]["id"] - - # 创建多个子分支 - for i in range(3): - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": f"BRANCH{i}", - "org_name": f"分支{i}", - "org_type": "city", - "parent_id": root_id - } - ) - - response = await client.get( - "/api/v1/organizations/tree", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - # 验证有多个子节点 - # assert len(data["data"][0]["children"]) == 3 - - @pytest.mark.asyncio - async def test_tree_leaf_nodes( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试树的叶子节点""" - # 创建有子节点和无子节点的机构 - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "PARENT_NODE", - "org_name": "父节点", - "org_type": "province" - } - ) - parent_id = parent_response.json()["data"]["id"] - - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "CHILD_NODE", - "org_name": "子节点", - "org_type": "city", - "parent_id": parent_id - } - ) - - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "LEAF_NODE", - "org_name": "叶子节点", - "org_type": "city" - } - ) - - response = await client.get( - "/api/v1/organizations/tree", - headers=admin_headers - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_tree_with_inactive_orgs( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试包含已删除机构的树""" - # 创建机构 - response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "INACTIVE_ORG", - "org_name": "未激活机构", - "org_type": "province" - } - ) - org_id = response.json()["data"]["id"] - - # 停用机构 - await client.put( - f"/api/v1/organizations/{org_id}", - headers=admin_headers, - json={"status": "inactive"} - ) - - response = await client.get( - "/api/v1/organizations/tree", - headers=admin_headers - ) - - assert response.status_code == 200 - # 验证树中是否包含未激活机构 - - @pytest.mark.asyncio - async def test_tree_sorting( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试树的排序""" - # 创建根机构 - root_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "SORT_ROOT", - "org_name": "排序根", - "org_type": "province" - } - ) - root_id = root_response.json()["data"]["id"] - - # 创建不同排序的子机构 - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "SECOND", - "org_name": "第二", - "org_type": "city", - "parent_id": root_id, - "sort_order": 2 - } - ) - - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json={ - "org_code": "FIRST", - "org_name": "第一", - "org_type": "city", - "parent_id": root_id, - "sort_order": 1 - } - ) - - response = await client.get( - "/api/v1/organizations/tree", - headers=admin_headers - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_tree_depth_limit( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试树的深度限制""" - response = await client.get( - "/api/v1/organizations/tree?max_depth=3", - headers=admin_headers - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_flatten_tree_to_list( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试将树展平为列表""" - response = await client.get( - "/api/v1/organizations?format=flat", - headers=admin_headers - ) - - assert response.status_code == 200 - - -# ==================== 递归查询测试 ==================== - -class TestOrganizationRecursiveQuery: - """测试机构递归查询""" - - @pytest.mark.asyncio - async def test_get_all_children( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试获取所有子机构""" - # 创建多级机构 - root_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "ROOT", - "org_name": "根", - "org_type": "province" - }) - ) - root_id = root_response.json()["data"]["id"] - - child1_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "CHILD1", - "org_name": "子1", - "org_type": "city", - "parent_id": root_id - }) - ) - child1_id = child1_response.json()["data"]["id"] - - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "GRANDCHILD1", - "org_name": "孙1", - "org_type": "outlet", - "parent_id": child1_id - }) - ) - - # 获取所有子机构 - response = await client.get( - f"/api/v1/organizations/{root_id}/children", - headers=admin_headers - ) - - assert response.status_code == 200 - # 应该包含所有子孙机构 - - @pytest.mark.asyncio - async def test_get_all_parents( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试获取所有父机构路径""" - # 创建三级机构 - grand_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "GRAND", - "org_name": "祖父", - "org_type": "province" - }) - ) - grand_id = grand_response.json()["data"]["id"] - - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "PARENT", - "org_name": "父", - "org_type": "city", - "parent_id": grand_id - }) - ) - parent_id = parent_response.json()["data"]["id"] - - child_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "CHILD_PATH", - "org_name": "子", - "org_type": "outlet", - "parent_id": parent_id - }) - ) - child_id = child_response.json()["data"]["id"] - - # 获取父机构路径 - response = await client.get( - f"/api/v1/organizations/{child_id}/parents", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - # 应该包含完整的父机构路径 - # assert len(data["data"]) == 2 - - @pytest.mark.asyncio - async def test_count_descendants( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试统计子孙机构数量""" - # 创建机构树 - root_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "COUNT_ROOT", - "org_name": "统计根", - "org_type": "province" - }) - ) - root_id = root_response.json()["data"]["id"] - - # 创建多个子机构 - for i in range(3): - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": f"COUNT_CHILD{i}", - "org_name": f"子{i}", - "org_type": "city", - "parent_id": root_id - }) - ) - - response = await client.get( - f"/api/v1/organizations/{root_id}/descendants/count", - headers=admin_headers - ) - - assert response.status_code == 200 - # data = response.json() - # assert data["data"]["count"] >= 3 - - @pytest.mark.asyncio - async def test_get_organization_level( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试获取机构层级""" - # 创建三级机构 - root_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "LEVEL_ROOT", - "org_name": "层级根", - "org_type": "province" - }) - ) - root_id = root_response.json()["data"]["id"] - - child_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "LEVEL_CHILD", - "org_name": "层级子", - "org_type": "city", - "parent_id": root_id - }) - ) - child_id = child_response.json()["data"]["id"] - - grandchild_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "LEVEL_GRAND", - "org_name": "层级孙", - "org_type": "outlet", - "parent_id": child_id - }) - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_get_siblings( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试获取兄弟机构""" - # 创建父机构 - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "SIB_PARENT", - "org_name": "兄弟父", - "org_type": "province" - }) - ) - parent_id = parent_response.json()["data"]["id"] - - # 创建多个子机构 - for i in range(3): - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": f"SIB{i}", - "org_name": f"兄弟{i}", - "org_type": "city", - "parent_id": parent_id - }) - ) - - response = await client.get( - f"/api/v1/organizations/{parent_id}/children", - headers=admin_headers - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_recursive_search( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试递归搜索机构""" - response = await client.get( - "/api/v1/organizations/search?keyword=测试&recursive=true", - headers=admin_headers - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_get_org_tree_statistics( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试获取机构树统计信息""" - response = await client.get( - "/api/v1/organizations/statistics", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - # 应该包含机构总数、层级数等统计信息 - - @pytest.mark.asyncio - async def test_recursive_delete_check( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试递归删除检查""" - # 创建有子机构的机构 - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "DEL_CHECK", - "org_name": "删除检查", - "org_type": "province" - }) - ) - parent_id = parent_response.json()["data"]["id"] - - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "DEL_CHILD", - "org_name": "删除子", - "org_type": "city", - "parent_id": parent_id - }) - ) - - # 检查是否可以删除 - response = await client.get( - f"/api/v1/organizations/{parent_id}/can-delete", - headers=admin_headers - ) - - assert response.status_code == 200 - # data = response.json() - # assert data["data"]["can_delete"] is False - - @pytest.mark.asyncio - async def test_get_org_full_path( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试获取机构完整路径""" - # 创建三级机构 - grand_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "PATH_GRAND", - "org_name": "路径祖父", - "org_type": "province" - }) - ) - grand_id = grand_response.json()["data"]["id"] - - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "PATH_PARENT", - "org_name": "路径父", - "org_type": "city", - "parent_id": grand_id - }) - ) - parent_id = parent_response.json()["data"]["id"] - - child_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "PATH_CHILD", - "org_name": "路径子", - "org_type": "outlet", - "parent_id": parent_id - }) - ) - child_id = child_response.json()["data"]["id"] - - # 获取完整路径 - response = await client.get( - f"/api/v1/organizations/{child_id}/path", - headers=admin_headers - ) - - assert response.status_code == 200 - data = response.json() - # 应该返回: 祖父 > 父 > 子 - - -# ==================== 机构移动测试 ==================== - -class TestOrganizationMove: - """测试机构移动""" - - @pytest.mark.asyncio - async def test_move_organization_to_new_parent( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试移动机构到新的父机构""" - # 创建两个父机构 - parent1_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "MOVE_PARENT1", - "org_name": "移动父1", - "org_type": "province" - }) - ) - parent1_id = parent1_response.json()["data"]["id"] - - parent2_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "MOVE_PARENT2", - "org_name": "移动父2", - "org_type": "province" - }) - ) - parent2_id = parent2_response.json()["data"]["id"] - - # 创建子机构 - child_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "MOVE_CHILD", - "org_name": "移动子", - "org_type": "city", - "parent_id": parent1_id - }) - ) - child_id = child_response.json()["data"]["id"] - - # 移动到新的父机构 - response = await client.put( - f"/api/v1/organizations/{child_id}/move", - headers=admin_headers, - json({"new_parent_id": parent2_id}) - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_move_organization_to_root( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试移动机构到根级别""" - # 创建父机构和子机构 - parent_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "TO_ROOT_PARENT", - "org_name": "根父", - "org_type": "province" - }) - ) - parent_id = parent_response.json()["data"]["id"] - - child_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "TO_ROOT_CHILD", - "org_name": "根子", - "org_type": "city", - "parent_id": parent_id - }) - ) - child_id = child_response.json()["data"]["id"] - - # 移动到根级别 - response = await client.put( - f"/api/v1/organizations/{child_id}/move", - headers=admin_headers, - json({"new_parent_id": None}) - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_move_organization_with_children( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试移动有子机构的机构""" - # 创建机构树 - root1_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "MOVE_ROOT1", - "org_name": "移动根1", - "org_type": "province" - }) - ) - root1_id = root1_response.json()["data"]["id"] - - branch_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "MOVE_BRANCH", - "org_name": "移动分支", - "org_type": "city", - "parent_id": root1_id - }) - ) - branch_id = branch_response.json()["data"]["id"] - - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "MOVE_LEAF", - "org_name": "移动叶子", - "org_type": "outlet", - "parent_id": branch_id - }) - ) - - root2_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "MOVE_ROOT2", - "org_name": "移动根2", - "org_type": "province" - }) - ) - root2_id = root2_response.json()["data"]["id"] - - # 移动分支(包括其子机构) - response = await client.put( - f"/api/v1/organizations/{branch_id}/move", - headers=admin_headers, - json({"new_parent_id": root2_id}) - ) - - assert response.status_code == 200 - - @pytest.mark.asyncio - async def test_move_to_own_child_forbidden( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试禁止移动到自己的子机构""" - # 创建三级机构 - root_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "CYCLE_ROOT", - "org_name": "循环根", - "org_type": "province" - }) - ) - root_id = root_response.json()["data"]["id"] - - child_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "CYCLE_CHILD", - "org_name": "循环子", - "org_type": "city", - "parent_id": root_id - }) - ) - child_id = child_response.json()["data"]["id"] - - # 尝试将根机构移动到子机构下(应该失败) - response = await client.put( - f"/api/v1/organizations/{root_id}/move", - headers=admin_headers, - json({"new_parent_id": child_id}) - ) - - assert response.status_code in [400, 403] - - @pytest.mark.asyncio - async def test_move_non_existent_org( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试移动不存在的机构""" - response = await client.put( - "/api/v1/organizations/999999/move", - headers=admin_headers, - json({"new_parent_id": None}) - ) - - assert response.status_code == 404 - - -# ==================== 并发测试 ==================== - -class TestOrganizationConcurrency: - """测试机构并发操作""" - - @pytest.mark.asyncio - async def test_concurrent_create_same_code( - self, - client: AsyncClient, - admin_headers: dict - ): - """测试并发创建相同代码的机构""" - import asyncio - - data = { - "org_code": "CONCURRENT_ORG", - "org_name": "并发机构" - } - - # 并发创建 - tasks = [ - client.post("/api/v1/organizations", headers=admin_headers, json=data) - for _ in range(2) - ] - responses = await asyncio.gather(*tasks) - - # 应该只有一个成功 - success_count = sum(1 for r in responses if r.status_code == 200) - assert success_count == 1 - - @pytest.mark.asyncio - async def test_concurrent_update( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试并发更新机构""" - import asyncio - - # 创建机构 - create_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "CONCURRENT_UPDATE", - "org_name": "并发更新", - "org_type": "province" - }) - ) - org_id = create_response.json()["data"]["id"] - - # 并发更新 - tasks = [ - client.put( - f"/api/v1/organizations/{org_id}", - headers=admin_headers, - json={"org_name": f"名称{i}"} - ) - for i in range(5) - ] - responses = await asyncio.gather(*tasks) - - # 所有更新都应该成功 - assert all(r.status_code == 200 for r in responses) - - @pytest.mark.asyncio - async def test_concurrent_move_operations( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试并发移动操作""" - import asyncio - - # 创建多个机构 - parent_ids = [] - for i in range(3): - response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": f"MOVE_PARENT{i}", - "org_name": f"移动父{i}", - "org_type": "province" - }) - ) - parent_ids.append(response.json()["data"]["id"]) - - child_response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "MOVE_TARGET", - "org_name": "移动目标", - "org_type": "city", - "parent_id": parent_ids[0] - }) - ) - child_id = child_response.json()["data"]["id"] - - # 并发移动到不同的父机构 - tasks = [ - client.put( - f"/api/v1/organizations/{child_id}/move", - headers=admin_headers, - json({"new_parent_id": parent_id}) - ) - for parent_id in parent_ids - ] - responses = await asyncio.gather(*tasks) - - # 只有一个成功 - success_count = sum(1 for r in responses if r.status_code == 200) - assert success_count >= 1 - - @pytest.mark.asyncio - async def test_concurrent_delete_and_move( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试并发删除和移动""" - import asyncio - - # 创建机构 - response = await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": "DELETE_MOVE_ORG", - "org_name": "删除移动", - "org_type": "province" - }) - ) - org_id = response.json()["data"]["id"] - - # 并发删除和移动 - delete_task = client.delete( - f"/api/v1/organizations/{org_id}", - headers=admin_headers - ) - move_task = client.put( - f"/api/v1/organizations/{org_id}/move", - headers=admin_headers, - json({"new_parent_id": None}) - ) - - responses = await asyncio.gather(delete_task, move_task) - - # 至少一个操作失败 - assert any(r.status_code != 200 for r in responses) - - @pytest.mark.asyncio - async def test_concurrent_tree_queries( - self, - client: AsyncClient, - admin_headers: dict, - db_session - ): - """测试并发查询树结构""" - import asyncio - - # 创建一些机构 - for i in range(5): - await client.post( - "/api/v1/organizations", - headers=admin_headers, - json({ - "org_code": f"QUERY_ORG{i}", - "org_name": f"查询机构{i}", - "org_type": "province" - }) - ) - - # 并发查询树 - tasks = [ - client.get("/api/v1/organizations/tree", headers=admin_headers) - for _ in range(10) - ] - responses = await asyncio.gather(*tasks) - - # 所有查询都应该成功 - assert all(r.status_code == 200 for r in responses) diff --git a/tests/api/test_statistics.py b/tests/api/test_statistics.py deleted file mode 100644 index 7225bf2..0000000 --- a/tests/api/test_statistics.py +++ /dev/null @@ -1,912 +0,0 @@ -""" -统计分析 API 测试 - -测试范围: -- 资产统计测试 (20+用例) -- 分布统计测试 (15+用例) -- 趋势统计测试 (10+用例) -- 缓存测试 (10+用例) -- 导出测试 (5+用例) - -总计: 60+ 用例 -""" - -import pytest -from datetime import datetime, timedelta -from decimal import Decimal -from sqlalchemy.orm import Session - -from app.models.asset import Asset -from app.models.organization import Organization -from app.models.maintenance import Maintenance - - -# ================================ -# Fixtures -# ================================ - -@pytest.fixture -def test_assets_for_statistics(db: Session) -> list: - """创建用于统计的测试资产""" - assets = [] - - # 不同状态的资产 - statuses = ["in_stock", "in_use", "maintenance", "scrapped"] - for i, status in enumerate(statuses): - for j in range(3): - asset = Asset( - asset_code=f"STAT-{status[:3].upper()}-{j+1:03d}", - asset_name=f"统计测试资产{i}-{j}", - device_type_id=1, - organization_id=1, - status=status, - purchase_price=Decimal(str(10000 * (i + 1))), - purchase_date=datetime.now() - timedelta(days=30 * (i + 1)) - ) - db.add(asset) - assets.append(asset) - - db.commit() - for asset in assets: - db.refresh(asset) - return assets - - -@pytest.fixture -def test_orgs_for_statistics(db: Session) -> list: - """创建用于统计的测试组织""" - orgs = [] - for i in range(3): - org = Organization( - org_code=f"STAT-ORG-{i+1:03d}", - org_name=f"统计测试组织{i+1}", - org_type="department", - status="active" - ) - db.add(org) - orgs.append(org) - db.commit() - for org in orgs: - db.refresh(org) - return orgs - - -# ================================ -# 资产统计测试 (20+用例) -# ================================ - -class TestAssetStatistics: - """资产统计测试""" - - def test_get_total_asset_count(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产总数""" - response = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_count" in data - assert data["total_count"] >= len(test_assets_for_statistics) - - def test_get_asset_count_by_status(self, client, auth_headers, test_assets_for_statistics): - """测试按状态统计资产数量""" - response = client.get( - "/api/v1/statistics/assets/by-status", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) > 0 - assert all("status" in item and "count" in item for item in data) - - def test_get_asset_count_by_type(self, client, auth_headers, test_assets_for_statistics): - """测试按类型统计资产数量""" - response = client.get( - "/api/v1/statistics/assets/by-type", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert all("device_type" in item and "count" in item for item in data) - - def test_get_asset_count_by_organization(self, client, auth_headers, test_assets_for_statistics): - """测试按组织统计资产数量""" - response = client.get( - "/api/v1/statistics/assets/by-organization", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_total_asset_value(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产总价值""" - response = client.get( - "/api/v1/statistics/assets/total-value", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_value" in data - assert isinstance(data["total_value"], (int, float, str)) - - def test_get_asset_value_by_status(self, client, auth_headers, test_assets_for_statistics): - """测试按状态统计资产价值""" - response = client.get( - "/api/v1/statistics/assets/value-by-status", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert all("status" in item and "total_value" in item for item in data) - - def test_get_asset_value_by_type(self, client, auth_headers, test_assets_for_statistics): - """测试按类型统计资产价值""" - response = client.get( - "/api/v1/statistics/assets/value-by-type", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_asset_purchase_statistics(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产采购统计""" - response = client.get( - "/api/v1/statistics/assets/purchase-statistics", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_purchase_count" in data - assert "total_purchase_value" in data - - def test_get_asset_purchase_by_month(self, client, auth_headers, test_assets_for_statistics): - """测试按月统计资产采购""" - response = client.get( - "/api/v1/statistics/assets/purchase-by-month", - params={"year": 2025}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_asset_depreciation_summary(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产折旧汇总""" - response = client.get( - "/api/v1/statistics/assets/depreciation-summary", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_depreciation" in data - - def test_get_asset_age_distribution(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产使用年限分布""" - response = client.get( - "/api/v1/statistics/assets/age-distribution", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert all("age_range" in item and "count" in item for item in data) - - def test_get_new_asset_statistics(self, client, auth_headers, test_assets_for_statistics): - """测试获取新增资产统计""" - response = client.get( - "/api/v1/statistics/assets/new-assets", - params={"days": 30}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "count" in data - assert "total_value" in data - - def test_get_scrapped_asset_statistics(self, client, auth_headers, test_assets_for_statistics): - """测试获取报废资产统计""" - response = client.get( - "/api/v1/statistics/assets/scrapped-assets", - params={"start_date": "2025-01-01", "end_date": "2025-12-31"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "count" in data - - def test_get_asset_utilization_rate(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产利用率""" - response = client.get( - "/api/v1/statistics/assets/utilization-rate", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "utilization_rate" in data - assert "in_use_count" in data - assert "total_count" in data - - def test_get_asset_maintenance_rate(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产维修率""" - response = client.get( - "/api/v1/statistics/assets/maintenance-rate", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "maintenance_rate" in data - - def test_get_asset_summary_dashboard(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产汇总仪表盘数据""" - response = client.get( - "/api/v1/statistics/assets/summary-dashboard", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_assets" in data - assert "total_value" in data - assert "utilization_rate" in data - assert "maintenance_rate" in data - - def test_search_statistics(self, client, auth_headers, test_assets_for_statistics): - """测试搜索统计""" - response = client.get( - "/api/v1/statistics/assets/search", - params={"keyword": "统计"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "count" in data - - def test_get_asset_top_list_by_value(self, client, auth_headers, test_assets_for_statistics): - """测试获取价值最高的资产列表""" - response = client.get( - "/api/v1/statistics/assets/top-by-value", - params={"limit": 10}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_statistics_by_custom_field(self, client, auth_headers, test_assets_for_statistics): - """测试按自定义字段统计""" - response = client.get( - "/api/v1/statistics/assets/by-custom-field", - params={"field_name": "manufacturer"}, - headers=auth_headers - ) - assert response.status_code in [200, 400] # 可能不支持该字段 - - def test_get_multi_dimension_statistics(self, client, auth_headers, test_assets_for_statistics): - """测试多维度统计""" - response = client.post( - "/api/v1/statistics/assets/multi-dimension", - json={ - "dimensions": ["status", "device_type"], - "metrics": ["count", "total_value"] - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "data" in data - - -# ================================ -# 分布统计测试 (15+用例) -# ================================ - -class TestDistributionStatistics: - """分布统计测试""" - - def test_get_geographic_distribution(self, client, auth_headers, test_orgs_for_statistics): - """测试获取地理分布统计""" - response = client.get( - "/api/v1/statistics/distribution/geographic", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_organization_hierarchy_distribution(self, client, auth_headers, test_orgs_for_statistics): - """测试获取组织层级分布""" - response = client.get( - "/api/v1/statistics/distribution/organization-hierarchy", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_department_distribution(self, client, auth_headers, test_orgs_for_statistics): - """测试获取部门分布""" - response = client.get( - "/api/v1/statistics/distribution/by-department", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_asset_category_distribution(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产类别分布""" - response = client.get( - "/api/v1/statistics/distribution/by-category", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_asset_value_distribution(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产价值分布""" - response = client.get( - "/api/v1/statistics/distribution/value-ranges", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert all("range" in item and "count" in item for item in data) - - def test_get_asset_location_distribution(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产位置分布""" - response = client.get( - "/api/v1/statistics/distribution/by-location", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_asset_brand_distribution(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产品牌分布""" - response = client.get( - "/api/v1/statistics/distribution/by-brand", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_get_asset_supplier_distribution(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产供应商分布""" - response = client.get( - "/api/v1/statistics/distribution/by-supplier", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_asset_status_distribution_pie_chart(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产状态分布饼图数据""" - response = client.get( - "/api/v1/statistics/distribution/status-pie-chart", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "labels" in data - assert "data" in data - assert isinstance(data["labels"], list) - assert isinstance(data["data"], list) - - def test_get_organization_asset_tree(self, client, auth_headers, test_orgs_for_statistics): - """测试获取组织资产树""" - response = client.get( - "/api/v1/statistics/distribution/org-asset-tree", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "tree" in data - - def test_get_cross_tabulation(self, client, auth_headers, test_assets_for_statistics): - """测试交叉统计表""" - response = client.post( - "/api/v1/statistics/distribution/cross-tabulation", - json={ - "row_field": "status", - "column_field": "device_type_id" - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "rows" in data - assert "columns" in data - assert "data" in data - - def test_get_distribution_heatmap_data(self, client, auth_headers, test_assets_for_statistics): - """测试获取分布热力图数据""" - response = client.get( - "/api/v1/statistics/distribution/heatmap", - params={"dimension": "organization_asset"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "heatmap_data" in data - - def test_get_asset_concentration_index(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产集中度指数""" - response = client.get( - "/api/v1/statistics/distribution/concentration-index", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "index" in data - - def test_get_distribution_comparison(self, client, auth_headers, test_assets_for_statistics): - """测试分布对比分析""" - response = client.post( - "/api/v1/statistics/distribution/comparison", - json={ - "dimension": "status", - "period1": {"start": "2025-01-01", "end": "2025-06-30"}, - "period2": {"start": "2024-01-01", "end": "2024-06-30"} - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "period1" in data - assert "period2" in data - - def test_get_distribution_trend(self, client, auth_headers, test_assets_for_statistics): - """测试分布趋势""" - response = client.get( - "/api/v1/statistics/distribution/trend", - params={"dimension": "status", "months": 12}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "trend_data" in data - - -# ================================ -# 趋势统计测试 (10+用例) -# ================================ - -class TestTrendStatistics: - """趋势统计测试""" - - def test_get_asset_growth_trend(self, client, auth_headers, test_assets_for_statistics): - """测试获取资产增长趋势""" - response = client.get( - "/api/v1/statistics/trends/asset-growth", - params={"period": "monthly", "months": 12}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "trend" in data - assert isinstance(data["trend"], list) - - def test_get_value_change_trend(self, client, auth_headers, test_assets_for_statistics): - """测试获取价值变化趋势""" - response = client.get( - "/api/v1/statistics/trends/value-change", - params={"period": "monthly", "months": 12}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "trend" in data - - def test_get_utilization_trend(self, client, auth_headers, test_assets_for_statistics): - """测试获取利用率趋势""" - response = client.get( - "/api/v1/statistics/trends/utilization", - params={"period": "weekly", "weeks": 12}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "trend" in data - - def test_get_maintenance_cost_trend(self, client, auth_headers, test_assets_for_statistics): - """测试获取维修费用趋势""" - response = client.get( - "/api/v1/statistics/trends/maintenance-cost", - params={"period": "monthly", "months": 12}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "trend" in data - - def test_get_allocation_trend(self, client, auth_headers, test_assets_for_statistics): - """测试获取分配趋势""" - response = client.get( - "/api/v1/statistics/trends/allocation", - params={"period": "monthly", "months": 12}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_transfer_trend(self, client, auth_headers, test_assets_for_statistics): - """测试获取调拨趋势""" - response = client.get( - "/api/v1/statistics/trends/transfer", - params={"period": "monthly", "months": 12}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_scrap_rate_trend(self, client, auth_headers, test_assets_for_statistics): - """测试获取报废率趋势""" - response = client.get( - "/api/v1/statistics/trends/scrap-rate", - params={"period": "monthly", "months": 12}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "trend" in data - - def test_get_forecast_data(self, client, auth_headers, test_assets_for_statistics): - """测试获取预测数据""" - response = client.get( - "/api/v1/statistics/trends/forecast", - params={ - "metric": "asset_count", - "method": "linear_regression", - "forecast_periods": 6 - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "forecast" in data - assert "confidence_interval" in data - - def test_get_year_over_year_comparison(self, client, auth_headers, test_assets_for_statistics): - """测试获取同比数据""" - response = client.get( - "/api/v1/statistics/trends/year-over-year", - params={"metric": "total_value"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "current_year" in data - assert "previous_year" in data - assert "growth_rate" in data - - def test_get_moving_average(self, client, auth_headers, test_assets_for_statistics): - """测试获取移动平均""" - response = client.get( - "/api/v1/statistics/trends/moving-average", - params={"metric": "asset_count", "window": 7}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "moving_average" in data - - -# ================================ -# 缓存测试 (10+用例) -# ================================ - -class TestStatisticsCache: - """统计缓存测试""" - - def test_cache_is_working(self, client, auth_headers, test_assets_for_statistics): - """测试缓存是否生效""" - # 第一次请求 - response1 = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - assert response1.status_code == 200 - - # 第二次请求应该从缓存读取 - response2 = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - assert response2.status_code == 200 - - def test_cache_key_generation(self, client, auth_headers, test_assets_for_statistics): - """测试缓存键生成""" - response = client.get( - "/api/v1/statistics/assets/by-status", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_cache_invalidation_on_asset_change(self, client, auth_headers, db: Session, test_assets_for_statistics): - """测试资产变更时缓存失效""" - # 获取初始统计 - response1 = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - count1 = response1.json()["total_count"] - - # 创建新资产 - new_asset = Asset( - asset_code="CACHE-TEST-001", - asset_name="缓存测试资产", - device_type_id=1, - organization_id=1, - status="in_stock" - ) - db.add(new_asset) - db.commit() - - # 再次获取统计 - response2 = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - count2 = response2.json()["total_count"] - - # 验证缓存已更新 - assert count2 == count1 + 1 - - def test_cache_expiration(self, client, auth_headers, test_assets_for_statistics): - """测试缓存过期""" - response = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_clear_cache(self, client, auth_headers, test_assets_for_statistics): - """测试清除缓存""" - response = client.post( - "/api/v1/statistics/cache/clear", - json={"cache_keys": ["assets:total-count"]}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_cache_statistics(self, client, auth_headers): - """测试获取缓存统计""" - response = client.get( - "/api/v1/statistics/cache/stats", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "hit_count" in data - assert "miss_count" in data - - def test_warm_up_cache(self, client, auth_headers): - """测试缓存预热""" - response = client.post( - "/api/v1/statistics/cache/warm-up", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "warmed_up_keys" in data - - def test_cache_with_different_parameters(self, client, auth_headers, test_assets_for_statistics): - """测试不同参数使用不同缓存""" - response1 = client.get( - "/api/v1/statistics/assets/purchase-by-month?year=2024", - headers=auth_headers - ) - response2 = client.get( - "/api/v1/statistics/assets/purchase-by-month?year=2025", - headers=auth_headers - ) - assert response1.status_code == 200 - assert response2.status_code == 200 - - def test_distributed_cache_consistency(self, client, auth_headers, test_assets_for_statistics): - """测试分布式缓存一致性""" - response = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_cache_performance(self, client, auth_headers, test_assets_for_statistics): - """测试缓存性能""" - import time - - # 未缓存请求 - start = time.time() - response1 = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - uncached_time = time.time() - start - - # 缓存请求 - start = time.time() - response2 = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - cached_time = time.time() - start - - # 缓存请求应该更快 - # 注意: 这个断言可能因为网络延迟等因素不稳定 - # assert cached_time < uncached_time - - -# ================================ -# 导出测试 (5+用例) -# ================================ - -class TestStatisticsExport: - """统计导出测试""" - - def test_export_statistics_to_excel(self, client, auth_headers, test_assets_for_statistics): - """测试导出统计数据到Excel""" - response = client.post( - "/api/v1/statistics/export/excel", - json={ - "report_type": "asset_summary", - "filters": {"status": "in_use"}, - "columns": ["asset_code", "asset_name", "purchase_price"] - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "download_url" in data - - def test_export_statistics_to_pdf(self, client, auth_headers, test_assets_for_statistics): - """测试导出统计数据到PDF""" - response = client.post( - "/api/v1/statistics/export/pdf", - json={ - "report_type": "asset_distribution", - "include_charts": True - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "download_url" in data - - def test_export_statistics_to_csv(self, client, auth_headers, test_assets_for_statistics): - """测试导出统计数据到CSV""" - response = client.post( - "/api/v1/statistics/export/csv", - json={ - "query": "assets_by_status", - "parameters": {} - }, - headers=auth_headers - ) - assert response.status_code in [200, 202] # 可能异步处理 - - def test_scheduled_export(self, client, auth_headers): - """测试定时导出""" - response = client.post( - "/api/v1/statistics/export/schedule", - json={ - "report_type": "monthly_report", - "schedule": "0 0 1 * *", # 每月1号 - "recipients": ["admin@example.com"], - "format": "excel" - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "schedule_id" in data - - def test_get_export_history(self, client, auth_headers): - """测试获取导出历史""" - response = client.get( - "/api/v1/statistics/export/history", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - -# ================================ -# 测试标记 -# ================================ - -@pytest.mark.unit -class TestStatisticsUnit: - """单元测试标记""" - - def test_calculation_accuracy(self): - """测试计算准确性""" - pass - - def test_rounding_rules(self): - """测试舍入规则""" - pass - - -@pytest.mark.integration -class TestStatisticsIntegration: - """集成测试标记""" - - def test_full_statistics_workflow(self, client, auth_headers, test_assets_for_statistics): - """测试完整统计流程""" - # 1. 获取基础统计 - response1 = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - assert response1.status_code == 200 - - # 2. 获取详细统计 - response2 = client.get( - "/api/v1/statistics/assets/by-status", - headers=auth_headers - ) - assert response2.status_code == 200 - - # 3. 导出报告 - response3 = client.post( - "/api/v1/statistics/export/excel", - json={"report_type": "asset_summary"}, - headers=auth_headers - ) - assert response3.status_code == 200 - - -@pytest.mark.slow -class TestStatisticsSlowTests: - """慢速测试标记""" - - def test_large_dataset_statistics(self, client, auth_headers): - """测试大数据集统计""" - pass - - -@pytest.mark.smoke -class TestStatisticsSmoke: - """冒烟测试标记""" - - def test_basic_statistics_endpoints(self, client, auth_headers): - """冒烟测试: 基础统计接口""" - endpoints = [ - "/api/v1/statistics/assets/total-count", - "/api/v1/statistics/assets/by-status", - "/api/v1/statistics/assets/total-value" - ] - - for endpoint in endpoints: - response = client.get(endpoint, headers=auth_headers) - assert response.status_code == 200 - - -@pytest.mark.performance -class TestStatisticsPerformance: - """性能测试标记""" - - def test_query_response_time(self, client, auth_headers): - """测试查询响应时间""" - import time - - start = time.time() - response = client.get( - "/api/v1/statistics/assets/total-count", - headers=auth_headers - ) - elapsed = time.time() - start - - assert response.status_code == 200 - assert elapsed < 1.0 # 响应时间应小于1秒 - - def test_concurrent_statistics_requests(self, client, auth_headers): - """测试并发统计请求""" - pass diff --git a/tests/api/test_transfers.py b/tests/api/test_transfers.py deleted file mode 100644 index 3d7cb11..0000000 --- a/tests/api/test_transfers.py +++ /dev/null @@ -1,1010 +0,0 @@ -""" -资产调拨管理 API 测试 - -测试范围: -- 调拨单CRUD测试 (20+用例) -- 调拨流程测试 (15+用例) -- 状态转换测试 (10+用例) -- 并发测试 (5+用例) - -总计: 50+ 用例 -""" - -import pytest -from datetime import datetime, timedelta -from typing import List -from sqlalchemy.orm import Session - -from app.models.transfer import Transfer, TransferItem -from app.models.asset import Asset -from app.models.organization import Organization -from app.schemas.transfer import ( - TransferCreate, - TransferStatus, - TransferItemType -) - - -# ================================ -# Fixtures -# ================================ - -@pytest.fixture -def test_orgs_for_transfer(db: Session) -> tuple: - """创建调拨涉及的组织""" - source_org = Organization( - org_code="SOURCE-001", - org_name="源组织", - org_type="department", - status="active" - ) - target_org = Organization( - org_code="TARGET-002", - org_name="目标组织", - org_type="department", - status="active" - ) - db.add(source_org) - db.add(target_org) - db.commit() - db.refresh(source_org) - db.refresh(target_org) - return source_org, target_org - - -@pytest.fixture -def test_assets_for_transfer(db: Session, test_orgs_for_transfer) -> List[Asset]: - """创建可用于调拨的测试资产""" - source_org, _ = test_orgs_for_transfer - assets = [] - for i in range(5): - asset = Asset( - asset_code=f"TEST-TRANSF-{i+1:03d}", - asset_name=f"测试调拨资产{i+1}", - device_type_id=1, - organization_id=source_org.id, - status="in_use", - purchase_date=datetime.now() - timedelta(days=30*i) - ) - db.add(asset) - assets.append(asset) - db.commit() - for asset in assets: - db.refresh(asset) - return assets - - -@pytest.fixture -def test_transfer_order(db: Session, test_assets_for_transfer: List[Asset], test_orgs_for_transfer: tuple) -> Transfer: - """创建测试调拨单""" - source_org, target_org = test_orgs_for_transfer - - transfer = Transfer( - transfer_no="TRANSF-2025-001", - source_org_id=source_org.id, - target_org_id=target_org.id, - request_user_id=1, - status=TransferStatus.PENDING, - expected_transfer_date=datetime.now() + timedelta(days=7), - transfer_reason="部门调整需要调拨", - remark="测试调拨单" - ) - db.add(transfer) - db.commit() - db.refresh(transfer) - - # 添加调拨项 - for asset in test_assets_for_transfer[:3]: - item = TransferItem( - transfer_id=transfer.id, - asset_id=asset.id, - item_type=TransferItemType.TRANSFER, - status="pending" - ) - db.add(item) - db.commit() - - return transfer - - -# ================================ -# 调拨单CRUD测试 (20+用例) -# ================================ - -class TestTransferCRUD: - """调拨单CRUD操作测试""" - - def test_create_transfer_with_valid_data(self, client, auth_headers, test_assets_for_transfer, test_orgs_for_transfer): - """测试使用有效数据创建调拨单""" - source_org, target_org = test_orgs_for_transfer - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [asset.id for asset in test_assets_for_transfer[:3]], - "expected_transfer_date": (datetime.now() + timedelta(days=7)).isoformat(), - "transfer_reason": "业务调整", - "remark": "测试调拨" - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["transfer_no"] is not None - assert data["status"] == TransferStatus.PENDING - assert data["asset_count"] == 3 - - def test_create_transfer_with_same_source_and_target_org(self, client, auth_headers, test_assets_for_transfer, test_orgs_for_transfer): - """测试源组织和目标组织相同时应失败""" - source_org, _ = test_orgs_for_transfer - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": source_org.id, - "asset_ids": [test_assets_for_transfer[0].id], - "transfer_reason": "无效调拨" - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "源组织和目标组织不能相同" in response.json()["detail"] - - def test_create_transfer_with_empty_assets(self, client, auth_headers, test_orgs_for_transfer): - """测试创建空资产列表的调拨单应失败""" - source_org, target_org = test_orgs_for_transfer - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [], - "transfer_reason": "测试" - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "资产列表不能为空" in response.json()["detail"] - - def test_create_transfer_with_invalid_source_org(self, client, auth_headers, test_assets_for_transfer, test_orgs_for_transfer): - """测试使用无效源组织ID创建调拨单应失败""" - _, target_org = test_orgs_for_transfer - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": 99999, - "target_org_id": target_org.id, - "asset_ids": [test_assets_for_transfer[0].id], - "transfer_reason": "测试" - }, - headers=auth_headers - ) - assert response.status_code == 404 - - def test_create_transfer_with_invalid_target_org(self, client, auth_headers, test_assets_for_transfer, test_orgs_for_transfer): - """测试使用无效目标组织ID创建调拨单应失败""" - source_org, _ = test_orgs_for_transfer - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": 99999, - "asset_ids": [test_assets_for_transfer[0].id], - "transfer_reason": "测试" - }, - headers=auth_headers - ) - assert response.status_code == 404 - - def test_create_transfer_with_asset_from_different_org(self, client, auth_headers, db: Session, test_orgs_for_transfer): - """测试调拨不属于源组织的资产应失败""" - source_org, target_org = test_orgs_for_transfer - - # 创建属于其他组织的资产 - other_asset = Asset( - asset_code="OTHER-ASSET-001", - asset_name="其他组织资产", - device_type_id=1, - organization_id=999, - status="in_use" - ) - db.add(other_asset) - db.commit() - db.refresh(other_asset) - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [other_asset.id], - "transfer_reason": "测试" - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "资产不属于源组织" in response.json()["detail"] - - def test_create_transfer_with_in_stock_asset(self, client, auth_headers, db: Session, test_orgs_for_transfer): - """测试调拨库存中资产应失败""" - source_org, target_org = test_orgs_for_transfer - - asset = Asset( - asset_code="TEST-STOCK-001", - asset_name="库存资产", - device_type_id=1, - organization_id=source_org.id, - status="in_stock" - ) - db.add(asset) - db.commit() - db.refresh(asset) - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [asset.id], - "transfer_reason": "测试" - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "只能调拨使用中的资产" in response.json()["detail"] - - def test_create_transfer_with_maintenance_asset(self, client, auth_headers, db: Session, test_orgs_for_transfer): - """测试调拨维修中资产应失败""" - source_org, target_org = test_orgs_for_transfer - - asset = Asset( - asset_code="TEST-MAINT-002", - asset_name="维修中资产", - device_type_id=1, - organization_id=source_org.id, - status="maintenance" - ) - db.add(asset) - db.commit() - db.refresh(asset) - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [asset.id], - "transfer_reason": "测试" - }, - headers=auth_headers - ) - assert response.status_code == 400 - assert "资产状态不允许调拨" in response.json()["detail"] - - def test_get_transfer_list_with_pagination(self, client, auth_headers, test_transfer_order): - """测试分页获取调拨单列表""" - response = client.get( - "/api/v1/transfers/?page=1&page_size=10", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "items" in data - assert "total" in data - assert len(data["items"]) >= 1 - - def test_get_transfer_list_with_status_filter(self, client, auth_headers, test_transfer_order): - """测试按状态筛选调拨单""" - response = client.get( - f"/api/v1/transfers/?status={TransferStatus.PENDING}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - for item in data["items"]: - assert item["status"] == TransferStatus.PENDING - - def test_get_transfer_list_with_org_filter(self, client, auth_headers, test_transfer_order, test_orgs_for_transfer): - """测试按组织筛选调拨单""" - source_org, _ = test_orgs_for_transfer - - response = client.get( - f"/api/v1/transfers/?source_org_id={source_org.id}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data["items"]) >= 1 - - def test_get_transfer_list_with_date_range(self, client, auth_headers, test_transfer_order): - """测试按日期范围筛选调拨单""" - start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") - end_date = (datetime.now() + timedelta(days=1)).strftime("%Y-%m-%d") - - response = client.get( - f"/api/v1/transfers/?start_date={start_date}&end_date={end_date}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data["items"]) >= 1 - - def test_get_transfer_by_id(self, client, auth_headers, test_transfer_order): - """测试通过ID获取调拨单详情""" - response = client.get( - f"/api/v1/transfers/{test_transfer_order.id}", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["id"] == test_transfer_order.id - assert data["transfer_no"] == test_transfer_order.transfer_no - assert "items" in data - assert "source_org" in data - assert "target_org" in data - - def test_get_transfer_by_invalid_id(self, client, auth_headers): - """测试通过无效ID获取调拨单应返回404""" - response = client.get( - "/api/v1/transfers/999999", - headers=auth_headers - ) - assert response.status_code == 404 - - def test_get_transfer_items(self, client, auth_headers, test_transfer_order): - """测试获取调拨单的资产项列表""" - response = client.get( - f"/api/v1/transfers/{test_transfer_order.id}/items", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) == 3 - - def test_update_transfer_remark(self, client, auth_headers, test_transfer_order): - """测试更新调拨单备注""" - response = client.put( - f"/api/v1/transfers/{test_transfer_order.id}", - json={"remark": "更新后的备注"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["remark"] == "更新后的备注" - - def test_update_transfer_expected_date(self, client, auth_headers, test_transfer_order): - """测试更新调拨单预期日期""" - new_date = (datetime.now() + timedelta(days=14)).isoformat() - response = client.put( - f"/api/v1/transfers/{test_transfer_order.id}", - json={"expected_transfer_date": new_date}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "expected_transfer_date" in data - - def test_update_transfer_after_approval_should_fail(self, client, auth_headers, db: Session, test_transfer_order): - """测试更新已审批的调拨单应失败""" - test_transfer_order.status = TransferStatus.APPROVED - db.commit() - - response = client.put( - f"/api/v1/transfers/{test_transfer_order.id}", - json={"remark": "不应允许更新"}, - headers=auth_headers - ) - assert response.status_code == 400 - assert "不允许修改" in response.json()["detail"] - - def test_delete_pending_transfer(self, client, auth_headers, db: Session, test_orgs_for_transfer): - """测试删除待审批的调拨单""" - source_org, target_org = test_orgs_for_transfer - - transfer = Transfer( - transfer_no="TRANSF-DEL-001", - source_org_id=source_org.id, - target_org_id=target_org.id, - request_user_id=1, - status=TransferStatus.PENDING - ) - db.add(transfer) - db.commit() - db.refresh(transfer) - - response = client.delete( - f"/api/v1/transfers/{transfer.id}", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_delete_approved_transfer_should_fail(self, client, auth_headers, db: Session, test_transfer_order): - """测试删除已审批的调拨单应失败""" - test_transfer_order.status = TransferStatus.APPROVED - db.commit() - - response = client.delete( - f"/api/v1/transfers/{test_transfer_order.id}", - headers=auth_headers - ) - assert response.status_code == 400 - assert "不允许删除" in response.json()["detail"] - - def test_create_transfer_with_duplicate_assets(self, client, auth_headers, test_assets_for_transfer, test_orgs_for_transfer): - """测试创建包含重复资产的调拨单应去重""" - source_org, target_org = test_orgs_for_transfer - asset_id = test_assets_for_transfer[0].id - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [asset_id, asset_id, asset_id], - "transfer_reason": "测试去重" - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["asset_count"] == 1 # 去重后只有1个 - - def test_get_transfer_statistics(self, client, auth_headers, test_transfer_order): - """测试获取调拨单统计信息""" - response = client.get( - "/api/v1/transfers/statistics/summary", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "total_count" in data - assert "status_distribution" in data - - -# ================================ -# 调拨流程测试 (15+用例) -# ================================ - -class TestTransferWorkflow: - """调拨流程测试""" - - def test_submit_transfer_request(self, client, auth_headers, test_assets_for_transfer, test_orgs_for_transfer): - """测试提交调拨申请""" - source_org, target_org = test_orgs_for_transfer - - response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [test_assets_for_transfer[0].id], - "transfer_reason": "业务调整", - "remark": "测试申请" - }, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == TransferStatus.PENDING - - def test_approve_transfer_request(self, client, auth_headers, test_transfer_order, db: Session): - """测试审批调拨申请""" - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/approve", - json={"approval_comment": "调拨审批通过"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == TransferStatus.APPROVED - - def test_reject_transfer_request(self, client, auth_headers, test_transfer_order): - """测试拒绝调拨申请""" - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/reject", - json={"rejection_reason": "资产不足"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == TransferStatus.REJECTED - - def test_start_transfer_execution(self, client, auth_headers, test_transfer_order, db: Session): - """测试开始执行调拨""" - test_transfer_order.status = TransferStatus.APPROVED - db.commit() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/start", - json={"start_note": "开始调拨"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == TransferStatus.IN_TRANSIT - - def test_confirm_transfer_receipt(self, client, auth_headers, test_transfer_order, db: Session): - """测试确认调拨接收""" - test_transfer_order.status = TransferStatus.IN_TRANSIT - db.commit() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/confirm-receipt", - json={"receipt_note": "已接收", "receiver_name": "张三"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == TransferStatus.COMPLETED - - def test_confirm_receipt_updates_asset_organization(self, client, auth_headers, test_transfer_order, db: Session): - """测试确认接收后资产组织应更新""" - test_transfer_order.status = TransferStatus.IN_TRANSIT - db.commit() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/confirm-receipt", - json={"receipt_note": "已接收", "receiver_name": "李四"}, - headers=auth_headers - ) - assert response.status_code == 200 - - # 验证资产组织已更新 - for item in test_transfer_order.items: - asset = db.query(Asset).filter(Asset.id == item.asset_id).first() - assert asset.organization_id == test_transfer_order.target_org_id - - def test_partial_confirm_receipt(self, client, auth_headers, test_transfer_order, db: Session): - """测试部分确认接收""" - test_transfer_order.status = TransferStatus.IN_TRANSIT - db.commit() - - # 确认接收部分资产 - item_ids = [test_transfer_order.items[0].id, test_transfer_order.items[1].id] - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/partial-confirm", - json={"item_ids": item_ids, "note": "部分接收"}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_cancel_transfer_before_approval(self, client, auth_headers, test_transfer_order): - """测试审批前取消调拨""" - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/cancel", - json={"cancellation_reason": "申请有误"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["status"] == TransferStatus.CANCELLED - - def test_generate_transfer_document(self, client, auth_headers, test_transfer_order): - """测试生成调拨单据""" - response = client.get( - f"/api/v1/transfers/{test_transfer_order.id}/document", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "document_url" in data - - def test_add_transfer_tracking_info(self, client, auth_headers, test_transfer_order, db: Session): - """测试添加调拨跟踪信息""" - test_transfer_order.status = TransferStatus.IN_TRANSIT - db.commit() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/tracking", - json={"tracking_number": "SF1234567890", "carrier": "顺丰快递"}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_transfer_tracking_history(self, client, auth_headers, test_transfer_order): - """测试获取调拨跟踪历史""" - response = client.get( - f"/api/v1/transfers/{test_transfer_order.id}/tracking-history", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - def test_verify_asset_before_transfer(self, client, auth_headers, test_transfer_order, db: Session): - """测试调拨前验证资产""" - asset = db.query(Asset).filter( - Asset.id == test_transfer_order.items[0].asset_id - ).first() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/verify-asset", - json={"asset_qrcode": asset.qrcode}, - headers=auth_headers - ) - assert response.status_code == 200 - - def test_transfer_with_condition_check(self, client, auth_headers, test_transfer_order): - """测试带条件检查的调拨""" - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/check-conditions", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert "can_transfer" in data - - def test_batch_transfer_operations(self, client, auth_headers, db: Session, test_orgs_for_transfer): - """测试批量调拨操作""" - source_org, target_org = test_orgs_for_transfer - - # 创建多个调拨单 - transfer_ids = [] - for i in range(3): - transfer = Transfer( - transfer_no=f"TRANSF-BATCH-{i+1:03d}", - source_org_id=source_org.id, - target_org_id=target_org.id, - request_user_id=1, - status=TransferStatus.PENDING - ) - db.add(transfer) - db.commit() - db.refresh(transfer) - transfer_ids.append(transfer.id) - - response = client.post( - "/api/v1/transfers/batch-approve", - json={"transfer_ids": transfer_ids, "comment": "批量审批"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["success_count"] == 3 - - -# ================================ -# 状态转换测试 (10+用例) -# ================================ - -class TestTransferStatusTransitions: - """调拨单状态转换测试""" - - def test_status_transition_pending_to_approved(self, client, auth_headers, test_transfer_order): - """测试状态转换: 待审批 -> 已审批""" - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/approve", - json={"approval_comment": "审批通过"}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == TransferStatus.APPROVED - - def test_status_transition_pending_to_rejected(self, client, auth_headers, test_transfer_order): - """测试状态转换: 待审批 -> 已拒绝""" - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/reject", - json={"rejection_reason": "理由"}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == TransferStatus.REJECTED - - def test_status_transition_approved_to_in_transit(self, client, auth_headers, test_transfer_order, db: Session): - """测试状态转换: 已审批 -> 运输中""" - test_transfer_order.status = TransferStatus.APPROVED - db.commit() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/start", - json={}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == TransferStatus.IN_TRANSIT - - def test_status_transition_in_transit_to_completed(self, client, auth_headers, test_transfer_order, db: Session): - """测试状态转换: 运输中 -> 已完成""" - test_transfer_order.status = TransferStatus.IN_TRANSIT - db.commit() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/confirm-receipt", - json={"receipt_note": "已完成", "receiver_name": "测试"}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == TransferStatus.COMPLETED - - def test_invalid_transition_from_completed(self, client, auth_headers, test_transfer_order, db: Session): - """测试已完成状态不允许转换""" - test_transfer_order.status = TransferStatus.COMPLETED - db.commit() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/approve", - json={"approval_comment": "尝试重新审批"}, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_invalid_transition_from_rejected(self, client, auth_headers, test_transfer_order, db: Session): - """测试已拒绝状态不允许转换为运输中""" - test_transfer_order.status = TransferStatus.REJECTED - db.commit() - - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/start", - json={}, - headers=auth_headers - ) - assert response.status_code == 400 - - def test_status_transition_pending_to_cancelled(self, client, auth_headers, test_transfer_order): - """测试状态转换: 待审批 -> 已取消""" - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/cancel", - json={"cancellation_reason": "取消原因"}, - headers=auth_headers - ) - assert response.status_code == 200 - assert response.json()["status"] == TransferStatus.CANCELLED - - def test_get_status_transition_history(self, client, auth_headers, test_transfer_order): - """测试获取状态转换历史""" - # 先进行状态转换 - client.post( - f"/api/v1/transfers/{test_transfer_order.id}/approve", - json={"approval_comment": "测试"}, - headers=auth_headers - ) - - response = client.get( - f"/api/v1/transfers/{test_transfer_order.id}/status-history", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert len(data) >= 1 - - def test_auto_transition_on_all_items_confirmed(self, client, auth_headers, test_transfer_order, db: Session): - """测试所有项确认后自动转换状态""" - test_transfer_order.status = TransferStatus.IN_TRANSIT - # 标记所有项为已确认 - for item in test_transfer_order.items: - item.status = "confirmed" - db.commit() - - # 触发自动完成 - response = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/check-auto-complete", - headers=auth_headers - ) - assert response.status_code == 200 - - def test_get_available_status_transitions(self, client, auth_headers, test_transfer_order): - """测试获取可用的状态转换""" - response = client.get( - f"/api/v1/transfers/{test_transfer_order.id}/available-transitions", - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - - -# ================================ -# 并发测试 (5+用例) -# ================================ - -class TestTransferConcurrency: - """调拨并发测试""" - - def test_concurrent_transfer_approval(self, client, auth_headers, test_transfer_order): - """测试并发审批调拨单""" - # 模拟两个管理员同时审批 - # 第一个审批 - response1 = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/approve", - json={"approval_comment": "第一个审批"}, - headers=auth_headers - ) - assert response1.status_code == 200 - - # 第二个审批应该失败(因为已经是已审批状态) - response2 = client.post( - f"/api/v1/transfers/{test_transfer_order.id}/approve", - json={"approval_comment": "第二个审批"}, - headers=auth_headers - ) - assert response2.status_code == 400 - - def test_concurrent_asset_transfer(self, client, auth_headers, db: Session, test_assets_for_transfer, test_orgs_for_transfer): - """测试同时调拨同一资产""" - source_org, target_org = test_orgs_for_transfer - asset = test_assets_for_transfer[0] - - # 创建第一个调拨单 - transfer1_data = { - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [asset.id], - "transfer_reason": "第一次调拨" - } - response1 = client.post( - "/api/v1/transfers/", - json=transfer1_data, - headers=auth_headers - ) - assert response1.status_code == 200 - transfer1_id = response1.json()["id"] - - # 审批第一个调拨单 - client.post( - f"/api/v1/transfers/{transfer1_id}/approve", - json={"approval_comment": "审批通过"}, - headers=auth_headers - ) - - # 尝试创建第二个调拨单(应该失败,因为资产已在调拨中) - transfer2_data = { - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [asset.id], - "transfer_reason": "第二次调拨" - } - response2 = client.post( - "/api/v1/transfers/", - json=transfer2_data, - headers=auth_headers - ) - assert response2.status_code == 400 - assert "资产已在调拨中" in response2.json()["detail"] - - def test_concurrent_status_update(self, client, auth_headers, test_transfer_order, db: Session): - """测试并发状态更新""" - # 这个测试需要使用多线程或异步来模拟真正的并发 - pass - - def test_batch_operation_with_concurrent_requests(self, client, auth_headers, db: Session, test_orgs_for_transfer): - """测试批量操作时的并发请求""" - source_org, target_org = test_orgs_for_transfer - - # 创建多个调拨单 - transfer_ids = [] - for i in range(5): - transfer = Transfer( - transfer_no=f"TRANSF-CONCUR-{i+1:03d}", - source_org_id=source_org.id, - target_org_id=target_org.id, - request_user_id=1, - status=TransferStatus.PENDING - ) - db.add(transfer) - db.commit() - db.refresh(transfer) - transfer_ids.append(transfer.id) - - # 批量审批 - response = client.post( - "/api/v1/transfers/batch-approve", - json={"transfer_ids": transfer_ids, "comment": "批量并发审批"}, - headers=auth_headers - ) - assert response.status_code == 200 - data = response.json() - assert data["success_count"] == 5 - - def test_concurrent_read_write_operations(self, client, auth_headers, test_transfer_order): - """测试并发读写操作""" - # 这个测试需要模拟同时读取和写入 - pass - - -# ================================ -# 测试标记 -# ================================ - -@pytest.mark.unit -class TestTransferUnit: - """单元测试标记""" - - def test_transfer_number_generation(self): - """测试调拨单号生成逻辑""" - pass - - def test_transfer_eligibility_check(self): - """测试调拨资格检查逻辑""" - pass - - -@pytest.mark.integration -class TestTransferIntegration: - """集成测试标记""" - - def test_full_transfer_workflow(self, client, auth_headers, test_assets_for_transfer, test_orgs_for_transfer): - """测试完整的调拨流程""" - source_org, target_org = test_orgs_for_transfer - - # 1. 创建调拨单 - create_response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [test_assets_for_transfer[0].id], - "transfer_reason": "完整流程测试" - }, - headers=auth_headers - ) - assert create_response.status_code == 200 - transfer_id = create_response.json()["id"] - - # 2. 审批 - approve_response = client.post( - f"/api/v1/transfers/{transfer_id}/approve", - json={"approval_comment": "审批通过"}, - headers=auth_headers - ) - assert approve_response.status_code == 200 - - # 3. 开始执行 - start_response = client.post( - f"/api/v1/transfers/{transfer_id}/start", - json={}, - headers=auth_headers - ) - assert start_response.status_code == 200 - - # 4. 确认接收 - confirm_response = client.post( - f"/api/v1/transfers/{transfer_id}/confirm-receipt", - json={"receipt_note": "已接收", "receiver_name": "测试"}, - headers=auth_headers - ) - assert confirm_response.status_code == 200 - - -@pytest.mark.slow -class TestTransferSlowTests: - """慢速测试标记""" - - def test_large_batch_transfer(self, client, auth_headers, db: Session, test_orgs_for_transfer): - """测试大批量调拨""" - pass - - -@pytest.mark.smoke -class TestTransferSmoke: - """冒烟测试标记 - 核心功能快速验证""" - - def test_create_and_approve_transfer(self, client, auth_headers, test_assets_for_transfer, test_orgs_for_transfer): - """冒烟测试: 创建并审批调拨单""" - source_org, target_org = test_orgs_for_transfer - - create_response = client.post( - "/api/v1/transfers/", - json={ - "source_org_id": source_org.id, - "target_org_id": target_org.id, - "asset_ids": [test_assets_for_transfer[0].id], - "transfer_reason": "冒烟测试" - }, - headers=auth_headers - ) - assert create_response.status_code == 200 - - transfer_id = create_response.json()["id"] - approve_response = client.post( - f"/api/v1/transfers/{transfer_id}/approve", - json={"approval_comment": "冒烟测试"}, - headers=auth_headers - ) - assert approve_response.status_code == 200 diff --git a/tests/scripts/generate_comprehensive_test_report.py b/tests/scripts/generate_comprehensive_test_report.py deleted file mode 100644 index b7f916a..0000000 --- a/tests/scripts/generate_comprehensive_test_report.py +++ /dev/null @@ -1,240 +0,0 @@ -""" -测试报告生成脚本 - -生成完整的测试报告,包括: -- 测试执行摘要 -- 代码覆盖率 -- 性能测试结果 -- Bug清单 -""" - -import os -import sys -import json -from datetime import datetime -from pathlib import Path - - -def generate_test_report(): - """生成完整的测试报告""" - # 确保报告目录存在 - report_dir = Path("test_reports") - report_dir.mkdir(exist_ok=True) - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - report_file = report_dir / f"test_report_{timestamp}.md" - - with open(report_file, "w", encoding="utf-8") as f: - f.write(f"# 资产管理系统测试报告\n\n") - f.write(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") - f.write("---\n\n") - - # 测试概览 - f.write("## 📊 测试概览\n\n") - f.write("| 测试类型 | 目标数量 | 状态 |\n") - f.write("|---------|---------|------|\n") - f.write("| 后端单元测试 | 200+ | ✅ 已完成 |\n") - f.write("| 前端单元测试 | 200+ | 🚧 进行中 |\n") - f.write("| E2E测试 | 40+ | 🚧 进行中 |\n") - f.write("| 性能测试 | 10+ | ⏸ 待完成 |\n") - f.write("| 安全测试 | 20+ | ⏸ 待完成 |\n\n") - - # 后端测试详情 - f.write("## 🔧 后端测试详情\n\n") - - f.write("### API测试\n\n") - f.write("| 模块 | 测试文件 | 用例数 | 状态 |\n") - f.write("|------|---------|--------|------|\n") - f.write("| 设备类型管理 | test_device_types.py | 50+ | ✅ 完成 |\n") - f.write("| 机构网点管理 | test_organizations.py | 45+ | ✅ 完成 |\n") - f.write("| 资产管理 | test_assets.py | 100+ | 🚧 补充中 |\n") - f.write("| 认证模块 | test_auth.py | 30+ | ✅ 完成 |\n\n") - - f.write("### 服务层测试\n\n") - f.write("| 模块 | 测试文件 | 用例数 | 状态 |\n") - f.write("|------|---------|--------|------|\n") - f.write("| 认证服务 | test_auth_service.py | 40+ | ✅ 完成 |\n") - f.write("| 资产状态机 | test_asset_state_machine.py | 55+ | ✅ 完成 |\n") - f.write("| 设备类型服务 | test_device_type_service.py | 15+ | ⏸ 待创建 |\n") - f.write("| 机构服务 | test_organization_service.py | 15+ | ⏸ 待创建 |\n\n") - - # 前端测试详情 - f.write("## 🎨 前端测试详情\n\n") - - f.write("### 单元测试\n\n") - f.write("| 模块 | 测试文件 | 用例数 | 状态 |\n") - f.write("|------|---------|--------|------|\n") - f.write("| 资产列表 | AssetList.test.ts | 10+ | ✅ 已有 |\n") - f.write("| 资产Composable | useAsset.test.ts | 15+ | ✅ 已有 |\n") - f.write("| 动态表单 | DynamicFieldRenderer.test.ts | 30+ | ⏸ 待创建 |\n") - f.write("| 其他组件 | 多个文件 | 150+ | ⏸ 待创建 |\n\n") - - # E2E测试 - f.write("## 🎭 E2E测试详情\n\n") - - f.write("| 业务流程 | 测试文件 | 场景数 | 状态 |\n") - f.write("|---------|---------|--------|------|\n") - f.write("| 登录流程 | login.spec.ts | 5+ | ✅ 已有 |\n") - f.write("| 资产流程 | assets.spec.ts | 5+ | ✅ 已有 |\n") - f.write("| 设备类型管理 | device_types.spec.ts | 5+ | ⏸ 待创建 |\n") - f.write("| 机构管理 | organizations.spec.ts | 5+ | ⏸ 待创建 |\n") - f.write("| 资产分配 | allocation.spec.ts | 10+ | ⏸ 待创建 |\n") - f.write("| 批量操作 | batch_operations.spec.ts | 10+ | ⏸ 待创建 |\n\n") - - # 代码覆盖率 - f.write("## 📈 代码覆盖率目标\n\n") - f.write("```text\n") - f.write("后端目标: ≥70%\n") - f.write("前端目标: ≥70%\n") - f.write("当前估计: 待运行pytest后生成\n") - f.write("```\n\n") - - # Bug清单 - f.write("## 🐛 Bug清单\n\n") - f.write("### 已发现的问题\n\n") - f.write("| ID | 严重程度 | 描述 | 状态 |\n") - f.write("|----|---------|------|------|\n") - f.write("| BUG-001 | 中 | 某些测试用例需要实际API实现 | 🔍 待确认 |\n") - f.write("| BUG-002 | 低 | 测试数据清理可能不完整 | 🔍 待确认 |\n\n") - - # 测试用例清单 - f.write("## 📋 测试用例清单\n\n") - - f.write("### 后端测试用例\n\n") - f.write("#### 设备类型管理 (50+用例)\n") - f.write("- [x] CRUD操作 (15+用例)\n") - f.write(" - [x] 创建设备类型成功\n") - f.write(" - [x] 创建重复代码失败\n") - f.write(" - [x] 获取设备类型列表\n") - f.write(" - [x] 根据ID获取设备类型\n") - f.write(" - [x] 更新设备类型\n") - f.write(" - [x] 删除设备类型\n") - f.write(" - [x] 按分类筛选\n") - f.write(" - [x] 按状态筛选\n") - f.write(" - [x] 关键词搜索\n") - f.write(" - [x] 分页查询\n") - f.write(" - [x] 排序\n") - f.write(" - [x] 获取不存在的设备类型\n") - f.write(" - [x] 更新不存在的设备类型\n") - f.write(" - [x] 未授权访问\n") - f.write(" - [x] 参数验证\n\n") - - f.write("- [x] 动态字段配置 (10+用例)\n") - f.write(" - [x] 添加字段\n") - f.write(" - [x] 添加必填字段\n") - f.write(" - [x] 添加选择字段\n") - f.write(" - [x] 添加数字字段\n") - f.write(" - [x] 获取字段列表\n") - f.write(" - [x] 更新字段\n") - f.write(" - [x] 删除字段\n") - f.write(" - [x] 重复字段代码\n") - f.write(" - [x] 字段排序\n") - f.write(" - [x] 字段类型验证\n\n") - - f.write("- [x] 字段验证测试 (10+用例)\n") - f.write(" - [x] 字段名称验证\n") - f.write(" - [x] 字段类型验证\n") - f.write(" - [x] 字段长度验证\n") - f.write(" - [x] 选择字段选项验证\n") - f.write(" - [x] 验证规则JSON格式\n") - f.write(" - [x] placeholder和help_text\n") - f.write(" - [x] 无效字段类型\n") - f.write(" - [x] 缺少必填选项\n") - f.write(" - [x] 边界值测试\n") - f.write(" - [x] 特殊字符处理\n\n") - - f.write("- [x] 参数验证测试 (10+用例)\n") - f.write(" - [x] 类型代码验证\n") - f.write(" - [x] 类型名称验证\n") - f.write(" - [x] 描述验证\n") - f.write(" - [x] 排序验证\n") - f.write(" - [x] 状态验证\n") - f.write(" - [x] 长度限制\n") - f.write(" - [x] 格式验证\n") - f.write(" - [x] 空值处理\n") - f.write(" - [x] 特殊字符处理\n") - f.write(" - [x] SQL注入防护\n\n") - - f.write("- [x] 异常处理测试 (5+用例)\n") - f.write(" - [x] 并发创建\n") - f.write(" - [x] 更新不存在的字段\n") - f.write(" - [x] 删除不存在的设备类型\n") - f.write(" - [x] 无效JSON验证规则\n") - f.write(" - [x] 无效选项格式\n\n") - - f.write("#### 机构网点管理 (45+用例)\n") - f.write("- [x] 机构CRUD (15+用例)\n") - f.write("- [x] 树形结构 (10+用例)\n") - f.write("- [x] 递归查询 (10+用例)\n") - f.write("- [x] 机构移动 (5+用例)\n") - f.write("- [x] 并发测试 (5+用例)\n\n") - - f.write("#### 资产管理 (100+用例 - 需补充)\n") - f.write("- [ ] 资产CRUD (20+用例)\n") - f.write("- [ ] 资产编码生成 (10+用例)\n") - f.write("- [ ] 状态机转换 (15+用例)\n") - f.write("- [ ] JSONB字段 (10+用例)\n") - f.write("- [ ] 高级搜索 (10+用例)\n") - f.write("- [ ] 分页查询 (10+用例)\n") - f.write("- [ ] 批量导入 (10+用例)\n") - f.write("- [ ] 批量导出 (10+用例)\n") - f.write("- [ ] 二维码生成 (5+用例)\n") - f.write("- [ ] 并发测试 (10+用例)\n\n") - - f.write("#### 认证模块 (30+用例)\n") - f.write("- [x] 登录测试 (15+用例)\n") - f.write("- [x] Token刷新 (5+用例)\n") - f.write("- [x] 登出测试 (3+用例)\n") - f.write("- [x] 修改密码 (5+用例)\n") - f.write("- [x] 验证码 (2+用例)\n\n") - - f.write("### 服务层测试用例\n\n") - f.write("#### 认证服务 (40+用例)\n") - f.write("- [x] 登录服务 (15+用例)\n") - f.write("- [x] Token管理 (10+用例)\n") - f.write("- [x] 密码管理 (10+用例)\n") - f.write("- [x] 验证码 (5+用例)\n\n") - - f.write("#### 资产状态机 (55+用例)\n") - f.write("- [x] 状态转换规则 (20+用例)\n") - f.write("- [x] 状态转换验证 (15+用例)\n") - f.write("- [x] 状态历史记录 (10+用例)\n") - f.write("- [x] 异常状态转换 (10+用例)\n\n") - - # 建议 - f.write("## 💡 改进建议\n\n") - f.write("1. **补充资产管理测试**: test_assets.py需要大幅扩充到100+用例\n") - f.write("2. **创建服务层测试**: 设备类型服务、机构服务等\n") - f.write("3. **前端测试补充**: 需要补充约200+前端单元测试用例\n") - f.write("4. **E2E测试**: 需要补充约30+E2E测试场景\n") - f.write("5. **性能测试**: 需要补充关键接口的性能测试\n") - f.write("6. **安全测试**: 需要补充完整的安全测试用例\n\n") - - f.write("## ✅ 完成标准\n\n") - f.write("- [ ] 所有后端单元测试通过\n") - f.write("- [ ] 代码覆盖率达到70%\n") - f.write("- [ ] 所有前端单元测试通过\n") - f.write("- [ ] E2E测试通过\n") - f.write("- [ ] 性能测试通过\n") - f.write("- [ ] 安全测试通过\n\n") - - f.write("---\n\n") - f.write("**报告生成者**: 测试用例补充组\n") - f.write(f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") - - print(f"\n[OK] Test report generated: {report_file}") - print(f"\n[INFO] View report: type {report_file}") - - return report_file - - -if __name__ == "__main__": - print("=" * 60) - print("资产管理系统 - 测试报告生成器") - print("=" * 60) - - report_file = generate_test_report() - - print("\n" + "=" * 60) - print("报告生成完成!") - print("=" * 60) diff --git a/tests/scripts/generate_test_report.py b/tests/scripts/generate_test_report.py deleted file mode 100644 index 9e07cd3..0000000 --- a/tests/scripts/generate_test_report.py +++ /dev/null @@ -1,500 +0,0 @@ -""" -测试报告生成脚本 - -生成完整的测试报告,包括: -- 测试执行摘要 -- 覆盖率报告 -- 性能测试结果 -- 安全测试结果 -- Bug清单 -""" - -import os -import json -import subprocess -from datetime import datetime -from pathlib import Path - - -class TestReportGenerator: - """测试报告生成器""" - - def __init__(self, project_root: str): - self.project_root = Path(project_root) - self.report_dir = self.project_root / "test_reports" - self.report_dir.mkdir(exist_ok=True) - - self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - self.report_data = { - "timestamp": datetime.now().isoformat(), - "project": "资产管理系统", - "version": "1.0.0", - "summary": {}, - "unit_tests": {}, - "integration_tests": {}, - "e2e_tests": {}, - "coverage": {}, - "performance": {}, - "security": {}, - "bugs": [] - } - - def run_unit_tests(self): - """运行单元测试""" - print("=" * 60) - print("运行单元测试...") - print("=" * 60) - - cmd = [ - "pytest", - "-v", - "-m", "unit", - "--html=test_reports/unit_test_report.html", - "--self-contained-html", - "--json-report", - "--json-report-file=test_reports/unit_test_results.json" - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - - # 解析结果 - if os.path.exists("test_reports/unit_test_results.json"): - with open("test_reports/unit_test_results.json", "r") as f: - data = json.load(f) - self.report_data["unit_tests"] = { - "total": data.get("summary", {}).get("total", 0), - "passed": data.get("summary", {}).get("passed", 0), - "failed": data.get("summary", {}).get("failed", 0), - "skipped": data.get("summary", {}).get("skipped", 0), - "duration": data.get("summary", {}).get("duration", 0) - } - - return result.returncode == 0 - - def run_integration_tests(self): - """运行集成测试""" - print("\n" + "=" * 60) - print("运行集成测试...") - print("=" * 60) - - cmd = [ - "pytest", - "-v", - "-m", "integration", - "--html=test_reports/integration_test_report.html", - "--self-contained-html" - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - return result.returncode == 0 - - def run_coverage_tests(self): - """运行覆盖率测试""" - print("\n" + "=" * 60) - print("生成覆盖率报告...") - print("=" * 60) - - cmd = [ - "pytest", - "--cov=app", - "--cov-report=html:test_reports/htmlcov", - "--cov-report=term-missing", - "--cov-report=json:test_reports/coverage.json", - "--cov-fail-under=70" - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - - # 解析覆盖率数据 - if os.path.exists("test_reports/coverage.json"): - with open("test_reports/coverage.json", "r") as f: - data = json.load(f) - totals = data.get("totals", {}) - self.report_data["coverage"] = { - "line_coverage": totals.get("percent_covered", 0), - "lines_covered": totals.get("covered_lines", 0), - "lines_missing": totals.get("missing_lines", 0), - "num_statements": totals.get("num_statements", 0) - } - - return result.returncode == 0 - - def run_security_tests(self): - """运行安全测试""" - print("\n" + "=" * 60) - print("运行安全测试...") - print("=" * 60) - - cmd = [ - "pytest", - "-v", - "tests/security/", - "-m", "security", - "--html=test_reports/security_test_report.html" - ] - - result = subprocess.run(cmd, capture_output=True, text=True) - return result.returncode == 0 - - def collect_bugs(self): - """收集测试中发现的Bug""" - print("\n" + "=" * 60) - print("分析测试结果,收集Bug...") - print("=" * 60) - - bugs = [] - - # 从失败的测试中提取Bug - test_results = [ - "test_reports/unit_test_results.json", - "test_reports/integration_test_results.json" - ] - - for result_file in test_results: - if os.path.exists(result_file): - with open(result_file, "r") as f: - data = json.load(f) - - for test in data.get("tests", []): - if test.get("outcome") == "failed": - bugs.append({ - "test_name": test.get("name"), - "error": test.get("call", {}).get("crash", {}).get("message", ""), - "severity": "high" if "critical" in test.get("name", "").lower() else "medium", - "status": "open" - }) - - self.report_data["bugs"] = bugs - return bugs - - def generate_html_report(self): - """生成HTML测试报告""" - print("\n" + "=" * 60) - print("生成HTML测试报告...") - print("=" * 60) - - html_template = """ - - - - - - 资产管理系统 - 测试报告 - - - -
-

📊 资产管理系统 - 测试报告

- -
-
-
{total_tests}
-
总测试数
-
-
-
{passed_tests}
-
通过
-
-
-
{failed_tests}
-
失败
-
-
-
{coverage}%
-
代码覆盖率
-
-
- -

📋 测试摘要

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
测试类型总数通过失败通过率
单元测试{unit_total}{unit_passed}{unit_failed}{unit_pass_rate}%
集成测试{integration_total}{integration_passed}{integration_failed}{integration_pass_rate}%
E2E测试{e2e_total}{e2e_passed}{e2e_failed}{e2e_pass_rate}%
- -

🐛 Bug清单 ({bug_count})

-
    - {bug_items} -
- -
-

生成时间: {timestamp}

-

资产管理系统 v{version} | 测试框架: Pytest + Vitest + Playwright

-
-
- - - """ - - # 计算统计数据 - total_tests = ( - self.report_data["unit_tests"].get("total", 0) + - self.report_data["integration_tests"].get("total", 0) + - self.report_data["e2e_tests"].get("total", 0) - ) - - passed_tests = ( - self.report_data["unit_tests"].get("passed", 0) + - self.report_data["integration_tests"].get("passed", 0) + - self.report_data["e2e_tests"].get("passed", 0) - ) - - failed_tests = ( - self.report_data["unit_tests"].get("failed", 0) + - self.report_data["integration_tests"].get("failed", 0) + - self.report_data["e2e_tests"].get("failed", 0) - ) - - coverage = self.report_data["coverage"].get("line_coverage", 0) - - # 生成Bug列表HTML - bug_items = "" - for bug in self.report_data.get("bugs", []): - bug_items += f""" -
  • - {bug.get('test_name', '')}
    - {bug.get('error', '')} -
  • - """ - - html = html_template.format( - total_tests=total_tests, - passed_tests=passed_tests, - failed_tests=failed_tests, - coverage=int(coverage), - failed_class="success" if failed_tests == 0 else "danger", - coverage_class="success" if coverage >= 70 else "warning" if coverage >= 50 else "danger", - unit_total=self.report_data["unit_tests"].get("total", 0), - unit_passed=self.report_data["unit_tests"].get("passed", 0), - unit_failed=self.report_data["unit_tests"].get("failed", 0), - unit_pass_rate=0, - integration_total=self.report_data["integration_tests"].get("total", 0), - integration_passed=self.report_data["integration_tests"].get("passed", 0), - integration_failed=self.report_data["integration_tests"].get("failed", 0), - integration_pass_rate=0, - e2e_total=self.report_data["e2e_tests"].get("total", 0), - e2e_passed=self.report_data["e2e_tests"].get("passed", 0), - e2e_failed=self.report_data["e2e_tests"].get("failed", 0), - e2e_pass_rate=0, - bug_count=len(self.report_data.get("bugs", [])), - bug_items=bug_items if bug_items else "
  • 暂无Bug
  • ", - timestamp=self.report_data["timestamp"], - version=self.report_data["version"] - ) - - report_path = self.report_dir / f"test_report_{self.timestamp}.html" - with open(report_path, "w", encoding="utf-8") as f: - f.write(html) - - print(f"✓ HTML报告已生成: {report_path}") - return report_path - - def generate_json_report(self): - """生成JSON测试报告""" - json_path = self.report_dir / f"test_report_{self.timestamp}.json" - - with open(json_path, "w", encoding="utf-8") as f: - json.dump(self.report_data, f, ensure_ascii=False, indent=2) - - print(f"✓ JSON报告已生成: {json_path}") - return json_path - - def generate_all_reports(self): - """生成所有报告""" - print("\n" + "=" * 60) - print("🚀 开始生成测试报告...") - print("=" * 60) - - # 运行各类测试 - self.run_unit_tests() - self.run_integration_tests() - self.run_coverage_tests() - self.run_security_tests() - - # 收集Bug - self.collect_bugs() - - # 生成报告 - html_report = self.generate_html_report() - json_report = self.generate_json_report() - - print("\n" + "=" * 60) - print("✅ 测试报告生成完成!") - print("=" * 60) - print(f"\n📄 HTML报告: {html_report}") - print(f"📄 JSON报告: {json_report}") - print(f"📄 覆盖率报告: {self.report_dir}/htmlcov/index.html") - print(f"📄 单元测试报告: {self.report_dir}/unit_test_report.html") - print(f"📄 集成测试报告: {self.report_dir}/integration_test_report.html") - print(f"📄 安全测试报告: {self.report_dir}/security_test_report.html") - print("\n" + "=" * 60) - - -if __name__ == "__main__": - import sys - - # 项目根目录 - project_root = sys.argv[1] if len(sys.argv) > 1 else "." - - # 生成测试报告 - generator = TestReportGenerator(project_root) - generator.generate_all_reports() diff --git a/tests/security/test_security.py b/tests/security/test_security.py deleted file mode 100644 index 2c3077a..0000000 --- a/tests/security/test_security.py +++ /dev/null @@ -1,524 +0,0 @@ -""" -安全测试 - -测试内容: -- SQL注入测试 -- XSS测试 -- CSRF测试 -- 权限绕过测试 -- 敏感数据泄露测试 -- 认证绕过测试 -""" - -import pytest - - -# class TestSQLInjection: -# """测试SQL注入攻击""" -# -# def test_sql_injection_in_login(self, client: TestClient): -# """测试登录接口的SQL注入""" -# malicious_inputs = [ -# "admin' OR '1'='1", -# "admin'--", -# "admin'/*", -# "' OR 1=1--", -# "'; DROP TABLE users--", -# "admin' UNION SELECT * FROM users--", -# "' OR '1'='1' /*", -# "1' AND 1=1--", -# "admin'; INSERT INTO users VALUES--", -# ] -# -# for malicious_input in malicious_inputs: -# response = client.post( -# "/api/v1/auth/login", -# json={ -# "username": malicious_input, -# "password": "Test123", -# "captcha": "1234", -# "captcha_key": "test" -# } -# ) -# -# # 应该返回认证失败,而不是数据库错误或成功登录 -# assert response.status_code in [401, 400, 422] -# -# # 如果返回成功,说明存在SQL注入漏洞 -# if response.status_code == 200: -# pytest.fail(f"SQL注入漏洞检测: {malicious_input}") -# -# def test_sql_injection_in_search(self, client: TestClient, auth_headers): -# """测试搜索接口的SQL注入""" -# malicious_inputs = [ -# "'; DROP TABLE assets--", -# "1' OR '1'='1", -# "'; SELECT * FROM users--", -# "admin' UNION SELECT * FROM assets--", -# ] -# -# for malicious_input in malicious_inputs: -# response = client.get( -# "/api/v1/assets", -# params={"keyword": malicious_input}, -# headers=auth_headers -# ) -# -# # 应该正常返回或参数错误,不应该报数据库错误 -# assert response.status_code in [200, 400, 422] -# -# def test_sql_injection_in_id_parameter(self, client: TestClient, auth_headers): -# """测试ID参数的SQL注入""" -# malicious_ids = [ -# "1 OR 1=1", -# "1; DROP TABLE assets--", -# "1' UNION SELECT * FROM users--", -# "1' AND 1=1--", -# ] -# -# for malicious_id in malicious_ids: -# response = client.get( -# f"/api/v1/assets/{malicious_id}", -# headers=auth_headers -# ) -# -# # 应该返回404或参数错误 -# assert response.status_code in [404, 400, 422] -# -# def test_sql_injection_in_order_by(self, client: TestClient, auth_headers): -# """测试排序参数的SQL注入""" -# malicious_inputs = [ -# "id; DROP TABLE users--", -# "id OR 1=1", -# "id' AND '1'='1", -# ] -# -# for malicious_input in malicious_inputs: -# response = client.get( -# "/api/v1/assets", -# params={"sort_by": malicious_input}, -# headers=auth_headers -# ) -# -# # 应该返回参数错误 -# assert response.status_code in [400, 422] -# -# def test_second_order_sql_injection(self, client: TestClient, auth_headers): -# """测试二阶SQL注入""" -# # 先创建包含恶意代码的数据 -# malicious_data = { -# "asset_name": "test'; DROP TABLE assets--", -# "device_type_id": 1, -# "organization_id": 1 -# } -# -# create_response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json=malicious_data -# ) -# -# # 如果创建成功,尝试查询 -# if create_response.status_code == 200: -# # 查询应该不会触发SQL注入 -# response = client.get( -# "/api/v1/assets", -# headers=auth_headers -# ) -# assert response.status_code == 200 - - -# class TestXSS: -# """测试XSS跨站脚本攻击""" -# -# def test_xss_in_asset_name(self, client: TestClient, auth_headers): -# """测试资产名称的XSS""" -# xss_payloads = [ -# "", -# "", -# "", -# "javascript:alert('XSS')", -# "", -# ] -# -# for payload in xss_payloads: -# response = client.post( -# "/api/v1/assets", -# headers=auth_headers, -# json={ -# "asset_name": payload, -# "device_type_id": 1, -# "organization_id": 1 -# } -# ) -# -# if response.status_code == 200: -# # 获取数据 -# asset_id = response.json()["data"]["id"] -# get_response = client.get( -# f"/api/v1/assets/{asset_id}", -# headers=auth_headers -# ) -# -# # 验证XSS payload被转义或过滤 -# content = get_response.text -# assert "" -# -# response = client.get( -# "/api/v1/assets", -# params={"keyword": xss_payload}, -# headers=auth_headers -# ) -# -# # 验证XSS payload被转义 -# content = response.text -# assert "