This commit is contained in:
jeremygan2021
2026-02-10 23:35:53 +08:00
parent 6a025a7534
commit 0b3b81915b
24 changed files with 148 additions and 4 deletions

Binary file not shown.

View File

@@ -19,3 +19,4 @@ uritemplate==4.2.0
wechatpayv3==2.0.1
drf-spectacular-sidecar==2026.1.1
gunicorn==21.2.0
requests

View File

@@ -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',)
}),

View File

@@ -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='快递单号'),
),
]

View File

@@ -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="微信支付单号")

View File

@@ -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)),
]

View File

@@ -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):
"""