1
0
forked from erp-dev/erp

feat: agent api

This commit is contained in:
2026-04-14 18:05:56 +08:00
parent 0167478a25
commit 714d6287fe
6 changed files with 374 additions and 0 deletions

View File

@@ -10,9 +10,11 @@ from rest_framework.test import APIClient, APIRequestFactory
from basic_info import models as basic_models
from printing import models as printing_models
from business import models as business_models
from shipment import models as shipment_models
from api_v2.views.printing import PrintingJobByCustomerView
from django.contrib.contenttypes.models import ContentType
from stateflow import models as stateflow_models
from django.test.utils import override_settings
class QuickCreateEmployeeUserAPITest(TestCase):
@@ -277,6 +279,144 @@ class PrintingJobByCustomerAPITest(TestCase):
self.assertEqual(data['billed_quantity'], '20.00')
@override_settings(AGENT_ACCESS_KEY='agent-test-key')
class AgentUnshippedShipmentListAPITest(TestCase):
def setUp(self):
self.client = APIClient()
self.url = '/api/v2/ai/shipments/unshipped/'
self.merchant = basic_models.Merchant.objects.create(
name='Agent商户',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.other_merchant = basic_models.Merchant.objects.create(
name='其他商户',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='客户A',
area='华东',
created_by=None,
)
self.other_customer = basic_models.Customer.objects.create(
merchant=self.other_merchant,
name='客户B',
area='华东',
created_by=None,
)
self.unshipped_target = shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer,
shipment_date=datetime.date(2026, 4, 14),
area='华东',
address='',
contact_name='张三',
contact_phone='13800000000',
remark='目标记录',
)
self.unshipped_other_area = shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer,
shipment_date=datetime.date(2026, 4, 14),
area='华南',
)
delivery = shipment_models.ShipmentDelivery.objects.create(
merchant=self.merchant,
driver_name='李司机',
vehicle_trip='TRIP-001',
)
self.shipped_shipment = shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer,
shipment_date=datetime.date(2026, 4, 14),
area='华东',
delivery=delivery,
)
self.other_merchant_shipment = shipment_models.Shipment.objects.create(
merchant=self.other_merchant,
customer=self.other_customer,
shipment_date=datetime.date(2026, 4, 14),
area='华东',
)
def _headers(self, key='agent-test-key'):
return {'HTTP_AUTHORIZATION': key}
def test_list_unshipped_shipments_success(self):
response = self.client.get(
self.url,
{'merchant_id': self.merchant.id, 'area': '华东'},
**self._headers(),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
self.assertEqual(response.data['results'][0]['id'], self.unshipped_target.id)
self.assertEqual(response.data['results'][0]['customer_name'], '客户A')
self.assertIsNone(response.data['results'][0]['delivery'])
def test_list_unshipped_shipments_requires_access_key(self):
response = self.client.get(
self.url,
{'merchant_id': self.merchant.id, 'area': '华东'},
)
self.assertEqual(response.status_code, 401)
def test_list_unshipped_shipments_rejects_invalid_access_key(self):
response = self.client.get(
self.url,
{'merchant_id': self.merchant.id, 'area': '华东'},
**self._headers(key='wrong-key'),
)
self.assertEqual(response.status_code, 401)
def test_list_unshipped_shipments_requires_merchant_and_area(self):
response = self.client.get(
self.url,
{'area': '华东'},
**self._headers(),
)
self.assertEqual(response.status_code, 400)
self.assertIn('merchant_id', response.data)
response = self.client.get(
self.url,
{'merchant_id': self.merchant.id},
**self._headers(),
)
self.assertEqual(response.status_code, 400)
self.assertIn('area', response.data)
def test_list_unshipped_shipments_filters_by_merchant(self):
response = self.client.get(
self.url,
{'merchant_id': self.other_merchant.id, 'area': '华东'},
**self._headers(),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 1)
self.assertEqual(response.data['results'][0]['id'], self.other_merchant_shipment.id)
def test_list_unshipped_shipments_supports_limit_offset(self):
shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer,
shipment_date=datetime.date(2026, 4, 15),
area='华东',
remark='第二条',
)
response = self.client.get(
self.url,
{'merchant_id': self.merchant.id, 'area': '华东', 'limit': 1, 'offset': 0},
**self._headers(),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['count'], 2)
self.assertEqual(len(response.data['results']), 1)
class PrintingJobBatchAdvanceV2APITest(TestCase):
def setUp(self):
self.client = APIClient()

View File

