微信小程序-智慧社区项目开发完整技术文档(上)
学习项目(仅作参考)
一、项目概述
1.1 项目背景与目标
智慧社区项目旨在通过数字化手段提升社区管理效率和服务质量。该项目采用微信小程序作为前端载体,结合Django后端框架,为社区居民和网格员提供便捷的信息采集、人脸识别、语音识别等功能。
1.2 核心功能架构
┌─ 欢迎页面 (动态广告页)
├─ 首页 (轮播图 + 公告栏 + 功能入口)
├─ 信息采集模块
│ ├─ 采集列表
│ ├─ 表单填写
│ ├─ 拍照功能
│ └─ 数据统计
├─ 人脸识别模块
│ ├─ 人脸注册
│ ├─ 人脸搜索
│ └─ 识别记录
├─ 语音识别模块
│ ├─ 录音功能
│ └─ 语音转文字
├─ 社区活动
├─ 积分商城
└─ 个人中心
1.3 技术栈详解
-
前端技术栈:
- 微信小程序原生框架
- Vant Weapp UI组件库
- WXML/WXSS/JavaScript
- 微信原生API(相机、录音等)
-
后端技术栈:
- Django 3.2+ Web框架
- Django REST Framework (DRF)
- SQLite/MySQL数据库
- SimpleUI后台管理
- Pillow图像处理库
- 百度AI SDK
二、环境搭建与项目初始化
2.1 小程序端详细配置
项目结构规划
smart-community/
├── pages/
│ ├── welcome/
│ ├── index/
│ ├── collection/
│ ├── form/
│ ├── camera/
│ ├── face/
│ ├── voice/
│ └── statistics/
├── components/
├── static/
│ ├── images/
│ ├── css/
│ └── icons/
├── config/
├── utils/
└── app.js
详细的app.json配置
json
{
"pages": [
"pages/welcome/welcome",
"pages/index/index",
"pages/collection/collection",
"pages/form/form",
"pages/camera/camera",
"pages/face/face",
"pages/voice/voice",
"pages/statistics/statistics",
"pages/activity/activity",
"pages/goods/goods",
"pages/profile/profile"
],
"tabBar": {
"color": "#999999",
"selectedColor": "#007AFF",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/images/icon/home.png",
"selectedIconPath": "static/images/icon/home-active.png"
},
{
"pagePath": "pages/activity/activity",
"text": "活动",
"iconPath": "static/images/icon/activity.png",
"selectedIconPath": "static/images/icon/activity-active.png"
},
{
"pagePath": "pages/profile/profile",
"text": "我的",
"iconPath": "static/images/icon/profile.png",
"selectedIconPath": "static/images/icon/profile-active.png"
}
]
},
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#007AFF",
"navigationBarTitleText": "智慧社区",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": true
},
"permission": {
"scope.camera": {
"desc": "需要访问相机进行拍照和人脸识别"
},
"scope.record": {
"desc": "需要访问麦克风进行语音识别"
}
},
"requiredPrivateInfos": [
"camera",
"record"
]
}
项目配置文件优化
javascript
// project.config.json
{
"description": "项目配置文件",
"packOptions": {
"ignore": [
{
"type": "file",
"value": ".eslintrc.js"
}
]
},
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"enableEngineNative": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./miniprogram/"
}
],
"minifyWXSS": true,
"showES6CompileOption": false,
"minifyWXML": true
},
"compileType": "miniprogram",
"libVersion": "2.19.4",
"appid": "你的小程序AppID",
"projectname": "smart-community",
"debugOptions": {
"hidedInDevtools": []
},
"scripts": {},
"staticServerOptions": {
"baseURL": "",
"servePath": ""
},
"isGameTourist": false,
"condition": {
"search": {
"list": []
},
"conversation": {
"list": []
},
"game": {
"list": []
},
"plugin": {
"list": []
},
"gamePlugin": {
"list": []
},
"miniprogram": {
"list": []
}
}
}
2.2 后端Django项目详细搭建
项目结构设计
smart-backend/
├── manage.py
├── smart_backend/
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
├── smart/
│ ├── migrations/
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── serializers.py
│ ├── views.py
│ ├── urls.py
│ └── libs/
│ └── baidu_ai.py
├── media/
│ ├── welcome/
│ ├── banner/
│ ├── collection/
│ └── notice/
└── requirements.txt
详细的settings.py配置
python
"""
Django settings for smart_backend project.
"""
import os
from pathlib import Path
from datetime import timedelta
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-your-secret-key-here'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'simpleui',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'django_filters',
'smart.apps.SmartConfig',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'smart_backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'smart_backend.wsgi.application'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# REST Framework configuration
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.MultiPartParser',
'rest_framework.parsers.FormParser',
],
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20
}
# CORS settings
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = [
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
]
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
# Baidu AI configuration
BAIDU_AI_APP_ID = 'your_app_id'
BAIDU_AI_API_KEY = 'your_api_key'
BAIDU_AI_SECRET_KEY = 'your_secret_key'
# SimpleUI configuration
SIMPLEUI_HOME_INFO = False
SIMPLEUI_ANALYSIS = False
SIMPLEUI_DEFAULT_THEME = 'admin.lte.css'
详细的URL配置
python
# smart_backend/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('smart/', include('smart.urls')),
]
# 开发环境下的媒体文件服务
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
2.3 Vant Weapp深度集成
详细的npm配置和构建
json
// package.json
{
"name": "smart-community",
"version": "1.0.0",
"description": "智慧社区微信小程序",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["微信小程序", "智慧社区"],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"@vant/weapp": "^1.10.0"
},
"devDependencies": {}
}
组件全局注册
javascript
// app.js
App({
onLaunch() {
// 展示本地存储能力
const logs = wx.getStorageSync('logs') || []
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)
// 登录
wx.login({
success: res => {
console.log('登录成功', res.code)
// 发送 res.code 到后台换取 openId, sessionKey, unionId
}
})
},
globalData: {
userInfo: null,
baseUrl: 'http://192.168.1.100:8000/smart/',
uploadUrl: 'http://192.168.1.100:8000/smart/upload/'
}
})
css
/* app.wxss */
page {
background-color: #f7f7f7;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
'Segoe UI', Arial, Roboto, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft Yahei', sans-serif;
}
.container {
padding: 20rpx;
box-sizing: border-box;
}
/* 引入Vant Weapp样式 */
@import '/miniprogram_npm/@vant/weapp/common/index.wxss';
/* 引入图标字体 */
@import '/static/css/iconfont.wxss';
三、欢迎页面深度开发
3.1 前端完整实现
WXML结构优化
xml
<!-- pages/welcome/welcome.wxml -->
<view class="welcome-container">
<!-- 背景图片 -->
<image
src="{{splashImage}}"
mode="aspectFill"
class="splash-image"
bindload="onImageLoad"
binderror="onImageError"
></image>
<!-- 遮罩层 -->
<view class="overlay">
<!-- 应用Logo -->
<image src="/static/images/logo.png" class="app-logo"></image>
<!-- 应用名称 -->
<text class="app-name">智慧社区</text>
<!-- 应用标语 -->
<text class="app-slogan">数字化社区管理解决方案</text>
</view>
<!-- 跳过按钮 -->
<view class="skip-container">
<view class="skip-btn" bindtap="skipWelcome">
<text class="skip-text">跳过</text>
<view class="countdown-badge">{{countdown}}s</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{isLoading}}">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
</view>
WXSS样式深度优化
css
/* pages/welcome/welcome.wxss */
.welcome-container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.splash-image {
width: 100%;
height: 100%;
transition: opacity 0.5s ease;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.3) 0%,
rgba(0, 0, 0, 0.1) 50%,
rgba(0, 0, 0, 0.3) 100%
);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 60rpx;
box-sizing: border-box;
}
.app-logo {
width: 120rpx;
height: 120rpx;
border-radius: 24rpx;
margin-bottom: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
}
.app-name {
font-size: 48rpx;
font-weight: bold;
color: #ffffff;
margin-bottom: 20rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.5);
}
.app-slogan {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.9);
text-align: center;
line-height: 1.5;
}
.skip-container {
position: absolute;
top: 120rpx;
right: 40rpx;
z-index: 10;
}
.skip-btn {
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.4);
border-radius: 40rpx;
padding: 16rpx 32rpx;
backdrop-filter: blur(10rpx);
border: 1rpx solid rgba(255, 255, 255, 0.2);
}
.skip-text {
font-size: 28rpx;
color: #ffffff;
margin-right: 16rpx;
}
.countdown-badge {
background: #ff3b30;
color: #ffffff;
font-size: 24rpx;
width: 48rpx;
height: 48rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.loading-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
background: rgba(0, 0, 0, 0.7);
padding: 40rpx;
border-radius: 20rpx;
backdrop-filter: blur(20rpx);
}
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top: 4rpx solid #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
.loading-text {
font-size: 28rpx;
color: #ffffff;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式适配 */
@media (max-width: 375px) {
.app-name {
font-size: 42rpx;
}
.app-slogan {
font-size: 26rpx;
}
}
JavaScript逻辑完整实现
javascript
// pages/welcome/welcome.js
const app = getApp()
Page({
data: {
splashImage: '/static/images/default-splash.jpg',
countdown: 5,
isLoading: true,
hasError: false,
timer: null
},
onLoad() {
this.initWelcomePage()
},
onUnload() {
// 清理定时器
if (this.data.timer) {
clearInterval(this.data.timer)
}
},
async initWelcomePage() {
try {
// 并行执行加载任务
await Promise.all([
this.loadSplashImage(),
this.preloadHomeData()
])
this.startCountdown()
} catch (error) {
console.error('初始化欢迎页失败:', error)
this.handleInitError()
}
},
async loadSplashImage() {
return new Promise((resolve, reject) => {
wx.request({
url: app.globalData.baseUrl + 'welcome/',
method: 'GET',
timeout: 10000,
success: (res) => {
if (res.statusCode === 200 && res.data.code === 200) {
this.setData({
splashImage: res.data.data.image,
isLoading: false
})
resolve(res.data)
} else {
reject(new Error('获取欢迎页图片失败'))
}
},
fail: (error) => {
console.warn('使用默认欢迎图片:', error)
this.setData({ isLoading: false })
resolve() // 使用默认图片,不阻断流程
}
})
})
},
async preloadHomeData() {
// 预加载首页数据
return new Promise((resolve) => {
wx.request({
url: app.globalData.baseUrl + 'home/',
method: 'GET',
success: () => {
console.log('首页数据预加载成功')
resolve()
},
fail: () => {
console.warn('首页数据预加载失败')
resolve() // 预加载失败不影响主流程
}
})
})
},
startCountdown() {
let countdown = this.data.countdown
const timer = setInterval(() => {
countdown--
this.setData({ countdown })
if (countdown <= 0) {
clearInterval(timer)
this.navigateToHome()
}
}, 1000)
this.setData({ timer })
},
skipWelcome() {
if (this.data.timer) {
clearInterval(this.data.timer)
}
this.navigateToHome()
},
navigateToHome() {
wx.switchTab({
url: '/pages/index/index',
success: () => {
console.log('跳转到首页成功')
},
fail: (error) => {
console.error('跳转到首页失败:', error)
wx.showToast({
title: '跳转失败',
icon: 'error'
})
}
})
},
onImageLoad(e) {
console.log('欢迎页图片加载成功')
this.setData({ isLoading: false })
},
onImageError(e) {
console.error('欢迎页图片加载失败:', e.detail)
this.setData({
splashImage: '/static/images/default-splash.jpg',
isLoading: false,
hasError: true
})
},
handleInitError() {
this.setData({
isLoading: false,
hasError: true
})
wx.showToast({
title: '加载失败,请重试',
icon: 'error',
duration: 2000
})
// 3秒后自动跳转
setTimeout(() => {
this.navigateToHome()
}, 3000)
},
onShareAppMessage() {
return {
title: '智慧社区 - 数字化社区管理',
path: '/pages/welcome/welcome',
imageUrl: this.data.splashImage
}
}
})
json
// pages/welcome/welcome.json
{
"navigationStyle": "custom",
"disableScroll": true
}
3.2 后端数据模型详细设计
欢迎页模型扩展
python
# smart/models.py
from django.db import models
from django.utils import timezone
from django.core.validators import MinValueValidator, MaxValueValidator
class Welcome(models.Model):
"""欢迎页图片模型"""
IMAGE_TYPES = (
('splash', '启动页'),
('ad', '广告页'),
('promotion', '推广页'),
)
title = models.CharField(max_length=100, verbose_name="图片标题", blank=True)
image = models.ImageField(
upload_to='welcome/%Y/%m/%d/',
default='welcome/default-splash.png',
verbose_name="欢迎页图片"
)
image_type = models.CharField(
max_length=20,
choices=IMAGE_TYPES,
default='splash',
verbose_name="图片类型"
)
order = models.IntegerField(
default=0,
validators=[MinValueValidator(0), MaxValueValidator(999)],
verbose_name="排序权重",
help_text="数字越大排序越靠前"
)
start_time = models.DateTimeField(
default=timezone.now,
verbose_name="开始时间",
help_text="图片开始显示的时间"
)
end_time = models.DateTimeField(
null=True,
blank=True,
verbose_name="结束时间",
help_text="图片结束显示的时间,为空表示长期有效"
)
is_active = models.BooleanField(default=True, verbose_name="是否启用")
is_delete = models.BooleanField(default=False, verbose_name="逻辑删除")
click_url = models.URLField(
blank=True,
verbose_name="点击链接",
help_text="点击图片跳转的链接"
)
description = models.TextField(blank=True, verbose_name="图片描述")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
db_table = 'smart_welcome'
verbose_name = '欢迎页图片'
verbose_name_plural = verbose_name
ordering = ['-order', '-create_time']
indexes = [
models.Index(fields=['is_active', 'is_delete', 'image_type']),
models.Index(fields=['start_time', 'end_time']),
]
def __str__(self):
return self.title or f"欢迎页图片-{self.id}"
@property
def is_valid(self):
"""检查图片是否在有效期内"""
now = timezone.now()
if not self.is_active or self.is_delete:
return False
if self.start_time and self.start_time > now:
return False
if self.end_time and self.end_time < now:
return False
return True
def get_absolute_image_url(self, request=None):
"""获取完整的图片URL"""
if self.image and hasattr(self.image, 'url'):
if request:
return request.build_absolute_uri(self.image.url)
return self.image.url
return None
管理后台配置
python
# smart/admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Welcome
@admin.register(Welcome)
class WelcomeAdmin(admin.ModelAdmin):
list_display = [
'id', 'image_preview', 'title', 'image_type', 'order',
'is_active', 'is_valid', 'create_time'
]
list_filter = ['image_type', 'is_active', 'is_delete', 'create_time']
search_fields = ['title', 'description']
list_editable = ['order', 'is_active']
readonly_fields = ['create_time', 'update_time', 'image_preview']
fieldsets = (
('基本信息', {
'fields': ('title', 'image', 'image_preview', 'image_type', 'description')
}),
('时间设置', {
'fields': ('start_time', 'end_time')
}),
('配置设置', {
'fields': ('order', 'click_url', 'is_active')
}),
('系统信息', {
'fields': ('create_time', 'update_time'),
'classes': ('collapse',)
}),
)
def image_preview(self, obj):
if obj.image:
return format_html(
'<img src="{}" style="max-height: 50px; max-width: 100px;" />',
obj.image.url
)
return "-"
image_preview.short_description = '图片预览'
def is_valid(self, obj):
return obj.is_valid
is_valid.boolean = True
is_valid.short_description = '是否有效'
def get_queryset(self, request):
return super().get_queryset(request).filter(is_delete=False)
def delete_model(self, request, obj):
"""软删除"""
obj.is_delete = True
obj.is_active = False
obj.save()
def delete_queryset(self, request, queryset):
"""批量软删除"""
queryset.update(is_delete=True, is_active=False)
3.3 后端API接口完整实现
序列化器
python
# smart/serializers.py
from rest_framework import serializers
from .models import Welcome
class WelcomeSerializer(serializers.ModelSerializer):
image_url = serializers.SerializerMethodField()
is_valid = serializers.ReadOnlyField()
class Meta:
model = Welcome
fields = [
'id', 'title', 'image_url', 'image_type', 'order',
'click_url', 'description', 'is_valid', 'create_time'
]
read_only_fields = ['is_valid']
def get_image_url(self, obj):
request = self.context.get('request')
return obj.get_absolute_image_url(request)
视图集实现
python
# smart/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.utils import timezone
from django.db.models import Q
from .models import Welcome
from .serializers import WelcomeSerializer
class WelcomeAPIView(APIView):
"""
欢迎页图片API
支持获取当前有效的欢迎页图片
"""
def get(self, request):
try:
now = timezone.now()
# 查询当前有效的欢迎页图片
welcome_images = Welcome.objects.filter(
is_active=True,
is_delete=False,
start_time__lte=now
).filter(
Q(end_time__isnull=True) | Q(end_time__gte=now)
).order_by('-order', '-create_time')
if not welcome_images.exists():
return Response({
'code': 404,
'message': '未找到有效的欢迎页图片',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 获取排序最高的图片
welcome = welcome_images.first()
serializer = WelcomeSerializer(welcome, context={'request': request})
return Response({
'code': 200,
'message': 'success',
'data': serializer.data
})
except Exception as e:
return Response({
'code': 500,
'message': f'服务器错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class WelcomeListAPIView(APIView):
"""
欢迎页图片列表API
用于管理端查看所有图片
"""
def get(self, request):
try:
# 获取查询参数
image_type = request.GET.get('type')
is_active = request.GET.get('is_active')
queryset = Welcome.objects.filter(is_delete=False)
# 过滤条件
if image_type:
queryset = queryset.filter(image_type=image_type)
if is_active is not None:
queryset = queryset.filter(is_active=is_active.lower() == 'true')
queryset = queryset.order_by('-order', '-create_time')
serializer = WelcomeSerializer(
queryset,
many=True,
context={'request': request}
)
return Response({
'code': 200,
'message': 'success',
'data': serializer.data
})
except Exception as e:
return Response({
'code': 500,
'message': f'服务器错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
URL路由配置
python
# smart/urls.py
from django.urls import path
from .views import WelcomeAPIView, WelcomeListAPIView
urlpatterns = [
path('welcome/', WelcomeAPIView.as_view(), name='welcome'),
path('welcome/list/', WelcomeListAPIView.as_view(), name='welcome-list'),
]
3.4 配置管理深度优化
环境配置管理
javascript
// config/environment.js
// 环境配置管理
const environments = {
development: {
name: '开发环境',
baseUrl: 'http://192.168.1.100:8000/smart/',
uploadUrl: 'http://192.168.1.100:8000/smart/upload/',
debug: true,
logLevel: 'debug'
},
test: {
name: '测试环境',
baseUrl: 'https://test-api.yourdomain.com/smart/',
uploadUrl: 'https://test-api.yourdomain.com/smart/upload/',
debug: true,
logLevel: 'info'
},
production: {
name: '生产环境',
baseUrl: 'https://api.yourdomain.com/smart/',
uploadUrl: 'https://api.yourdomain.com/smart/upload/',
debug: false,
logLevel: 'warn'
}
}
// 自动检测环境
const getEnvironment = () => {
// 微信小程序开发工具
if (typeof __wxConfig !== 'undefined') {
const accountInfo = wx.getAccountInfoSync()
if (accountInfo.miniProgram.envVersion === 'develop') {
return 'development'
} else if (accountInfo.miniProgram.envVersion === 'trial') {
return 'test'
} else if (accountInfo.miniProgram.envVersion === 'release') {
return 'production'
}
}
// 默认开发环境
return 'development'
}
const currentEnv = getEnvironment()
const config = environments[currentEnv]
console.log(`当前环境: ${config.name}`)
module.exports = config
API配置管理
javascript
// config/api.js
const environment = require('./environment')
const apiConfig = {
// 欢迎页
welcome: {
get: environment.baseUrl + 'welcome/',
list: environment.baseUrl + 'welcome/list/'
},
// 首页
home: {
get: environment.baseUrl + 'home/'
},
// 轮播图
banner: {
list: environment.baseUrl + 'banner/'
},
// 公告
notice: {
list: environment.baseUrl + 'notice/',
detail: environment.baseUrl + 'notice/{id}/'
},
// 信息采集
collection: {
list: environment.baseUrl + 'collection/',
detail: environment.baseUrl + 'collection/{id}/',
create: environment.uploadUrl + 'collection/',
delete: environment.baseUrl + 'collection/{id}/'
},
// 网格区域
area: {
list: environment.baseUrl + 'area/'
},
// 人脸识别
face: {
detect: environment.uploadUrl + 'face/detect/'
},
// 语音识别
speech: {
recognize: environment.uploadUrl + 'speech/'
},
// 数据统计
statistics: {
collection: environment.baseUrl + 'statistics/collection/'
}
}
// API URL构建工具
const buildUrl = (url, params = {}) => {
let builtUrl = url
Object.keys(params).forEach(key => {
builtUrl = builtUrl.replace(`{${key}}`, params[key])
})
return builtUrl
}
module.exports = {
...apiConfig,
buildUrl,
environment
}
工具函数封装
javascript
// utils/request.js
const { environment, buildUrl } = require('../config/api')
/**
* 统一的网络请求封装
*/
class Request {
constructor() {
this.baseConfig = {
timeout: 10000,
header: {
'Content-Type': 'application/json'
}
}
}
// 请求拦截器
interceptorsRequest(config) {
// 添加认证token
const token = wx.getStorageSync('token')
if (token) {
config.header['Authorization'] = `Bearer ${token}`
}
// 添加时间戳防止缓存
if (config.method === 'GET') {
config.url += (config.url.includes('?') ? '&' : '?') + `_t=${Date.now()}`
}
if (environment.debug) {
console.log('🚀 [Request]', config)
}
return config
}
// 响应拦截器
interceptorsResponse(response) {
if (environment.debug) {
console.log('📨 [Response]', response)
}
const { statusCode, data } = response
if (statusCode === 200) {
return Promise.resolve(data)
} else if (statusCode === 401) {
// token过期,跳转到登录页
wx.removeStorageSync('token')
wx.showToast({
title: '登录已过期',
icon: 'error'
})
return Promise.reject(new Error('认证失败'))
} else if (statusCode >= 500) {
return Promise.reject(new Error('服务器错误'))
} else {
return Promise.reject(new Error(`网络错误: ${statusCode}`))
}
}
// 统一请求方法
request(config) {
config = { ...this.baseConfig, ...config }
config = this.interceptorsRequest(config)
return new Promise((resolve, reject) => {
wx.request({
...config,
success: (res) => {
this.interceptorsResponse(res)
.then(resolve)
.catch(reject)
},
fail: (error) => {
console.error('请求失败:', error)
wx.showToast({
title: '网络连接失败',
icon: 'error'
})
reject(error)
}
})
})
}
// GET请求
get(url, data = {}, config = {}) {
return this.request({
url,
data,
method: 'GET',
...config
})
}
// POST请求
post(url, data = {}, config = {}) {
return this.request({
url,
data,
method: 'POST',
...config
})
}
// PUT请求
put(url, data = {}, config = {}) {
return this.request({
url,
data,
method: 'PUT',
...config
})
}
// DELETE请求
delete(url, data = {}, config = {}) {
return this.request({
url,
data,
method: 'DELETE',
...config
})
}
// 文件上传
upload(url, filePath, formData = {}, config = {}) {
return new Promise((resolve, reject) => {
const uploadTask = wx.uploadFile({
url,
filePath,
name: 'file',
formData,
...config,
success: (res) => {
if (res.statusCode === 200) {
try {
const data = JSON.parse(res.data)
resolve(data)
} catch (error) {
reject(new Error('解析响应数据失败'))
}
} else {
reject(new Error(`上传失败: ${res.statusCode}`))
}
},
fail: reject
})
// 上传进度监听
uploadTask.onProgressUpdate((res) => {
if (config.onProgress) {
config.onProgress(res)
}
})
})
}
}
// 创建请求实例
const request = new Request()
module.exports = request
javascript
// utils/util.js
/**
* 通用工具函数
*/
// 格式化时间
const formatTime = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
if (!date) return ''
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hour = String(d.getHours()).padStart(2, '0')
const minute = String(d.getMinutes()).padStart(2, '0')
const second = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hour)
.replace('mm', minute)
.replace('ss', second)
}
// 防抖函数
const debounce = (func, wait = 300) => {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
// 节流函数
const throttle = (func, limit = 300) => {
let inThrottle
return function(...args) {
if (!inThrottle) {
func.apply(this, args)
inThrottle = true
setTimeout(() => inThrottle = false, limit)
}
}
}
// 深拷贝
const deepClone = (obj) => {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj)
if (obj instanceof Array) return obj.map(item => deepClone(item))
if (obj instanceof Object) {
const clonedObj = {}
Object.keys(obj).forEach(key => {
clonedObj[key] = deepClone(obj[key])
})
return clonedObj
}
}
// 显示加载提示
const showLoading = (title = '加载中...') => {
wx.showLoading({
title,
mask: true
})
}
// 隐藏加载提示
const hideLoading = () => {
wx.hideLoading()
}
// 显示成功提示
const showSuccess = (title = '操作成功') => {
wx.showToast({
title,
icon: 'success',
duration: 2000
})
}
// 显示错误提示
const showError = (title = '操作失败') => {
wx.showToast({
title,
icon: 'error',
duration: 2000
})
}
// 验证手机号
const validatePhone = (phone) => {
const reg = /^1[3-9]\d{9}$/
return reg.test(phone)
}
// 验证身份证号
const validateIDCard = (idCard) => {
const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
return reg.test(idCard)
}
// 生成随机字符串
const generateRandomString = (length = 8) => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
module.exports = {
formatTime,
debounce,
throttle,
deepClone,
showLoading,
hideLoading,
showSuccess,
showError,
validatePhone,
validateIDCard,
generateRandomString
}
四、首页(Index)深度开发
4.1 前端完整实现
WXML结构优化
xml
<!-- pages/index/index.wxml -->
<view class="index-container">
<!-- 轮播图 -->
<view class="banner-section">
<van-swiper
class="banner-swiper"
autoplay="{{3000}}"
indicator-dots="{{true}}"
indicator-color="rgba(255,255,255,0.5)"
indicator-active-color="#ffffff"
circular="{{true}}"
>
<van-swiper-item wx:for="{{bannerList}}" wx:key="id">
<image
src="{{item.image_url}}"
mode="aspectFill"
class="banner-image"
bindtap="onBannerClick"
data-url="{{item.click_url}}"
data-id="{{item.id}}"
/>
</van-swiper-item>
</van-swiper>
</view>
<!-- 公告栏 -->
<view class="notice-section">
<van-notice-bar
left-icon="volume-o"
color="#333333"
background="#fff9e6"
text="{{notice.content || '暂无公告'}}"
scrollable="{{true}}"
speed="50"
bind:click="onNoticeClick"
/>
</view>
<!-- 功能网格 -->
<view class="grid-section">
<van-grid
column-num="3"
clickable="{{true}}"
border="{{false}}"
>
<van-grid-item
wx:for="{{gridItems}}"
wx:key="id"
icon="{{item.icon}}"
text="{{item.text}}"
bind:click="navigateToPage"
data-page="{{item.page}}"
/>
</van-grid>
</view>
<!-- 社区动态 -->
<view class="news-section" wx:if="{{newsList.length > 0}}">
<view class="section-header">
<text class="section-title">社区动态</text>
<text class="section-more" bindtap="viewMoreNews">更多</text>
</view>
<view class="news-list">
<view
class="news-item"
wx:for="{{newsList}}"
wx:key="id"
bindtap="viewNewsDetail"
data-id="{{item.id}}"
>
<image class="news-image" src="{{item.image_url}}" mode="aspectFill" />
<view class="news-content">
<text class="news-title">{{item.title}}</text>
<text class="news-time">{{item.create_time}}</text>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{isLoading}}">
<van-loading size="24px" vertical>加载中...</van-loading>
</view>
<!-- 错误状态 -->
<view class="error-container" wx:if="{{hasError}}">
<view class="error-content">
<image src="/static/images/error.png" class="error-image" />
<text class="error-text">加载失败,请重试</text>
<button class="retry-btn" bindtap="loadHomeData">重新加载</button>
</view>
</view>
</view>
WXSS样式深度优化
css
/* pages/index/index.wxss */
.index-container {
min-height: 100vh;
background: #f7f7f7;
}
.banner-section {
padding: 20rpx;
padding-bottom: 0;
}
.banner-swiper {
height: 300rpx;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.banner-image {
width: 100%;
height: 100%;
}
.notice-section {
margin: 20rpx;
border-radius: 12rpx;
overflow: hidden;
}
.grid-section {
margin: 30rpx 20rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 20rpx 0;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.news-section {
margin: 30rpx 20rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.section-more {
font-size: 26rpx;
color: #999999;
}
.news-list {
display: flex;
flex-direction: column;
gap: 30rpx;
}
.news-item {
display: flex;
gap: 20rpx;
}
.news-image {
width: 160rpx;
height: 120rpx;
border-radius: 12rpx;
flex-shrink: 0;
}
.news-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.news-title {
font-size: 28rpx;
color: #333333;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.news-time {
font-size: 24rpx;
color: #999999;
}
.loading-container,
.error-container {
display: flex;
justify-content: center;
align-items: center;
padding: 100rpx 0;
}
.error-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.error-image {
width: 120rpx;
height: 120rpx;
}
.error-text {
font-size: 28rpx;
color: #999999;
}
.retry-btn {
background: #007AFF;
color: #ffffff;
border: none;
border-radius: 40rpx;
padding: 16rpx 40rpx;
font-size: 28rpx;
}
/* 响应式适配 */
@media (max-width: 375px) {
.banner-swiper {
height: 250rpx;
}
.news-image {
width: 140rpx;
height: 100rpx;
}
}
JavaScript逻辑完整实现
javascript
// pages/index/index.js
const { request, buildUrl } = require('../../utils/request')
const { showError, showLoading, hideLoading } = require('../../utils/util')
Page({
data: {
bannerList: [],
notice: {},
newsList: [],
gridItems: [
{
id: 1,
icon: 'user-circle-o',
text: '信息采集',
page: 'collection'
},
{
id: 2,
icon: 'calender-o',
text: '社区活动',
page: 'activity'
},
{
id: 3,
icon: 'user-o',
text: '人脸检测',
page: 'face'
},
{
id: 4,
icon: 'volume-o',
text: '语音识别',
page: 'voice'
},
{
id: 5,
icon: 'heart-o',
text: '心率检测',
page: 'heart'
},
{
id: 6,
icon: 'shopping-cart-o',
text: '积分商城',
page: 'goods'
}
],
isLoading: true,
hasError: false
},
onLoad() {
this.loadHomeData()
},
onPullDownRefresh() {
this.loadHomeData().finally(() => {
wx.stopPullDownRefresh()
})
},
onShareAppMessage() {
return {
title: '智慧社区 - 首页',
path: '/pages/index/index'
}
},
async loadHomeData() {
this.setData({ isLoading: true, hasError: false })
try {
// 并行请求首页数据
const [bannerRes, homeRes] = await Promise.all([
request.get(buildUrl('banner.list')),
request.get(buildUrl('home.get'))
])
const bannerList = bannerRes.code === 200 ? bannerRes.data : []
const homeData = homeRes.code === 200 ? homeRes.data : {}
this.setData({
bannerList,
notice: homeData.notice || {},
newsList: homeData.news || [],
isLoading: false
})
} catch (error) {
console.error('加载首页数据失败:', error)
this.setData({ hasError: true, isLoading: false })
showError('加载失败')
}
},
onBannerClick(e) {
const { url, id } = e.currentTarget.dataset
if (url) {
// 跳转到指定页面或网页
wx.navigateTo({
url: `/pages/webview/webview?url=${encodeURIComponent(url)}`
})
}
// 记录点击统计(可选)
this.recordBannerClick(id)
},
onNoticeClick() {
const { notice } = this.data
if (notice.id) {
wx.navigateTo({
url: `/pages/notice/detail/detail?id=${notice.id}`
})
}
},
navigateToPage(e) {
const { page } = e.currentTarget.dataset
if (!page) {
showError('功能开发中')
return
}
const pageMap = {
collection: '/pages/collection/collection',
activity: '/pages/activity/activity',
face: '/pages/face/face',
voice: '/pages/voice/voice',
heart: '/pages/heart/heart',
goods: '/pages/goods/goods'
}
const url = pageMap[page]
if (url) {
if (page === 'activity' || page === 'goods') {
wx.switchTab({ url })
} else {
wx.navigateTo({ url })
}
} else {
showError('功能暂未开放')
}
},
viewMoreNews() {
wx.navigateTo({
url: '/pages/news/list/list'
})
},
viewNewsDetail(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/news/detail/detail?id=${id}`
})
},
recordBannerClick(bannerId) {
// 记录banner点击统计,可选实现
request.post(buildUrl('banner.click'), { banner_id: bannerId })
.catch(console.error)
}
})
json
// pages/index/index.json
{
"usingComponents": {
"van-swiper": "@vant/weapp/swiper/index",
"van-swiper-item": "@vant/weapp/swiper-item/index",
"van-notice-bar": "@vant/weapp/notice-bar/index",
"van-grid": "@vant/weapp/grid/index",
"van-grid-item": "@vant/weapp/grid-item/index",
"van-loading": "@vant/weapp/loading/index"
},
"enablePullDownRefresh": true,
"backgroundTextStyle": "dark"
}
4.2 后端数据模型详细设计
轮播图模型扩展
python
# smart/models.py
class Banner(models.Model):
"""轮播图模型"""
POSITION_CHOICES = (
('home', '首页'),
('activity', '活动页'),
('mall', '商城页'),
)
title = models.CharField(max_length=100, verbose_name="标题", blank=True)
image = models.ImageField(upload_to='banner/%Y/%m/%d/', verbose_name="轮播图片")
position = models.CharField(
max_length=20,
choices=POSITION_CHOICES,
default='home',
verbose_name="显示位置"
)
order = models.IntegerField(
default=0,
validators=[MinValueValidator(0), MaxValueValidator(999)],
verbose_name="排序权重"
)
click_url = models.URLField(blank=True, verbose_name="点击链接")
start_time = models.DateTimeField(
default=timezone.now,
verbose_name="开始时间"
)
end_time = models.DateTimeField(
null=True,
blank=True,
verbose_name="结束时间"
)
is_active = models.BooleanField(default=True, verbose_name="是否启用")
is_delete = models.BooleanField(default=False, verbose_name="逻辑删除")
description = models.TextField(blank=True, verbose_name="描述")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
db_table = 'smart_banner'
verbose_name = '轮播图'
verbose_name_plural = verbose_name
ordering = ['-order', '-create_time']
indexes = [
models.Index(fields=['position', 'is_active', 'is_delete']),
]
def __str__(self):
return self.title or f"轮播图-{self.id}"
@property
def is_valid(self):
"""检查轮播图是否在有效期内"""
now = timezone.now()
if not self.is_active or self.is_delete:
return False
if self.start_time and self.start_time > now:
return False
if self.end_time and self.end_time < now:
return False
return True
def get_absolute_image_url(self, request=None):
"""获取完整的图片URL"""
if self.image and hasattr(self.image, 'url'):
if request:
return request.build_absolute_uri(self.image.url)
return self.image.url
return None
新闻动态模型
python
class News(models.Model):
"""新闻动态模型"""
CATEGORY_CHOICES = (
('news', '社区新闻'),
('notice', '公告通知'),
('activity', '活动预告'),
)
title = models.CharField(max_length=200, verbose_name="标题")
content = models.TextField(verbose_name="内容")
summary = models.TextField(blank=True, verbose_name="摘要")
image = models.ImageField(
upload_to='news/%Y/%m/%d/',
blank=True,
null=True,
verbose_name="封面图片"
)
category = models.CharField(
max_length: '20',
choices=CATEGORY_CHOICES,
default='news',
verbose_name="分类"
)
is_top = models.BooleanField(default=False, verbose_name="是否置顶")
is_active = models.BooleanField(default=True, verbose_name="是否发布")
view_count = models.PositiveIntegerField(default=0, verbose_name="浏览数")
publish_time = models.DateTimeField(
default=timezone.now,
verbose_name="发布时间"
)
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
db_table = 'smart_news'
verbose_name = '新闻动态'
verbose_name_plural = verbose_name
ordering = ['-is_top', '-publish_time', '-create_time']
indexes = [
models.Index(fields=['category', 'is_active']),
]
def __str__(self):
return self.title
def get_absolute_image_url(self, request=None):
"""获取完整的图片URL"""
if self.image and hasattr(self.image, 'url'):
if request:
return request.build_absolute_uri(self.image.url)
return self.image.url
return None
def increase_view_count(self):
"""增加浏览数"""
self.view_count += 1
self.save(update_fields=['view_count'])
4.3 后端API接口完整实现
序列化器
python
# smart/serializers.py
class BannerSerializer(serializers.ModelSerializer):
image_url = serializers.SerializerMethodField()
is_valid = serializers.ReadOnlyField()
class Meta:
model = Banner
fields = [
'id', 'title', 'image_url', 'position', 'order',
'click_url', 'description', 'is_valid', 'create_time'
]
read_only_fields = ['is_valid']
def get_image_url(self, obj):
request = self.context.get('request')
return obj.get_absolute_image_url(request)
class NewsSerializer(serializers.ModelSerializer):
image_url = serializers.SerializerMethodField()
create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
class Meta:
model = News
fields = [
'id', 'title', 'summary', 'image_url', 'category',
'is_top', 'view_count', 'publish_time', 'create_time'
]
def get_image_url(self, obj):
request = self.context.get('request')
return obj.get_absolute_image_url(request)
class NoticeSerializer(serializers.ModelSerializer):
create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M')
class Meta:
model = Notice
fields = ['id', 'title', 'content', 'create_time']
class HomeDataSerializer(serializers.Serializer):
"""首页数据序列化器"""
banners = BannerSerializer(many=True)
notice = NoticeSerializer()
news = NewsSerializer(many=True)
视图集实现
python
# smart/views.py
class BannerListAPIView(APIView):
"""
轮播图列表API
"""
def get(self, request):
try:
position = request.GET.get('position', 'home')
banners = Banner.objects.filter(
position=position,
is_active=True,
is_delete=False
).filter(
Q(start_time__lte=timezone.now()) &
(Q(end_time__isnull=True) | Q(end_time__gte=timezone.now()))
).order_by('-order', '-create_time')
serializer = BannerSerializer(
banners,
many=True,
context={'request': request}
)
return Response({
'code': 200,
'message': 'success',
'data': serializer.data
})
except Exception as e:
return Response({
'code': 500,
'message': f'服务器错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class HomeAPIView(APIView):
"""
首页聚合数据API
"""
def get(self, request):
try:
# 获取轮播图
banners = Banner.objects.filter(
position='home',
is_active=True,
is_delete=False
).filter(
Q(start_time__lte=timezone.now()) &
(Q(end_time__isnull=True) | Q(end_time__gte=timezone.now()))
).order_by('-order', '-create_time')[:5]
# 获取最新公告
notice = Notice.objects.filter(
is_active=True
).order_by('-create_time').first()
# 获取最新新闻
news = News.objects.filter(
is_active=True
).order_by('-is_top', '-publish_time')[:3]
banner_serializer = BannerSerializer(
banners,
many=True,
context={'request': request}
)
notice_serializer = NoticeSerializer(
notice,
context={'request': request}
) if notice else None
news_serializer = NewsSerializer(
news,
many=True,
context={'request': request}
)
data = {
'banners': banner_serializer.data,
'notice': notice_serializer.data if notice_serializer else {},
'news': news_serializer.data
}
return Response({
'code': 200,
'message': 'success',
'data': data
})
except Exception as e:
return Response({
'code': 500,
'message': f'服务器错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
URL路由配置
python
# smart/urls.py
urlpatterns = [
# ... 其他路由
path('banner/', BannerListAPIView.as_view(), name='banner-list'),
path('home/', HomeAPIView.as_view(), name='home'),
]
4.4 管理后台配置
python
# smart/admin.py
@admin.register(Banner)
class BannerAdmin(admin.ModelAdmin):
list_display = [
'id', 'title', 'position', 'order', 'is_active',
'is_valid', 'start_time', 'end_time', 'create_time'
]
list_filter = ['position', 'is_active', 'create_time']
search_fields = ['title', 'description']
list_editable = ['order', 'is_active']
readonly_fields = ['create_time', 'update_time']
fieldsets = (
('基本信息', {
'fields': ('title', 'image', 'position', 'description')
}),
('链接设置', {
'fields': ('click_url',)
}),
('时间设置', {
'fields': ('start_time', 'end_time')
}),
('配置设置', {
'fields': ('order', 'is_active')
}),
)
@admin.register(News)
class NewsAdmin(admin.ModelAdmin):
list_display = [
'id', 'title', 'category', 'is_top', 'is_active',
'view_count', 'publish_time', 'create_time'
]
list_filter = ['category', 'is_top', 'is_active', 'publish_time']
search_fields = ['title', 'content']
list_editable = ['is_top', 'is_active']
readonly_fields = ['view_count', 'create_time', 'update_time']
fieldsets = (
('基本信息', {
'fields': ('title', 'summary', 'content', 'image', 'category')
}),
('发布设置', {
'fields': ('is_top', 'is_active', 'publish_time')
}),
)
def save_model(self, request, obj, form, change):
if not obj.summary and obj.content:
# 自动生成摘要
obj.summary = obj.content[:100] + '...' if len(obj.content) > 100 else obj.content
super().save_model(request, obj, form, change)
五、信息采集模块深度开发
5.1 数据模型详细设计
用户和区域模型
python
# smart/models.py
class UserInfo(models.Model):
"""用户表(网格员)"""
ROLE_CHOICES = (
('admin', '管理员'),
('grid_member', '网格员'),
('resident', '居民'),
)
openid = models.CharField(max_length=100, unique=True, verbose_name="微信OpenID")
nickname = models.CharField(max_length=50, verbose_name="昵称")
avatar = models.URLField(blank=True, verbose_name="头像")
phone = models.CharField(max_length=20, blank=True, verbose_name="手机号")
role = models.CharField(
max_length=20,
choices=ROLE_CHOICES,
default='grid_member',
verbose_name="角色"
)
is_active = models.BooleanField(default=True, verbose_name="是否启用")
last_login = models.DateTimeField(null=True, blank=True, verbose_name="最后登录时间")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
db_table = 'smart_user'
verbose_name = '用户信息'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.nickname}({self.role})"
class Area(models.Model):
"""网格区域表"""
name = models.CharField(max_length=50, verbose_name="区域名称")
code = models.CharField(max_length=20, unique=True, verbose_name="区域编码")
manager = models.ManyToManyField(
UserInfo,
related_name='managed_areas',
verbose_name="负责人",
limit_choices_to={'role': 'grid_member'}
)
parent = models.ForeignKey(
'self',
null=True,
blank=True,
on_delete=models.CASCADE,
related_name='children',
verbose_name="上级区域"
)
level = models.IntegerField(default=1, verbose_name="区域层级")
population = models.IntegerField(default=0, verbose_name="人口数量")
address = models.TextField(blank=True, verbose_name="详细地址")
is_active = models.BooleanField(default=True, verbose_name="是否启用")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
db_table = 'smart_area'
verbose_name = '网格区域'
verbose_name_plural = verbose_name
ordering = ['level', 'code']
def __str__(self):
return f"{self.name}({self.code})"
def get_full_path(self):
"""获取完整区域路径"""
path = []
current = self
while current:
path.append(current.name)
current = current.parent
return ' - '.join(reversed(path))
class Collection(models.Model):
"""信息采集表"""
GENDER_CHOICES = (
('M', '男'),
('F', '女'),
('U', '未知'),
)
name = models.CharField(max_length=20, verbose_name="姓名")
name_pinyin = models.CharField(max_length=100, verbose_name="姓名拼音")
gender = models.CharField(
max_length=1,
choices=GENDER_CHOICES,
default='U',
verbose_name="性别"
)
id_card = models.CharField(max_length=18, blank=True, verbose_name="身份证号")
phone = models.CharField(max_length=20, blank=True, verbose_name="手机号")
avatar = models.ImageField(upload_to='collection/%Y/%m/%d/', verbose_name="头像")
area = models.ForeignKey(
Area,
on_delete=models.CASCADE,
verbose_name="所属网格"
)
address = models.TextField(blank=True, verbose_name="详细地址")
emergency_contact = models.CharField(max_length=20, blank=True, verbose_name="紧急联系人")
emergency_phone = models.CharField(max_length=20, blank=True, verbose_name="紧急联系电话")
health_status = models.TextField(blank=True, verbose_name="健康状况")
special_needs = models.TextField(blank=True, verbose_name="特殊需求")
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
face_token = models.CharField(max_length=100, blank=True, verbose_name="人脸Token")
collector = models.ForeignKey(
UserInfo,
on_delete=models.CASCADE,
verbose_name="采集员"
)
is_verified = models.BooleanField(default=False, verbose_name="是否核验")
class Meta:
db_table = 'smart_collection'
verbose_name = '信息采集'
verbose_name_plural = verbose_name
ordering = ['-create_time']
indexes = [
models.Index(fields=['name', 'area']),
models.Index(fields=['create_time']),
]
def __str__(self):
return f"{self.name}-{self.area.name}"
def get_absolute_avatar_url(self, request=None):
"""获取完整的头像URL"""
if self.avatar and hasattr(self.avatar, 'url'):
if request:
return request.build_absolute_uri(self.avatar.url)
return self.avatar.url
return None
def save(self, *args, **kwargs):
# 自动生成姓名拼音
if self.name and not self.name_pinyin:
self.name_pinyin = self.generate_pinyin(self.name)
super().save(*args, **kwargs)
def generate_pinyin(self, name):
"""生成姓名拼音"""
try:
from pypinyin import lazy_pinyin
return ''.join(lazy_pinyin(name))
except ImportError:
# 如果没有安装pypinyin,使用简单实现
return name
5.2 采集列表页面深度实现
前端页面优化
xml
<!-- pages/collection/collection.wxml -->
<view class="collection-container">
<!-- 统计卡片 -->
<view class="stats-cards">
<view class="stats-card today">
<view class="stats-icon">
<text class="iconfont icon-user"></text>
</view>
<view class="stats-content">
<text class="stats-value">{{todayCount}}</text>
<text class="stats-label">今日采集</text>
</view>
</view>
<view class="stats-card total">
<view class="stats-icon">
<text class="iconfont icon-list"></text>
</view>
<view class="stats-content">
<text class="stats-value">{{totalCount}}</text>
<text class="stats-label">总采集数</text>
</view>
</view>
</view>
<!-- 操作按钮 -->
<view class="action-buttons">
<button class="btn primary" bindtap="navigateToForm">
<text class="iconfont icon-plus"></text>
新采集
</button>
<button class="btn secondary" bindtap="navigateToStatistics">
<text class="iconfont icon-chart"></text>
数据统计
</button>
<button class="btn outline" bindtap="showFilterPanel">
<text class="iconfont icon-filter"></text>
筛选
</button>
</view>
<!-- 筛选面板 -->
<view class="filter-panel" wx:if="{{showFilter}}">
<view class="filter-content">
<view class="filter-item">
<text class="filter-label">网格区域</text>
<picker
range="{{areaOptions}}"
range-key="name"
value="{{filterAreaIndex}}"
bindchange="onAreaFilterChange"
>
<view class="filter-value">
{{areaOptions[filterAreaIndex].name || '全部区域'}}
</view>
</picker>
</view>
<view class="filter-item">
<text class="filter-label">采集时间</text>
<picker
mode="date"
value="{{filterDate}}"
bindchange="onDateFilterChange"
>
<view class="filter-value">
{{filterDate || '全部时间'}}
</view>
</picker>
</view>
<view class="filter-actions">
<button class="btn outline" bindtap="resetFilter">重置</button>
<button class="btn primary" bindtap="applyFilter">应用</button>
</view>
</view>
</view>
<!-- 采集列表 -->
<view class="collection-list">
<view class="list-header">
<text class="header-title">采集记录</text>
<text class="header-count">共 {{collectionList.length}} 条</text>
</view>
<view
class="list-item"
wx:for="{{collectionList}}"
wx:key="id"
bindtap="viewCollectionDetail"
data-id="{{item.id}}"
>
<image class="item-avatar" src="{{item.avatar_url}}" mode="aspectFill" />
<view class="item-content">
<view class="item-header">
<text class="item-name">{{item.name}}</text>
<text class="item-gender">{{item.gender_display}}</text>
</view>
<view class="item-info">
<text class="item-area">{{item.area.name}}</text>
<text class="item-time">{{item.create_time}}</text>
</view>
<view class="item-tags">
<text class="tag verified" wx:if="{{item.is_verified}}">已核验</text>
<text class="tag face" wx:if="{{item.face_token}}">已录入人脸</text>
</view>
</view>
<view class="item-actions">
<text
class="iconfont icon-shanchu"
bindtap="doDeleteRow"
data-nid="{{item.id}}"
></text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" wx:if="{{collectionList.length === 0 && !isLoading}}">
<image src="/static/images/empty.png" class="empty-image" />
<text class="empty-text">暂无采集记录</text>
<button class="btn primary" bindtap="navigateToForm">开始采集</button>
</view>
<!-- 加载更多 -->
<view class="load-more" wx:if="{{hasMore && !isLoading}}">
<text class="load-more-text" bindtap="loadMore">加载更多</text>
</view>
<view class="load-more" wx:if="{{!hasMore && collectionList.length > 0}}">
<text class="load-more-text">没有更多数据了</text>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{isLoading}}">
<van-loading size="24px" vertical>加载中...</van-loading>
</view>
</view>
采集列表样式优化
css
/* pages/collection/collection.wxss */
.collection-container {
min-height: 100vh;
background: #f7f7f7;
padding: 20rpx;
}
.stats-cards {
display: flex;
gap: 20rpx;
margin-bottom: 30rpx;
}
.stats-card {
flex: 1;
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.stats-card.today {
border-left: 6rpx solid #007AFF;
}
.stats-card.total {
border-left: 6rpx solid #34C759;
}
.stats-icon {
width: 60rpx;
height: 60rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #ffffff;
}
.stats-card.today .stats-icon {
background: #007AFF;
}
.stats-card.total .stats-icon {
background: #34C759;
}
.stats-content {
flex: 1;
}
.stats-value {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333333;
line-height: 1.2;
}
.stats-label {
display: block;
font-size: 24rpx;
color: #999999;
margin-top: 8rpx;
}
.action-buttons {
display: flex;
gap: 20rpx;
margin-bottom: 30rpx;
}
.btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 20rpx;
border-radius: 12rpx;
font-size: 28rpx;
border: none;
}
.btn.primary {
background: #007AFF;
color: #ffffff;
}
.btn.secondary {
background: #34C759;
color: #ffffff;
}
.btn.outline {
background: transparent;
border: 2rpx solid #007AFF;
color: #007AFF;
}
.filter-panel {
background: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: flex-end;
}
.filter-content {
background: #ffffff;
width: 100%;
border-radius: 20rpx 20rpx 0 0;
padding: 40rpx;
box-sizing: border-box;
}
.filter-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.filter-label {
font-size: 28rpx;
color: #333333;
}
.filter-value {
font-size: 28rpx;
color: #007AFF;
padding: 16rpx 30rpx;
background: #f8f8f8;
border-radius: 8rpx;
}
.filter-actions {
display: flex;
gap: 20rpx;
margin-top: 40rpx;
}
.collection-list {
background: #ffffff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.header-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.header-count {
font-size: 26rpx;
color: #999999;
}
.list-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.3s;
}
.list-item:active {
background: #f8f8f8;
}
.item-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.item-content {
flex: 1;
min-width: 0;
}
.item-header {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 12rpx;
}
.item-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.item-gender {
font-size: 24rpx;
color: #999999;
background: #f8f8f8;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.item-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.item-area {
font-size: 26rpx;
color: #666666;
}
.item-time {
font-size: 24rpx;
color: #999999;
}
.item-tags {
display: flex;
gap: 10rpx;
}
.tag {
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 6rpx;
}
.tag.verified {
background: #e6f7ff;
color: #1890ff;
}
.tag.face {
background: #f6ffed;
color: #52c41a;
}
.item-actions {
margin-left: 20rpx;
}
.item-actions .iconfont {
font-size: 32rpx;
color: #ff4d4f;
padding: 20rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 80rpx 40rpx;
}
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.6;
}
.empty-text {
font-size: 28rpx;
color: #999999;
margin-bottom: 40rpx;
}
.load-more {
text-align: center;
padding: 40rpx;
}
.load-more-text {
font-size: 28rpx;
color: #999999;
}
.loading-container {
padding: 80rpx 0;
}
采集列表逻辑实现
javascript
// pages/collection/collection.js
const { request, buildUrl } = require('../../utils/request')
const { showLoading, hideLoading, showError, showSuccess, formatTime } = require('../../utils/util')
Page({
data: {
collectionList: [],
areaOptions: [{ id: 0, name: '全部区域' }],
filterAreaIndex: 0,
filterDate: '',
showFilter: false,
isLoading: false,
hasMore: true,
page: 1,
pageSize: 10,
todayCount: 0,
totalCount: 0
},
onLoad() {
this.initPage()
},
onShow() {
this.refreshList()
},
onPullDownRefresh() {
this.refreshList().finally(() => {
wx.stopPullDownRefresh()
})
},
onReachBottom() {
if (this.data.hasMore && !this.data.isLoading) {
this.loadMore()
}
},
async initPage() {
await Promise.all([
this.loadAreaOptions(),
this.loadStatistics()
])
},
async loadAreaOptions() {
try {
const res = await request.get(buildUrl('area.list'))
if (res.code === 200) {
const areaOptions = [{ id: 0, name: '全部区域' }, ...res.data]
this.setData({ areaOptions })
}
} catch (error) {
console.error('加载区域选项失败:', error)
}
},
async loadStatistics() {
try {
const res = await request.get(buildUrl('collection.statistics'))
if (res.code === 200) {
this.setData({
todayCount: res.data.today_count || 0,
totalCount: res.data.total_count || 0
})
}
} catch (error) {
console.error('加载统计信息失败:', error)
}
},
async refreshList() {
this.setData({ page: 1, hasMore: true })
await this.loadCollectionList(true)
},
async loadMore() {
if (!this.data.hasMore) return
this.setData({ page: this.data.page + 1 })
await this.loadCollectionList(false)
},
async loadCollectionList(refresh = false) {
if (this.data.isLoading) return
this.setData({ isLoading: true })
try {
const params = {
page: this.data.page,
page_size: this.data.pageSize
}
// 添加筛选条件
if (this.data.filterAreaIndex > 0) {
const selectedArea = this.data.areaOptions[this.data.filterAreaIndex]
params.area_id = selectedArea.id
}
if (this.data.filterDate) {
params.date = this.data.filterDate
}
const res = await request.get(buildUrl('collection.list'), params)
if (res.code === 200) {
const { list, pagination } = res.data
const formattedList = list.map(item => ({
...item,
create_time: formatTime(item.create_time, 'MM-DD HH:mm'),
gender_display: this.getGenderDisplay(item.gender)
}))
if (refresh) {
this.setData({ collectionList: formattedList })
} else {
this.setData({
collectionList: [...this.data.collectionList, ...formattedList]
})
}
this.setData({
hasMore: pagination.has_next,
isLoading: false
})
} else {
throw new Error(res.message)
}
} catch (error) {
console.error('加载采集列表失败:', error)
this.setData({ isLoading: false })
if (refresh) {
showError('加载失败')
}
}
},
getGenderDisplay(gender) {
const genderMap = {
'M': '男',
'F': '女',
'U': '未知'
}
return genderMap[gender] || '未知'
},
navigateToForm() {
wx.navigateTo({
url: '/pages/form/form'
})
},
navigateToStatistics() {
wx.navigateTo({
url: '/pages/statistics/statistics'
})
},
showFilterPanel() {
this.setData({ showFilter: true })
},
hideFilterPanel() {
this.setData({ showFilter: false })
},
onAreaFilterChange(e) {
this.setData({ filterAreaIndex: e.detail.value })
},
onDateFilterChange(e) {
this.setData({ filterDate: e.detail.value })
},
resetFilter() {
this.setData({
filterAreaIndex: 0,
filterDate: ''
})
},
applyFilter() {
this.hideFilterPanel()
this.refreshList()
},
viewCollectionDetail(e) {
const { id } = e.currentTarget.dataset
wx.navigateTo({
url: `/pages/collection/detail/detail?id=${id}`
})
},
doDeleteRow(e) {
const { nid } = e.currentTarget.dataset
const item = this.data.collectionList.find(i => i.id === nid)
if (!item) return
wx.showModal({
title: '确认删除',
content: `确定要删除 ${item.name} 的采集信息吗?此操作不可恢复。`,
confirmColor: '#ff4d4f',
success: async (res) => {
if (res.confirm) {
await this.deleteCollection(nid)
}
}
})
// 阻止事件冒泡
e.stopPropagation()
},
async deleteCollection(id) {
try {
showLoading('删除中...')
const res = await request.delete(buildUrl('collection.detail', { id }))
if (res.code === 200) {
showSuccess('删除成功')
// 从列表中移除
this.setData({
collectionList: this.data.collectionList.filter(item => item.id !== id)
})
// 重新加载统计信息
this.loadStatistics()
} else {
throw new Error(res.message)
}
} catch (error) {
console.error('删除失败:', error)
showError('删除失败')
} finally {
hideLoading()
}
}
})
5.3 后端API接口完整实现
序列化器
python
# smart/serializers.py
class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
fields = ['id', 'nickname', 'avatar', 'role', 'phone']
class AreaSerializer(serializers.ModelSerializer):
full_path = serializers.ReadOnlyField()
class Meta:
model = Area
fields = ['id', 'name', 'code', 'full_path', 'level', 'population', 'address']
class CollectionSerializer(serializers.ModelSerializer):
area = AreaSerializer(read_only=True)
collector = UserInfoSerializer(read_only=True)
avatar_url = serializers.SerializerMethodField()
area_id = serializers.IntegerField(write_only=True)
class Meta:
model = Collection
fields = [
'id', 'name', 'name_pinyin', 'gender', 'id_card', 'phone',
'avatar', 'avatar_url', 'area', 'area_id', 'address',
'emergency_contact', 'emergency_phone', 'health_status',
'special_needs', 'create_time', 'update_time', 'face_token',
'collector', 'is_verified'
]
read_only_fields = ['name_pinyin', 'face_token', 'collector', 'create_time', 'update_time']
def get_avatar_url(self, obj):
request = self.context.get('request')
return obj.get_absolute_avatar_url(request)
def validate_id_card(self, value):
"""验证身份证号格式"""
if value and len(value) not in [15, 18]:
raise serializers.ValidationError("身份证号格式不正确")
return value
def validate_phone(self, value):
"""验证手机号格式"""
if value and not re.match(r'^1[3-9]\d{9}$', value):
raise serializers.ValidationError("手机号格式不正确")
return value
class CollectionListSerializer(serializers.ModelSerializer):
"""用于列表显示的简化序列化器"""
area = AreaSerializer(read_only=True)
avatar_url = serializers.SerializerMethodField()
class Meta:
model = Collection
fields = [
'id', 'name', 'gender', 'avatar_url', 'area',
'create_time', 'face_token', 'is_verified'
]
def get_avatar_url(self, obj):
request = self.context.get('request')
return obj.get_absolute_avatar_url(request)
class CollectionStatisticsSerializer(serializers.Serializer):
"""采集统计序列化器"""
today_count = serializers.IntegerField()
total_count = serializers.IntegerField()
verified_count = serializers.IntegerField()
face_registered_count = serializers.IntegerField()
视图集实现
python
# smart/views.py
from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Q
from django.utils import timezone
from datetime import datetime, timedelta
from .models import Collection, Area
from .serializers import (
CollectionSerializer,
CollectionListSerializer,
CollectionStatisticsSerializer,
AreaSerializer
)
class AreaViewSet(ModelViewSet):
"""网格区域视图集"""
queryset = Area.objects.filter(is_active=True)
serializer_class = AreaSerializer
pagination_class = None
def get_queryset(self):
queryset = super().get_queryset()
# 可以根据需要添加权限过滤
return queryset
class CollectionViewSet(ModelViewSet):
"""信息采集视图集"""
queryset = Collection.objects.all()
filter_backends = [DjangoFilterBackend]
filterset_fields = ['area', 'collector']
def get_serializer_class(self):
if self.action == 'list':
return CollectionListSerializer
return CollectionSerializer
def get_queryset(self):
queryset = super().get_queryset()
# 日期过滤
date = self.request.GET.get('date')
if date:
try:
filter_date = datetime.strptime(date, '%Y-%m-%d').date()
queryset = queryset.filter(create_time__date=filter_date)
except ValueError:
pass
# 区域过滤
area_id = self.request.GET.get('area_id')
if area_id and area_id != '0':
queryset = queryset.filter(area_id=area_id)
return queryset.select_related('area', 'collector').order_by('-create_time')
def perform_create(self, serializer):
# 自动设置采集员为当前用户
# 这里需要先集成用户认证系统
# serializer.save(collector=self.request.user)
serializer.save()
def list(self, request, *args, **kwargs):
"""重写list方法以支持分页和自定义响应格式"""
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response({
'code': 200,
'message': 'success',
'data': {
'list': serializer.data,
'pagination': {
'total': queryset.count(),
'page': int(request.GET.get('page', 1)),
'page_size': int(request.GET.get('page_size', 10)),
'has_next': False # 简化实现
}
}
})
@action(detail=False, methods=['get'])
def statistics(self, request):
"""采集统计信息"""
today = timezone.now().date()
# 今日采集数
today_count = Collection.objects.filter(
create_time__date=today
).count()
# 总采集数
total_count = Collection.objects.count()
# 已核验数量
verified_count = Collection.objects.filter(is_verified=True).count()
# 已录入人脸数量
face_registered_count = Collection.objects.exclude(face_token='').count()
data = {
'today_count': today_count,
'total_count': total_count,
'verified_count': verified_count,
'face_registered_count': face_registered_count
}
serializer = CollectionStatisticsSerializer(data)
return Response({
'code': 200,
'message': 'success',
'data': serializer.data
})
@action(detail=True, methods=['post'])
def verify(self, request, pk=None):
"""核验采集信息"""
collection = self.get_object()
collection.is_verified = True
collection.save()
return Response({
'code': 200,
'message': '核验成功',
'data': None
})
URL路由配置
python
# smart/urls.py
from rest_framework.routers import DefaultRouter
from .views import AreaViewSet, CollectionViewSet
router = DefaultRouter()
router.register(r'area', AreaViewSet)
router.register(r'collection', CollectionViewSet)
urlpatterns = [
# ... 其他路由
] + router.urls