Skip to content

章节 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.jspackage.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-id

3.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 api
python
# 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.name

3.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" });
    }
  },
});

小结

  1. 云开发:Serverless 模式无需维护服务器,云函数运行在 Node.js 环境,云数据库是 JSON 文档型
  2. 云函数wx.cloud.callFunction() 调用,cloud.getWXContext() 获取 OPENID
  3. 云数据库db.collection().add() / .doc().update() / .where().get() 为 CRUD 核心
  4. Django 联调:DRF 的 ViewSet + Serializer 快速构建 RESTful API,django-cors-headers 处理跨域
  5. JWT 认证:登录流程为 wx.login → 后端 code2session → 返回 JWT Token → 前端存储并在后续请求 Authorization 头携带

练习

  1. 云函数入门:创建一个云函数 sum,接收两个数字参数,返回它们的和。在小程序端调用并显示结果。
  2. 云数据库 CRUD:创建一个"待办事项"集合,实现添加、查询、标记完成、删除四个功能。
  3. 云存储上传:实现图片上传功能——从相册选择图片后上传到云存储,并将文件 ID 保存到数据库。
  4. Django 后端:搭建 Django 后端,创建商品模型并编写 API(列表、详情、按分类筛选),在本地测试。
  5. JWT 登录:实现完整的小程序登录流程——wx.login 获取 code → 后端换取 openid → 返回 JWT → 前端保存并在后续请求中携带。

Python 学习资料