@@ -24,6 +24,7 @@ from api_v2.views import (
MissionReplyListCreateView,
MissionReplyRejectView,
MissionSetUrgentView,
AgentUnshippedShipmentListView,
)
from api_v2.views.basic_info import CustomerEmployeeBindingView, MyVisiblePagesView
@@ -44,6 +45,7 @@ urlpatterns = [
path('plate-orders/batch-update/', PlateOrderBatchUpdateView.as_view(), name='api_v2_plate_order_batch_update'),
path('stateflow/business-objects/clone/', BusinessObjectCloneView.as_view(), name='api_v2_stateflow_business_object_clone'),
path('content-types/', ContentTypeListView.as_view(), name='api_v2_content_type_list'),
path('ai/shipments/unshipped/', AgentUnshippedShipmentListView.as_view(), name='api_v2_ai_unshipped_shipment_list'),
path('mission-categories/', MissionCategoryListCreateView.as_view(), name='api_v2_mission_category_list_create'),
path('mission-categories/<int:category_id>/', MissionCategoryDetailView.as_view(), name='api_v2_mission_category_detail'),
path('missions/', MissionListCreateView.as_view(), name='api_v2_mission_list_create'),

View File

@@ -29,6 +29,7 @@ from .mission import (
MissionReplyRejectView,
MissionSetUrgentView,
)
from .ai import AgentUnshippedShipmentListView
__all__ = [
'HealthCheckView',
@@ -55,4 +56,5 @@ __all__ = [
'MissionReplyRejectView',
'MissionSetUrgentView',
'ContentTypeListView',
'AgentUnshippedShipmentListView',
]

136
api_v2/views/ai.py Normal file
View File

@@ -0,0 +1,136 @@
import logging
from django.conf import settings
from rest_framework import authentication, mixins, serializers
from rest_framework.authentication import get_authorization_header
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from basic_info import models as basic_models
from flower.viewsets import LimitedLimitOffsetPagination
from shipment.models import Shipment
logger = logging.getLogger(__name__)
class AgentAccessPrincipal:
id = None
username = "agent"
is_staff = False
is_superuser = False
is_anonymous = False
is_authenticated = True
class AgentAccessKeyAuthentication(authentication.BaseAuthentication):
"""
使用固定 AGENT_ACCESS_KEY 的轻量认证。
约定:
- 通过 Authorization 头直接传入密钥
- 不使用 Bearer 前缀
"""
def authenticate(self, request):
configured_key = str(getattr(settings, "AGENT_ACCESS_KEY", "") or "").strip()
if not configured_key:
logger.warning("Agent API authentication failed: AGENT_ACCESS_KEY is not configured")
raise AuthenticationFailed("AGENT_ACCESS_KEY 未配置")
provided_key = get_authorization_header(request).decode("utf-8").strip()
if not provided_key:
raise AuthenticationFailed("缺少 Authorization 请求头")
if provided_key != configured_key:
raise AuthenticationFailed("AGENT_ACCESS_KEY 无效")
return AgentAccessPrincipal(), None
def authenticate_header(self, request):
return "AgentAccessKey"
class AgentUnshippedShipmentQuerySerializer(serializers.Serializer):
merchant_id = serializers.IntegerField(min_value=1)
area = serializers.CharField(allow_blank=False, max_length=30)
limit = serializers.IntegerField(required=False, min_value=1)
offset = serializers.IntegerField(required=False, min_value=0)
def validate(self, attrs):
area = attrs["area"].strip()
if not area:
raise serializers.ValidationError({"area": "地区不能为空"})
merchant = basic_models.Merchant.objects.filter(id=attrs["merchant_id"]).first()
if merchant is None:
raise serializers.ValidationError({"merchant_id": "商户不存在"})
attrs["area"] = area
attrs["merchant"] = merchant
return attrs
class AgentShipmentListSerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name", read_only=True)
status_display = serializers.CharField(source="get_status_display", read_only=True)
class Meta:
model = Shipment
fields = [
"id",
"merchant_id",
"customer",
"customer_name",
"shipment_date",
"address",
"contact_name",
"contact_phone",
"area",
"remark",
"status",
"status_display",
"external_id",
"delivery",
"created_at",
"updated_at",
]
class AgentUnshippedShipmentListView(mixins.ListModelMixin, GenericAPIView):
"""
Agent 专用:查询尚未进入送货单的出货单列表。
GET /api/v2/ai/shipments/unshipped/?merchant_id=<id>&area=<地区>&limit=<n>&offset=<n>
"""
authentication_classes = [AgentAccessKeyAuthentication]
permission_classes = [IsAuthenticated]
serializer_class = AgentShipmentListSerializer
pagination_class = LimitedLimitOffsetPagination
def get(self, request):
query_serializer = AgentUnshippedShipmentQuerySerializer(data=request.query_params)
query_serializer.is_valid(raise_exception=True)
self.validated_query = query_serializer.validated_data
return self.list(request)
def get_queryset(self):
validated_query = getattr(self, "validated_query", None)
if validated_query is None:
query_serializer = AgentUnshippedShipmentQuerySerializer(data=self.request.query_params)
query_serializer.is_valid(raise_exception=True)
validated_query = query_serializer.validated_data
merchant = validated_query["merchant"]
area = validated_query["area"]
return (
Shipment.objects.filter(
merchant=merchant,
area=area,
delivery__isnull=True,
)
.select_related("customer", "merchant")
.order_by("-created_at", "-id")
)