1
0
forked from erp-dev/erp

fix: added pgBouncer for conn pool manage

This commit is contained in:
2025-12-24 21:26:31 +08:00
parent 3d3f00232d
commit c832e26907
13 changed files with 160926 additions and 31 deletions

View File

@@ -161,11 +161,15 @@ class DeviceInfoSerializer(BaseSerializer):
class VehicleTransportRecordSerializer(BaseSerializer):
vehicle_type_name = serializers.CharField(source='vehicle_type.name', read_only=True)
# 显式定义 vehicle_type 字段,避免 depth=1 影响反序列化
vehicle_type = serializers.PrimaryKeyRelatedField(
queryset=basic_models.VehicleType.objects.all()
)
class Meta:
model = basic_models.VehicleTransportRecord
fields = '__all__'
depth = 1 # 展开外键关系
depth = 1 # 展开外键关系(仅影响序列化输出)
class UserSerializerSimple(serializers.ModelSerializer):

View File

@@ -4,7 +4,9 @@ from rest_framework.test import APIClient
from rest_framework import status
from basic_info.models import (
Merchant, MerchantTypeEnum, WareHouse, WarehouseTypeEnum,
Employee, EmployeeType, EmployeeStatusEnum
Employee, EmployeeType, EmployeeStatusEnum,
QuickInput, Product, ProductCategory, Supplier, Customer,
VehicleType, BankAccount, DeviceInfo, VehicleTransportRecord
)
@@ -122,6 +124,31 @@ class WarehouseAPITestCase(TestCase):
self.assertEqual(response.data['type'], WarehouseTypeEnum.SCATTERED.value)
self.assertEqual(response.data['name'], '单个仓库')
def test_filter_warehouses_by_name(self):
"""测试通过 name 查询参数过滤仓库icontains 模糊匹配)"""
WareHouse.objects.create(merchant=self.merchant, name='主仓库')
WareHouse.objects.create(merchant=self.merchant, name='备用仓库')
WareHouse.objects.create(merchant=self.merchant, name='临时仓库')
# 测试部分匹配
response = self.client.get('/api/backend/warehouses/?name=主')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '主仓库')
# 测试包含"仓库"的所有记录
response = self.client.get('/api/backend/warehouses/?name=仓库')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 3)
# 测试不匹配
response = self.client.get('/api/backend/warehouses/?name=不存在')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 0)
class EmployeeTypeAPITestCase(TestCase):
"""测试员工职位类型 API"""
@@ -194,6 +221,31 @@ class EmployeeTypeAPITestCase(TestCase):
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertFalse(EmployeeType.objects.filter(id=emp_type.id).exists())
def test_filter_employee_types_by_title(self):
"""测试通过 title 查询参数过滤职位类型icontains 模糊匹配)"""
EmployeeType.objects.create(merchant=self.merchant, title='打纸工')
EmployeeType.objects.create(merchant=self.merchant, title='滚筒工')
EmployeeType.objects.create(merchant=self.merchant, title='仓库管理员')
# 测试部分匹配
response = self.client.get('/api/backend/employee-types/?title=打纸')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['title'], '打纸工')
# 测试包含"工"的所有记录
response = self.client.get('/api/backend/employee-types/?title=工')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 2) # 打纸工、滚筒工
# 测试不匹配
response = self.client.get('/api/backend/employee-types/?title=不存在')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 0)
class EmployeeAPITestCase(TestCase):
"""测试员工 API包含 job_type 兼容性)"""
@@ -304,6 +356,25 @@ class EmployeeAPITestCase(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['job_type'], '打纸工') # 应该保持不变
def test_filter_employees_by_name(self):
"""测试通过 name 查询参数过滤员工icontains 模糊匹配)"""
Employee.objects.create(merchant=self.merchant, name='张三')
Employee.objects.create(merchant=self.merchant, name='李四')
Employee.objects.create(merchant=self.merchant, name='王五')
# 测试部分匹配
response = self.client.get('/api/backend/employees/?name=三')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '张三')
# 测试不匹配
response = self.client.get('/api/backend/employees/?name=不存在')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 0)
class UserProfileAPITestCase(TestCase):
"""测试用户资料 API"""
@@ -456,3 +527,442 @@ class UserProfileAPITestCase(TestCase):
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1) # 只应该返回当前商户的用户资料
self.assertEqual(results[0]['description'], '本商户用户资料')
# ==================== 补充缺失的 ViewSet 测试 ====================
class QuickInputAPITestCase(TestCase):
"""测试快捷输入 API支持 name 和 group 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_quick_input(self):
"""测试创建快捷输入"""
data = {
'name': '测试输入',
'value': '测试值',
'group': '测试组'
}
response = self.client.post('/api/backend/quick-inputs/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试输入')
self.assertEqual(response.data['group'], '测试组')
def test_filter_quick_inputs_by_name(self):
"""测试通过 name 查询参数过滤快捷输入"""
QuickInput.objects.create(name='输入1', value='值1', group='组1')
QuickInput.objects.create(name='输入2', value='值2', group='组1')
QuickInput.objects.create(name='输入3', value='值3', group='组2')
response = self.client.get('/api/backend/quick-inputs/?name=输入1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '输入1')
def test_filter_quick_inputs_by_group(self):
"""测试通过 group 查询参数过滤快捷输入"""
QuickInput.objects.create(name='输入1', value='值1', group='组1')
QuickInput.objects.create(name='输入2', value='值2', group='组1')
QuickInput.objects.create(name='输入3', value='值3', group='组2')
response = self.client.get('/api/backend/quick-inputs/?group=组1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 2)
for item in results:
self.assertEqual(item['group'], '组1')
def test_quick_input_groups_action(self):
"""测试获取所有分组"""
QuickInput.objects.create(name='输入1', value='值1', group='组1')
QuickInput.objects.create(name='输入2', value='值2', group='组1')
QuickInput.objects.create(name='输入3', value='值3', group='组2')
response = self.client.get('/api/backend/quick-inputs/groups/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
groups = list(response.data)
self.assertEqual(len(groups), 2)
self.assertIn('组1', groups)
self.assertIn('组2', groups)
class ProductAPITestCase(TestCase):
"""测试产品 API支持 name 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
self.category = ProductCategory.objects.create(
merchant=self.merchant,
name='测试类别',
product_prefix='TEST'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_product(self):
"""测试创建产品"""
data = {
'name': '测试产品',
'category': self.category.id
}
response = self.client.post('/api/backend/products/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试产品')
def test_filter_products_by_name(self):
"""测试通过 name 查询参数过滤产品"""
Product.objects.create(merchant=self.merchant, category=self.category, name='产品A')
Product.objects.create(merchant=self.merchant, category=self.category, name='产品B')
Product.objects.create(merchant=self.merchant, category=self.category, name='产品C')
response = self.client.get('/api/backend/products/?name=产品A')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '产品A')
class ProductCategoryAPITestCase(TestCase):
"""测试产品类别 API支持 name 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_product_category(self):
"""测试创建产品类别"""
data = {
'name': '测试类别',
'product_prefix': 'TEST'
}
response = self.client.post('/api/backend/product-categories/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试类别')
def test_filter_product_categories_by_name(self):
"""测试通过 name 查询参数过滤产品类别"""
ProductCategory.objects.create(merchant=self.merchant, name='类别A', product_prefix='A')
ProductCategory.objects.create(merchant=self.merchant, name='类别B', product_prefix='B')
response = self.client.get('/api/backend/product-categories/?name=类别A')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '类别A')
class SupplierAPITestCase(TestCase):
"""测试供应商 API支持 name 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_supplier(self):
"""测试创建供应商"""
data = {
'name': '测试供应商'
}
response = self.client.post('/api/backend/suppliers/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试供应商')
def test_filter_suppliers_by_name(self):
"""测试通过 name 查询参数过滤供应商"""
Supplier.objects.create(merchant=self.merchant, name='供应商A')
Supplier.objects.create(merchant=self.merchant, name='供应商B')
response = self.client.get('/api/backend/suppliers/?name=供应商A')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '供应商A')
class CustomerAPITestCase(TestCase):
"""测试客户 API支持 name 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
# CustomerViewSet 使用 DjangoModelPermissions需要添加权限
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
content_type = ContentType.objects.get_for_model(Customer)
permissions = Permission.objects.filter(content_type=content_type)
self.user.user_permissions.set(permissions)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_customer(self):
"""测试创建客户"""
data = {
'name': '测试客户'
}
response = self.client.post('/api/backend/customers/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试客户')
def test_filter_customers_by_name(self):
"""测试通过 name 查询参数过滤客户"""
Customer.objects.create(merchant=self.merchant, name='客户A', created_by=self.employee)
Customer.objects.create(merchant=self.merchant, name='客户B', created_by=self.employee)
response = self.client.get('/api/backend/customers/?name=客户A')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '客户A')
class VehicleTypeAPITestCase(TestCase):
"""测试车辆类型 API支持 name 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_vehicle_type(self):
"""测试创建车辆类型"""
data = {
'name': '测试车辆类型'
}
response = self.client.post('/api/backend/vehicle-types/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试车辆类型')
def test_filter_vehicle_types_by_name(self):
"""测试通过 name 查询参数过滤车辆类型"""
VehicleType.objects.create(merchant=self.merchant, name='类型A')
VehicleType.objects.create(merchant=self.merchant, name='类型B')
response = self.client.get('/api/backend/vehicle-types/?name=类型A')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '类型A')
class BankAccountAPITestCase(TestCase):
"""测试银行账户 API支持 name 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_bank_account(self):
"""测试创建银行账户"""
data = {
'name': '测试账户',
'auto_number': 'ACC001'
}
response = self.client.post('/api/backend/bank-accounts/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试账户')
def test_filter_bank_accounts_by_name(self):
"""测试通过 name 查询参数过滤银行账户"""
BankAccount.objects.create(merchant=self.merchant, name='账户A', auto_number='A001')
BankAccount.objects.create(merchant=self.merchant, name='账户B', auto_number='B001')
response = self.client.get('/api/backend/bank-accounts/?name=账户A')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '账户A')
class DeviceInfoAPITestCase(TestCase):
"""测试设备信息 API支持 name 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_device_info(self):
"""测试创建设备信息"""
data = {
'name': '测试设备'
}
response = self.client.post('/api/backend/device-info/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['name'], '测试设备')
def test_filter_device_info_by_name(self):
"""测试通过 name 查询参数过滤设备信息"""
DeviceInfo.objects.create(merchant=self.merchant, name='设备A')
DeviceInfo.objects.create(merchant=self.merchant, name='设备B')
response = self.client.get('/api/backend/device-info/?name=设备A')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['name'], '设备A')
class VehicleTransportRecordAPITestCase(TestCase):
"""测试司机车次 API支持 driver_name 查询参数)"""
def setUp(self):
self.merchant = Merchant.objects.create(
name='测试商户',
type=MerchantTypeEnum.STORE
)
self.user = User.objects.create_user(
username='testuser',
password='testpass123'
)
self.employee = Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='测试员工'
)
self.vehicle_type = VehicleType.objects.create(
merchant=self.merchant,
name='测试车辆类型'
)
self.client = APIClient()
self.client.force_authenticate(user=self.user)
def test_create_vehicle_transport_record(self):
"""测试创建司机车次
注意:由于 VehicleTransportRecordSerializer 使用 depth=1 可能影响反序列化,
如果创建失败,可能是序列化器配置问题,但过滤功能测试仍然有效。
"""
from datetime import date
data = {
'vehicle_type': self.vehicle_type.id,
'driver_name': '测试司机',
'delivery_date': date.today().isoformat()
}
response = self.client.post('/api/backend/vehicle-transport-records/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data['driver_name'], '测试司机')
self.assertEqual(response.data['vehicle_type'], self.vehicle_type.id)
def test_filter_vehicle_transport_records_by_driver_name(self):
"""测试通过 driver_name 查询参数过滤司机车次"""
from datetime import date
VehicleTransportRecord.objects.create(
merchant=self.merchant,
vehicle_type=self.vehicle_type,
driver_name='司机A',
delivery_date=date.today()
)
VehicleTransportRecord.objects.create(
merchant=self.merchant,
vehicle_type=self.vehicle_type,
driver_name='司机B',
delivery_date=date.today()
)
response = self.client.get('/api/backend/vehicle-transport-records/?driver_name=司机A')
self.assertEqual(response.status_code, status.HTTP_200_OK)
results = response.data['results'] if isinstance(response.data, dict) else response.data
self.assertEqual(len(results), 1)
self.assertEqual(results[0]['driver_name'], '司机A')

View File

@@ -124,32 +124,32 @@ class QuickInputViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
return Response(groups)
class ProductViewSet(BaseViewSet):
class ProductViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.Product.objects
serializer_class = serializers.ProductSerializer
class WareHouseViewSet(BaseViewSet):
class WareHouseViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.WareHouse.objects
serializer_class = serializers.WareHouseSerializer
class ProductCategoryViewSet(BaseViewSet):
class ProductCategoryViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.ProductCategory.objects
serializer_class = serializers.ProductCategorySerializer
class SupplierViewSet(BaseViewSet):
class SupplierViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.Supplier.objects
serializer_class = serializers.SupplierSerializer
class EmployeeViewSet(BaseViewSet):
class EmployeeViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.Employee.objects
serializer_class = serializers.EmployeeSerializer
class EmployeeTypeViewSet(BaseViewSet):
class EmployeeTypeViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.EmployeeType.objects
serializer_class = serializers.EmployeeTypeSerializer
@@ -181,17 +181,17 @@ class CustomerViewSet(BaseViewSet, BasicInfoFilterMixin):
return super().has_permission(request, view)
class VehicleTypeViewSet(BaseViewSet):
class VehicleTypeViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.VehicleType.objects
serializer_class = serializers.VehicleTypeSerializer
class BankAccountViewSet(BaseViewSet):
class BankAccountViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.BankAccount.objects
serializer_class = serializers.BankAccountSerializer
class DeviceInfoViewSet(BaseViewSet):
class DeviceInfoViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.DeviceInfo.objects
serializer_class = serializers.DeviceInfoSerializer
@@ -201,7 +201,7 @@ class VehicleTransportRecordViewSet(BaseViewSet):
serializer_class = serializers.VehicleTransportRecordSerializer
class UserProfileViewSet(BaseViewSet):
class UserProfileViewSet(BaseViewSet, BasicInfoFilterMixin):
queryset = serializers.basic_models.UserProfile.objects
serializer_class = serializers.UserProfileSerializer

150
api_man_test_report.md Normal file
View File

@@ -0,0 +1,150 @@
# api_man 模块 API 测试报告
**测试执行时间**: 2025-12-23
**测试框架**: Django TestCase + DRF APIClient
**数据库**: PostgreSQL (test_flower)
## 测试摘要
- **总测试数**: 21
- **通过**: 21 ✅
- **失败**: 0 ❌
- **执行时间**: ~2.7 秒
- **测试状态**: **全部通过**
---
## 测试详情
### 1. WarehouseAPITestCase (仓库 API 测试)
**测试类**: `api_man.tests.WarehouseAPITestCase`
| 测试方法 | 描述 | 状态 |
|---------|------|------|
| `test_create_warehouse_with_default_type` | 测试创建仓库时使用默认类型 | ✅ 通过 |
| `test_create_warehouse_with_whole_type` | 测试创建整仓 | ✅ 通过 |
| `test_create_warehouse_with_scattered_type` | 测试创建散仓 | ✅ 通过 |
| `test_list_warehouses_includes_type` | 测试列表接口返回类型字段 | ✅ 通过 |
| `test_update_warehouse_type` | 测试更新仓库类型 | ✅ 通过 |
| `test_retrieve_warehouse_includes_type` | 测试检索单个仓库时包含类型字段 | ✅ 通过 |
**测试覆盖**:
- ✅ 创建仓库(默认类型、整仓、散仓)
- ✅ 列表查询(包含类型字段)
- ✅ 更新仓库类型
- ✅ 单个仓库检索
---
### 2. EmployeeTypeAPITestCase (员工职位类型 API 测试)
**测试类**: `api_man.tests.EmployeeTypeAPITestCase`
| 测试方法 | 描述 | 状态 |
|---------|------|------|
| `test_create_employee_type` | 测试创建职位类型 | ✅ 通过 |
| `test_list_employee_types` | 测试列出所有职位类型 | ✅ 通过 |
| `test_update_employee_type` | 测试更新职位类型 | ✅ 通过 |
| `test_delete_employee_type` | 测试删除职位类型 | ✅ 通过 |
**测试覆盖**:
- ✅ CRUD 操作(创建、读取、更新、删除)
- ✅ 列表查询
---
### 3. EmployeeAPITestCase (员工 API 测试)
**测试类**: `api_man.tests.EmployeeAPITestCase`
| 测试方法 | 描述 | 状态 |
|---------|------|------|
| `test_create_employee_with_position` | 测试创建带职位的员工 | ✅ 通过 |
| `test_create_employee_without_position` | 测试创建不带职位的员工 | ✅ 通过 |
| `test_list_employees_includes_job_type` | 测试列表接口包含 job_type 字段 | ✅ 通过 |
| `test_update_employee_position` | 测试更新员工职位 | ✅ 通过 |
| `test_job_type_is_read_only` | 测试 job_type 是只读字段 | ✅ 通过 |
**测试覆盖**:
- ✅ 创建员工(带职位/不带职位)
- ✅ 列表查询(包含 job_type 兼容字段)
- ✅ 更新员工职位
- ✅ job_type 只读验证(兼容性字段)
---
### 4. UserProfileAPITestCase (用户资料 API 测试)
**测试类**: `api_man.tests.UserProfileAPITestCase`
| 测试方法 | 描述 | 状态 |
|---------|------|------|
| `test_create_user_profile` | 测试创建用户资料 | ✅ 通过 |
| `test_list_user_profiles` | 测试列出所有用户资料 | ✅ 通过 |
| `test_retrieve_user_profile` | 测试检索单个用户资料 | ✅ 通过 |
| `test_update_user_profile` | 测试更新用户资料 | ✅ 通过 |
| `test_delete_user_profile` | 测试删除用户资料 | ✅ 通过 |
| `test_user_profile_filter_by_merchant` | 测试用户资料按商户过滤 | ✅ 通过 |
**测试覆盖**:
- ✅ CRUD 操作(创建、读取、更新、删除)
- ✅ 列表查询
- ✅ 单个资料检索(包含嵌套用户对象)
- ✅ 按商户过滤(多租户隔离)
---
## 测试 API 端点
### 仓库 API
- `POST /api/backend/warehouses/` - 创建仓库
- `GET /api/backend/warehouses/` - 列表查询
- `GET /api/backend/warehouses/{id}/` - 单个查询
- `PATCH /api/backend/warehouses/{id}/` - 更新仓库
### 员工职位类型 API
- `POST /api/backend/employee-types/` - 创建职位类型
- `GET /api/backend/employee-types/` - 列表查询
- `PATCH /api/backend/employee-types/{id}/` - 更新职位类型
- `DELETE /api/backend/employee-types/{id}/` - 删除职位类型
### 员工 API
- `POST /api/backend/employees/` - 创建员工
- `GET /api/backend/employees/` - 列表查询
- `PATCH /api/backend/employees/{id}/` - 更新员工
### 用户资料 API
- `POST /api/backend/user-profiles/` - 创建用户资料
- `GET /api/backend/user-profiles/` - 列表查询
- `GET /api/backend/user-profiles/{id}/` - 单个查询
- `PATCH /api/backend/user-profiles/{id}/` - 更新用户资料
- `DELETE /api/backend/user-profiles/{id}/` - 删除用户资料
---
## 失败测试
**无失败测试**
所有 21 个测试用例均通过,未发现任何问题。
---
## 运行测试命令
```bash
# 使用环境变量配置数据库连接
DB_HOST=localhost DB_PORT=5432 DB_NAME=flower DB_USER=postgres DB_PASSWORD=postgres \
uv run python manage.py test api_man --verbosity=2 --keepdb
```
---
## 注意事项
1. **数据库要求**: 测试需要 PostgreSQL 数据库连接
2. **测试数据库**: Django 会自动创建 `test_flower` 数据库
3. **认证**: 测试使用 `APIClient.force_authenticate()` 进行身份验证
4. **数据隔离**: 每个测试用例在事务中运行,测试结束后自动回滚
---
**报告生成时间**: 2025-12-23
**测试执行环境**: Python 3.14, Django 5.2.8, DRF

BIN
celerybeat-schedule-shm Normal file

Binary file not shown.

BIN
celerybeat-schedule-wal Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,38 @@ services:
- db_data:/var/lib/postgresql/data
restart: unless-stopped
pgbouncer:
image: pgbouncer/pgbouncer:latest
container_name: pgbouncer
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DB: flower
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DATABASES_HOST: postgres
DATABASES_PORT: "5432"
DATABASES_USER: postgres
DATABASES_PASSWORD: postgres
DATABASES_DBNAME: flower
PGBOUNCER_POOL_MODE: transaction
PGBOUNCER_MAX_CLIENT_CONN: "100"
PGBOUNCER_DEFAULT_POOL_SIZE: "25"
PGBOUNCER_MIN_POOL_SIZE: "5"
PGBOUNCER_RESERVE_POOL_SIZE: "5"
PGBOUNCER_RESERVE_POOL_TIMEOUT: "3"
PGBOUNCER_SERVER_IDLE_TIMEOUT: "600"
PGBOUNCER_LOG_CONNECTIONS: "1"
PGBOUNCER_LOG_DISCONNECTIONS: "1"
PGBOUNCER_LOG_POOLER_ERRORS: "1"
ports:
- "6432:6432"
volumes:
- ./pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro
depends_on:
- postgres
restart: unless-stopped
pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
@@ -66,12 +98,13 @@ services:
PYTHONPATH: /app
DEBUG: "1"
ALLOWED_HOSTS: "localhost,127.0.0.1,0.0.0.0,8.148.215.233,yuwenerp.yuwen.cloud"
DB_HOST: postgres
DB_PORT: "5432"
DB_HOST: pgbouncer
DB_PORT: "6432"
DB_NAME: flower
DB_USER: postgres
DB_PASSWORD: postgres
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq
@@ -94,6 +127,7 @@ services:
volumes:
- .:/app
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq
@@ -101,8 +135,8 @@ services:
CELERY_BROKER_URL: amqp://guest:guest@rabbitmq:5672//
CELERY_RESULT_BACKEND: redis://redis:6379/0
PYTHONPATH: /app
DB_HOST: postgres
DB_PORT: "5432"
DB_HOST: pgbouncer
DB_PORT: "6432"
DB_NAME: flower
DB_USER: postgres
DB_PASSWORD: postgres
@@ -132,13 +166,18 @@ services:
environment:
DJANGO_SETTINGS_MODULE: flower.settings
PYTHONPATH: /app
DB_HOST: postgres
DB_PORT: "5432"
DB_HOST: pgbouncer
DB_PORT: "6432"
DB_NAME: flower
DB_USER: postgres
DB_PASSWORD: postgres
CELERY_BROKER_URL: amqp://guest:guest@rabbitmq:5672//
CELERY_RESULT_BACKEND: redis://redis:6379/0
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq
restart: unless-stopped
volumes:

View File

@@ -15,6 +15,38 @@ services:
- db_data:/var/lib/postgresql/data
restart: unless-stopped
pgbouncer:
image: pgbouncer/pgbouncer:latest
container_name: pgbouncer
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "${POSTGRES_PORT:-5432}"
POSTGRES_DB: ${POSTGRES_DB:-flower}
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
DATABASES_HOST: postgres
DATABASES_PORT: "${POSTGRES_PORT:-5432}"
DATABASES_USER: ${POSTGRES_USER:-postgres}
DATABASES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
DATABASES_DBNAME: ${POSTGRES_DB:-flower}
PGBOUNCER_POOL_MODE: transaction
PGBOUNCER_MAX_CLIENT_CONN: "100"
PGBOUNCER_DEFAULT_POOL_SIZE: "25"
PGBOUNCER_MIN_POOL_SIZE: "5"
PGBOUNCER_RESERVE_POOL_SIZE: "5"
PGBOUNCER_RESERVE_POOL_TIMEOUT: "3"
PGBOUNCER_SERVER_IDLE_TIMEOUT: "600"
PGBOUNCER_LOG_CONNECTIONS: "1"
PGBOUNCER_LOG_DISCONNECTIONS: "1"
PGBOUNCER_LOG_POOLER_ERRORS: "1"
ports:
- "${PGBOUNCER_PORT:-6432}:6432"
volumes:
- ./pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro
depends_on:
- postgres
restart: unless-stopped
redis:
image: redis:7-alpine
container_name: redis
@@ -51,14 +83,15 @@ services:
PYTHONPATH: /app
DEBUG: "${DEBUG:-0}"
ALLOWED_HOSTS: "${ALLOWED_HOSTS:-yuwenerp.yuwen.cloud}"
DB_HOST: ${DB_HOST:-postgres}
DB_PORT: "${DB_PORT:-5432}"
DB_HOST: ${DB_HOST:-pgbouncer}
DB_PORT: "${DB_PORT:-6432}"
DB_NAME: "${DB_NAME:-flower}"
DB_USER: "${DB_USER:-postgres}"
DB_PASSWORD: "${DB_PASSWORD:-postgres}"
CELERY_BROKER_URL: "${CELERY_BROKER_URL:-amqp://guest:guest@rabbitmq:5672//}"
CELERY_RESULT_BACKEND: "${CELERY_RESULT_BACKEND:-redis://redis:6379/0}"
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq
@@ -78,8 +111,8 @@ services:
- --pool=solo
environment:
PYTHONPATH: /app
DB_HOST: ${DB_HOST:-postgres}
DB_PORT: "${DB_PORT:-5432}"
DB_HOST: ${DB_HOST:-pgbouncer}
DB_PORT: "${DB_PORT:-6432}"
DB_NAME: "${DB_NAME:-flower}"
DB_USER: "${DB_USER:-postgres}"
DB_PASSWORD: "${DB_PASSWORD:-postgres}"
@@ -91,6 +124,7 @@ services:
# 也可通过 BACKUP_DIR 指定绝对路径,如 /srv/flower/data-bak
- ${BACKUP_DIR:-./data-bak}:/app/data-bak
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq
@@ -111,8 +145,8 @@ services:
environment:
DJANGO_SETTINGS_MODULE: flower.settings
PYTHONPATH: /app
DB_HOST: ${DB_HOST:-postgres}
DB_PORT: "${DB_PORT:-5432}"
DB_HOST: ${DB_HOST:-pgbouncer}
DB_PORT: "${DB_PORT:-6432}"
DB_NAME: "${DB_NAME:-flower}"
DB_USER: "${DB_USER:-postgres}"
DB_PASSWORD: "${DB_PASSWORD:-postgres}"
@@ -122,6 +156,7 @@ services:
volumes:
- celery_beat_data:/var/lib/celery
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq

View File

@@ -14,6 +14,38 @@ services:
- db_data:/var/lib/postgresql/data
restart: unless-stopped
pgbouncer:
image: pgbouncer/pgbouncer:latest
container_name: pgbouncer
environment:
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_DB: flower
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DATABASES_HOST: postgres
DATABASES_PORT: "5432"
DATABASES_USER: postgres
DATABASES_PASSWORD: postgres
DATABASES_DBNAME: flower
PGBOUNCER_POOL_MODE: transaction
PGBOUNCER_MAX_CLIENT_CONN: "100"
PGBOUNCER_DEFAULT_POOL_SIZE: "25"
PGBOUNCER_MIN_POOL_SIZE: "5"
PGBOUNCER_RESERVE_POOL_SIZE: "5"
PGBOUNCER_RESERVE_POOL_TIMEOUT: "3"
PGBOUNCER_SERVER_IDLE_TIMEOUT: "600"
PGBOUNCER_LOG_CONNECTIONS: "1"
PGBOUNCER_LOG_DISCONNECTIONS: "1"
PGBOUNCER_LOG_POOLER_ERRORS: "1"
ports:
- "6432:6432"
volumes:
- ./pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro
depends_on:
- postgres
restart: unless-stopped
pgadmin:
image: dpage/pgadmin4:latest
container_name: pgadmin
@@ -68,12 +100,13 @@ services:
PYTHONPATH: /app
DEBUG: "1"
ALLOWED_HOSTS: "localhost,127.0.0.1,0.0.0.0,8.148.215.233,yuwenerp.yuwen.cloud"
DB_HOST: postgres
DB_PORT: "5432"
DB_HOST: pgbouncer
DB_PORT: "6432"
DB_NAME: flower
DB_USER: postgres
DB_PASSWORD: postgres
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq
@@ -103,11 +136,16 @@ services:
CELERY_BROKER_URL: amqp://guest:guest@rabbitmq:5672//
CELERY_RESULT_BACKEND: redis://redis:6379/0
PYTHONPATH: /app
DB_HOST: postgres
DB_PORT: "5432"
DB_HOST: pgbouncer
DB_PORT: "6432"
DB_NAME: flower
DB_USER: postgres
DB_PASSWORD: postgres
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq
restart: unless-stopped
celery_beat:
@@ -131,13 +169,18 @@ services:
environment:
DJANGO_SETTINGS_MODULE: flower.settings
PYTHONPATH: /app
DB_HOST: postgres
DB_PORT: "5432"
DB_HOST: pgbouncer
DB_PORT: "6432"
DB_NAME: flower
DB_USER: postgres
DB_PASSWORD: postgres
CELERY_BROKER_URL: amqp://guest:guest@rabbitmq:5672//
CELERY_RESULT_BACKEND: redis://redis:6379/0
depends_on:
- pgbouncer
- postgres
- redis
- rabbitmq
restart: unless-stopped
volumes:

View File

@@ -196,7 +196,7 @@ DATABASES = {
'HOST': env('DB_HOST', default='localhost'),
'PORT': env('DB_PORT', default='5432'),
'CONN_HEALTH_CHECK': True,
'CONN_MAX_AGE': 60,
'CONN_MAX_AGE': 0, # 使用 PgBouncer 连接池时,必须设置为 0
}
}

40
pgbouncer/pgbouncer.ini Normal file
View File

@@ -0,0 +1,40 @@
[databases]
; 数据库配置将通过环境变量 DATABASES_* 自动生成
; 如果环境变量未设置,可以在这里手动配置:
; flower = host=postgres port=5432 dbname=flower
[pgbouncer]
; 监听地址和端口
listen_addr = 0.0.0.0
listen_port = 6432
; 认证方式trust 表示信任所有连接(适用于容器内部网络)
; 生产环境建议使用 md5 或 scram-sha-256
auth_type = trust
; 连接池配置
; pool_mode: session, transaction, statement
; transaction 模式最适合 Django每个事务结束后释放连接
pool_mode = transaction
max_client_conn = 100
default_pool_size = 25
min_pool_size = 5
reserve_pool_size = 5
reserve_pool_timeout = 3
; 连接超时设置
server_connect_timeout = 15
server_idle_timeout = 600
query_timeout = 0
query_wait_timeout = 120
; 日志配置
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
; 其他设置
; 忽略 PostgreSQL 16 的额外启动参数
ignore_startup_parameters = extra_float_digits
application_name_add_host = 1