diff --git a/backend/config/__pycache__/__init__.cpython-312.pyc b/backend/config/__pycache__/__init__.cpython-312.pyc index eaacd89..4dc6672 100644 Binary files a/backend/config/__pycache__/__init__.cpython-312.pyc and b/backend/config/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/config/__pycache__/settings.cpython-312.pyc b/backend/config/__pycache__/settings.cpython-312.pyc index 8180c21..340c2b4 100644 Binary files a/backend/config/__pycache__/settings.cpython-312.pyc and b/backend/config/__pycache__/settings.cpython-312.pyc differ diff --git a/backend/config/__pycache__/urls.cpython-312.pyc b/backend/config/__pycache__/urls.cpython-312.pyc index ee1d8c5..6343ef6 100644 Binary files a/backend/config/__pycache__/urls.cpython-312.pyc and b/backend/config/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index 6e2c1da..5e31f0f 100644 Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ diff --git a/backend/requirements.txt b/backend/requirements.txt index c1117d8..2b17c5a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -19,3 +19,4 @@ uritemplate==4.2.0 wechatpayv3==2.0.1 drf-spectacular-sidecar==2026.1.1 gunicorn==21.2.0 +requests diff --git a/backend/shop/__pycache__/__init__.cpython-312.pyc b/backend/shop/__pycache__/__init__.cpython-312.pyc index e32a5b1..3c80300 100644 Binary files a/backend/shop/__pycache__/__init__.cpython-312.pyc and b/backend/shop/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/admin.cpython-312.pyc b/backend/shop/__pycache__/admin.cpython-312.pyc index 62e2bfa..a844a80 100644 Binary files a/backend/shop/__pycache__/admin.cpython-312.pyc and b/backend/shop/__pycache__/admin.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/apps.cpython-312.pyc b/backend/shop/__pycache__/apps.cpython-312.pyc index f4d538f..34fd6d9 100644 Binary files a/backend/shop/__pycache__/apps.cpython-312.pyc and b/backend/shop/__pycache__/apps.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/models.cpython-312.pyc b/backend/shop/__pycache__/models.cpython-312.pyc index 9ba06d1..76b3ce2 100644 Binary files a/backend/shop/__pycache__/models.cpython-312.pyc and b/backend/shop/__pycache__/models.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/serializers.cpython-312.pyc b/backend/shop/__pycache__/serializers.cpython-312.pyc index 0a983e4..7459f72 100644 Binary files a/backend/shop/__pycache__/serializers.cpython-312.pyc and b/backend/shop/__pycache__/serializers.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/urls.cpython-312.pyc b/backend/shop/__pycache__/urls.cpython-312.pyc index 716190f..e427f95 100644 Binary files a/backend/shop/__pycache__/urls.cpython-312.pyc and b/backend/shop/__pycache__/urls.cpython-312.pyc differ diff --git a/backend/shop/__pycache__/views.cpython-312.pyc b/backend/shop/__pycache__/views.cpython-312.pyc index bb7436f..45a7d66 100644 Binary files a/backend/shop/__pycache__/views.cpython-312.pyc and b/backend/shop/__pycache__/views.cpython-312.pyc differ diff --git a/backend/shop/admin.py b/backend/shop/admin.py index b60089e..4c3eed3 100644 --- a/backend/shop/admin.py +++ b/backend/shop/admin.py @@ -77,13 +77,13 @@ class WeChatPayConfigAdmin(ModelAdmin): @admin.register(ESP32Config) class ESP32ConfigAdmin(ModelAdmin): form = ESP32ConfigAdminForm - list_display = ('name', 'chip_type', 'price', 'has_camera', 'has_microphone') + list_display = ('name', 'chip_type', 'price', 'stock', 'has_camera', 'has_microphone') list_filter = ('chip_type', 'has_camera') search_fields = ('name', 'description') inlines = [ProductFeatureInline] fieldsets = ( ('基本信息', { - 'fields': ('name', 'price', 'description') + 'fields': ('name', 'price', 'stock', 'description') }), ('硬件参数', { 'fields': ('chip_type', 'flash_size', 'ram_size', 'has_camera', 'has_microphone') @@ -209,7 +209,7 @@ class SalespersonAdmin(ModelAdmin): @admin.register(Order) class OrderAdmin(ModelAdmin): - list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'salesperson', 'created_at') + list_display = ('id', 'customer_name', 'config', 'total_price', 'status', 'courier_name', 'tracking_number', 'salesperson', 'created_at') list_filter = ('status', 'salesperson', 'created_at') search_fields = ('id', 'customer_name', 'phone_number', 'wechat_trade_no') readonly_fields = ('total_price', 'created_at', 'wechat_trade_no') @@ -221,6 +221,9 @@ class OrderAdmin(ModelAdmin): ('客户信息', { 'fields': ('customer_name', 'phone_number', 'shipping_address') }), + ('物流信息', { + 'fields': ('courier_name', 'tracking_number') + }), ('销售归属', { 'fields': ('salesperson',) }), diff --git a/backend/shop/migrations/0014_esp32config_stock_order_courier_name_and_more.py b/backend/shop/migrations/0014_esp32config_stock_order_courier_name_and_more.py new file mode 100644 index 0000000..b7cb7d2 --- /dev/null +++ b/backend/shop/migrations/0014_esp32config_stock_order_courier_name_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-02-10 15:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0013_order_out_trade_no'), + ] + + operations = [ + migrations.AddField( + model_name='esp32config', + name='stock', + field=models.IntegerField(default=0, verbose_name='库存数量'), + ), + migrations.AddField( + model_name='order', + name='courier_name', + field=models.CharField(blank=True, max_length=50, null=True, verbose_name='快递公司'), + ), + migrations.AddField( + model_name='order', + name='tracking_number', + field=models.CharField(blank=True, max_length=100, null=True, verbose_name='快递单号'), + ), + ] diff --git a/backend/shop/migrations/__pycache__/0001_initial.cpython-312.pyc b/backend/shop/migrations/__pycache__/0001_initial.cpython-312.pyc index 34f7cba..c2255e2 100644 Binary files a/backend/shop/migrations/__pycache__/0001_initial.cpython-312.pyc and b/backend/shop/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/backend/shop/migrations/__pycache__/0002_order_customer_name_order_phone_number_and_more.cpython-312.pyc b/backend/shop/migrations/__pycache__/0002_order_customer_name_order_phone_number_and_more.cpython-312.pyc index 656f003..083a119 100644 Binary files a/backend/shop/migrations/__pycache__/0002_order_customer_name_order_phone_number_and_more.cpython-312.pyc and b/backend/shop/migrations/__pycache__/0002_order_customer_name_order_phone_number_and_more.cpython-312.pyc differ diff --git a/backend/shop/migrations/__pycache__/0003_salesperson_alter_esp32config_options_and_more.cpython-312.pyc b/backend/shop/migrations/__pycache__/0003_salesperson_alter_esp32config_options_and_more.cpython-312.pyc index 8d69258..fd712b5 100644 Binary files a/backend/shop/migrations/__pycache__/0003_salesperson_alter_esp32config_options_and_more.cpython-312.pyc and b/backend/shop/migrations/__pycache__/0003_salesperson_alter_esp32config_options_and_more.cpython-312.pyc differ diff --git a/backend/shop/migrations/__pycache__/0004_wechatpayconfig_alter_esp32config_id_alter_order_id_and_more.cpython-312.pyc b/backend/shop/migrations/__pycache__/0004_wechatpayconfig_alter_esp32config_id_alter_order_id_and_more.cpython-312.pyc index d4fc861..f06f933 100644 Binary files a/backend/shop/migrations/__pycache__/0004_wechatpayconfig_alter_esp32config_id_alter_order_id_and_more.cpython-312.pyc and b/backend/shop/migrations/__pycache__/0004_wechatpayconfig_alter_esp32config_id_alter_order_id_and_more.cpython-312.pyc differ diff --git a/backend/shop/migrations/__pycache__/0005_service_alter_esp32config_id_alter_order_id_and_more.cpython-312.pyc b/backend/shop/migrations/__pycache__/0005_service_alter_esp32config_id_alter_order_id_and_more.cpython-312.pyc index 5dd8cf1..19e3997 100644 Binary files a/backend/shop/migrations/__pycache__/0005_service_alter_esp32config_id_alter_order_id_and_more.cpython-312.pyc and b/backend/shop/migrations/__pycache__/0005_service_alter_esp32config_id_alter_order_id_and_more.cpython-312.pyc differ diff --git a/backend/shop/migrations/__pycache__/0006_arservice_esp32config_detail_image_and_more.cpython-312.pyc b/backend/shop/migrations/__pycache__/0006_arservice_esp32config_detail_image_and_more.cpython-312.pyc index 49b6045..3ea1411 100644 Binary files a/backend/shop/migrations/__pycache__/0006_arservice_esp32config_detail_image_and_more.cpython-312.pyc and b/backend/shop/migrations/__pycache__/0006_arservice_esp32config_detail_image_and_more.cpython-312.pyc differ diff --git a/backend/shop/migrations/__pycache__/__init__.cpython-312.pyc b/backend/shop/migrations/__pycache__/__init__.cpython-312.pyc index f8091b3..3030ee0 100644 Binary files a/backend/shop/migrations/__pycache__/__init__.cpython-312.pyc and b/backend/shop/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/shop/models.py b/backend/shop/models.py index c241244..5fec7ab 100644 --- a/backend/shop/models.py +++ b/backend/shop/models.py @@ -15,6 +15,7 @@ class ESP32Config(models.Model): ram_size = models.IntegerField(verbose_name="PSRAM大小(MB)", default=2) has_camera = models.BooleanField(default=False, verbose_name="是否包含摄像头") has_microphone = models.BooleanField(default=False, verbose_name="是否包含麦克风") + stock = models.IntegerField(default=0, verbose_name="库存数量") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="价格") description = models.TextField(verbose_name="描述", blank=True) detail_image = models.ImageField(upload_to='products/details/', blank=True, null=True, verbose_name="详情页长图 (上传)") @@ -120,6 +121,10 @@ class Order(models.Model): phone_number = models.CharField(max_length=20, verbose_name="联系电话", default="") shipping_address = models.TextField(verbose_name="发货地址", default="") + # 物流信息 + courier_name = models.CharField(max_length=50, blank=True, null=True, verbose_name="快递公司") + tracking_number = models.CharField(max_length=100, blank=True, null=True, verbose_name="快递单号") + # 微信支付相关字段 out_trade_no = models.CharField(max_length=100, blank=True, null=True, verbose_name="商户订单号") wechat_trade_no = models.CharField(max_length=100, blank=True, null=True, verbose_name="微信支付单号") diff --git a/backend/shop/urls.py b/backend/shop/urls.py index 8e6e7ba..e0a57d9 100644 --- a/backend/shop/urls.py +++ b/backend/shop/urls.py @@ -3,7 +3,7 @@ from rest_framework.routers import DefaultRouter from .views import ( ESP32ConfigViewSet, OrderViewSet, order_check_view, ServiceViewSet, ARServiceViewSet, ServiceOrderViewSet, - payment_finish, pay + payment_finish, pay, send_sms_code ) router = DefaultRouter() @@ -16,6 +16,7 @@ router.register(r'service-orders', ServiceOrderViewSet) urlpatterns = [ re_path(r'^finish/?$', payment_finish, name='payment-finish'), re_path(r'^pay/?$', pay, name='wechat-pay-v3'), + path('auth/send-sms/', send_sms_code, name='send-sms'), path('page/check-order/', order_check_view, name='check-order-page'), path('', include(router.urls)), ] diff --git a/backend/shop/views.py b/backend/shop/views.py index 1fd53e8..b91fd58 100644 --- a/backend/shop/views.py +++ b/backend/shop/views.py @@ -18,6 +18,9 @@ import os import base64 from cryptography.hazmat.primitives.ciphers.aead import AESGCM from django.conf import settings +import requests +import random +from django.core.cache import cache # 猴子补丁:绕过微信支付响应签名验证 # 原因是:在开发环境或证书未能正确下载时,SDK 会因为无法验证微信返回的签名而抛出异常。 @@ -108,6 +111,70 @@ def get_wechat_pay_client(): except Exception as e: return None, str(e) +@extend_schema( + summary="发送短信验证码", + description="发送6位数字验证码到指定手机号", + request={ + 'application/json': { + 'type': 'object', + 'properties': { + 'phone_number': {'type': 'string', 'description': '手机号码'}, + }, + 'required': ['phone_number'] + } + }, + responses={ + 200: OpenApiExample('成功', value={'message': '验证码已发送'}), + 400: OpenApiExample('失败', value={'error': '手机号不能为空'}) + } +) +@api_view(['POST']) +def send_sms_code(request): + phone = request.data.get('phone_number') + if not phone: + return Response({'error': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST) + + # 生成6位验证码 + code = ''.join([str(random.randint(0, 9)) for _ in range(6)]) + + # 缓存验证码 (5分钟有效) + cache_key = f"sms_code_{phone}" + cache.set(cache_key, code, timeout=300) + + # 调用外部短信API + try: + api_url = "https://data.tangledup-ai.com/api/send-sms" + payload = { + "phone_number": phone, + "code": code, + "template_code": "SMS_493295002", + "sign_name": "叠加态科技云南" + } + headers = { + "Content-Type": "application/json", + "accept": "application/json" + } + + response = requests.post(api_url, json=payload, headers=headers, timeout=15) + + if response.status_code == 200: + print(f"短信发送成功: {phone} -> {code}") + return Response({'message': '验证码已发送'}) + else: + print(f"短信发送失败: {response.text}") + return Response({'error': '短信发送失败,请稍后重试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + except requests.exceptions.Timeout: + print(f"短信发送超时: {phone}") + # 超时并不一定代表失败,可能是对方响应慢。但为了安全起见,提示用户稍后重试或检查手机。 + # 考虑到用户反馈短信实际已收到,这里返回一个较为温和的错误或成功提示(视业务逻辑而定)。 + # 这里我们选择返回一个特定的错误,前端可以据此提示用户。 + return Response({'message': '短信请求已发送,请留意查收(如未收到请重试)'}, status=status.HTTP_200_OK) + + except Exception as e: + print(f"发送短信异常: {str(e)}") + return Response({'error': '短信服务异常'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + @extend_schema( summary="微信支付 V3 Native 下单", description="创建订单并获取微信支付二维码链接(code_url)。参数包括商品ID、数量、客户信息等。", @@ -181,6 +248,10 @@ def pay(request): print(f"商品不存在: {good_id}") return Response({'error': f'找不到 ID 为 {good_id} 的商品'}, status=status.HTTP_404_NOT_FOUND) + # 检查库存 + if product.stock < quantity: + return Response({'error': f'库存不足,仅剩 {product.stock} 件'}, status=status.HTTP_400_BAD_REQUEST) + salesperson = None if ref_code: from .models import Salesperson @@ -200,6 +271,10 @@ def pay(request): status='pending' ) + # 扣减库存 + product.stock -= quantity + product.save() + # 4. 调用微信支付接口 out_trade_no = f"PAY{order.id}T{int(time.time())}" description = f"购买 {product.name} x {quantity}" @@ -469,6 +544,37 @@ class OrderViewSet(viewsets.ModelViewSet): serializer = self.get_serializer(orders, many=True) return Response(serializer.data) + @action(detail=False, methods=['post'], authentication_classes=[], permission_classes=[]) + def my_orders(self, request): + """ + 查询我的订单 + 需要提供手机号和验证码 + """ + phone = request.data.get('phone_number') + code = request.data.get('code') + + if not phone or not code: + return Response({'error': '请提供手机号和验证码'}, status=status.HTTP_400_BAD_REQUEST) + + # 验证验证码 + cache_key = f"sms_code_{phone}" + cached_code = cache.get(cache_key) + + # 开发/测试方便,如果验证码是 888888 且没有缓存,允许通过(可选,但为了演示方便) + # if code == '888888': pass + + if not cached_code or cached_code != code: + return Response({'error': '验证码错误或已过期'}, status=status.HTTP_400_BAD_REQUEST) + + # 查询订单 + orders = Order.objects.filter(phone_number=phone).order_by('-created_at') + serializer = self.get_serializer(orders, many=True) + + # 验证通过后清除验证码 (防止重放) + cache.delete(cache_key) + + return Response(serializer.data) + @action(detail=True, methods=['post']) def initiate_payment(self, request, pk=None): """