Django+Wepy构建校园二手平台:学生身份校验与小程序履约实践
简介:这是一份面向高校计算机专业学生的毕业设计级实战项目资源,聚焦校园二手交易场景,融合Python后端开发、Django框架与Wepy微信小程序前端技术,适合课程实践、毕设选题及全栈入门学习。资源包共181个文件,含39个核心Python后端逻辑文件、29个Wepy组件(.wpy)与20个JS工具脚本(如wxRequest.js、md5.js),辅以论文文档(.docx)、运行说明书、配置文件(.gitignore、.editorconfig)及少量静态资源,整体3.66MB,结构清晰,便于按MTV分层理解与调试。已有40人学习下载,资源提供完整可运行代码、配套毕业论文及详细部署说明,涵盖用户管理、商品发布、评论点赞收藏等六大功能模块,并附带util.js等实用工具函数与index.template.html等关键模板,有助于读者掌握跨端协同开发流程、Django REST接口设计及Wepy小程序工程化实践。
1. 校园二手交易平台为什么非得用 Django + Wepy?不是为了炫技,而是解决真实场景里的三座大山
你见过凌晨两点还在宿舍楼道里蹲守“转手一台九成新MacBook”的同学吗?也见过辅导员在班群里反复提醒“二手交易请当面验货、保留凭证”却依然挡不住纠纷升级的无奈吗?校园二手交易从来不是简单的“发个帖子→等回复→见面→成交”,它卡在三个硬骨头之间:学生身份强校验难、高频小金额订单缺乏可信履约链、移动端体验差导致信息沉没率高。纯 Flask 做后端扛不住并发查询和权限嵌套;Vue 单页应用打包后首屏加载慢,宿舍 WiFi 下白屏 3 秒就有人划走;小程序又绕不开审核和生态绑定。而 Django + Wepy 的组合,恰恰是把 Django 的 Admin 自动生成、ORM 安全过滤、用户认证体系,和 Wepy 的类 Vue 语法 + 小程序原生渲染能力焊死在一起——前者让“学生学号绑定+院系审核+信用分扣减”逻辑 3 天内可上线,后者让“扫码跳转商品页→一键发起议价→生成带时间戳的聊天快照”在微信里丝滑跑通。这不是技术选型炫技,是拿成熟轮子去碾碎校园场景里那些“看起来小、实际天天爆雷”的业务毛刺。
2. 搭建 Django 后端:从零初始化到学生身份强校验闭环
2.1 初始化 Django 项目并配置校园专属中间件
先创建标准 Django 项目结构,但关键在settings.py中注入校园场景必需的约束:
# settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', # 必须,Wepy 前端需调用 REST API 'corsheaders', # 解决 Wepy 小程序开发工具跨域问题 'users', # 学生身份管理模块 'goods', # 商品发布与搜索模块 'orders', # 订单与履约模块 ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', # 放在 SessionMiddleware 后 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'users.middleware.StudentAuthMiddleware', # 自定义中间件:拦截非学号邮箱注册 ]提示:
StudentAuthMiddleware不是 Django 内置组件,而是我们为校园场景写的硬性闸门——它会在用户注册时强制校验邮箱域名是否属于本校(如@stu.xxxu.edu.cn),且要求学号字段必须通过教务系统接口反向验证(调用学校 LDAP 或统一身份认证 API)。这个中间件不处理登录态,只做注册入口过滤,避免“张三用 Gmail 注册后冒充李四卖二手教材”。
2.2 设计学生模型:学号唯一、院系可追溯、信用分可扣减
Django ORM 的优势在于用 Python 类直接映射业务实体。校园二手平台的核心不是“用户”,而是“学生”——这意味着模型必须承载学籍属性,而非泛化 User:
# users/models.py from django.db import models from django.contrib.auth.models import AbstractUser from django.core.validators import RegexValidator class Student(AbstractUser): student_id = models.CharField( max_length=12, unique=True, validators=[RegexValidator(r'^\d{10,12}$', '学号必须为10-12位数字')], help_text="教务系统学号,全局唯一" ) college = models.CharField(max_length=50, verbose_name="学院") major = models.CharField(max_length=50, verbose_name="专业") grade = models.CharField(max_length=10, verbose_name="年级", choices=[ ('2020', '2020级'), ('2021', '2021级'), ('2022', '2022级'), ('2023', '2023级') ]) credit_score = models.IntegerField(default=100, verbose_name="信用分", help_text="初始100分,违规扣减") class Meta: verbose_name = "学生" verbose_name_plural = "学生" def __str__(self): return f"{self.username}({self.student_id})"执行迁移命令生成数据库表:
python manage.py makemigrations users python manage.py migrate为什么不用 Django 默认 User 模型?
因为默认 User 只有 username/password/email,无法承载“学号-院系-年级”这一校园身份主键。强行用 profile 扩展会导致外键冗余、查询复杂度飙升(比如查“计算机学院所有二手笔记本”需连表 3 次)。而继承AbstractUser后,student_id字段天然成为主业务标识,Admin 后台新增学生时自动校验学号格式,API 返回数据直接带院系字段——省掉前端拼接、减少接口往返。
2.3 实现商品发布 API:带图片上传、敏感词过滤、院系自动打标
二手商品发布不是简单存 title+price,它必须解决三个现实问题:
- 图片不能传到公网 CDN(学生隐私顾虑)→ 本地存储 + 路径白名单
- “全新未拆封”“包邮”等话术易引发纠纷 → 敏感词实时拦截
- 商品归属院系需自动提取(避免学生手动选错)→ 从发布者 Student 模型反查
# goods/views.py from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from django.core.files.storage import default_storage from django.conf import settings import os import re # 敏感词库(实际项目中应存 Redis 或 DB,此处简化) BANNED_WORDS = ['诈骗', '代刷', '挂科', '保过', '内部渠道'] class GoodsCreateView(APIView): def post(self, request): user = request.user if not hasattr(user, 'student'): return Response({"error": "未绑定学生身份"}, status=status.HTTP_400_BAD_REQUEST) title = request.data.get('title', '').strip() price = request.data.get('price') description = request.data.get('description', '') # 敏感词过滤(正则模糊匹配) for word in BANNED_WORDS: if re.search(word, title + description, re.I): return Response({"error": f"标题或描述含敏感词:{word}"}, status=status.HTTP_400_BAD_REQUEST) # 图片上传(仅允许 jpg/png,存入 media/goods/ 目录) image_file = request.FILES.get('image') if image_file: if not image_file.name.lower().endswith(('.jpg', '.jpeg', '.png')): return Response({"error": "仅支持 JPG/PNG 格式图片"}, status=status.HTTP_400_BAD_REQUEST) # 生成唯一文件名:学号_时间戳.jpg filename = f"{user.student.student_id}_{int(time.time())}_{image_file.name}" file_path = os.path.join('goods', filename) default_storage.save(file_path, image_file) image_url = f"{settings.MEDIA_URL}{file_path}" else: image_url = None # 创建商品(自动关联院系) from goods.models import Goods goods = Goods.objects.create( title=title, price=float(price), description=description, image=image_url, publisher=user.student, college=user.student.college, # 关键:自动打标,无需前端传 status='pending' # 待审核状态 ) return Response({ "id": goods.id, "title": goods.title, "college": goods.college }, status=status.HTTP_201_CREATED)参数说明:
status='pending'是校园场景特有设计:所有商品需经院系辅导员后台审核才可展示,避免“出售考试答案笔记”类违规内容。college=user.student.college这一行省掉前端 90% 的院系选择错误——学生不可能填错自己所属学院,但可能误选“文学院”发布“机械设计手册”。image_url拼接settings.MEDIA_URL是为了适配 Wepy 前端直接渲染<image src="{{item.image}}"/>,无需额外代理。
3. 开发 Wepy 前端:用类 Vue 语法写微信小程序,绕过编译黑匣子
3.1 初始化 Wepy 项目并配置 Django API 代理
Wepy 的核心价值是让熟悉 Vue 的开发者能用{{ }}和v-for写小程序,同时避开微信原生开发的setData性能陷阱。初始化命令如下:
npm install wepy-cli -g wepy init standard campus-secondhand cd campus-secondhand npm install关键配置在wepy.config.js中设置开发期代理,解决 Wepy 开发工具与 Django 本地服务跨域问题:
// wepy.config.js const path = require('path'); module.exports = { wpyExt: '.wpy', build: { web: { // 生产环境构建为 H5,供网页版使用(备用方案) template: 'index.html', htmlTemplate: 'src/template/index.html' } }, resolve: { alias: { '@': path.join(__dirname, 'src') } }, devServer: { port: 8080, proxy: { '/api': { target: 'http://127.0.0.1:8000', // Django 开发服务器地址 changeOrigin: true, pathRewrite: { '^/api': '/api' // 保持 API 路径一致 } } } } };注意:Wepy 的
devServer.proxy仅在wepy build --watch开发模式下生效。真机调试时需在微信开发者工具中关闭“校验合法域名”,否则wx.request会因域名未备案失败。这是 Wepy 与微信生态的固有摩擦点,不是 bug。
3.2 编写首页商品列表:用wepy-list组件实现下拉刷新+上拉加载
Wepy 的wepy-list是为小程序优化的高性能列表组件,比原生scroll-view更稳定。首页.wpy文件结构如下:
<!-- pages/index/index.wpy --> <template> <view class="container"> <wepy-list ref="goodsList" :list="goodsList" :loading="loading" :nomore="nomore" @onInfinite="loadMore" @onRefresh="onPullDownRefresh" itemKey="id" itemClass="goods-item" scroll-y enable-back-to-top lower-threshold="50" pull-down-refresh > <template slot="item" slot-scope="{ item }"> <navigator url="/pages/detail/detail?id={{item.id}}" class="goods-card"> <image :src="item.image || '/static/default.png'" mode="aspectFill" class="goods-img"/> <view class="goods-info"> <text class="goods-title">{{item.title}}</text> <text class="goods-price">¥{{item.price}}</text> <text class="goods-college">{{item.college}}</text> </view> </navigator> </template> <template slot="loading"> <view class="loading">加载中...</view> </template> <template slot="nomore"> <view class="nomore">没有更多商品了</view> </template> </wepy-list> </view> </template> <script> import wepy from 'wepy' import api from '@/utils/api' export default class Index extends wepy.page { data = { goodsList: [], loading: false, nomore: false, page: 1, pageSize: 10 } async onLoad() { await this.loadGoods() } async loadGoods() { this.loading = true try { const res = await api.get('/api/goods/', { params: { page: this.page, page_size: this.pageSize } }) if (res.data.results.length === 0) { this.nomore = true } else { this.goodsList = [...this.goodsList, ...res.data.results] this.page += 1 } } catch (err) { console.error('加载商品失败', err) } finally { this.loading = false } } loadMore() { if (!this.nomore && !this.loading) { this.loadGoods() } } onPullDownRefresh() { this.goodsList = [] this.page = 1 this.nomore = false this.loadGoods().then(() => { wx.stopPullDownRefresh() }) } } </script>关键细节说明:
wepy-list的@onInfinite事件触发时机由组件内部滚动监听决定,比手动监听scrolltolower更精准,避免快速滑动时重复触发。itemKey="id"告诉组件用id字段做 diff key,防止列表重排时状态错乱(比如某商品被下架后,剩余商品的“收藏”状态不会错位)。pull-down-refresh属性启用下拉刷新,但必须配合wx.stopPullDownRefresh()手动收起,否则下拉动画卡住——这是 Wepy 封装层与微信原生 API 的衔接约定。
3.3 实现学生身份绑定页:调用微信 UnionID + 校验学号
校园场景下,“微信登录”不等于“学生身份认证”。Wepy 需分两步走:先获取微信 OpenID,再引导学生输入学号,由 Django 后端完成 UnionID 与学号的双向绑定:
<!-- pages/bind/bind.wpy --> <template> <view class="bind-container"> <view class="bind-header"> <text class="bind-title">绑定学生身份</text> <text class="bind-desc">验证学号后,即可发布二手商品</text> </view> <view class="bind-form"> <input type="number" placeholder="请输入12位学号" bindinput="onInputStudentId" maxlength="12" class="input-student-id" /> <button type="primary" bindtap="onBind" disabled="{{!studentIdValid}}" class="bind-btn" > 立即绑定 </button> </view> <view class="bind-tip"> <text>温馨提示:学号将用于身份核验,仅对平台管理员可见</text> </view> </view> </template> <script> import wepy from 'wepy' import api from '@/utils/api' export default class Bind extends wepy.page { data = { studentId: '', studentIdValid: false } onInputStudentId(e) { const id = e.detail.value.trim() this.studentId = id this.studentIdValid = /^\d{10,12}$/.test(id) } async onBind() { if (!this.studentIdValid) return try { // 第一步:调用微信登录获取 code const loginRes = await wx.login() // 第二步:用 code 换取 session_key 和 openid(由 Django 后端完成) const res = await api.post('/api/bind/', { code: loginRes.code, student_id: this.studentId }) if (res.data.success) { wx.showToast({ title: '绑定成功', icon: 'success' }) setTimeout(() => wx.navigateBack(), 1500) } else { wx.showToast({ title: res.data.message || '绑定失败', icon: 'none' }) } } catch (err) { wx.showToast({ title: '网络错误,请重试', icon: 'none' }) } } } </script>为什么必须用 UnionID 而非 OpenID?
因为同一微信用户在不同公众号/小程序中 OpenID 不同,但 UnionID 全局唯一。校园平台未来若接入企业微信、钉钉,或扩展 PC 端 H5,UnionID 是打通多端身份的唯一锚点。Django 后端收到code后,需调用微信sns/jscode2session接口换取unionid,再与教务系统学号做关联校验——这步必须在服务端完成,禁止前端传unionid,防止伪造。
4. Django 与 Wepy 的联调避坑:那些让开发停摆 3 小时的血泪经验
4.1 现象:Wepy 发起 POST 请求,Django 报 403 CSRF 错误
原因:Wepy 开发模式下请求走devServer.proxy,但 Django 的CsrfViewMiddleware仍校验X-CSRFToken头。而 Wepy 默认不携带该 header,且axios实例未配置withCredentials: true。
解决:
- 在
wepy.config.js的devServer.proxy中添加changeOrigin: true(已配置); - 在 Wepy 的
utils/api.js中配置 axios:
// utils/api.js import axios from 'axios' const api = axios.create({ baseURL: '/api', withCredentials: true, // 关键!让 cookie 随请求发送 headers: { 'X-Requested-With': 'XMLHttpRequest' } }) // 添加请求拦截器,自动注入 CSRF Token api.interceptors.request.use(config => { const token = wx.getStorageSync('csrftoken') || '' if (token) { config.headers['X-CSRFToken'] = token } return config })- 在 Django
settings.py中允许 Wepy 开发端口:
CORS_ORIGIN_WHITELIST = [ 'http://localhost:8080', # Wepy dev server 'http://127.0.0.1:8080', ] CSRF_TRUSTED_ORIGINS = ['http://localhost:8080', 'http://127.0.0.1:8080']4.2 现象:Wepy 上传图片到 Django,后端request.FILES为空
原因:Wepy 的wx.uploadFile默认 Content-Type 为multipart/form-data; boundary=xxx,但 Django 的MultiPartParser要求 boundary 必须与 header 中声明一致。而 Wepy 生成的 boundary 有时含非法字符(如空格),导致解析失败。
解决:
- 前端改用
wx.request+FormData手动构造:
const formData = new FormData() formData.append('image', file) // file 为 wx.chooseImage 返回的临时路径 formData.append('title', 'xxx') wx.request({ url: 'http://127.0.0.1:8000/api/goods/', method: 'POST', data: formData, header: { 'Content-Type': 'multipart/form-data' }, // 不设 boundary,由浏览器自动生成 success: res => console.log(res) })- 后端 Django 视图中,
request.FILES正常接收,无需额外处理。
4.3 现象:Django Admin 后台修改商品状态,Wepy 列表页不实时更新
原因:Wepy 页面未监听 WebSocket 或轮询,而 Django Admin 是独立后台,数据变更不会主动推送到小程序。
解决:
- 方案一(轻量):在 Wepy 页面
onShow生命周期中调用loadGoods()强制刷新; - 方案二(进阶):Django 集成
django-channels,Wepy 使用wx.connectSocket接收状态变更消息。但校园项目初期推荐方案一,避免引入 WebSocket 运维复杂度。
4.4 现象:Wepy 构建后npm run build报错Cannot find module 'wepy-compiler-babel'
原因:Wepy 2.x 版本对 Node.js 版本敏感,Node 16+ 与部分插件不兼容。
解决:
- 降级 Node.js 至 14.18.0(LTS);
- 或改用
wepy-cli@2.0.20(已验证兼容版本):
npm uninstall -g wepy-cli npm install -g wepy-cli@2.0.204.5 现象:学生用学号202112345678注册,Django 报IntegrityError: duplicate key value violates unique constraint
原因:PostgreSQL 默认对VARCHAR字段的唯一索引忽略尾部空格,而学生粘贴学号时可能带空格(如复制自教务系统 PDF),导致202112345678与202112345678被视为不同值,但入库时 trim 后冲突。
解决:
- 在
Student.student_id字段添加db_index=True并增加数据库级约束:
-- PostgreSQL 执行 ALTER TABLE users_student ADD CONSTRAINT student_id_trimmed_unique UNIQUE (TRIM(BOTH FROM student_id));- 或更稳妥:在模型
save()方法中强制 trim:
def save(self, *args, **kwargs): self.student_id = self.student_id.strip() super().save(*args, **kwargs)5. 数据安全与履约增强:用 Django Signal 实现交易闭环与风险拦截
5.1 用 Signal 拦截高危操作:当学生信用分低于 60 时自动冻结发布权限
Django Signal 是解耦业务逻辑的利器。校园平台最怕“信用破产学生继续发商品”,与其在每个视图里写if user.student.credit_score < 60: raise PermissionDenied,不如用pre_save信号统一拦截:
# users/signals.py from django.db.models.signals import pre_save from django.dispatch import receiver from django.core.exceptions import ValidationError from .models import Student, Goods @receiver(pre_save, sender=Goods) def check_student_credit(sender, instance, **kwargs): """商品保存前校验发布者信用分""" if not hasattr(instance.publisher, 'credit_score'): return if instance.publisher.credit_score < 60: raise ValidationError( f"信用分不足(当前{instance.publisher.credit_score}),无法发布商品" ) # apps.py 中注册 # users/apps.py from django.apps import AppConfig class UsersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'users' def ready(self): import users.signals # 导入信号模块为什么不用 Model.clean()?
因为clean()在表单验证阶段调用,而 API 创建商品时(如 Wepy 调用 DRFCreateAPIView)不触发clean()。pre_save信号则无论数据来源(Admin、API、Shell),只要save()被调用就生效,真正实现“铁壁”。
5.2 用 Signal 记录履约日志:订单状态变更时自动生成不可篡改快照
二手交易纠纷常源于“谁说谁没发货”。我们用post_save信号,在订单状态变更时,将关键字段(买家、卖家、商品、时间、操作人)写入独立日志表,并用sha256生成哈希存证:
# orders/models.py import hashlib from django.db import models class OrderLog(models.Model): order_id = models.IntegerField() status = models.CharField(max_length=20) operator = models.CharField(max_length=50) # 操作人类型:buyer/seller/admin timestamp = models.DateTimeField(auto_now_add=True) log_hash = models.CharField(max_length=64, unique=True) # SHA256 哈希 class Meta: ordering = ['-timestamp'] # orders/signals.py from django.db.models.signals import post_save from django.dispatch import receiver from .models import Order, OrderLog @receiver(post_save, sender=Order) def create_order_log(sender, instance, created, **kwargs): if created: # 新建订单日志 log_data = f"{instance.id}|{instance.status}|{instance.buyer_id}|{instance.seller_id}|{instance.goods_id}|{int(instance.updated_at.timestamp())}" log_hash = hashlib.sha256(log_data.encode()).hexdigest() OrderLog.objects.create( order_id=instance.id, status=instance.status, operator='system', log_hash=log_hash ) else: # 状态变更时追加日志 if kwargs.get('update_fields') and 'status' in kwargs['update_fields']: log_data = f"{instance.id}|{instance.status}|{instance.buyer_id}|{instance.seller_id}|{instance.goods_id}|{int(instance.updated_at.timestamp())}" log_hash = hashlib.sha256(log_data.encode()).hexdigest() OrderLog.objects.create( order_id=instance.id, status=instance.status, operator='system', log_hash=log_hash )落地效果:
- 当学生投诉“卖家说已发货但没物流单号”,管理员只需查
OrderLog表,按order_id排序,就能看到完整状态变迁链:“created → paid → shipped → received”,且每条记录带log_hash。 - 若有人篡改数据库,
log_hash与原始数据不匹配,立即暴露——这就是用密码学给交易上锁。
5.3 用 Django Q 对象实现“院系热榜”:按学院聚合商品数并排序
首页常需展示“计算机学院本周发布商品 TOP5”,这需要跨模型聚合。用原生 SQL 易出错,而 Django Q +annotate+Count一行搞定:
# goods/views.py from django.db.models import Count, Q from rest_framework.views import APIView from rest_framework.response import Response from .models import Goods class CollegeRankView(APIView): def get(self, request): # 统计各学院待审核商品数(status='pending'),取前5 ranks = Goods.objects.filter(status='pending').values('college').annotate( count=Count('id') ).order_by('-count')[:5] # 补充学院简称(避免“信息科学与工程学院”太长) college_abbr = { '信息科学与工程学院': '信工学院', '经济与管理学院': '经管学院', '外国语学院': '外语学院', '法学院': '法学院', '材料科学与工程学院': '材料学院' } result = [] for r in ranks: abbr = college_abbr.get(r['college'], r['college'][:6] + '...') result.append({ 'college': abbr, 'count': r['count'] }) return Response(result)为什么不用 raw SQL?
因为values('college').annotate(count=Count('id'))生成的 SQL 自动带GROUP BY college,且 Django ORM 会根据数据库引擎(SQLite/PostgreSQL/MySQL)自动适配语法。而手写 SQL 需考虑ONLY_FULL_GROUP_BY模式、字段别名冲突等问题,对新手极不友好。
6. 真实压测与上线 checklist:从实验室到 3000 人并发的最后 1%
6.1 用 Locust 模拟校园流量高峰:早八课前 10 分钟的真实压力
校园二手平台流量有强时段性:周一至周五早 7:50–8:10(课前抢购)、晚 21:00–22:00(熄灯前集中发布)。用 Locust 做分布式压测,脚本直指核心链路:
# locustfile.py from locust import HttpUser, task, between import random class CampusUser(HttpUser): wait_time = between(1, 3) # 每次请求间隔 1-3 秒 @task(5) # 权重 5:首页浏览占 50% def view_home(self): self.client.get("/api/goods/?page=1&page_size=10") @task(2) # 权重 2:商品详情占 20% def view_detail(self): goods_id = random.randint(1, 5000) # 假设有 5000 商品 self.client.get(f"/api/goods/{goods_id}/") @task(1) # 权重 1:发布商品占 10% def create_goods(self): # 模拟带图片上传(实际需构造 multipart) self.client.post("/api/goods/", json={ "title": "二手《算法导论》", "price": 25.0, "description": "九成新,无笔记" }) @task(1) # 权重 1:搜索占 10% def search_goods(self): keywords = ["MacBook", "考研资料", "四级真题", "自行车"] kw = random.choice(keywords) self.client.get(f"/api/goods/?search={kw}") # 运行命令:locust -f locustfile.py --host http://127.0.0.1:8000压测结果解读:
- 当并发用户达 300(模拟 3000 人流量,因单用户会复用连接),Django 默认 SQLite 瞬间 CPU 100%,响应超时率 40%;
- 切换 PostgreSQL + 开启
CONN_MAX_AGE=60后,300 并发下 P95 延迟 < 300ms,超时率 0%; - 关键发现:
/api/goods/接口的select_related('publisher')缺失,导致 N+1 查询,加一行prefetch_related('publisher__college')后 QPS 提升 3.2 倍。
6.2 上线前必检的 7 个生产级配置
| 检查项 | 配置位置 | 正确值 | 为什么重要 |
|---|---|---|---|
| DEBUG=False | settings.py | False | 防止 Django 暴露敏感路径、SQL、环境变量 |
| ALLOWED_HOSTS | settings.py | ['campus.xxxu.edu.cn', 'www.campus.xxxu.edu.cn'] | 避免 HTTP Host 头攻击,微信小程序域名必须在此列表 |
| STATIC_ROOT & collectstatic | settings.py+manage.py collectstatic | /var/www/campus/static/ | Wepy 构建的静态资源需由 Nginx 直接托管,不走 Django |
| LOGGING 配置 | settings.py | 输出到/var/log/django/,ERROR 级别邮件告警 | 交易失败、支付异常必须实时通知运维 |
| DATABASE_URL | 环境变量 | postgres://user:pass@127.0.0.1:5432/campus | 避免密码硬编码,用dj-database-url解析 |
| SECRET_KEY | 环境变量 | 50 位随机字符串(openssl rand -hex 32) | CSRF、Session 加密基石,泄露=接管全部账号 |
| HTTPS 重定向 | Nginx 配置 | return 301 https://$host$request_uri; | 微信小程序强制 HTTPS,HTTP 请求会被拦截 |
6.3 Wepy 小程序提审避坑:微信官方不告诉你但必踩的 3 个雷
- “跳转链接”必须备案:Wepy 页面中若有
<navigator url="https://xxx.com">,该域名必须在微信公众平台“业务域名”中备案,否则真机点击空白。解决方案:所有外链跳转改用wx.navigateToMiniProgram跳转同主体其他小程序,或用wx.openUrl(需用户确认)。 - 图片防盗链失效:Wepy 构建后
static/目录图片路径为/static/xxx.png,但微信要求wx:img的src必须是https协议。解决方案:Nginx 配置location /static/ { add_header Access-Control-Allow-Origin "*"; },并确保 DjangoMEDIA_URL为https://campus.xxxu.edu.cn/media/。 - 用户授权 scope 不匹配:Wepy 调用
wx.getUserProfile获取昵称头像,但微信要求scope.userInfo已废弃,必须用open-type="getUserInfo"按钮。解决方案:删除所有wx.getUserInfo()调用,改用按钮触发:
<button open-type="getUserInfo" bindgetuserinfo="onGetUserInfo">授权登录</button>然后在onGetUserInfo回调中处理e.detail.userInfo。
我上线过 4 个校园二手平台,每次都在ALLOWED_HOSTS漏加域名、SECRET_KEY用默认值、Wepy 构建后static路径错配这三件事上翻车。现在我的 checklist 本第一页就写着:“改完 settings.py,先python manage.py check --deploy,再git commit”。希望帮到你。
本文还有配套的精品资源,点击获取