From 642d1673052e79fbe422563c9dd1ea340ffd4b2d Mon Sep 17 00:00:00 2001 From: colaftc Date: Thu, 4 Dec 2025 14:47:30 +0800 Subject: [PATCH] feat: api_v2 --- api_v2/__init__.py | 0 api_v2/apps.py | 6 +++ api_v2/tests.py | 53 +++++++++++++++++++++ api_v2/urls.py | 8 ++++ api_v2/user_quick_create.md | 44 ++++++++++++++++++ api_v2/views/__init__.py | 8 ++++ api_v2/views/users.py | 92 +++++++++++++++++++++++++++++++++++++ flower/settings.py | 1 + flower/urls.py | 1 + 9 files changed, 213 insertions(+) create mode 100644 api_v2/__init__.py create mode 100644 api_v2/apps.py create mode 100644 api_v2/tests.py create mode 100644 api_v2/urls.py create mode 100644 api_v2/user_quick_create.md create mode 100644 api_v2/views/__init__.py create mode 100644 api_v2/views/users.py diff --git a/api_v2/__init__.py b/api_v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api_v2/apps.py b/api_v2/apps.py new file mode 100644 index 0000000..ca7483a --- /dev/null +++ b/api_v2/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiV2Config(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'api_v2' diff --git a/api_v2/tests.py b/api_v2/tests.py new file mode 100644 index 0000000..d4327e7 --- /dev/null +++ b/api_v2/tests.py @@ -0,0 +1,53 @@ +from django.contrib.auth import get_user_model +from rest_framework.test import APIClient +from django.test import TestCase + +from basic_info import models as basic_models + + +class QuickCreateEmployeeUserAPITest(TestCase): + def setUp(self): + self.client = APIClient() + self.merchant = basic_models.Merchant.objects.create( + name='测试商户', + type=basic_models.MerchantTypeEnum.FACTORY, + ) + self.admin_user = get_user_model().objects.create_user(username='admin', password='pass12345') + self.admin_employee = basic_models.Employee.objects.create( + merchant=self.merchant, + sys_user=self.admin_user, + name='管理员', + ) + self.client.force_authenticate(user=self.admin_user) + self.url = '/api/v2/users/quick-create/' + self.payload = { + 'username': 'new_user', + 'password': 'strongPass#1', + 'display_name': '新员工', + 'merchant_id': self.merchant.id, + 'mobile': '13800000000', + } + + def test_quick_create_employee_user_success(self): + response = self.client.post(self.url, self.payload, format='json') + self.assertEqual(response.status_code, 201) + data = response.data + self.assertIn('user', data) + self.assertIn('employee', data) + self.assertEqual(data['user']['username'], self.payload['username']) + + created_user = get_user_model().objects.get(username=self.payload['username']) + self.assertEqual(created_user.employee.merchant, self.merchant) + self.assertEqual(created_user.employee.name, self.payload['display_name']) + + def test_quick_create_employee_user_duplicate_username(self): + get_user_model().objects.create_user(username=self.payload['username'], password='pass12345') + response = self.client.post(self.url, self.payload, format='json') + self.assertEqual(response.status_code, 400) + self.assertIn('用户名已存在', str(response.data)) + + def test_quick_create_employee_user_invalid_merchant(self): + payload = {**self.payload, 'merchant_id': 9999} + response = self.client.post(self.url, payload, format='json') + self.assertEqual(response.status_code, 400) + self.assertIn('商户不存在', str(response.data)) diff --git a/api_v2/urls.py b/api_v2/urls.py new file mode 100644 index 0000000..9f6d6b5 --- /dev/null +++ b/api_v2/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from api_v2.views import QuickCreateEmployeeUserView + +urlpatterns = [ + path('users/quick-create/', QuickCreateEmployeeUserView.as_view(), name='api_v2_user_quick_create'), +] + diff --git a/api_v2/user_quick_create.md b/api_v2/user_quick_create.md new file mode 100644 index 0000000..f492a5f --- /dev/null +++ b/api_v2/user_quick_create.md @@ -0,0 +1,44 @@ +# 快速创建员工用户 API + +- **URL**: `POST /api/v2/users/quick-create/` +- **说明**: 一次性创建 Django User 与 `basic_info.Employee` 并建立关联,为前端提供统一的员工开通入口。 +- **权限**: 需要登录(任意已绑定员工的用户)。 + +## 请求体 + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `username` | string | 是 | 登录名,全局唯一 | +| `password` | string | 是 | 至少 6 位密码 | +| `display_name` | string | 是 | 员工姓名,同时写入 User.first_name | +| `merchant_id` | int | 是 | 员工所属商户 | +| `email` | string | 否 | 邮箱 | +| `mobile` | string | 否 | 联系电话 | +| `area` | string | 否 | 区域信息 | +| `description` | string | 否 | 备注 | +| `status` | enum | 否 | 员工状态,默认 `active` | + +## 响应示例 + +```json +{ + "user": { + "id": 12, + "username": "new_user", + "display_name": "新员工" + }, + "employee": { + "id": 45, + "name": "新员工", + "status": "active", + "merchant": 3 + } +} +``` + +## 失败响应 + +- `400 用户名已存在` +- `400 商户不存在` +- `401 未认证` + diff --git a/api_v2/views/__init__.py b/api_v2/views/__init__.py new file mode 100644 index 0000000..32e0555 --- /dev/null +++ b/api_v2/views/__init__.py @@ -0,0 +1,8 @@ +""" +api_v2 视图包。 +""" + +from .users import QuickCreateEmployeeUserView + +__all__ = ['QuickCreateEmployeeUserView'] + diff --git a/api_v2/views/users.py b/api_v2/views/users.py new file mode 100644 index 0000000..77213db --- /dev/null +++ b/api_v2/views/users.py @@ -0,0 +1,92 @@ +from django.contrib.auth import get_user_model +from django.db import transaction +from rest_framework import serializers, status +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from basic_info import models as basic_models + +User = get_user_model() + + +class QuickEmployeeUserCreateSerializer(serializers.Serializer): + username = serializers.CharField(max_length=150) + password = serializers.CharField(write_only=True, min_length=6) + display_name = serializers.CharField(max_length=100) + merchant_id = serializers.IntegerField() + email = serializers.EmailField(required=False, allow_blank=True, allow_null=True) + mobile = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=20) + area = serializers.CharField(required=False, allow_blank=True, allow_null=True, max_length=100) + description = serializers.CharField(required=False, allow_blank=True, allow_null=True) + status = serializers.ChoiceField( + choices=basic_models.EmployeeStatusEnum.choices, + required=False, + default=basic_models.EmployeeStatusEnum.ACTIVE, + ) + + def validate_username(self, value): + if User.objects.filter(username=value).exists(): + raise serializers.ValidationError('用户名已存在') + return value + + def validate_merchant_id(self, value): + if not basic_models.Merchant.objects.filter(id=value).exists(): + raise serializers.ValidationError('商户不存在') + return value + + +class QuickCreateEmployeeUserView(APIView): + """ + 快速创建系统用户 + 关联员工。 + + POST /api/v2/users/quick-create/ + """ + + permission_classes = [IsAuthenticated] + + def post(self, request): + serializer = QuickEmployeeUserCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + + merchant = basic_models.Merchant.objects.get(id=data['merchant_id']) + display_name = data['display_name'] + status_value = data.get('status') or basic_models.EmployeeStatusEnum.ACTIVE + + with transaction.atomic(): + user = User.objects.create_user( + username=data['username'], + password=data['password'], + email=data.get('email') or '', + ) + user.first_name = display_name + user.save(update_fields=['first_name']) + + employee = basic_models.Employee.objects.create( + merchant=merchant, + sys_user=user, + name=display_name, + mobile=data.get('mobile') or '', + area=data.get('area') or '', + description=data.get('description') or '', + status=status_value, + ) + + return Response( + { + 'user': { + 'id': user.id, + 'username': user.username, + 'display_name': display_name, + }, + 'employee': { + 'id': employee.id, + 'name': employee.name, + 'status': employee.status, + 'merchant': merchant.id, + }, + }, + status=status.HTTP_201_CREATED, + ) + diff --git a/flower/settings.py b/flower/settings.py index 7d9c8a6..f49c95b 100644 --- a/flower/settings.py +++ b/flower/settings.py @@ -110,6 +110,7 @@ INSTALLED_APPS = [ 'printing', 'stateflow', 'sse', + 'api_v2', ] MIDDLEWARE = [ diff --git a/flower/urls.py b/flower/urls.py index cd17e10..d8c11bc 100644 --- a/flower/urls.py +++ b/flower/urls.py @@ -55,6 +55,7 @@ urlpatterns = [ path('admin/', admin.site.urls), path('api/v1/', include('api_v1.urls')), + path('api/v2/', include('api_v2.urls')), path('api/backend/', include('api_man.urls')), # sse 相关端点