Files
doodle-mini/docs/用户登录与信息授权方案.md
T
2026-05-06 18:32:15 +08:00

9.7 KiB
Raw Blame History

用户登录与信息授权方案

版本:v1.0 最后更新:2026-05-06 适用范围:微信小程序云开发环境下的用户身份管理


一、方案概述

采用静默登录策略:用户进入小程序后无感知地完成身份识别与用户创建,不弹出任何授权弹窗。昵称和头像等个人信息在后续需要时(如个人中心、评论等场景)再引导用户主动授权。

核心流程

用户打开小程序
    │
    ├─ 本地 Storage 有用户数据且未过期?
    │       │
    │       ├─ 是 → 直接使用本地数据,跳过登录
    │       │
    │       └─ 否 → 调用云函数 silentLogin
    │                   │
    │                   ├─ cloud.getWXContext() 获取 openid/unionid
    │                   │
    │                   ├─ 查询 users 集合(openid
    │                   │       │
    │                   │       ├─ 存在 → 更新 lastActiveAt,返回用户数据
    │                   │       │
    │                   │       └─ 不存在 → 创建用户(nickName/avatarUrl 为空),返回用户数据
    │                   │
    │                   └─ 前端收到用户数据 → 写入内存 + Storage
    │
    └─ 完成,进入正常使用

二、技术细节

2.1 云函数:userLogin

职责:静默登录,upsert 用户记录。

// cloudfunctions/userLogin/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 wxContext = cloud.getWXContext();
    const openid = wxContext.OPENID;
    const unionid = wxContext.UNIONID || null;

    const usersCollection = db.collection('users');

    // 查询是否已存在
    const { data } = await usersCollection.where({ openid }).limit(1).get();

    const now = db.serverDate();

    if (data.length > 0) {
        // 已存在:更新 lastActiveAt 和 unionid(如果有新值)
        const user = data[0];
        const updateData = { lastActiveAt: now };
        if (unionid && !user.unionid) {
            updateData.unionid = unionid;
        }
        await usersCollection.doc(user._id).update({ data: updateData });

        return {
            code: 0,
            data: { ...user, ...updateData, lastActiveAt: new Date() },
        };
    } else {
        // 不存在:创建新用户
        const newUser = {
            openid,
            unionid,
            nickName: null,
            avatarUrl: null,
            totalDownloads: 0,
            createdAt: now,
            lastActiveAt: now,
        };
        const { _id } = await usersCollection.add({ data: newUser });

        return {
            code: 0,
            data: {
                _id,
                ...newUser,
                createdAt: new Date(),
                lastActiveAt: new Date(),
            },
        };
    }
};

要点

  • cloud.getWXContext() 在云函数中自动获取调用者的 OPENID,无需前端传 code
  • UNIONID 仅在小程序绑定了开放平台且满足条件时才有值,所以设为可选
  • 使用 db.serverDate() 确保时间由服务端生成,避免客户端时间不准
  • _id 由云数据库自动生成(add() 时不指定),为随机唯一字符串;业务上通过 openid 唯一索引定位用户,_id 作为主键供子表(favorites、download_logs)的 userId 外键引用

2.2 前端登录逻辑

位置:建议封装为 miniprogram/utils/auth.ts

// miniprogram/utils/auth.ts

interface UserInfo {
    _id: string;
    openid: string;
    unionid: string | null;
    nickName: string | null;
    avatarUrl: string | null;
    totalDownloads: number;
    createdAt: string;
    lastActiveAt: string;
}

const STORAGE_KEY = 'user_info';
const STORAGE_EXPIRE_KEY = 'user_info_expire';
const EXPIRE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 天

/** 内存中的用户数据(App 生命周期内有效) */
let currentUser: UserInfo | null = null;

/**
 * 获取当前用户,优先内存 → Storage → 云函数登录
 */
export async function getUser(): Promise<UserInfo> {
    // 1. 内存中有,直接返回
    if (currentUser) return currentUser;

    // 2. 尝试从 Storage 读取
    const cached = loadFromStorage();
    if (cached) {
        currentUser = cached;
        return cached;
    }

    // 3. 静默登录
    return await silentLogin();
}

/**
 * 静默登录:调用云函数
 */
async function silentLogin(): Promise<UserInfo> {
    const { result } = await wx.cloud.callFunction({
        name: 'userLogin',
    });

    if (result.code !== 0) {
        throw new Error('登录失败');
    }

    const user = result.data as UserInfo;
    currentUser = user;
    saveToStorage(user);
    return user;
}

