Appearance
章节 3:云开发与后端联调
学习目标
- 理解微信小程序 Serverless 云开发的核心概念
- 掌握云函数的编写、部署与调用
- 熟练使用云数据库和云存储
- 实现 Django 后端与小程序的前后端联调
- 掌握登录授权与 JWT 用户体系
3.1 Serverless 云开发
3.1.1 什么是云开发
定义:微信云开发(Tencent Cloud Base,TCB)是微信提供的一站式 Serverless 后端服务,包含云数据库、云存储、云函数三大能力,开发者无需自行搭建服务器。
云开发三大能力:
┌─────────────────────────────────────────┐
│ 微信云开发 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ 云数据库 │ │ 云存储 │ │ 云函数 │ │
│ │ (JSON文档) │ │ (文件管理) │ │ (Node) │ │
│ └──────────┘ └──────────┘ └────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 数据持久化 文件/图片 业务逻辑 │
└─────────────────────────────────────────┘3.1.2 开通与初始化
bash
# 1. 在微信开发者工具中
# - 工具栏 → 云开发 → 开通(选择环境后确认)
# 2. 项目根目录初始化 cloud 目录
# 右键 cloud 文件夹 → 新建 Node.js 云函数javascript
// app.js —— 初始化云开发
App({
onLaunch() {
wx.cloud.init({
env: "your-env-id-xxxx", // 云开发环境 ID
traceUser: true, // 跟踪用户
});
},
});3.1.3 前端调用云 API
javascript
// 在页面中直接使用云 API(无需云函数)
const db = wx.cloud.database();
// 查询集合
db.collection("products")
.where({
price: db.command.gt(100), // 价格大于 100
})
.orderBy("price", "asc")
.limit(20)
.get()
.then((res) => {
console.log("查询结果:", res.data);
});3.2 云函数编写调用
3.2.1 创建云函数
定义:云函数运行在 Node.js 环境中,每个云函数对应一个目录,包含 index.js 和 package.json。
cloud/
├── functions/
│ ├── login/ ← 云函数名称
│ │ ├── index.js ← 入口文件
│ │ └── package.json ← 依赖声明
│ ├── getProductList/
│ │ ├── index.js
│ │ └── package.json
│ └── submitOrder/
│ ├── index.js
│ └── package.json
├── database/
│ └── collection.schema.json ← 集合权限定义
└── cloudfunctions.js基础云函数示例:
javascript
// cloud/functions/getProductList/index.js
const cloud = require("wx-server-sdk");
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); // 使用当前环境
const db = cloud.database();
// 云函数入口
exports.main = async (event, context) => {
const { page = 1, pageSize = 10, category = "" } = event;
try {
// 构建查询条件
const whereCondition = {};
if (category) {
whereCondition.category = category;
}
const countResult = await db.collection("products")
.where(whereCondition)
.count();
const { data } = await db.collection("products")
.where(whereCondition)
.skip((page - 1) * pageSize)
.limit(pageSize)
.orderBy("createdAt", "desc")
.get();
return {
code: 0,
data: {
list: data,
total: countResult.total,
page,
pageSize,
hasMore: page * pageSize < countResult.total,
},
};
} catch (err) {
return {
code: -1,
message: err.message,
};
}
};3.2.2 部署云函数
bash
# 方式一:开发者工具中部署
# 右键云函数目录 → 上传并部署(云端安装依赖)
# 方式二:命令行部署
# 需要安装 @cloudbase/cli
npm install -g @cloudbase/cli
cloudbase login
cloudbase functions:deploy getProductList -e your-env-id3.2.3 前端调用云函数
javascript
// 调用云函数
wx.cloud.callFunction({
name: "getProductList",
data: {
page: 1,
pageSize: 10,
category: "电子产品",
},
success(res) {
console.log("云函数返回:", res.result);
// { code: 0, data: { list: [...], total: 50, page: 1, pageSize: 10, hasMore: true } }
},
fail(err) {
console.error("调用失败:", err);
},
});
// 使用 async/await
async function loadProducts() {
wx.showLoading({ title: "加载中" });
try {
const res = await wx.cloud.callFunction({
name: "getProductList",
data: { page: 1, pageSize: 20 },
});
return res.result.data;
} catch (err) {
wx.showToast({ title: "加载失败", icon: "error" });
return { list: [], total: 0 };
} finally {
wx.hideLoading();
}
}3.2.4 云函数获取用户身份
javascript
// cloud/functions/login/index.js
const cloud = require("wx-server-sdk");
cloud.init();
const db = cloud.database();
exports.main = async (event, context) => {
// 获取用户身份信息(由微信自动注入)
const { OPENID, APPID, UNIONID } = cloud.getWXContext();
// 在数据库中查找或创建用户
const userCollection = db.collection("users");
const { data: users } = await userCollection
.where({ openId: OPENID })
.get();
let user;
if (users.length === 0) {
// 新用户——注册
const result = await userCollection.add({
data: {
openId: OPENID,
nickname: event.nickname || "微信用户",
avatarUrl: event.avatarUrl || "",
createdAt: db.serverDate(),
lastLogin: db.serverDate(),
},
});
user = { _id: result._id, openId: OPENID };
} else {
// 老用户——更新登录时间
user = users[0];
await userCollection.doc(user._id).update({
data: { lastLogin: db.serverDate() },
});
}
return {
code: 0,
data: {
openId: OPENID,
userId: user._id,
isNewUser: users.length === 0,
},
};
};3.3 云数据库与云存储
3.3.1 云数据库
定义:云数据库是 JSON 文档型数据库,每个集合可包含多条记录,每条记录是一个 JSON 对象(最多 64 层嵌套)。
javascript
// ===== 数据库基本操作 =====
const db = wx.cloud.database();
const _ = db.command; // 查询指令
// --- 增 ---
db.collection("products").add({
data: {
name: "无线蓝牙耳机",
price: 199.9,
category: "电子产品",
stock: 100,
specs: ["黑色", "白色"],
createdAt: db.serverDate(), // 服务器时间
},
success(res) {
console.log("新增 ID:", res._id);
},
});
// --- 删 ---
db.collection("products").doc("document_id").remove({
success(res) {
console.log("删除成功, 记录:", res.stats.removed);
},
});
// --- 改 ---
db.collection("products").doc("document_id").update({
data: {
price: 159.9,
stock: _.inc(-1), // 原子操作:减 1
updatedAt: db.serverDate(),
},
success(res) {
console.log("更新成功");
},
});
// --- 查 ---
// 查询所有
db.collection("products").get();
// 条件查询
db.collection("products")
.where({
category: "电子产品",
price: _.gte(100).and(_.lte(500)), // 100 ≤ price ≤ 500
name: _.regexp({ regex: "耳机", options: "i" }), // 模糊匹配
})
.orderBy("price", "asc")
.skip(0)
.limit(20)
.get()
.then((res) => console.log(res.data));
// 计数
db.collection("products")
.where({ category: "电子产品" })
.count()
.then((res) => console.log("总数:", res.total));3.3.2 权限控制
javascript
// 数据库集合的权限设置
// 在云开发控制台 → 数据库 → 集合 → 权限设置
// 权限级别:
// 1. 仅创建者可读写(默认)
// 2. 仅创建者可写,所有用户可读
// 3. 所有用户可读可写(需谨慎)
// 4. 自定义安全规则(高级)
// 安全规则示例(JSON)
// {
// "read": "doc._openid == auth.openid", // 只能读自己的数据
// "write": "doc._openid == auth.openid", // 只能写自己的数据
// ".validate": "newData().price > 0" // 数据验证:价格必须大于 0
// }3.3.3 云存储
定义:云存储用于管理文件资源(图片、视频、文档),自动 CDN 加速,支持防盗链。
javascript
// ===== 上传文件 =====
wx.chooseImage({
count: 1,
success(chooseResult) {
wx.cloud.uploadFile({
cloudPath: `products/${Date.now()}-${Math.random().toString(36).slice(2)}.jpg`,
filePath: chooseResult.tempFilePaths[0],
success(res) {
console.log("上传成功,文件 ID:", res.fileID);
// fileID 格式: cloud://env-id.xxx/products/123.jpg
this.setData({ imageUrl: res.fileID });
},
fail(err) {
console.error("上传失败:", err);
},
});
},
});
// ===== 下载文件 =====
wx.cloud.downloadFile({
fileID: "cloud://env-id.xxx/products/123.jpg",
success(res) {
console.log("临时路径:", res.tempFilePath);
},
});
// ===== 删除文件(需在云函数中执行)=====
// cloud/functions/deleteFile/index.js
const cloud = require("wx-server-sdk");
cloud.init();
exports.main = async (event) => {
const result = await cloud.deleteFile({
fileList: [event.fileID],
});
return result;
};
// ===== 获取文件临时 URL =====
wx.cloud.getTempFileURL({
fileList: ["cloud://env-id.xxx/products/123.jpg"],
success(res) {
console.log("临时 URL:", res.fileList[0].tempFileURL);
},
});3.4 Django 后端与接口联调
3.4.1 Django 项目搭建
bash
# 创建虚拟环境
python -m venv venv
venv\Scripts\activate # Windows
# 安装依赖
pip install django djangorestframework django-cors-headers pymysql
# 创建项目
django-admin startproject miniprogram_backend
cd miniprogram_backend
python manage.py startapp apipython
# miniprogram_backend/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework", # DRF
"corsheaders", # 跨域
"api", # 应用
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", # 放在第一
# ... 其他中间件
]
# 允许小程序跨域请求
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOW_CREDENTIALS = True
# 数据库配置(MySQL)
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "miniprogram",
"USER": "root",
"PASSWORD": "password",
"HOST": "localhost",
"PORT": "3306",
"OPTIONS": {
"charset": "utf8mb4",
},
}
}
# REST Framework 配置
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"api.authentication.JWTAuthentication",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S",
}3.4.2 创建数据模型
python
# api/models.py
from django.db import models
class User(models.Model):
"""用户模型"""
openid = models.CharField(max_length=64, unique=True, verbose_name="微信 OpenID")
nickname = models.CharField(max_length=50, blank=True, verbose_name="昵称")
avatar_url = models.URLField(blank=True, verbose_name="头像")
phone = models.CharField(max_length=20, blank=True, verbose_name="手机号")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="注册时间")
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
db_table = "users"
verbose_name = "用户"
verbose_name_plural = "用户"
def __str__(self):
return self.nickname or self.openid[:10]
class Product(models.Model):
"""商品模型"""
name = models.CharField(max_length=200, verbose_name="商品名称")
description = models.TextField(blank=True, verbose_name="商品描述")
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="价格")
stock = models.IntegerField(default=0, verbose_name="库存")
category = models.CharField(max_length=50, verbose_name="分类")
image = models.URLField(blank=True, verbose_name="图片")
is_active = models.BooleanField(default=True, verbose_name="上架")
sales = models.IntegerField(default=0, verbose_name="销量")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = "products"
ordering = ["-created_at"]
def __str__(self):
return self.name3.4.3 序列化器
python
# api/serializers.py
from rest_framework import serializers
from .models import User, Product
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "nickname", "avatar_url", "phone", "created_at"]
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["id", "name", "description", "price", "stock",
"category", "image", "sales", "is_active"]3.4.4 视图与路由
python
# api/views.py
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Product, User
from .serializers import ProductSerializer, UserSerializer
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
"""商品接口——仅读"""
queryset = Product.objects.filter(is_active=True)
serializer_class = ProductSerializer
def get_queryset(self):
qs = super().get_queryset()
category = self.request.query_params.get("category")
search = self.request.query_params.get("search")
if category:
qs = qs.filter(category=category)
if search:
qs = qs.filter(name__icontains=search)
return qs
class UserViewSet(viewsets.GenericViewSet):
"""用户接口"""
queryset = User.objects.all()
serializer_class = UserSerializer
@action(detail=False, methods=["post"])
def login(self, request):
"""登录/注册"""
openid = request.data.get("openid")
if not openid:
return Response({"code": -1, "message": "缺少 openid"},
status=status.HTTP_400_BAD_REQUEST)
user, created = User.objects.get_or_create(
openid=openid,
defaults={
"nickname": request.data.get("nickname", ""),
"avatar_url": request.data.get("avatar_url", ""),
},
)
# 生成 JWT Token
import jwt
import datetime
token = jwt.encode(
{
"user_id": user.id,
"openid": user.openid,
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=7),
},
"YOUR_SECRET_KEY",
algorithm="HS256",
)
return Response({
"code": 0,
"data": {
"token": token,
"user": UserSerializer(user).data,
"is_new": created,
},
})python
# api/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r"products", views.ProductViewSet, basename="product")
router.register(r"users", views.UserViewSet, basename="user")
urlpatterns = [
path("api/", include(router.urls)),
]3.4.5 小程序端联调
javascript
// 封装 API 请求(指向 Django 后端)
const DJANGO_BASE_URL = "https://your-domain.com/api";
function request(options) {
const token = wx.getStorageSync("token");
return new Promise((resolve, reject) => {
wx.request({
url: DJANGO_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 < 300) {
resolve(res.data);
} else {
reject(res.data);
}
},
fail(err) {
reject(err);
},
});
});
}
// 页面中使用
Page({
async onLoad() {
const products = await request({ url: "/products/?category=电子产品" });
this.setData({ products });
// 分页
const more = await request({ url: "/products/?page=2" });
},
});3.5 登录授权与 JWT 用户体系
3.5.1 wx.login 流程
定义:小程序通过 wx.login() 获取临时 code,由后端用 code 向微信服务器换取 openid 和 session_key。
小程序 后端服务器 微信服务器
│ │ │
├── wx.login() ───────────→ │ │
│ ←── code ─────────────── │ │
│ │ │
├── 请求登录 ── (code) ──→ │ │
│ ├── code2session ────────→ │
│ │ ←── openid + session_key│
│ │ │
│ ├── 生成 JWT Token │
│ ←── token + userInfo ──── │ │
│ │ │
├── 存储 token 到本地 │ │
└── 后续请求携带 token ────→ │ │3.5.2 Django JWT 认证
python
# api/authentication.py
import jwt
from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from .models import User
class JWTAuthentication(BaseAuthentication):
"""JWT 认证类"""
def authenticate(self, request):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return None # 未携带 Token,匿名访问
token = auth.split(" ")[1]
try:
payload = jwt.decode(
token,
"YOUR_SECRET_KEY",
algorithms=["HS256"],
)
except jwt.ExpiredSignatureError:
raise AuthenticationFailed("Token 已过期")
except jwt.InvalidTokenError:
raise AuthenticationFailed("无效的 Token")
try:
user = User.objects.get(id=payload["user_id"])
except User.DoesNotExist:
raise AuthenticationFailed("用户不存在")
return (user, token)3.5.3 小程序完整登录实现
javascript
// pages/login/login.js
Page({
async handleLogin() {
wx.showLoading({ title: "登录中..." });
try {
// 1. 获取微信登录凭证
const loginResult = await wx.login();
const { code } = loginResult;
// 2. 获取用户信息(头像、昵称)
// 需使用 <button open-type="getUserInfo"> 触发
// 或 wx.getUserProfile()
// 3. 发送 code 到 Django 后端
const res = await wx.request({
url: "https://your-domain.com/api/users/login/",
method: "POST",
data: { code },
});
if (res.data.code === 0) {
const { token, user } = res.data.data;
// 4. 保存 Token 和用户信息
wx.setStorageSync("token", token);
wx.setStorageSync("userInfo", user);
// 5. 跳转到首页
wx.switchTab({ url: "/pages/index/index" });
wx.showToast({ title: "登录成功", icon: "success" });
}
} catch (err) {
console.error("登录失败:", err);
wx.showToast({ title: "登录失败", icon: "error" });
} finally {
wx.hideLoading();
}
},
});3.5.4 获取手机号
xml
<!-- 使用 button 触发手机号授权 -->
<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
获取手机号
</button>javascript
Page({
async onGetPhoneNumber(e) {
if (e.detail.errMsg !== "getPhoneNumber:ok") {
wx.showToast({ title: "已取消", icon: "none" });
return;
}
// 加密数据交给后端解密
const res = await wx.request({
url: "https://your-domain.com/api/users/bind-phone/",
method: "POST",
data: {
encryptedData: e.detail.encryptedData,
iv: e.detail.iv,
},
});
if (res.data.code === 0) {
wx.showToast({ title: "绑定成功", icon: "success" });
}
},
});小结
- 云开发:Serverless 模式无需维护服务器,云函数运行在 Node.js 环境,云数据库是 JSON 文档型
- 云函数:
wx.cloud.callFunction()调用,cloud.getWXContext()获取 OPENID - 云数据库:
db.collection().add()/.doc().update()/.where().get()为 CRUD 核心 - Django 联调:DRF 的 ViewSet + Serializer 快速构建 RESTful API,django-cors-headers 处理跨域
- JWT 认证:登录流程为 wx.login → 后端 code2session → 返回 JWT Token → 前端存储并在后续请求 Authorization 头携带
练习
- 云函数入门:创建一个云函数
sum,接收两个数字参数,返回它们的和。在小程序端调用并显示结果。 - 云数据库 CRUD:创建一个"待办事项"集合,实现添加、查询、标记完成、删除四个功能。
- 云存储上传:实现图片上传功能——从相册选择图片后上传到云存储,并将文件 ID 保存到数据库。
- Django 后端:搭建 Django 后端,创建商品模型并编写 API(列表、详情、按分类筛选),在本地测试。
- JWT 登录:实现完整的小程序登录流程——wx.login 获取 code → 后端换取 openid → 返回 JWT → 前端保存并在后续请求中携带。