社區資源媒體管理系統設計與實現
1. 系統概述
社區資源媒體管理系統是一個專為社區戶外廣告打造的高效、專業化平臺,旨在實現社區媒體的數字化管理、智能投放和便捷交易。該系統將整合社區各類廣告資源,為廣告主、物業公司和社區居民提供一站式服務。
1.1 系統目標
- 實現社區戶外廣告資源的數字化管理
- 提供精準廣告投放功能
- 建立廣告交易平臺
- 優化廣告資源利用率
- 提升廣告投放效果分析能力
1.2 系統特點
- 專業化:針對社區戶外廣告場景定制
- 智能化:利用算法實現精準投放
- 可視化:直觀展示廣告資源分布和效果
- 安全可靠:完善的權限管理和數據保護機制
2. 系統架構設計
2.1 技術棧選擇
- 后端:Python + Django/Django REST framework
- 前端:Vue.js/React + Element UI/Ant Design
- 數據庫:PostgreSQL/MySQL
- 緩存:Redis
- 搜索引擎:Elasticsearch
- 文件存儲:阿里云OSS/七牛云
- 消息隊列:RabbitMQ/Celery
- GIS支持:PostGIS/GeoDjango
2.2 系統架構圖
┌───────────────────────────────────────────────────────────────┐
│ 客戶端層 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Web端 │ │ 移動端APP │ │ 管理后臺 │ │ 第三方接入│ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└───────────────────────────────────────────────────────────────┘│▼
┌───────────────────────────────────────────────────────────────┐
│ 應用服務層 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ API網關 │ │ 用戶服務 │ │ 廣告服務 │ │ 支付服務 │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 數據服務 │ │ 文件服務 │ │ 消息服務 │ │ 定時任務 │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└───────────────────────────────────────────────────────────────┘│▼
┌───────────────────────────────────────────────────────────────┐
│ 數據存儲層 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 關系數據庫 │ │ 緩存系統 │ │ 搜索引擎 │ │ 文件存儲 │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└───────────────────────────────────────────────────────────────┘
2.3 微服務劃分
- 用戶服務:處理用戶注冊、登錄、權限管理
- 廣告服務:廣告資源管理、投放策略
- 交易服務:訂單管理、支付處理
- 數據服務:數據分析、報表生成
- 消息服務:通知、站內信
- 文件服務:圖片、視頻等資源管理
3. 數據庫設計
3.1 主要數據表結構
用戶相關表
class User(AbstractUser):USER_TYPE_CHOICES = (('admin', '管理員'),('advertiser', '廣告主'),('property', '物業'),('resident', '居民'),)user_type = models.CharField(max_length=20, choices=USER_TYPE_CHOICES)phone = models.CharField(max_length=20, unique=True)company = models.CharField(max_length=100, blank=True)avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)verified = models.BooleanField(default=False)class UserProfile(models.Model):user = models.OneToOneField(User, on_delete=models.CASCADE)id_card = models.CharField(max_length=20, blank=True)address = models.TextField(blank=True)credit_score = models.IntegerField(default=100)
社區相關表
class Community(models.Model):name = models.CharField(max_length=100)address = models.TextField()location = models.PointField() # 使用GeoDjangototal_buildings = models.IntegerField()total_households = models.IntegerField()property_company = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, limit_choices_to={'user_type': 'property'})created_at = models.DateTimeField(auto_now_add=True)class Building(models.Model):community = models.ForeignKey(Community, on_delete=models.CASCADE)name = models.CharField(max_length=50)floor_count = models.IntegerField()household_count = models.IntegerField()location = models.PointField()
廣告資源相關表
class AdSpace(models.Model):SPACE_TYPE_CHOICES = (('elevator', '電梯廣告'),('gate', '大門廣告'),('billboard', '廣告牌'),('parking', '停車場廣告'),('other', '其他'),)community = models.ForeignKey(Community, on_delete=models.CASCADE)space_type = models.CharField(max_length=20, choices=SPACE_TYPE_CHOICES)location = models.PointField()description = models.TextField()size = models.CharField(max_length=50) # 如"60cm×90cm"price_per_day = models.DecimalField(max_digits=10, decimal_places=2)is_available = models.BooleanField(default=True)images = models.ManyToManyField('FileResource', blank=True)class AdSpaceImage(models.Model):ad_space = models.ForeignKey(AdSpace, on_delete=models.CASCADE)image = models.ImageField(upload_to='ad_space_images/')is_primary = models.BooleanField(default=False)uploaded_at = models.DateTimeField(auto_now_add=True)
廣告內容相關表
class AdContent(models.Model):AD_TYPE_CHOICES = (('image', '圖片廣告'),('video', '視頻廣告'),('text', '文字廣告'),('interactive', '互動廣告'),)advertiser = models.ForeignKey(User, on_delete=models.CASCADE, limit_choices_to={'user_type': 'advertiser'})title = models.CharField(max_length=100)ad_type = models.CharField(max_length=20, choices=AD_TYPE_CHOICES)content = models.TextField() # 或JSONField存儲結構化內容target_audience = models.JSONField(default=dict) # 目標受眾篩選條件start_date = models.DateField()end_date = models.DateField()budget = models.DecimalField(max_digits=12, decimal_places=2)status = models.CharField(max_length=20, default='draft') # draft, pending, approved, rejected, running, completedcreated_at = models.DateTimeField(auto_now_add=True)class AdMaterial(models.Model):ad_content = models.ForeignKey(AdContent, on_delete=models.CASCADE)file = models.ForeignKey('FileResource', on_delete=models.CASCADE)material_type = models.CharField(max_length=20) # image, video, etc.display_order = models.IntegerField(default=0)
訂單交易相關表
class AdOrder(models.Model):ORDER_STATUS_CHOICES = (('pending', '待支付'),('paid', '已支付'),('deployed', '已投放'),('completed', '已完成'),('cancelled', '已取消'),('refunded', '已退款'),)order_no = models.CharField(max_length=50, unique=True)advertiser = models.ForeignKey(User, on_delete=models.CASCADE, limit_choices_to={'user_type': 'advertiser'})ad_content = models.ForeignKey(AdContent, on_delete=models.CASCADE)total_amount = models.DecimalField(max_digits=12, decimal_places=2)actual_amount = models.DecimalField(max_digits=12, decimal_places=2)discount = models.DecimalField(max_digits=5, decimal_places=2, default=0)status = models.CharField(max_length=20, choices=ORDER_STATUS_CHOICES, default='pending')created_at = models.DateTimeField(auto_now_add=True)paid_at = models.DateTimeField(null=True, blank=True)class OrderItem(models.Model):order = models.ForeignKey(AdOrder, on_delete=models.CASCADE)ad_space = models.ForeignKey(AdSpace, on_delete=models.CASCADE)start_date = models.DateField()end_date = models.DateField()price_per_day = models.DecimalField(max_digits=10, decimal_places=2)total_days = models.IntegerField()subtotal = models.DecimalField(max_digits=12, decimal_places=2)deployed_at = models.DateTimeField(null=True, blank=True)completed_at = models.DateTimeField(null=True, blank=True)
效果統計相關表
class AdImpression(models.Model):ad_content = models.ForeignKey(AdContent, on_delete=models.CASCADE)ad_space = models.ForeignKey(AdSpace, on_delete=models.CASCADE)date = models.DateField()view_count = models.IntegerField(default=0)interaction_count = models.IntegerField(default=0)class AdInteraction(models.Model):INTERACTION_TYPE_CHOICES = (('click', '點擊'),('scan', '掃碼'),('call', '電話'),('share', '分享'),)ad_content = models.ForeignKey(AdContent, on_delete=models.CASCADE)ad_space = models.ForeignKey(AdSpace, on_delete=models.CASCADE)user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)interaction_type = models.CharField(max_length=20, choices=INTERACTION_TYPE_CHOICES)interaction_data = models.JSONField(default=dict) # 額外數據如掃碼內容等created_at = models.DateTimeField(auto_now_add=True)ip_address = models.GenericIPAddressField(null=True, blank=True)device_info = models.CharField(max_length=200, blank=True)
系統管理相關表
class SystemConfig(models.Model):key = models.CharField(max_length=50, unique=True)value = models.JSONField()description = models.TextField(blank=True)is_public = models.BooleanField(default=False)class OperationLog(models.Model):ACTION_CHOICES = (('create', '創建'),('update', '更新'),('delete', '刪除'),('login', '登錄'),('logout', '登出'),('approve', '審批'),('reject', '拒絕'),)user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)action = models.CharField(max_length=20, choices=ACTION_CHOICES)model = models.CharField(max_length=50)object_id = models.CharField(max_length=50, blank=True)data_before = models.JSONField(null=True, blank=True)data_after = models.JSONField(null=True, blank=True)ip_address = models.GenericIPAddressField()created_at = models.DateTimeField(auto_now_add=True)
3.2 數據庫關系圖
┌───────────┐ ┌───────────┐ ┌──────────────┐
│ User │───────│ Community │───────│ Building │
└───────────┘ └───────────┘ └──────────────┘| || |▼ ▼
┌───────────┐ ┌───────────┐ ┌──────────────┐
│AdContent │───────│ AdSpace │───────│ AdSpaceImage │
└───────────┘ └───────────┘ └──────────────┘| || |▼ ▼
┌───────────┐ ┌──────────────┐ ┌──────────────┐
│ AdOrder │───────│ OrderItem │─────│ AdImpression │
└───────────┘ └──────────────┘ └──────────────┘||▼┌──────────────────┐│ AdInteraction │└──────────────────┘
4. 核心功能模塊實現
4.1 用戶認證與權限管理
# authentication/backends.py
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
from .models import Userclass MultiFieldModelBackend(ModelBackend):def authenticate(self, request, username=None, password=None, **kwargs):try:user = User.objects.get(Q(username=username) | Q(phone=username) | Q(email=username))if user.check_password(password):return userexcept User.DoesNotExist:return None# authentication/permissions.py
from rest_framework.permissions import BasePermissionclass IsAdvertiser(BasePermission):def has_permission(self, request, view):return request.user.is_authenticated and request.user.user_type == 'advertiser'class IsPropertyManager(BasePermission):def has_permission(self, request, view):return request.user.is_authenticated and request.user.user_type == 'property'# authentication/serializers.py
from rest_framework import serializers
from django.contrib.auth import authenticate
from .models import User, UserProfileclass UserLoginSerializer(serializers.Serializer):username = serializers.CharField()password = serializers.CharField(write_only=True)def validate(self, data):user = authenticate(username=data['username'], password=data['password'])if not user:raise serializers.ValidationError("Invalid credentials")if not user.is_active:raise serializers.ValidationError("User account is disabled")return userclass UserProfileSerializer(serializers.ModelSerializer):class Meta:model = UserProfilefields = ['id_card', 'address', 'credit_score']class UserSerializer(serializers.ModelSerializer):profile = UserProfileSerializer()class Meta:model = Userfields = ['id', 'username', 'email', 'phone', 'user_type', 'company', 'verified', 'profile']read_only_fields = ['verified']def update(self, instance, validated_data):profile_data = validated_data.pop('profile', {})profile = instance.profilefor attr, value in validated_data.items():setattr(instance, attr, value)instance.save()for attr, value in profile_data.items():setattr(profile, attr, value)profile.save()return instance
4.2 廣告資源管理模塊
# advertisements/views.py
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import AdSpace, AdSpaceImage
from .serializers import AdSpaceSerializer, AdSpaceImageSerializer
from .filters import AdSpaceFilterclass AdSpaceViewSet(viewsets.ModelViewSet):queryset = AdSpace.objects.all()serializer_class = AdSpaceSerializerfilter_backends = [DjangoFilterBackend]filterset_class = AdSpaceFilterdef get_permissions(self):if self.action in ['create', 'update', 'partial_update', 'destroy']:permission_classes = [permissions.IsAuthenticated, IsPropertyManager]else:permission_classes = [permissions.IsAuthenticatedOrReadOnly]return [permission() for permission in permission_classes]def get_queryset(self):queryset = super().get_queryset()# 物業用戶只能看到自己社區的廣告位if self.request.user.user_type == 'property':queryset = queryset.filter(community__property_company=self.request.user)# 廣告主可以看到所有可用的廣告位elif self.request.user.user_type == 'advertiser':queryset = queryset.filter(is_available=True)return queryset@action(detail=True, methods=['post'], serializer_class=AdSpaceImageSerializer)def upload_image(self, request, pk=None):ad_space = self.get_object()serializer = self.get_serializer(data=request.data)serializer.is_valid(raise_exception=True)serializer.save(ad_space=ad_space)return Response(serializer.data, status=status.HTTP_201_CREATED)@action(detail=True, methods=['get'])def stats(self, request, pk=None):ad_space = self.get_object()# 獲取廣告位的統計數據data = {'total_orders': ad_space.order_items.count(),'current_orders': ad_space.order_items.filter(order__status__in=['paid', 'deployed']).count(),'revenue': sum([item.subtotal for item in ad_space.order_items.filter(order__status__in=['paid', 'deployed', 'completed'])]),}return Response(data)# advertisements/serializers.py
from rest_framework import serializers
from django.contrib.gis.geos import Point
from .models import AdSpace, AdSpaceImageclass PointField(serializers.Field):def to_representation(self, value):if value:return {'lng': value.x, 'lat': value.y}return Nonedef to_internal_value(self, data):if data and 'lng' in data and 'lat' in data:return Point(float(data['lng']), float(data['lat']))return Noneclass AdSpaceImageSerializer(serializers.ModelSerializer):class Meta:model = AdSpaceImagefields = ['id', 'image', 'is_primary', 'uploaded_at']read_only_fields = ['uploaded_at']class AdSpaceSerializer(serializers.ModelSerializer):location = PointField()images = AdSpaceImageSerializer(many=True, read_only=True)community_name = serializers.CharField(source='community.name', read_only=True)class Meta:model = AdSpacefields = ['id', 'community', 'community_name', 'space_type', 'location', 'description', 'size', 'price_per_day', 'is_available', 'images']def validate(self, data):if self.instance and 'community' in data and data['community'] != self.instance.community:raise serializers.ValidationError("Cannot change community of an existing ad space")return data# advertisements/filters.py
import django_filters
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
from .models import AdSpaceclass AdSpaceFilter(django_filters.FilterSet):space_type = django_filters.CharFilter(field_name='space_type')min_price = django_filters.NumberFilter(field_name='price_per_day', lookup_expr='gte')max_price = django_filters.NumberFilter(field_name='price_per_day', lookup_expr='lte')available = django_filters.BooleanFilter(field_name='is_available')community = django_filters.NumberFilter(field_name='community')near = django_filters.CharFilter(method='filter_near')class Meta:model = AdSpacefields = ['space_type', 'price_per_day', 'is_available', 'community']def filter_near(self, queryset, name, value):try:lng, lat, radius = map(float, value.split(','))point = Point(lng, lat, srid=4326)return queryset.filter(location__distance_lte=(point, radius)).annotate(distance=Distance('location', point)).order_by('distance')except (ValueError, TypeError):return queryset
4.3 廣告內容管理模塊
# contents/views.py
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import AdContent, AdMaterial
from .serializers import AdContentSerializer, AdMaterialSerializer
from .filters import AdContentFilter
from .tasks import process_ad_contentclass AdContentViewSet(viewsets.ModelViewSet):queryset = AdContent.objects.all()serializer_class = AdContentSerializerfilter_backends = [DjangoFilterBackend]filterset_class = AdContentFilterdef get_permissions(self):if self.action in ['create', 'update', 'partial_update', 'destroy']:permission_classes = [permissions.IsAuthenticated, IsAdvertiser]else:permission_classes = [permissions.IsAuthenticatedOrReadOnly]return [permission() for permission in permission_classes]def get_queryset(self):queryset = super().get_queryset()# 廣告主只能看到自己的廣告if self.request.user.user_type == 'advertiser':queryset = queryset.filter(advertiser=self.request.user)# 物業可以看到自己社區的廣告elif self.request.user.user_type == 'property':queryset = queryset.filter(order_items__order__status__in=['paid', 'deployed', 'completed'],order_items__ad_space__community__property_company=self.request.user).distinct()return querysetdef perform_create(self, serializer):serializer.save(advertiser=self.request.user)@action(detail=True, methods=['post'])def submit_for_review(self, request, pk=None):ad_content = self.get_object()if ad_content.status != 'draft':return Response({'detail': 'Only draft ads can be submitted for review'},status=status.HTTP_400_BAD_REQUEST)ad_content.status = 'pending'ad_content.save()# 異步處理審核流程process_ad_content.delay(ad_content.id)return Response({'status': 'submitted for review'})@action(detail=True, methods=['get'])def materials(self, request, pk=None):ad_content = self.get_object()materials = ad_content.materials.all().order_by('display_order')serializer = AdMaterialSerializer(materials, many=True)return Response(serializer.data)# contents/serializers.py
from rest_framework import serializers
from .models import AdContent, AdMaterial
from files.serializers import FileResourceSerializerclass AdMaterialSerializer(serializers.ModelSerializer):file_details = FileResourceSerializer(source='file', read_only=True)class Meta:model = AdMaterialfields = ['id', 'ad_content', 'file', 'material_type', 'display_order', 'file_details']extra_kwargs = {'ad_content': {'write_only': True}}class AdContentSerializer(serializers.ModelSerializer):advertiser_name = serializers.CharField(source='advertiser.company', read_only=True)materials = AdMaterialSerializer(many=True, read_only=True)class Meta:model = AdContentfields = ['id', 'advertiser', 'advertiser_name', 'title', 'ad_type', 'content', 'target_audience', 'start_date', 'end_date', 'budget', 'status', 'created_at', 'materials']read_only_fields = ['status', 'created_at']def validate(self, data):if 'start_date' in data and 'end_date' in data:if data['start_date'] > data['end_date']:raise serializers.ValidationError("End date must be after start date")return data# contents/tasks.py
from celery import shared_task
from django.core.mail import send_mail
from django.conf import settings
from .models import AdContent@shared_task
def process_ad_content(ad_content_id):ad_content = AdContent.objects.get(id=ad_content_id)# 這里可以添加復雜的審核邏輯# 模擬審核過程import timetime.sleep(10) # 模擬審核耗時# 90%的概率通過審核import randomif random.random() < 0.9:ad_content.status = 'approved'subject = '您的廣告已通過審核'message = f'您的廣告 "{ad_content.title}" 已通過審核,可以開始投放。'else:ad_content.status = 'rejected'subject = '您的廣告未通過審核'message = f'您的廣告 "{ad_content.title}" 未通過審核,請修改后重新提交。'ad_content.save()# 發送郵件通知send_mail(subject=subject,message=message,from_email=settings.DEFAULT_FROM_EMAIL,recipient_list=[ad_content.advertiser.email],fail_silently=True,)
4.4 訂單交易模塊
# orders/views.py
from rest_framework import viewsets, permissions, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django.utils import timezone
from django.db import transaction
from .models import AdOrder, OrderItem
from .serializers import AdOrderSerializer, OrderItemSerializer, CreateOrderSerializer
from .services import OrderService
from advertisements.models import AdSpace
from contents.models import AdContentclass AdOrderViewSet(viewsets.ModelViewSet):queryset = AdOrder.objects.all()serializer_class = AdOrderSerializerdef get_permissions(self):if self.action in ['create', 'update', 'partial_update', 'destroy']:permission_classes = [permissions.IsAuthenticated, IsAdvertiser]else:permission_classes = [permissions.IsAuthenticated]return [permission() for permission in permission_classes]def get_queryset(self):queryset = super().get_queryset()# 廣告主只能看到自己的訂單if self.request.user.user_type == 'advertiser':queryset = queryset.filter(advertiser=self.request.user)# 物業可以看到自己社區的訂單elif self.request.user.user_type == 'property':queryset = queryset.filter(items__ad_space__community__property_company=self.request.user).distinct()return querysetdef get_serializer_class(self):if self.action == 'create':return CreateOrderSerializerreturn super().get_serializer_class()@transaction.atomicdef create(self, request, *args, **kwargs):serializer = self.get_serializer(data=request.data)serializer.is_valid(raise_exception=True)# 獲取廣告內容ad_content = AdContent.objects.get(id=serializer.validated_data['ad_content_id'],advertiser=request.user,status='approved')# 創建訂單order = OrderService.create_order(advertiser=request.user,ad_content=ad_content,items_data=serializer.validated_data['items'])headers = self.get_success_headers(serializer.data)return Response(AdOrderSerializer(order).data,status=status.HTTP_201_CREATED,headers=headers)@action(detail=True, methods=['post'])def pay(self, request, pk=None):order = self.get_object()if order.status != 'pending':return Response({'detail': 'Only pending orders can be paid'},status=status.HTTP_400_BAD_REQUEST)# 這里應該調用支付接口,簡化處理直接標記為已支付order.status = 'paid'order.paid_at = timezone.now()order.save()# 更新訂單項狀態order.items.update(deployed_at=timezone.now())# 更新廣告內容狀態order.ad_content.status = 'running'order.ad_content.save()return Response({'status': 'paid'})# orders/serializers.py
from rest_framework import serializers
from .models import AdOrder, OrderItem
from advertisements.models import AdSpace
from advertisements.serializers import AdSpaceSerializer
from contents.serializers import AdContentSerializerclass OrderItemSerializer(serializers.ModelSerializer):ad_space_details = AdSpaceSerializer(source='ad_space', read_only=True)class Meta:model = OrderItemfields = ['id', 'order', 'ad_space', 'ad_space_details', 'start_date', 'end_date', 'price_per_day', 'total_days', 'subtotal', 'deployed_at', 'completed_at']read_only_fields = ['total_days', 'subtotal', 'deployed_at', 'completed_at']class AdOrderSerializer(serializers.ModelSerializer):items = OrderItemSerializer(many=True, read_only=True)ad_content_details = AdContentSerializer(source='ad_content', read_only=True)class Meta:model = AdOrderfields = ['id', 'order_no', 'advertiser', 'ad_content', 'ad_content_details','total_amount', 'actual_amount', 'discount', 'status','created_at', 'paid_at', 'items']read_only_fields = ['order_no', 'advertiser', 'total_amount', 'actual_amount','discount', 'status', 'created_at', 'paid_at']class CreateOrderItemSerializer(serializers.Serializer):ad_space_id = serializers.IntegerField()start_date = serializers.DateField()end_date = serializers.DateField()def validate(self, data):ad_space = AdSpace.objects.filter(id=data['ad_space_id'], is_available=True).first()if not ad_space:raise serializers.ValidationError("Ad space not available")# 檢查廣告位是否在選定日期內可用conflicting_items = OrderItem.objects.filter(ad_space=ad_space,start_date__lte=data['end_date'],end_date__gte=data['start_date'],order__status__in=['paid', 'deployed'])if conflicting_items.exists():raise serializers.ValidationError("Ad space is not available for the selected dates")data['ad_space'] = ad_spacedata['price_per_day'] = ad_space.price_per_dayreturn dataclass CreateOrderSerializer(serializers.Serializer):ad_content_id = serializers.IntegerField()items = CreateOrderItemSerializer(many=True, min_length=1)def validate_ad_content_id(self, value):if not AdContent.objects.filter(id=value, advertiser=self.context['request'].user, status='approved').exists():raise serializers.ValidationError("Invalid ad content")return valuedef validate(self, data):if not data['items']:raise serializers.ValidationError("At least one order item is required")return data# orders/services.py
from django.utils import timezone
from django.db import transaction
import random
import string
from .models import AdOrder, OrderItemclass OrderService:@staticmethod@transaction.atomicdef create_order(advertiser, ad_content, items_data):# 生成訂單號order_no = f'ORD{timezone.now().strftime("%Y%m%d")}{"".join(random.choices(string.digits, k=6))}'# 計算總金額total_amount = sum((item['end_date'] - item['start_date']).days * item['price_per_day']for item in items_data)# 創建訂單order = AdOrder.objects.create(order_no=order_no,advertiser=advertiser,ad_content=ad_content,total_amount=total_amount,actual_amount=total_amount, # 實際支付金額,這里簡化處理status='pending')# 創建訂單項order_items = []for item_data in items_data:total_days = (item_data['end_date'] - item_data['start_date']).dayssubtotal = total_days * item_data['price_per_day']order_item = OrderItem(order=order,ad_space=item_data['ad_space'],start_date=item_data['start_date'],end_date=item_data['end_date'],price_per_day=item_data['price_per_day'],total_days=total_days,subtotal=subtotal)order_items.append(order_item)OrderItem.objects.bulk_create(order_items)return order
4.5 數據統計與分析模塊
# analytics/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions
from django.db.models import Sum, Count, Q
from django.utils import timezone
from datetime import timedelta
from advertisements.models import AdSpace
from contents.models import AdContent
from orders.models import AdOrder, OrderItem
from .serializers import StatsSerializerclass DashboardStatsAPIView(APIView):permission_classes = [permissions.IsAuthenticated]def get(self, request):user = request.usertime_threshold = timezone.now() - timedelta(days=30)if user.user_type == 'advertiser':# 廣告主數據ads = AdContent.objects.filter(advertiser=user)orders = AdOrder.objects.filter(advertiser=user)stats = {'total_ads': ads.count(),'active_ads': ads.filter(status='running').count(),'total_orders': orders.count(),'total_spent': orders.aggregate(total=Sum('actual_amount'))['total'] or 0,'recent_orders': orders.filter(created_at__gte=time_threshold).count(),}elif user.user_type == 'property':# 物業數據communities = user.managed_communities.all()ad_spaces = AdSpace.objects.filter(community__in=communities)order_items = OrderItem.objects.filter(ad_space__in=ad_spaces)stats = {'total_spaces': ad_spaces.count(),'available_spaces': ad_spaces.filter(is_available=True).count(),'total_orders': order_items.count(),'total_income': order_items.aggregate(total=Sum('subtotal'))['total'] or 0,'recent_orders': order_items.filter(order__created_at__gte=time_threshold).count(),}else:stats = {}serializer = StatsSerializer(stats)return Response(serializer.data)class AdPerformanceAPIView(APIView):permission_classes = [permissions.IsAuthenticated, IsAdvertiser]def get(self, request, ad_id):ad_content = AdContent.objects.filter(id=ad_id, advertiser=request.user).first()if not ad_content:return Response({'detail': 'Not found'}, status=status.HTTP_404_NOT_FOUND)# 獲取廣告的訂單和投放數據order_items = OrderItem.objects.filter(order__ad_content=ad_content).select_related('ad_space', 'ad_space__community')# 計算基本統計total_spaces = order_items.count()total_days = sum(item.total_days for item in order_items)total_cost = sum(item.subtotal for item in order_items)# 按社區分組統計by_community = []communities = set(item.ad_space.community for item in order_items)for community in communities:items = order_items.filter(ad_space__community=community)by_community.append({'community_id': community.id,'community_name': community.name,'total_spaces': items.count(),'total_days': sum(item.total_days for item in items),'total_cost': sum(item.subtotal for item in items),})# 按時間分組統計(簡化處理)time_data = []for i in range(30, -1, -1):date = timezone.now() - timedelta(days=i)items = order_items.filter(start_date__lte=date,end_date__gte=date)time_data.append({'date': date.date(),'active_spaces': items.count(),})data = {'ad_id': ad_content.id,'ad_title': ad_content.title,'total_spaces': total_spaces,'total_days': total_days,'total_cost': total_cost,'by_community': by_community,'time_series': time_data,}return Response(data)# analytics/serializers.py
from rest_framework import serializersclass StatsSerializer(serializers.Serializer):total_ads = serializers.IntegerField(required=False)active_ads = serializers.IntegerField(required=False)total_orders = serializers.IntegerField(required=False)total_spent = serializers.DecimalField(max_digits=12, decimal_places=2, required=False)recent_orders = serializers.IntegerField(required=False)total_spaces = serializers.IntegerField(required=False)available_spaces = serializers.IntegerField(required=False)total_income = serializers.DecimalField(max_digits=12, decimal_places=2, required=False)
5. 高級功能實現
5.1 智能推薦系統
# recommendations/services.py
from django.db.models import Q
from advertisements.models import AdSpace
from contents.models import AdContent
from orders.models import OrderItem
from datetime import date, timedelta
from collections import defaultdict
import mathclass AdRecommendationService:@staticmethoddef recommend_spaces_for_ad(ad_content, limit=10):"""為廣告內容推薦合適的廣告位"""# 獲取廣告的目標受眾特征target_audience = ad_content.target_audience or {}# 基礎查詢:可用的廣告位queryset = AdSpace.objects.filter(is_available=True)# 根據廣告類型篩選if ad_content.ad_type == 'video':queryset = queryset.filter(space_type__in=['elevator', 'parking'])elif ad_content.ad_type == 'image':queryset = queryset.exclude(space_type='other')# 根據目標社區篩選if 'communities' in target_audience:queryset = queryset.filter(community_id__in=target_audience['communities'])# 根據價格預算篩選if ad_content.budget:max_price = ad_content.budget / ((ad_content.end_date - ad_content.start_date).days or 1)queryset = queryset.filter(price_per_day__lte=max_price)# 排除已經預訂的廣告位booked_spaces = OrderItem.objects.filter(Q(start_date__lte=ad_content.end_date) & Q(end_date__gte=ad_content.start_date),order__status__in=['paid', 'deployed']).values_list('ad_space_id', flat=True)queryset = queryset.exclude(id__in=booked_spaces)# 計算每個廣告位的得分spaces = list(queryset)scored_spaces = []for space in spaces:score = 0# 價格得分(越便宜得分越高)price_score = 1 / (space.price_per_day or 1)score += price_score * 0.3# 社區規模得分community_score = math.log(space.community.total_households or 1)score += community_score * 0.4# 歷史表現得分(簡化處理)performance_score = OrderItem.objects.filter(ad_space=space,order__status='completed').count() * 0.1score += performance_score * 0.3scored_spaces.append((space, score))# 按得分排序scored_spaces.sort(key=lambda x: x[1], reverse=True)return [space for space, score in scored_spaces[:limit]]@staticmethoddef recommend_ads_for_space(ad_space, limit=5):"""為廣告位推薦合適的廣告內容"""# 基礎查詢:已批準的廣告內容queryset = AdContent.objects.filter(status='approved')# 根據廣告位類型篩選if ad_space.space_type == 'elevator':queryset = queryset.filter(ad_type__in=['image', 'video'])elif ad_space.space_type == 'billboard':queryset = queryset.filter(ad_type__in=['image', 'text'])# 排除已經預訂的廣告內容booked_ads = OrderItem.objects.filter(Q(start_date__lte=date.today() + timedelta(days=30)) &Q(end_date__gte=date.today()),order__status__in=['paid', 'deployed'],ad_space=ad_space).values_list('order__ad_content_id', flat=True)queryset = queryset.exclude(id__in=booked_ads)# 計算每個廣告的得分ads = list(queryset)scored_ads = []for ad in ads:score = 0# 預算得分(預算越高得分越高)budget_score = math.log(ad.budget or 1)score += budget_score * 0.4# 持續時間得分(持續時間越長得分越高)duration_score = (ad.end_date - ad.start_date).daysscore += duration_score * 0.3# 廣告主信用得分advertiser_score = ad.advertiser.profile.credit_score / 100score += advertiser_score * 0.3scored_ads.append((ad, score))# 按得分排序scored_ads.sort(key=lambda x: x[1], reverse=True)return [ad for ad, score in scored_ads[:limit]]# recommendations/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from .services import AdRecommendationService
from advertisements.models import AdSpace
from contents.models import AdContent
from contents.serializers import AdContentSerializer
from advertisements.serializers import AdSpaceSerializerclass RecommendSpacesAPIView(APIView):permission_classes = [permissions.IsAuthenticated, IsAdvertiser]def get(self, request, ad_id):ad_content = AdContent.objects.filter(id=ad_id, advertiser=request.user).first()if not ad_content:return Response({'detail': 'Not found'}, status=status.HTTP_404_NOT_FOUND)spaces = AdRecommendationService.recommend_spaces_for_ad(ad_content)serializer = AdSpaceSerializer(spaces, many=True)return Response(serializer.data)class RecommendAdsAPIView(APIView):permission_classes = [permissions.IsAuthenticated, IsPropertyManager]def get(self, request, space_id):ad_space = AdSpace.objects.filter(id=space_id, community__property_company=request.user).first()if not ad_space:return Response({'detail': 'Not found'}, status=status.HTTP_404_NOT_FOUND)ads = AdRecommendationService.recommend_ads_for_space(ad_space)serializer = AdContentSerializer(ads, many=True)return Response(serializer.data)
5.2 廣告競價系統
# bidding/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from django.utils import timezone
from datetime import timedelta
from django.db import transaction
from advertisements.models import AdSpace
from contents.models import AdContent
from orders.models import AdOrder, OrderItem
from orders.services import OrderService
from .serializers import BidSerializerclass BidAPIView(APIView):permission_classes = [permissions.IsAuthenticated, IsAdvertiser]@transaction.atomicdef post(self, request):serializer = BidSerializer(data=request.data, context={'request': request})serializer.is_valid(raise_exception=True)ad_content = AdContent.objects.get(id=serializer.validated_data['ad_content_id'],advertiser=request.user,status='approved')ad_space = AdSpace.objects.get(id=serializer.validated_data['ad_space_id'],is_available=True)# 檢查廣告位是否可用start_date = serializer.validated_data['start_date']end_date = serializer.validated_data['end_date']conflicting_items = OrderItem.objects.filter(ad_space=ad_space,start_date__lte=end_date,end_date__gte=start_date,order__status__in=['paid', 'deployed'])if conflicting_items.exists():return Response({'detail': 'Ad space is not available for the selected dates'},status=status.HTTP_400_BAD_REQUEST)# 檢查是否有更高價格的競價min_price = serializer.validated_data['price_per_day']higher_bids = OrderItem.objects.filter(ad_space=ad_space,start_date__lte=end_date,end_date__gte=start_date,price_per_day__gt=min_price,order__status='pending')if higher_bids.exists():return Response({'detail': 'There are higher bids for this ad space'},status=status.HTTP_400_BAD_REQUEST)# 創建競價訂單total_days = (end_date - start_date).dayssubtotal = total_days * min_priceorder = OrderService.create_order(advertiser=request.user,ad_content=ad_content,items_data=[{'ad_space': ad_space,'start_date': start_date,'end_date': end_date,'price_per_day': min_price}])return Response({'order_id': order.id, 'order_no': order.order_no},status=status.HTTP_201_CREATED)# bidding/serializers.py
from rest_framework import serializers
from django.utils import timezone
from datetime import timedelta
from advertisements.models import AdSpace
from contents.models import AdContentclass BidSerializer(serializers.Serializer):ad_content_id = serializers.IntegerField()ad_space_id = serializers.IntegerField()start_date = serializers.DateField()end_date = serializers.DateField()price_per_day = serializers.DecimalField(max_digits=10, decimal_places=2)def validate_ad_content_id(self, value):if not AdContent.objects.filter(id=value, advertiser=self.context['request'].user,status='approved').exists():raise serializers.ValidationError("Invalid ad content")return valuedef validate_ad_space_id(self, value):if not AdSpace.objects.filter(id=value, is_available=True).exists():raise serializers.ValidationError("Ad space not available")return valuedef validate(self, data):if data['start_date'] > data['end_date']:raise serializers.ValidationError("End date must be after start date")# 開始日期不能早于明天if data['start_date'] < timezone.now().date() + timedelta(days=1):raise serializers.ValidationError("Start date must be at least tomorrow")# 持續時間不能超過90天if (data['end_date'] - data['start_date']).days > 90:raise serializers.ValidationError("Duration cannot exceed 90 days")# 檢查價格是否高于廣告位的基礎價格ad_space = AdSpace.objects.get(id=data['ad_space_id'])if data['price_per_day'] < ad_space.price_per_day:raise serializers.ValidationError(f"Bid price must be at least {ad_space.price_per_day}")return data
5.3 實時數據監控
# monitoring/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from advertisements.models import AdSpace
from contents.models import AdContent
from orders.models import OrderItemclass AdMonitoringConsumer(AsyncWebsocketConsumer):async def connect(self):self.user = self.scope['user']if not self.user.is_authenticated:await self.close()returnself.advertiser_group = Noneself.property_group = Noneif self.user.user_type == 'advertiser':self.advertiser_group = f'advertiser_{self.user.id}'await self.channel_layer.group_add(self.advertiser_group,self.channel_name)elif self.user.user_type == 'property':self.property_group = f'property_{self.user.id}'await self.channel_layer.group_add(self.property_group,self.channel_name)await self.accept()async def disconnect(self, close_code):if self.advertiser_group:await self.channel_layer.group_discard(self.advertiser_group,self.channel_name)if self.property_group:await self.channel_layer.group_discard(self.property_group,self.channel_name)async def receive(self, text_data):data = json.loads(text_data)action = data.get('action')if action == 'subscribe_ad':ad_id = data.get('ad_id')if ad_id and self.user.user_type == 'advertiser':ad = await self.get_ad_content(ad_id)if ad and ad.advertiser == self.user:group = f'ad_{ad_id}'await self.channel_layer.group_add(group,self.channel_name)await self.send(text_data=json.dumps({'type': 'subscription','status': 'subscribed','ad_id': ad_id}))elif action == 'subscribe_space':space_id = data.get('space_id')if space_id and self.user.user_type == 'property':space = await self.get_ad_space(space_id)if space and space.community.property_company == self.user:group = f'space_{space_id}'await self.channel_layer.group_add(group,self.channel_name)await self.send(text_data=json.dumps({'type': 'subscription','status': 'subscribed','space_id': space_id}))async def ad_update(self, event):await self.send(text_data=json.dumps(event))async def space_update(self, event):await self.send(text_data=json.dumps(event))async def order_update(self, event):await self.send(text_data=json.dumps(event))async def impression_update(self, event):await self.send(text_data=json.dumps(event))@database_sync_to_asyncdef get_ad_content(self, ad_id):try:return AdContent.objects.get(id=ad_id)except AdContent.DoesNotExist:return None@database_sync_to_asyncdef get_ad_space(self, space_id):try:return AdSpace.objects.get(id=space_id)except AdSpace.DoesNotExist:return None# monitoring/signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from contents.models import AdContent
from advertisements.models import AdSpace
from orders.models import AdOrder, OrderItem
from analytics.models import AdImpression, AdInteraction@receiver(post_save, sender=AdContent)
def ad_content_updated(sender, instance, created, **kwargs):channel_layer = get_channel_layer()group_name = f'ad_{instance.id}'async_to_sync(channel_layer.group_send)(group_name,{'type': 'ad_update','ad_id': instance.id,'status': instance.status,'updated': True,'created': created})if instance.advertiser:advertiser_group = f'advertiser_{instance.advertiser.id}'async_to_sync(channel_layer.group_send)(advertiser_group,{'type': 'ad_update','ad_id': instance.id,'status': instance.status,'updated': True,'created': created})@receiver(post_save, sender=AdSpace)
def ad_space_updated(sender, instance, created, **kwargs):channel_layer = get_channel_layer()group_name = f'space_{instance.id}'async_to_sync(channel_layer.group_send)(group_name,{'type': 'space_update','space_id': instance.id,'is_available': instance.is_available,'updated': True,'created': created})if instance.community and instance.community.property_company:property_group = f'property_{instance.community.property_company.id}'async_to_sync(channel_layer.group_send)(property_group,{'type': 'space_update','space_id': instance.id,'is_available': instance.is_available,'updated': True,'created': created})@receiver(post_save, sender=AdOrder)
def order_updated(sender, instance, created, **kwargs):channel_layer = get_channel_layer()if instance.advertiser:advertiser_group = f'advertiser_{instance.advertiser.id}'async_to_sync(channel_layer.group_send)(advertiser_group,{'type': 'order_update','order_id': instance.id,'status': instance.status,'updated': True,'created': created})# 通知物業公司property_companies = set()for item in instance.items.all():if item.ad_space.community and item.ad_space.community.property_company:property_companies.add(item.ad_space.community.property_company)for company in property_companies:property_group = f'property_{company.id}'async_to_sync(channel_layer.group_send)(property_group,{'type': 'order_update','order_id': instance.id,'status': instance.status,'updated': True,'created': created})@receiver(post_save, sender=AdImpression)
def impression_updated(sender, instance, created, **kwargs):channel_layer = get_channel_layer()group_name = f'ad_{instance.ad_content.id}'async_to_sync(channel_layer.group_send)(group_name,{'type': 'impression_update','ad_id': instance.ad_content.id,'date': instance.date.isoformat(),'view_count': instance.view_count,'interaction_count': instance.interaction_count,'updated': True,'created': created})
6. 系統部署與運維
6.1 部署架構
┌───────────────────────────────────────────────────────────────┐
│ 負載均衡層 (Nginx) │
└───────────────────────────────────────────────────────────────┘│┌─────────┴─────────┐▼ ▼
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ Web服務器1 │ │ Web服務器2 │
│ ┌───────────┐ ┌───────────┐ │ │ ┌───────────┐ ┌───────────┐ │
│ │ Django │ │ Celery │ │ │ │ Django │ │ Celery │ │
│ └───────────┘ └───────────┘ │ │ └───────────┘ └───────────┘ │
└─────────────────────────────────┘ └─────────────────────────────────┘│ │└─────────┬─────────┘▼
┌───────────────────────────────────────────────────────────────┐
│ 數據存儲層 │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ PostgreSQL│ │ Redis │ │ RabbitMQ │ │ OSS │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└───────────────────────────────────────────────────────────────┘
6.2 Docker部署配置
# docker-compose.yml
version: '3.8'services:web:build: .command: gunicorn config.wsgi:application --bind 0.0.0.0:8000volumes:- .:/codeports:- "8000:8000"env_file:- .envdepends_on:- redis- db- rabbitmqcelery:build: .command: celery -A config worker -l infovolumes:- .:/codeenv_file:- .envdepends_on:- redis- db- rabbitmqcelery-beat:build: .command: celery -A config beat -l infovolumes:- .:/codeenv_file:- .envdepends_on:- redis- db- rabbitmqdb:image: postgres:13volumes:- postgres_data:/var/lib/postgresql/data/environment:- POSTGRES_USER=${DB_USER}- POSTGRES_PASSWORD=${DB_PASSWORD}- POSTGRES_DB=${DB_NAME}ports:- "5432:5432"redis:image: redis:6ports:- "6379:6379"rabbitmq:image: rabbitmq:3-managementports:- "5672:5672"- "15672:15672"volumes:- rabbitmq_data:/var/lib/rabbitmqvolumes:postgres_data:rabbitmq_data:
6.3 性能優化策略
-
數據庫優化:
- 使用索引優化查詢性能
- 配置數據庫連接池
- 讀寫分離
-
緩存策略:
- 使用Redis緩存熱點數據
- 實現多級緩存
- 緩存廣告位和廣告內容的列表
-
異步處理:
- 使用Celery處理耗時任務
- 異步生成報表
- 異步處理圖片和視頻
-
CDN加速:
- 靜態資源使用CDN分發
- 廣告素材使用CDN加速
-
負載均衡:
- 使用Nginx做負載均衡
- 配置多臺應用服務器
7. 系統安全設計
7.1 安全措施
-
認證與授權:
- JWT認證
- 細粒度的權限控制
- 防止越權訪問
-
數據安全:
- 敏感數據加密存儲
- 數據庫備份策略
- 防止SQL注入
-
API安全:
- 接口限流
- 防止CSRF攻擊
- 參數校驗
-
日志與監控:
- 操作日志記錄
- 異常監控
- 安全審計
7.2 安全代碼示例
# security/middleware.py
from django.utils.deprecation import MiddlewareMixin
from django.conf import settings
from django.core.exceptions import SuspiciousOperation
import reclass SecurityHeadersMiddleware(MiddlewareMixin):def process_response(self, request, response):# 設置安全相關的HTTP頭response['X-Content-Type-Options'] = 'nosniff'response['X-Frame-Options'] = 'DENY'response['X-XSS-Protection'] = '1; mode=block'if settings.SECURE_SSL_REDIRECT:response['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'return responseclass InputValidationMiddleware(MiddlewareMixin):SQLI_PATTERNS = [re.compile(r'(\s*([\0\b\'\"\n\r\t\%\_\\]*\s*(((select\s*.+\s*from\s*.+)|(insert\s*.+\s*into\s*.+)|(update\s*.+\s*set\s*.+)|(delete\s*.+\s*from\s*.+)|(drop\s*.+)|(truncate\s*.+)|(alter\s*.+)|(exec\s*.+)|(\s*(all|any|not|and|between|in|like|or|some|contains|containsall|containskey)\s*.+[\=\>\<=\!\~]+.+)|(let\s+.+[\=]\s*.*)|(begin\s*.*\s*end)|(\s*[\/\*]+\s*.*\s*[\*\/]+)|(\s*(\-\-)\s*.*\s+)|(\s*(contains|containsall|containskey)\s+.*)))(\s*[\,)\;\s]*\s*)*)+', re.I),]XSS_PATTERNS = [re.compile(r'<script.*?>.*?</script>', re.I),re.compile(r'on[a-z]+\s*=', re.I),]def process_request(self, request):# 檢查GET參數for key, value in request.GET.items():self._check_input(key, value)# 檢查POST參數for key, value in request.POST.items():self._check_input(key, value)# 檢查JSON bodyif request.content_type == 'application/json' and request.body:try:import jsondata = json.loads(request.body)self._check_json(data)except ValueError:passdef _check_input(self, key, value):if isinstance(value, str):for pattern in self.SQLI_PATTERNS:if pattern.search(value):raise SuspiciousOperation(f'Potential SQL injection detected in parameter {key}')for pattern in self.XSS_PATTERNS:if pattern.search(value):raise SuspiciousOperation(f'Potential XSS detected in parameter {key}')def _check_json(self, data):if isinstance(data, dict):for key, value in data.items():self._check_input(key, value)self._check_json(value)elif isinstance(data, list):for item in data:self._check_json(item)
8. 系統測試方案
8.1 測試策略
- 單元測試:測試各個模塊的功能
- 集成測試:測試模塊間的交互
- 性能測試:測試系統在高負載下的表現
- 安全測試:測試系統的安全性
- UI測試:測試用戶界面
8.2 測試代碼示例
# tests/test_advertisements.py
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APIClient
from rest_framework import status
from advertisements.models import AdSpace
from users.models import Userclass AdSpaceTests(TestCase):def setUp(self):self.client = APIClient()self.property_user = User.objects.create_user(username='property',password='password',user_type='property')self.advertiser_user = User.objects.create_user(username='advertiser',password='password',user_type='advertiser')self.community = Community.objects.create(name='Test Community',address='Test Address',property_company=self.property_user,total_buildings=10,total_households=1000)self.ad_space = AdSpace.objects.create(community=self.community,space_type='elevator',description='Test Ad Space',size='60x90cm',price_per_day=100.00,is_available=True)def test_create_ad_space_as_property(self):self.client.force_authenticate(user=self.property_user)url = reverse('adspace-list')data = {'community': self.community.id,'space_type': 'billboard','description': 'New Ad Space','size': '200x300cm','price_per_day': '200.00','is_available': True}response = self.client.post(url, data, format='json')self.assertEqual(response.status_code, status.HTTP_201_CREATED)self.assertEqual(AdSpace.objects.count(), 2)def test_create_ad_space_as_advertiser(self):self.client.force_authenticate(user=self.advertiser_user)url = reverse('adspace-list')data = {'community': self.community.id,'space_type': 'billboard','description': 'New Ad Space','size': '200x300cm','price_per_day': '200.00','is_available': True}response = self.client.post(url, data, format='json')self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)def test_list_ad_spaces(self):url = reverse('adspace-list')response = self.client.get(url, format='json')self.assertEqual(response.status_code, status.HTTP_200_OK)self.assertEqual(len(response.data), 1)def test_filter_ad_spaces(self):# 創建另一個廣告位AdSpace.objects.create(community=self.community,space_type='billboard',description='Billboard',size='200x300cm',price_per_day=200.00,is_available=True)url = reverse('adspace-list')# 按類型過濾response = self.client.get(url, {'space_type': 'elevator'}, format='json')self.assertEqual(response.status_code, status.HTTP_200_OK)self.assertEqual(len(response.data), 1)self.assertEqual(response.data[0]['space_type'], 'elevator')# 按價格范圍過濾response = self.client.get(url, {'min_price': 150, 'max_price': 250}, format='json')self.assertEqual(response.status_code, status.HTTP_200_OK)self.assertEqual(len(response.data), 1)self.assertEqual(response.data[0]['space_type'], 'billboard')
9. 系統擴展與未來規劃
9.1 擴展功能
- AI內容審核:使用機器學習自動審核廣告內容
- 智能定價:根據歷史數據和需求動態調整廣告位價格
- AR預覽:使用增強現實技術預覽廣告效果
- 區塊鏈合約:使用智能合約管理廣告交易
- 跨平臺投放:整合線上和線下廣告資源
9.2 技術演進
- 微服務化:將系統拆分為更小的微服務
- Serverless架構:部分功能使用無服務器架構
- 大數據分析:更深入的廣告效果分析
- 邊緣計算:在邊緣節點處理部分計算任務
- 5G應用:利用5G網絡實現更豐富的廣告形式
10. 結論
本文詳細設計并實現了一個基于Python的社區資源媒體管理系統,該系統專為社區戶外廣告打造,提供了從廣告資源管理、廣告內容制作、廣告投放到效果分析的全流程解決方案。系統采用現代化的技術架構,具有良好的擴展性和性能表現,能夠滿足不同規模社區廣告管理的需求。
系統的主要創新點包括:
- 專業化設計:針對社區戶外廣告場景的深度定制
- 智能化推薦:基于算法的廣告位推薦系統
- 可視化操作:直觀的廣告資源管理和投放界面
- 安全可靠:多層次的安全防護機制
未來,系統可以進一步整合AI和大數據技術,提供更智能的廣告投放服務和更精準的效果分析,成為社區戶外廣告領域的標桿解決方案。