This commit is contained in:
jeremygan2021
2026-02-12 15:02:53 +08:00
parent b4ac97c3c2
commit 9e81eaaaab
23 changed files with 844 additions and 104 deletions

View File

@@ -311,9 +311,9 @@ class OrderAdmin(ModelAdmin):
@admin.register(WeChatUser)
class WeChatUserAdmin(ModelAdmin):
list_display = ('nickname', 'avatar_display', 'gender_display', 'province', 'city', 'created_at')
list_display = ('nickname', 'is_star', 'title', 'avatar_display', 'gender_display', 'province', 'city', 'created_at')
search_fields = ('nickname', 'openid')
list_filter = ('gender', 'province', 'city', 'created_at')
list_filter = ('is_star', 'gender', 'province', 'city', 'created_at')
readonly_fields = ('openid', 'unionid', 'session_key', 'created_at', 'updated_at')
def avatar_display(self, obj):
@@ -331,6 +331,10 @@ class WeChatUserAdmin(ModelAdmin):
('基本信息', {
'fields': ('user', 'nickname', 'avatar_url', 'gender')
}),
('专家认证', {
'fields': ('is_star', 'title'),
'description': '标记为明星技术用户/专家,将在社区中展示'
}),
('位置信息', {
'fields': ('country', 'province', 'city')
}),

View File

@@ -4,7 +4,7 @@ from .views import (
ESP32ConfigViewSet, OrderViewSet, order_check_view,
ServiceViewSet, VCCourseViewSet, ServiceOrderViewSet,
payment_finish, pay, send_sms_code, wechat_login, update_user_info, DistributorViewSet,
CourseEnrollmentViewSet, phone_login, bind_phone
CourseEnrollmentViewSet, phone_login, bind_phone, WeChatUserViewSet, upload_image
)
router = DefaultRouter()
@@ -15,6 +15,7 @@ router.register(r'courses', VCCourseViewSet)
router.register(r'course-enrollments', CourseEnrollmentViewSet)
router.register(r'service-orders', ServiceOrderViewSet)
router.register(r'distributor', DistributorViewSet, basename='distributor')
router.register(r'users', WeChatUserViewSet)
urlpatterns = [
re_path(r'^finish/?$', payment_finish, name='payment-finish'),
@@ -24,6 +25,7 @@ urlpatterns = [
path('auth/phone-login/', phone_login, name='phone-login'),
path('auth/bind-phone/', bind_phone, name='bind-phone'),
path('wechat/update/', update_user_info, name='wechat-update'),
path('upload/image/', upload_image, name='upload-image'),
path('page/check-order/', order_check_view, name='check-order-page'),
path('', include(router.urls)),
]

View File

@@ -1,6 +1,9 @@
from rest_framework import viewsets, status
from rest_framework.decorators import action, api_view
from rest_framework.decorators import action, api_view, parser_classes
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.response import Response
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
@@ -1309,3 +1312,95 @@ class DistributorViewSet(viewsets.GenericViewSet):
serializer = OrderSerializer(orders, many=True)
return Response(serializer.data)
class WeChatUserViewSet(viewsets.ReadOnlyModelViewSet):
"""
微信用户视图集
"""
queryset = WeChatUser.objects.all()
serializer_class = WeChatUserSerializer
@action(detail=False, methods=['get'])
def stars(self, request):
"""
获取明星技术用户列表
"""
stars = WeChatUser.objects.filter(is_star=True).order_by('-created_at')
serializer = self.get_serializer(stars, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'], url_path='paid-items')
def paid_items(self, request):
"""
获取当前用户已购买的项目(硬件、课程、服务)
用于论坛发帖时关联
"""
user = get_current_wechat_user(request)
if not user:
return Response({'error': 'Unauthorized'}, status=401)
# 1. 硬件 (ESP32Config)
paid_orders = Order.objects.filter(wechat_user=user, status__in=['paid', 'shipped'])
config_ids = paid_orders.exclude(config__isnull=True).values_list('config_id', flat=True).distinct()
configs = ESP32Config.objects.filter(id__in=config_ids)
# 2. 课程 (VCCourse)
course_ids = paid_orders.exclude(course__isnull=True).values_list('course_id', flat=True).distinct()
courses = VCCourse.objects.filter(id__in=course_ids)
# 3. 服务 (Service)
# 暂时没有强关联 WeChatUser 的 ServiceOrder如果有 phone_number 匹配逻辑可在此添加
# 简单起见,暂不返回服务,或基于 phone_number 匹配
service_orders = ServiceOrder.objects.filter(phone_number=user.phone_number, status='paid')
service_ids = service_orders.values_list('service_id', flat=True).distinct()
services = Service.objects.filter(id__in=service_ids)
return Response({
'configs': ESP32ConfigSerializer(configs, many=True).data,
'courses': VCCourseSerializer(courses, many=True).data,
'services': ServiceSerializer(services, many=True).data
})
@extend_schema(
summary="上传图片",
description="上传图片文件返回图片URL",
request={
'multipart/form-data': {
'type': 'object',
'properties': {
'file': {'type': 'string', 'format': 'binary'}
},
'required': ['file']
}
},
responses={
200: OpenApiExample('成功', value={'url': 'http://.../media/uploads/xxx.jpg'})
}
)
@api_view(['POST'])
@parser_classes([MultiPartParser, FormParser])
def upload_image(request):
file_obj = request.FILES.get('file')
if not file_obj:
return Response({'error': 'No file uploaded'}, status=400)
# 验证文件类型
if not file_obj.content_type.startswith('image/'):
return Response({'error': 'File is not an image'}, status=400)
# 生成唯一文件名
ext = os.path.splitext(file_obj.name)[1]
filename = f"uploads/avatars/{uuid.uuid4()}{ext}"
# 保存文件
path = default_storage.save(filename, ContentFile(file_obj.read()))
# 获取完整URL
# 注意如果使用了云存储url会自动包含域名如果是本地存储可能需要手动拼接
file_url = default_storage.url(path)
# 确保 URL 是完整的 (如果是相对路径,拼接当前 host)
if not file_url.startswith('http'):
file_url = request.build_absolute_uri(file_url)
return Response({'url': file_url})