# 用户登录与信息授权方案 > 版本:v1.0 > 最后更新:2026-05-06 > 适用范围:微信小程序云开发环境下的用户身份管理 --- ## 一、方案概述 采用**静默登录**策略:用户进入小程序后无感知地完成身份识别与用户创建,不弹出任何授权弹窗。昵称和头像等个人信息在后续需要时(如个人中心、评论等场景)再引导用户主动授权。 **核心流程**: ``` 用户打开小程序 │ ├─ 本地 Storage 有用户数据且未过期? │ │ │ ├─ 是 → 直接使用本地数据,跳过登录 │ │ │ └─ 否 → 调用云函数 silentLogin │ │ │ ├─ cloud.getWXContext() 获取 openid/unionid │ │ │ ├─ 查询 users 集合(openid) │ │ │ │ │ ├─ 存在 → 更新 lastActiveAt,返回用户数据 │ │ │ │ │ └─ 不存在 → 创建用户(nickName/avatarUrl 为空),返回用户数据 │ │ │ └─ 前端收到用户数据 → 写入内存 + Storage │ └─ 完成,进入正常使用 ``` --- ## 二、技术细节 ### 2.1 云函数:`userLogin` **职责**:静默登录,upsert 用户记录。 ```javascript // 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` ```typescript // 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 { // 1. 内存中有,直接返回 if (currentUser) return currentUser; // 2. 尝试从 Storage 读取 const cached = loadFromStorage(); if (cached) { currentUser = cached; return cached; } // 3. 静默登录 return await silentLogin(); } /** * 静默登录:调用云函数 */ async function silentLogin(): Promise { 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 { clearUser(); return await silentLogin(); } ``` --- ### 2.3 调用时机 在 `app.ts` 的 `onLaunch` 中触发登录: ```typescript // 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()` 可随时强制重新登录 | --- ## 三、昵称与头像授权(后续实现) 静默登录不获取用户昵称和头像。当需要展示或使用这些信息时(如个人中心页),引导用户主动填写: ```typescript // 使用微信头像昵称填写能力(基础库 2.21.2+) // 在 wxml 中使用