forked from erp-dev/erp
fix: added pgBouncer for conn pool manage
This commit is contained in:
@@ -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):
|
||||
|
||||
512
api_man/tests.py
512
api_man/tests.py
@@ -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')
|
||||
|
||||
@@ -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
150
api_man_test_report.md
Normal 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
BIN
celerybeat-schedule-shm
Normal file
Binary file not shown.
BIN
celerybeat-schedule-wal
Normal file
BIN
celerybeat-schedule-wal
Normal file
Binary file not shown.
74166
data-bak/db-backup-20251223-093741.sql
Normal file
74166
data-bak/db-backup-20251223-093741.sql
Normal file
File diff suppressed because it is too large
Load Diff
85908
data-bak/db-backup-20251223-190004.sql
Normal file
85908
data-bak/db-backup-20251223-190004.sql
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
40
pgbouncer/pgbouncer.ini
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user