这是一篇聚焦“Django + 微信小程序”的明星艺人剧组演艺信息服务平台开发全记录。我尽量还原实际开发中的真实流程:从需求拆解、技术选型、数据库设计、接口实现,到小程序端联调、服务器部署,再到具体的避坑经验。项目以Django为后端核心,小程序做C端展示,覆盖艺人资料维护、剧组招募、演艺通告报名、后台审核等典型业务闭环,是一套完整的全栈实践。
1. 项目定位与技术选型:为什么是"Django + 微信小程序"
1.1 这类平台到底要解决什么问题
先别急着写代码,先说清楚这个项目在业务上是怎么个事。明星艺人、剧组、演艺信息,这三个关键词放一起,核心需求就是:让艺人方(包括经纪公司、个人演员、群演)和剧组方(导演组、选角导演、制片组)之间有一个高效的信息撮合通道。
传统模式里,剧组招演员靠微信群、朋友圈、通告单转发;艺人找活儿靠跑组、靠人脉、靠各种通告群。信息极度分散,而且时效性差、透明度低。你这个平台要做的,就是把“剧组发通告-艺人查通告-在线报名-剧组筛选-确定人选-产出结算”这个链路搬到线上,再叠加艺人资料管理(照片、作品、身高体重、从业年限、特长标签),让筛选效率提上来。
我用Django + Python 这个组合来做后端,用微信小程序做前端触达,最大的原因有几点:Django的Admin后台能极大降低后台管理系统的开发量,艺人报名审核、通告置顶、用户禁言这些运营操作,Django自带Admin就能覆盖八成;Python语言生态在爬虫、数据分析、后续可能的算法推荐扩展上非常友好;小程序端则看中微信的流量入口和免安装属性,用户点开即用,不用下载App,艺人和剧组双方的使用门槛都低。
1.2 技术栈选择的底层逻辑
先讲后端。Django确实是这类“信息管理+业务交互”平台的首选。有人会问,用Flask行不行?行,但你会发现后面要自己接ORM、接Admin、接分页、接认证,工程量翻倍。Django把用户认证、Admin后台、ORM、表单校验、缓存框架、信号机制全都内置了,写业务的速度完全不在一个量级上。
再讲前端。微信小程序是目前国内C端应用成本最低的载体。不用考虑IOS和Android双端适配,不用上架应用商店,微信审核通过了就能用。配合微信的登录体系,用户不需要额外注册账号,通过wx.login拿到code,后端再换openid,用户体系就打通了。对于艺人这种需要“被看到”的群体,小程序还方便分享到微信群、朋友圈,传播路径天然顺畅。
整个项目的架构,我用的是标准的前后端分离模式:
- 小程序端:WXML + WXSS + JavaScript,负责页面渲染和用户交互
- 后端:Django + Django REST Framework,负责业务逻辑和API接口
- 数据库:MySQL(生产环境)/ SQLite(本地开发)
- 缓存:Redis(用于存储小程序登录态session_key,避免频繁调用微信接口)
数据流大致是:小程序端发起请求 → Django REST Framework 接收 → 经过权限认证 → 业务逻辑处理 → ORM操作MySQL → 返回JSON数据 → 小程序渲染。
1.3 项目目录结构和开发环境搭建
开发前建议先把环境弄干净。我习惯用虚拟环境隔离项目依赖,避免和本机其他Python项目冲突。
bash复制# 创建虚拟环境
python -m venv venv
# 激活虚拟环境
source venv/bin/activate # Windows: venv\Scripts\activate
# 安装核心依赖
pip install django djangorestframework django-cors-headers Pillow mysqlclient
# 创建工程
django-admin startproject artist_platform
cd artist_platform
python manage.py startapp users
python manage.py startapp artists
python manage.py startapp projects
python manage.py startapp notices
这里有个项目结构设计,我按业务域拆分了app,而不是所有代码堆在一个app里:
| App名称 | 职责范围 |
|---|---|
| users | 用户注册登录、微信认证、用户角色管理 |
| artists | 艺人资料管理、作品集、资质信息 |
| projects | 剧组项目发布、项目详情、组讯管理 |
| notices | 演艺通告发布、报名申请、审核结果 |
这样拆的好处是,每个App的业务边界清晰。比如后续要加“通告收藏”功能,直接在notices里加一个收藏表就行,不会影响其他模块。
提示:开发阶段用SQLite足够,但生产环境务必切到MySQL,并发和稳定性不是一个级别。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库与核心模型设计:把"艺人-剧组-通告"变成表
2.1 核心数据模型的业务拆解
建表之前先梳理业务实体之间的关系。这个平台核心涉及的角色有三类:普通浏览用户、艺人用户、剧组发布方。实际设计时,我统一用Django的User模型做基础账号,再利用Profile扩展表区分角色属性,而不是给每个角色单独建一张用户表。
核心的实体关系我用大白话理一遍:
- 一个用户(User)可以拥有一个艺人档案(ArtistProfile),也可以是一个剧组账号(CrewProfile)
- 一个剧组账号(CrewProfile)可以发布多个剧组项目(Project)
- 一个剧组项目(Project)下可以关联多个演艺通告(CastingNotice)
- 艺人(ArtistProfile)可以针对通告提交报名申请(AuditionApplication)
这种关系设计跟招聘网站的“公司-职位-简历投递”非常像。你只要把“剧组项目”理解成“公司”,“演艺通告”理解成“职位”,“艺人档案”理解成“简历”,整个模型就通了。
2.2 艺人模块与剧组模块的字段设计
艺人档案是整个平台的核心资产,字段设计要兼顾展示效果和搜索匹配。我实际建模时用的核心字段如下:
python复制class ArtistProfile(models.Model):
"""艺人档案"""
GENDER_CHOICES = (
('male', '男'),
('female', '女'),
('other', '其他'),
)
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='artist_profile',
verbose_name='关联用户'
)
real_name = models.CharField('真实姓名', max_length=50)
stage_name = models.CharField('艺名', max_length=50, blank=True)
gender = models.CharField('性别', max_length=10, choices=GENDER_CHOICES)
height = models.FloatField('身高(cm)', null=True, blank=True)
weight = models.FloatField('体重(kg)', null=True, blank=True)
birthday = models.DateField('出生日期', null=True, blank=True)
city = models.CharField('常驻城市', max_length=50, blank=True)
occupation = models.CharField('职业类型', max_length=50, blank=True,
help_text='演员/歌手/模特/舞蹈/主持等')
years_of_experience = models.IntegerField('从业年限', default=0)
talent_tags = models.CharField('特长标签', max_length=200, blank=True,
help_text='多个标签用逗号分隔')
avatar = models.ImageField('头像', upload_to='artists/avatars/%Y/%m/',
blank=True, null=True)
bio = models.TextField('个人简介', blank=True)
view_count = models.IntegerField('被查看次数', default=0)
status = models.CharField('状态', max_length=20, default='active',
choices=(('active', '已开放'), ('hidden', '已隐藏')))
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
db_table = 'artist_profile'
verbose_name = '艺人档案'
verbose_name_plural = verbose_name
这里有个细节值得说:没有把身高体重直接写成IntegerField,而是用FloatField。为什么?因为有些人简历上写的是"175cm",有些人可能写"1.75m",如果你用整数存,后续做筛选条件(比如高于170cm且低于180cm)的时候,浮点数处理更灵活。
剧组侧的建模相对简单,核心是剧组的资质信息、过往作品展示和项目偏好:
python复制class CrewProfile(models.Model):
"""剧组方档案"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='crew_profile'
)
company_name = models.CharField('公司/工作室名称', max_length=100)
contact_person = models.CharField('联系人', max_length=50)
contact_phone = models.CharField('联系电话', max_length=20)
license_number = models.CharField('营业执照号/统一社会信用代码', max_length=50,
blank=True)
intro = models.TextField('公司简介', blank=True)
verified = models.BooleanField('是否已认证', default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'crew_profile'
注意verified这个字段,这是平台后续做资质审核预留的关键标识。一开始不要求所有剧组都认证,但认证过的剧组发布的通告优先级更高、排序更靠前,这是一种非常实用的产品引导机制。
2.3 通告与报名模块的核心模型
通告是平台每天最活跃的数据,发布频率高、筛选条件多、状态流转复杂。我设计时把通告主表跟项目表做了外键关联,这样同一个剧组项目(比如一部30集网剧)可以发布多个角色通告(男一号、女二号、特约演员、跟组演员等)。
python复制class CastingNotice(models.Model):
"""演艺通告"""
STATUS_CHOICES = (
('draft', '草稿'),
('published', '招募中'),
('closed', '已截止'),
('cancelled', '已取消'),
)
project = models.ForeignKey('projects.Project', on_delete=models.CASCADE,
related_name='notices', verbose_name='所属剧组项目')
title = models.CharField('通告标题', max_length=100)
role_name = models.CharField('招募角色', max_length=50)
role_count = models.IntegerField('招募人数', default=1)
gender_requirement = models.CharField('性别要求', max_length=10,
choices=(('male', '男'), ('female', '女'),
('both', '男女不限')), default='both')
age_min = models.IntegerField('年龄下限', default=18)
age_max = models.IntegerField('年龄上限', default=50)
salary_range = models.CharField('片酬范围', max_length=100, blank=True,
help_text='例如:面议 / 6000-8000元/天')
shooting_location = models.CharField('拍摄地点', max_length=100)
shooting_dates = models.CharField('拍摄周期', max_length=100, blank=True)
description = models.TextField('角色描述')
requirements = models.TextField('任职要求', blank=True)
deadline = models.DateTimeField('报名截止时间')
contact_wechat = models.CharField('微信号', max_length=50, blank=True)
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES,
default='draft')
view_count = models.IntegerField('浏览数', default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'casting_notice'
ordering = ['-created_at']
报名申请表放在通告的子表里,一个艺人可以投递多个通告,每个通告下可以收到多个报名:
python复制class AuditionApplication(models.Model):
"""艺人报名申请"""
STATUS_CHOICES = (
('pending', '待审核'),
('accepted', '已通过'),
('rejected', '已拒绝'),
('withdrawn', '已撤回'),
)
notice = models.ForeignKey(CastingNotice, on_delete=models.CASCADE,
related_name='applications')
artist = models.ForeignKey('artists.ArtistProfile', on_delete=models.CASCADE,
related_name='applications')
cover_letter = models.TextField('自荐语', blank=True,
help_text='向剧组介绍自己的优势')
audition_time = models.DateTimeField('试镜时间', null=True, blank=True)
audition_location = models.CharField('试镜地点', max_length=200, blank=True)
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES,
default='pending')
feedback = models.TextField('剧组反馈', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'audition_application'
# 同一个艺人同一个通告只能报名一次
unique_together = ('notice', 'artist')
unique_together这个约束非常关键,避免同一个艺人手滑给同一条通告投了三次。不过这里有一个细节要注意:如果业务需求是“艺人可以修改报名信息”,那就不能简单唯一约束,需要改成“同一通告只保留一条有效报名”,这个后续可以用status字段配合逻辑实现。
2.4 用Django Migrations管理表结构变更
模型定义好之后,执行迁移命令:
bash复制python manage.py makemigrations users artists projects notices
python manage.py migrate
这里建议开发过程中不要乱删迁移记录文件。有时候改字段类型或字段名,直接修改models然后执行makemigrations可能出现兼容性提示,需要手动加--fake或者调整迁移文件。我踩过坑,migrate命令执行到一半报错,库里的表结构处于半迁移状态,处理起来很麻烦。稳妥的做法是:开发早期,如果表数据不重要,直接删库重建,不要纠结于迁移文件的完整性;但项目上线后,必须通过迁移文件平滑演进。
3. 后端API与业务逻辑实现:把流程变成接口
3.1 接口清单梳理与RESTful设计
小程序端和后端交互全靠API。接口设计遵循RESTful风格,以资源为中心,用HTTP方法表达操作语义。我列一下这个平台的核心接口清单:
| 模块 | 接口路径 | 方法 | 说明 | 权限 |
|---|---|---|---|---|
| 认证 | /api/auth/login/ | POST | 微信登录,换取token | 公开 |
| 用户 | /api/user/profile/ | GET | 获取当前用户信息 | 登录用户 |
| 艺人 | /api/artists/ | GET | 获取艺人列表 | 公开 |
| 艺人 | /api/artists/{id}/ | GET | 获取艺人详情 | 公开 |
| 艺人 | /api/artists/profile/ | GET/PUT | 获取/更新自己的艺人档案 | 艺人用户 |
| 剧组 | /api/crews/profile/ | GET/PUT | 获取/更新剧组信息 | 剧组用户 |
| 项目 | /api/projects/ | GET/POST | 剧组项目列表/发布 | 公开/剧组 |
| 通告 | /api/notices/ | GET | 通告列表(含筛选) | 公开 |
| 通告 | /api/notices/{id}/ | GET | 通告详情 | 公开 |
| 通告 | /api/notices/ | POST | 发布通告 | 剧组用户 |
| 报名 | /api/notices/{id}/apply/ | POST | 艺人报名通告 | 艺人用户 |
| 报名 | /api/applications/ | GET | 我的报名记录 | 登录用户 |
| 审核 | /api/applications/{id}/review/ | POST | 剧组审核报名 | 剧组用户 |
注意通告接口的列表页和详情页是公开可访问的,因为游客在小程序里应该能先浏览通告,看到感兴趣的才引导去登录注册。这是一个很重要的产品细节:不要一上来就强制登录,逛一逛再转化,用户体验会好很多。
3.2 微信登录认证闭环的完整实现
微信小程序登录是用户体系的入口,整个流程的核心是:小程序端调用wx.login()拿到临时code,传给后端,后端拿code去微信服务器换openid和session_key,然后签发自己的登录凭证给小程序。
先看后端的核心实现:
python复制import requests
from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
User = get_user_model()
def get_openid_from_wx(code):
"""调用微信接口,换取openid"""
url = 'https://api.weixin.qq.com/sns/jscode2session'
params = {
'appid': settings.WX_APPID,
'secret': settings.WX_SECRET,
'js_code': code,
'grant_type': 'authorization_code',
}
resp = requests.get(url, params=params, timeout=5)
data = resp.json()
if 'openid' in data:
return data['openid'], data.get('session_key', '')
raise Exception(f"微信登录失败: {data}")
class WeChatLoginView(APIView):
"""微信小程序登录接口"""
permission_classes = [] # 公开接口
def post(self, request):
code = request.data.get('code')
nickname = request.data.get('nickname', '')
avatar = request.data.get('avatar', '')
if not code:
return Response({'error': '缺少code参数'}, status=400)
try:
openid, session_key = get_openid_from_wx(code)
except Exception as e:
return Response({'error': str(e)}, status=500)
user, created = User.objects.get_or_create(
username=openid,
defaults={
'nickname': nickname or '微信用户',
'avatar': avatar,
}
)
# 签发JWT token
refresh = RefreshToken.for_user(user)
# 判断用户是否已完善资料
has_artist_profile = hasattr(user, 'artist_profile')
has_crew_profile = hasattr(user, 'crew_profile')
return Response({
'token': str(refresh.access_token),
'refresh_token': str(refresh),
'user_id': user.id,
'is_new_user': created,
'has_artist_profile': has_artist_profile,
'has_crew_profile': has_crew_profile,
})
这里我用了rest_framework_simplejwt来做token签发,而不是自己写token生成逻辑。JWT的好处是无状态,服务端不用存储session。不过微信登录有个安全细节:session_key不需要返回给前端,也不应该在数据库里明文保存。session_key是微信用于解密手机号、解密用户敏感信息的钥匙,存Redis里设置短时过期,或者干脆用完就丢,只在需要解密信息时才去微信换。我这里为了简单示意没有存,真实生产环境建议存一下,因为后面可能要用session_key解密微信的运动数据、手机号之类的敏感信息。
再来看小程序端的登录调用:
javascript复制// 小程序端pages/login/login.js
Page({
data: {
loading: false
},
onLoad() {
this.handleWeChatLogin();
},
handleWeChatLogin() {
this.setData({ loading: true });
wx.login({
success: async (res) => {
if (res.code) {
try {
// 调用后端登录接口
const result = await new Promise((resolve, reject) => {
wx.request({
url: 'https://api.yourdomain.com/api/auth/login/',
method: 'POST',
data: {
code: res.code
},
success: (resp) => resolve(resp.data),
fail: (err) => reject(err)
});
});
// 存储token
wx.setStorageSync('token', result.token);
wx.setStorageSync('user_id', result.user_id);
// 检查是否首次使用,引导完善资料
if (result.is_new_user) {
wx.redirectTo({ url: '/pages/register/register' });
} else {
wx.switchTab({ url: '/pages/home/home' });
}
} catch (error) {
wx.showToast({ title: '登录失败,请重试', icon: 'none' });
}
}
}
});
}
});
这个过程中,最容易出问题的是request域名校验。小程序真机调试时,要求后端接口地址必须配置到小程序后台的“服务器域名”中,且必须是HTTPS协议。开发阶段可以在开发者工具中勾选“不校验合法域名”,但上线前必须搞定正式域名和SSL证书。
3.3 通告列表与筛选逻辑的后端实现
通告列表是平台的流量入口,筛选条件多、排序逻辑重要,这里直接展示实际可用的做法。通告的筛选条件通常包含:关键词搜索、城市、性别要求、状态、片酬范围、发布时间等。
python复制from rest_framework.generics import ListAPIView
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
class NoticeListView(ListAPIView):
"""通告列表接口"""
serializer_class = NoticeListSerializer
permission_classes = []
queryset = CastingNotice.objects.filter(
status='published'
).select_related('project').prefetch_related('applications')
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter)
filterset_fields = {
'shooting_location': ['exact'],
'gender_requirement': ['exact'],
'deadline': ['gte', 'lte'],
'project__id': ['exact'],
}
search_fields = ['title', 'role_name', 'description']
ordering_fields = ['created_at', 'view_count', 'deadline']
ordering = ['-created_at']
有几个细节值得说:
第一,select_related和prefetch_related一定要加。 这是Django性能优化最核心的手段。如果不加,列表页循环取出每条通告的project信息时,会触发N+1查询,数据量一旦过百,接口响应就会明显变慢。select_related用于外键关联的单表对象,prefetch_related用于反向外键或多对多关系,这里是获取每条通告下的报名数量。
第二,状态筛选必须固定status='published'。 草稿、已截止、已取消的通告不能出现在公共列表里。这个在哪里做比较好?放在get_queryset里比放在filter_backends里更底层、更安全,因为即便有人通过API直接拼接参数也绕不过这一层。
第三,filterset_fields里我用了一个字典语法,可以定义gte/lte区间筛选。 这对报名截止时间的过滤很好用,比如用户想看“三天内截止的通告”。
3.4 艺人报名通告的完整业务闭环
报名这个动作虽然核心代码就几行,但牵扯的边界条件很多。我的实现逻辑是这样的:
python复制from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
class ApplyNoticeView(APIView):
"""艺人报名通告"""
permission_classes = [IsAuthenticated]
def post(self, request, notice_id):
# 1. 校验通告是否存在且招募中
try:
notice = CastingNotice.objects.get(
id=notice_id,
status='published'
)
except CastingNotice.DoesNotExist:
return Response({'error': '通告不存在或已截止'},
status=status.HTTP_404_NOT_FOUND)
# 2. 校验用户是否已完善的艺人档案
try:
artist_profile = request.user.artist_profile
except AttributeError:
return Response(
{'error': '请先完善艺人档案后再报名'},
status=status.HTTP_400_BAD_REQUEST
)
# 3. 校验是否在截止时间内
from django.utils import timezone
if notice.deadline < timezone.now():
return Response({'error': '报名已截止'},
status=status.HTTP_400_BAD_REQUEST)
# 4. 创建报名记录(唯一约束兜底)
application, created = AuditionApplication.objects.get_or_create(
notice=notice,
artist=artist_profile,
defaults={'cover_letter': request.data.get('cover_letter', '')}
)
if not created:
return Response({'error': '您已报名过该通告,请勿重复提交'},
status=status.HTTP_400_BAD_REQUEST)
return Response({'message': '报名成功', 'application_id': application.id},
status=status.HTTP_201_CREATED)
这段逻辑看着简单,但每一步都是坑。比如第2步的判断,如果request.user不是艺人用户,访问artist_profile属性会直接抛RelatedObjectDoesNotExist异常,这里用try/except AttributeError而不是user.artist_profile is None,是因为Django的反向一对一关系在不存在时会抛异常而不是返回None。用hasattr判断更优雅。另外,这里get_or_create虽然配合数据库层的unique_together约束,能避免并发请求下重复创建的竞态问题,但实际生产中如果同一毫秒内并发两个请求,get_or_create也不是绝对安全,最稳妥的是在数据库层约束加上IntegrityError捕获兜底。
3.5 剧组审核报名的权限控制
审核接口需要考虑数据归属权。一个剧组的用户,只能审核自己项目下的通告报名,不能越权审核别人的。这个逻辑我在权限类里实现:
python复制from rest_framework.permissions import BasePermission
class IsCrewOwner(BasePermission):
"""校验当前用户是否为报名所属通告的发布方"""
def has_object_permission(self, request, view, obj):
# obj 是 AuditionApplication 实例
if not hasattr(request.user, 'crew_profile'):
return False
return obj.notice.project.crew.user == request.user
有了这个权限类,审核接口的代码就非常清爽了:
python复制class ReviewApplicationView(APIView):
"""剧组审核报名"""
permission_classes = [IsAuthenticated, IsCrewOwner]
def post(self, request, application_id):
application = AuditionApplication.objects.select_related(
'notice__project__crew__user'
).get(id=application_id)
self.check_object_permissions(request, application)
review_result = request.data.get('status')
if review_result not in ('accepted', 'rejected'):
return Response({'error': '无效的审核状态'}, status=400)
application.status = review_result
application.feedback = request.data.get('feedback', '')
application.save()
# 未来扩展:审核通过后,可以推送消息给艺人(订阅消息)
return Response({'message': '审核完成'})
select_related在审核接口里也是必要的,因为权限校验时要查obj.notice.project.crew.user这条关系链,层级很深,不做预查询的话,每次权限校验都会触发4条SQL。
4. 微信小程序端实现要点:从页面到交互
4.1 小程序的项目结构与页面配置
小程序端我用的原生微信小程序框架,没有上uni-app或者Taro。原因很简单:这个项目页面不算多,TabBar + 二级页面结构清晰,原生框架完全够用,而且原生框架调试问题最方便,不用多一层编译转换。
小程序端的核心页面结构:
text复制miniprogram/
├── app.js # 全局逻辑入口
├── app.json # 小程序全局配置(页面路由、window、tabBar)
├── app.wxss # 全局样式
├── utils/
│ ├── request.js # 封装wx.request
│ └── auth.js # 登录态管理
└── pages/
├── home/ # 首页(通告列表)
├── notice-detail/ # 通告详情
├── artist-list/ # 艺人库
├── artist-detail/ # 艺人详情
├── profile/ # 个人中心
├── register/ # 角色选择&资料完善
├── artist-manage/ # 艺人管理
└── crew-manage/ # 剧组管理
app.json里TabBar配三个最核心的入口:首页、艺人库、我的。通告详情和艺人详情从列表页点进去,这类二级页面不要放在TabBar里。
4.2 封装统一的request请求层
小程序里wx.request是最底层的网络API,但直接裸用会有重复代码和错误处理缺失的问题。我建议封装一个全局的request工具,统一处理token注入、HTTP状态码、业务错误码和401跳转:
javascript复制// utils/request.js
const BASE_URL = 'https://api.yourdomain.com/api';
function request(options) {
const token = wx.getStorageSync('token');
return new Promise((resolve, reject) => {
wx.request({
url: `${BASE_URL}${options.url}`,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
if (res.statusCode === 200 || res.statusCode === 201) {
resolve(res.data);
} else if (res.statusCode === 401) {
// token过期或无效,跳转登录
wx.removeStorageSync('token');
wx.navigateTo({ url: '/pages/login/login' });
reject(res.data);
} else {
wx.showToast({
title: res.data.error || '请求失败',
icon: 'none'
});
reject(res.data);
}
},
fail: (err) => {
wx.showToast({ title: '网络异常,请检查网络', icon: 'none' });
reject(err);
}
});
});
}
module.exports = { request, BASE_URL };
封装之后,页面里调用就非常舒服:
javascript复制const { request } = require('../../utils/request');
Page({
onLoad() {
this.fetchNotices();
},
async fetchNotices(page = 1) {
const data = await request({
url: '/notices/',
data: { page, status: 'published' }
});
this.setData({ notices: data.results });
}
});
这里有一个很重要的体验细节:token过期不能直接无脑跳登录页。如果用户在浏览艺人库的时候,token静默过期了,这时候突然跳登录页,体验非常割裂。我实际项目里的做法是,401后先弹一个确认框让用户选择“重新登录”或“继续浏览”,只有点击“重新登录”才跳转。这个细节很影响用户好感度。
4.3 首页通告列表的前端实现
首页是流量最大的页面,我采用的布局是:顶部搜索栏 + 城市/性别筛选栏 + 通告卡片流列表。
通告卡片的核心结构:
xml复制<view class="notice-card" bindtap="goDetail" data-id="{{item.id}}">
<view class="notice-header">
<text class="notice-title">{{item.title}}</text>
<text class="notice-status" wx:if="{{item.status === 'published'}}">招募中</text>
</view>
<view class="notice-info">
<text>角色:{{item.role_name}}</text>
<text>城市:{{item.shooting_location}}</text>
</view>
<view class="notice-footer">
<text>{{item.project.company_name}}</text>
<text>{{item.view_count}}次浏览</text>
</view>
</view>
上拉加载更多是列表页必须处理的分页逻辑:
javascript复制// pages/home/home.js
const { request } = require('../../utils/request');
Page({
data: {
notices: [],
page: 1,
pageSize: 10,
hasMore: true,
loading: false,
filter: {
city: '',
gender: 'both'
}
},
onLoad() {
this.fetchNotices(true);
},
async fetchNotices(isRefresh = false) {
if (this.data.loading || (!isRefresh && !this.data.hasMore)) return;
this.setData({ loading: true });
const page = isRefresh ? 1 : this.data.page;
try {
const res = await request({
url: '/notices/',
data: {
page,
page_size: this.data.pageSize,
...this.data.filter
}
});
const notices = isRefresh ? res.results : this.data.notices.concat(res.results);
this.setData({
notices,
page: page + 1,
hasMore: !!res.next,
loading: false
});
} catch (e) {
this.setData({ loading: false });
}
},
// 上拉加载
onReachBottom() {
this.fetchNotices(false);
},
// 下拉刷新
onPullDownRefresh() {
this.fetchNotices(true).then(() => wx.stopPullDownRefresh());
}
});
这个分页逻辑本身不复杂,但有几个关键点:hasMore的判断必须依据后端返回的next字段,而不是results.length < pageSize这种简单判断,因为最后一页如果恰好凑满pageSize,就会重复请求一次;请求防重(this.data.loading判断)也必须有,微信小程序的onReachBottom在滚动到底部时会高频触发,不加防重会导致重复请求。
4.4 艺人档案的展示与资料完善
艺人详情页是整个平台转化率最高的页面,因为剧组方点开来就是看这个人合不合适。页面上半部分是头像、基本信息、特长标签;中间是个人简介和演艺经历;底部是“投递通告记录”或“联系TA”的按钮。
艺人资料完善页面是一个表单密集型页面,我在这里踩过不少坑:
第一个坑是图片上传。 头像上传不能走普通的wx.uploadFile裸调,要先通过后端获取一个上传凭证或者上传URL,再执行上传。Django这边用了django-cors-headers处理跨域,配合DRF的文件解析器。上传接口的写法:
python复制from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class AvatarUploadView(APIView):
"""艺人头像上传"""
parser_classes = (MultiPartParser, FormParser)
permission_classes = [IsAuthenticated]
def post(self, request):
file = request.FILES.get('file')
if not file:
return Response({'error': '未收到文件'}, status=400)
if file.size > 5 * 1024 * 1024:
return Response({'error': '图片大小不能超过5MB'}, status=400)
# 校验文件类型
allowed_types = ['image/jpeg', 'image/png', 'image/webp']
if file.content_type not in allowed_types:
return Response({'error': '仅支持jpg/png/webp格式'}, status=400)
profile, _ = ArtistProfile.objects.get_or_create(user=request.user)
profile.avatar = file
profile.save()
return Response({
'message': '上传成功',
'avatar_url': profile.avatar.url
})
这里要注意:Django默认的MEDIA_ROOT和MEDIA_URL必须先配置好,生产环境还需要Nginx来代理媒体文件访问路径。开发阶段用Django自带的static()方法可以临时服务媒体文件,上线必须切Nginx,否则并发上来图片加载会非常慢。
第二个坑是标签选择交互。 特长标签(表演、舞蹈、声乐、武术、乐器、主持等)使用checkbox-group做多重选择,选完后用逗号拼接成字符串传给后端。后端存的也是逗号分隔字符串,这样在列表页展示时可以直接split(',')成一个数组,用wx:for渲染成独立的tag。
小程序端核心代码:
xml复制<view class="tag-group">
<view class="tag-item {{selectedTags.includes(item) ? 'active' : ''}}"
wx:for="{{tagOptions}}" wx:key="*this"
bindtap="toggleTag" data-tag="{{item}}">
{{item}}
</view>
</view>
javascript复制toggleTag(e) {
const tag = e.currentTarget.dataset.tag;
let selectedTags = this.data.selectedTags;
if (selectedTags.includes(tag)) {
selectedTags = selectedTags.filter(item => item !== tag);
} else {
selectedTags.push(tag);
}
this.setData({ selectedTags });
}
标签选择器用起来简单,但注意wx:key="*this"这个写法,它表示直接用数组元素本身作为key。如果是纯字符串数组,这个写法没问题,但如果数组元素是对象,就必须用对象的某个唯一字段作为key,否则会有渲染性能问题。
5. 部署、联调与常见问题排查
5.1 Django后端部署到服务器的完整流程
项目开发完成后需要部署到云服务器,我用的是经典的Nginx + Gunicorn + MySQL组合。Gunicorn负责运行Django应用,Nginx负责反向代理和静态文件/媒体文件服务。
部署的核心步骤分为:准备服务器环境、拉取代码、安装依赖、配置MySQL、收集静态文件、配置Gunicorn、配置Nginx、启动服务。关键步骤代码:
bash复制# 1. 服务器上创建虚拟环境
cd /www/artist_platform
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
# 2. 安装Gunicorn
pip install gunicorn
# 3. 修改Django配置文件中的ALLOWED_HOSTS
# ALLOWED_HOSTS = ['your-domain.com', 'www.your-domain.com']
# 4. 收集静态文件
python manage.py collectstatic --noinput
# 5. 使用Gunicorn启动Django应用
# --workers: 进程数,一般 = CPU核数 * 2 + 1
# --bind: 绑定地址
gunicorn artist_platform.wsgi:application \
--bind 127.0.0.1:8001 \
--workers 3 \
--timeout 120 \
--daemon
Gunicorn配置里最难调的是workers参数。不是进程数越多越好,进程太多了会频繁切换上下文,CPU都被调度吃掉了。按经验值是CPU核心数×2+1。如果服务器是2核4G,配5个worker比较合理。另外--timeout对上传图片、某些慢接口很重要,默认30秒可能不够用,我设置了120秒。
Nginx配置里最核心的部分:
nginx复制server {
listen 80;
server_name your-domain.com;
# 前端静态文件(如果有的话)
location /static/ {
alias /www/artist_platform/static/;
}
# 用户上传的媒体文件
location /media/ {
alias /www/artist_platform/media/;
}
# 所有API请求转发给Gunicorn
location / {
proxy_pass http://127.0.0.1:8001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
上线前记得在云厂商控制台配好安全组,开放80/443端口,MySQL的3306端口绝对不能对外开放,不然数据库会被扫描爆破。
5.2 微信小程序域名配置与HTTPS要求
小程序上线前必须在微信公众平台配置服务器域名。这里的配置有几个硬性要求:
- 必须是HTTPS协议,且SSL证书有效
- 域名不能是IP地址,必须是备案过的域名
- 域名必须添加到request合法域名列表中,且是精确匹配,不能加端口号
我实际配置时踩了个坑:本地开发时用http://127.0.0.1:8000调接口非常顺,但一上线就忘了把BASE_URL改成正式的HTTPS域名。结果小程序打开后所有请求都失败,报request:fail,排查了半天发现是开发环境地址打进了正式包。建议在代码里做个环境判断:
javascript复制// utils/config.js
const ENV = 'production'; // 'development' 或 'production'
const config = {
development: {
baseUrl: 'http://127.0.0.1:8000/api'
},
production: {
baseUrl: 'https://api.your-domain.com/api'
}
};
module.exports = config[ENV];
另外,HTTPS证书建议直接用云厂商的免费证书(阿里云、腾讯云都有免费DV证书),不用自己折腾Let's Encrypt,省心不少。
5.3 高频报错与排查思路速查表
我在整个开发调试过程中遇到过不少问题,整理了一个实用的排查速查表:
| 问题现象 | 可能原因 | 排查方式 |
|---|---|---|
小程序请求一直报 request:fail |
域名未配置、未开启HTTPS、端口问题 | 真机调试看Network面板,开发者工具里勾选“不校验合法域名”仅用于开发 |
登录时报 errcode: 40029 |
code无效或已过期 | wx.login的code只能用一次,确认是后端拿code去换openid,而不是前端存着重复传 |
数据库迁移报 django.db.utils.OperationalError |
MySQL字符集问题或字段过长 | 建库时指定utf8mb4字符集,确认所有字段长度合理 |
| 上传图片失败 | 后端没有配置MEDIA_ROOT或Nginx没代理/media路径 | 本地开发看Django终端日志,线上看Nginx error.log |
| 列表接口响应慢 | N+1查询 | 用django-debug-toolbar定位SQL查询次数,补上select_related/prefetch_related |
| 用户登录后访问API返回401 | token未传到后端 | 检查封装的request请求头,确认Authorization: Bearer <token>格式 |
| 微信审核被拒,提示社交类目 | 艺人信息展示涉及陌生人社交 | 在小程序后台补充对应类目资质,或调整功能边界 |
5.4 线上运行后的监控与性能优化
平台跑起来后不是就完事了。我上线初期发现几个问题,很值得分享:
第一,接口响应时间变长。 排查后发现是通告列表接口每页10条数据,每条又要查报名数、公司信息、项目信息,SQL循环了30多次。后来我改成在序列化器里用SerializerMethodField加Prefetch对象一次性查出来,响应时间从800ms降到120ms。这里的关键代码优化:
python复制from django.db.models import Count, Prefetch
class NoticeListView(ListAPIView):
def get_queryset(self):
return CastingNotice.objects.filter(
status='published'
).select_related(
'project__crew'
).annotate(
application_count=Count('applications'),
)
annotate配合Count,一条SQL就把报名数量统计出来,比prefetch_related再循环统计又高效了一层。
第二,图片资源越来越大。 艺人头像上传原图,一个图两三MB,页面加载很卡。后面我引入Pillow在后端直接做压缩,头像统一压缩到400x400再保存,通告封面统一压缩到800宽。这一步对小程序端加载速度的提升非常明显。
第三,缓存策略。 首页通告列表的访问量最大,但更新频率不高(几分钟内最多新增几条)。我用Django的cache_page装饰器给列表接口加了个60秒缓存,瞬间把数据库压力降下去了。但注意:加了缓存的接口,数据更新后需要主动清缓存,或者用cache_page配合vary_on_cookie做区分。我实际用的是DRF的response_cache方式和手动清理逻辑:
python复制from django.core.cache import cache
def invalidate_notice_cache():
"""发布新通告后清缓存"""
cache.delete('notice_list_published')
说实话,加缓存这个动作要谨慎。对于数据一致性要求高的接口(比如报名状态),不做缓存或者只做非常短的缓存。只读列表数据加大一点的缓存没问题。
整体走下来,这个项目从零搭建到上线,核心难点不在某一个技术点,而在于把艺人、剧组、通告、报名这条业务链路从前到后打通。项目里的每一层都能深挖很多细节,但抓住“数据模型设计 + 接口权限控制 + 列表性能优化”这三个核心,整个平台的骨架就稳了。如果你也想做类似的信息服务平台,建议先从通告列表和艺人档案这两个核心模块入手,跑通后再扩展报名审核流程,迭代着来,踩坑成本会小很多。