/**
 * 从 Storage 读取用户数据(检查过期)
 */
function loadFromStorage(): UserInfo | null {
    try {
        const expire = wx.getStorageSync(STORAGE_EXPIRE_KEY);
        if (!expire || Date.now() > expire) {
            // 已过期,清除
            wx.removeStorageSync(STORAGE_KEY);
            wx.removeStorageSync(STORAGE_EXPIRE_KEY);
            return null;
        }
        const user = wx.getStorageSync(STORAGE_KEY);
        return user || null;
    } catch {
        return null;
    }
}

/**
 * 写入 Storage
 */
function saveToStorage(user: UserInfo): void {
    try {
        wx.setStorageSync(STORAGE_KEY, user);
        wx.setStorageSync(STORAGE_EXPIRE_KEY, Date.now() + EXPIRE_DURATION);
    } catch {
        // Storage 写入失败不影响主流程
    }
}

/**
 * 清除登录状态(用于退出登录或需要强制刷新时)
 */
export function clearUser(): void {
    currentUser = null;
    wx.removeStorageSync(STORAGE_KEY);
    wx.removeStorageSync(STORAGE_EXPIRE_KEY);
}

/**
 * 强制刷新用户数据(跳过缓存)
 */
export async function refreshUser(): Promise<UserInfo> {
    clearUser();
    return await silentLogin();
}

2.3 调用时机

app.tsonLaunch 中触发登录:

// app.ts
import { getUser } from './utils/auth';

App({
    onLaunch() {
        // 初始化云开发
        wx.cloud.init({ env: 'your-env-id' });

        // 静默登录(不阻塞页面渲染)
        getUser().catch((err) => {
            console.error('静默登录失败', err);
        });
    },
});

注意:登录是异步的,不阻塞页面渲染。页面中需要用户数据时通过 await getUser() 获取,如果登录已完成会立即返回内存数据。


2.4 缓存过期策略

策略 说明
有效期 7 天
过期后行为 下次 getUser() 时自动触发静默登录
过期目的 定期刷新 lastActiveAt,保持活跃数据准确
强制刷新 调用 refreshUser() 可随时强制重新登录

三、昵称与头像授权(后续实现)

静默登录不获取用户昵称和头像。当需要展示或使用这些信息时(如个人中心页),引导用户主动填写:

// 使用微信头像昵称填写能力(基础库 2.21.2+)
// 在 wxml 中使用 <button open-type="chooseAvatar"> 获取头像
// 使用 <input type="nickname"> 获取昵称

export async function updateProfile(
    nickName: string,
    avatarUrl: string,
): Promise<void> {
    const { result } = await wx.cloud.callFunction({
        name: 'userLogin',
        data: {
            action: 'updateProfile',
            nickName,
            avatarUrl,
        },
    });

    if (result.code === 0) {
        // 更新本地缓存
        const user = await getUser();
        user.nickName = nickName;
        user.avatarUrl = avatarUrl;
        saveToStorage(user);
    }
}

注:微信已废弃 wx.getUserProfilewx.getUserInfo 获取真实用户信息的能力,现在只能通过头像昵称填写组件让用户主动填写。


四、云函数目录结构

cloudfunctions/
└── userLogin/
    ├── index.js          ← 入口,支持 action 路由
    ├── package.json
    └── config.json       ← 云函数配置

若后续 updateProfile 等操作较多,可在同一个云函数内通过 event.action 路由:

exports.main = async (event, context) => {
    const { action } = event;

    switch (action) {
        case 'updateProfile':
            return await handleUpdateProfile(event, context);
        default:
            return await handleSilentLogin(event, context);
    }
};

五、安全考虑

项目 措施
openid 来源 仅从 cloud.getWXContext() 获取,不信任前端传入
unionid 服务端获取,不暴露给前端
Storage 数据 不存储敏感信息(openid 不算高敏感,但也不主动展示给用户)
接口幂等 upsert 逻辑天然幂等,重复调用不会创建多条记录

六、与现有架构的关系

  • 数据库:使用 users 集合,字段设计见 小程序云开发方案和数据库设计.md §3.5
  • 其他云函数:需要用户身份的云函数(如收藏、下载记录)通过 cloud.getWXContext().OPENID 查询 users 集合获取 _id,作为 userId 关联
  • 前端:各页面通过 getUser() 获取用户数据,无需关心登录细节