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

339 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 用户登录与信息授权方案
> 版本: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<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.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 中使用 <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.getUserProfile` 和 `wx.getUserInfo` 获取真实用户信息的能力,现在只能通过头像昵称填写组件让用户主动填写。
---
## 四、云函数目录结构
```
cloudfunctions/
└── userLogin/
├── index.js ← 入口,支持 action 路由
├── package.json
└── config.json ← 云函数配置
```
若后续 `updateProfile` 等操作较多,可在同一个云函数内通过 `event.action` 路由:
```javascript
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](./小程序云开发方案和数据库设计.md) §3.5
- **其他云函数**:需要用户身份的云函数(如收藏、下载记录)通过 `cloud.getWXContext().OPENID` 查询 `users` 集合获取 `_id`,作为 `userId` 关联
- **前端**:各页面通过 `getUser()` 获取用户数据,无需关心登录细节