order
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -19,3 +19,4 @@ uritemplate==4.2.0
|
||||
wechatpayv3==2.0.1
|
||||
drf-spectacular-sidecar==2026.1.1
|
||||
gunicorn==21.2.0
|
||||
requests
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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',)
|
||||
}),
|
||||
|
||||
@@ -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='快递单号'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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="微信支付单号")
|
||||
|
||||
@@ -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)),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user