feat: 开发收藏和下载功能

This commit is contained in:
R524809
2026-05-06 18:32:15 +08:00
parent 9bdf850f20
commit c0b1bfddb0
17 changed files with 1262 additions and 127 deletions
+111
View File
@@ -0,0 +1,111 @@
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;
/** 登录进行中的 Promise(防止并发重复调用) */
let loginPromise: Promise<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. 静默登录(防止并发)
if (!loginPromise) {
loginPromise = silentLogin().finally(() => {
loginPromise = null;
});
}
return loginPromise;
}
/**
* 静默登录:调用云函数
*/
async function silentLogin(): Promise<UserInfo> {
const res = await wx.cloud.callFunction({
name: 'userLogin',
});
const result = (res?.result as any) || {};
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();
}
export type { UserInfo };
+56
View File
@@ -0,0 +1,56 @@
import { getUser } from './auth';
export interface DownloadLogRecord {
_id: string;
userId: string;
worksheetId: string;
createdAt: string;
worksheet?: {
_id: string;
title: string;
subtitle: string;
category: string;
subcategory: string;
previewImg: string;
} | null;
}
/**
* 记录下载日志
*/
export async function addDownloadLog(worksheetId: string): Promise<boolean> {
if (!worksheetId) return false;
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userDownloadLogs',
data: { action: 'add', worksheetId },
});
const result = (res?.result as any) || {};
return result.code === 0;
} catch (e) {
console.error('addDownloadLog failed', e);
return false;
}
}
/**
* 获取下载历史
*/
export async function getDownloadLogs(
page = 1,
pageSize = 20,
): Promise<DownloadLogRecord[]> {
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userDownloadLogs',
data: { action: 'list', page, pageSize },
});
const result = (res?.result as any) || {};
return result.code === 0 ? result.data : [];
} catch (e) {
console.error('getDownloadLogs failed', e);
return [];
}
}
+2 -2
View File
@@ -5,7 +5,7 @@
import { checkAndSaveImage } from './saveImage';
import tracker from './tracker';
import { incrementWorksheetDownloads } from './worksheetStats';
import { addDownloadLog } from './downloadLogs';
// 存储键名
const STORAGE_KEY_DOWNLOAD_COUNT = 'downloadCount';
@@ -212,7 +212,7 @@ function doDownload(
if (saved) {
incrementDownloadCount();
if (options.worksheetId) {
incrementWorksheetDownloads(options.worksheetId);
addDownloadLog(options.worksheetId);
}
}
return saved;
+116
View File
@@ -0,0 +1,116 @@
import { getUser } from './auth';
export interface FavoriteRecord {
_id: string;
userId: string;
worksheetId: string;
createdAt: string;
worksheet?: {
_id: string;
title: string;
subtitle: string;
category: string;
subcategory: string;
ageMin: number;
ageMax: number;
difficulty: number;
previewImg: string;
tags: string[];
} | null;
}
/**
* 添加收藏
*/
export async function addFavorite(worksheetId: string): Promise<boolean> {
try {
await getUser(); // 确保已登录
const res = await wx.cloud.callFunction({
name: 'userFavorites',
data: { action: 'add', worksheetId },
});
const result = (res?.result as any) || {};
return result.code === 0;
} catch (e) {
console.error('addFavorite failed', e);
return false;
}
}
/**
* 取消收藏
*/
export async function removeFavorite(worksheetId: string): Promise<boolean> {
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userFavorites',
data: { action: 'remove', worksheetId },
});
const result = (res?.result as any) || {};
return result.code === 0;
} catch (e) {
console.error('removeFavorite failed', e);
return false;
}
}
/**
* 获取收藏列表
*/
export async function getFavoriteList(
page = 1,
pageSize = 20,
): Promise<FavoriteRecord[]> {
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userFavorites',
data: { action: 'list', page, pageSize },
});
const result = (res?.result as any) || {};
return result.code === 0 ? result.data : [];
} catch (e) {
console.error('getFavoriteList failed', e);
return [];
}
}
/**
* 检查是否已收藏
*/
export async function checkFavorited(worksheetId: string): Promise<boolean> {
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userFavorites',
data: { action: 'check', worksheetId },
});
const result = (res?.result as any) || {};
return result.code === 0 && result.data?.favorited;
} catch (e) {
console.error('checkFavorited failed', e);
return false;
}
}
/**
* 批量检查是否已收藏,返回 { worksheetId: true } 的 map
*/
export async function batchCheckFavorited(
worksheetIds: string[],
): Promise<Record<string, boolean>> {
if (!worksheetIds.length) return {};
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userFavorites',
data: { action: 'batchCheck', worksheetIds },
});
const result = (res?.result as any) || {};
return result.code === 0 ? result.data : {};
} catch (e) {
console.error('batchCheckFavorited failed', e);
return {};
}
}