feat: 完成全部接口替换为新后端接口

This commit is contained in:
R524809
2026-10-19 13:19:07 +08:00
parent 8de06ce66b
commit 997042789d
40 changed files with 1946 additions and 785 deletions
+39 -148
View File
@@ -1,165 +1,56 @@
interface UserInfo {
_id: string;
openid: string;
unionid: string | null;
nickName: string | null;
avatarUrl: string | null;
totalDownloads: number;
createdAt: string;
lastActiveAt: string;
}
import { apiRequest } from '../services/api-request';
import {
cachedServerUser,
clearSession,
ensureSession,
getAuthContext,
updateSessionUser,
type ServerUser,
} from '../services/auth-session';
const STORAGE_KEY = 'user_info';
const STORAGE_EXPIRE_KEY = 'user_info_expire';
const EXPIRE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 天
export type UserInfo = ServerUser;
export { ensureSession as ensureAuthenticated } from '../services/auth-session';
/** 内存中的用户数据(App 生命周期内有效) */
let currentUser: UserInfo | null = null;
/** 登录进行中的 Promise(防止并发重复调用) */
let loginPromise: Promise<UserInfo> | null = null;
function syncAppGlobalUser(user: UserInfo | null): void {
try {
const app = getApp<IAppOption>();
if (app?.globalData) {
app.globalData.user = user;
}
} catch {
// App 尚未初始化时忽略
}
}
function syncUserState(user: UserInfo, persist = true): UserInfo {
currentUser = user;
if (persist) {
saveToStorage(user);
}
syncAppGlobalUser(user);
return user;
}
/**
* 获取当前用户,优先内存 → Storage → 云函数登录
*/
/** 用户摘要与新后端认证绑定,所有远端用户业务只使用本站用户 ID。 */
export async function getUser(): Promise<UserInfo> {
// 1. 内存中有,直接返回
if (currentUser) {
syncAppGlobalUser(currentUser);
return currentUser;
}
// 2. 尝试从 Storage 读取
const cached = loadFromStorage();
if (cached) {
return syncUserState(cached, false);
}
// 3. 静默登录(防止并发)
if (!loginPromise) {
loginPromise = silentLogin().finally(() => {
loginPromise = null;
});
}
return loginPromise;
return (await ensureSession()).user;
}
/**
* 静默登录:调用云函数
*/
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;
return syncUserState(user);
}
/** 缓存只供展示,不能证明服务端仍认可当前凭证。 */
export function getCachedUser(): UserInfo | null {
if (currentUser) {
syncAppGlobalUser(currentUser);
return currentUser;
}
const cached = loadFromStorage();
if (!cached) return null;
return syncUserState(cached, false);
}
/**
* 从 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;
return cachedServerUser();
} 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;
syncAppGlobalUser(null);
wx.removeStorageSync(STORAGE_KEY);
wx.removeStorageSync(STORAGE_EXPIRE_KEY);
}
/**
* 强制刷新用户数据(跳过缓存)
*/
/** 读取本人资料;GET /me 会按统一规则完成过期与 401 恢复。 */
export async function refreshUser(): Promise<UserInfo> {
clearUser();
return await silentLogin();
const context = getAuthContext();
const user = await apiRequest<UserInfo>('/me');
if (getAuthContext().key !== context.key)
throw new Error('接口环境已变化,请重新操作');
return updateSessionUser(user, context);
}
/** 暂停账户自动恢复时保留标记,不能通过清缓存自动注册。 */
export function clearUser(blocked = false): void {
clearSession(blocked);
}
/** 仅供用户主动确认“重新开始使用账户”的动作调用,禁止在失败回调自动调用。 */
export async function startAccount(): Promise<UserInfo> {
return (await ensureSession(getAuthContext(), true, true)).user;
}
/** 本人资料更新只提交头像资产 ID,不允许传任意外链或用户身份字段。 */
export async function updateUserProfile(payload: {
nickName?: string | null;
avatarUrl?: string | null;
nickName?: string;
avatarAssetId?: string | null;
}): Promise<UserInfo> {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userLogin',
data: {
action: 'updateProfile',
...payload,
},
const context = getAuthContext();
const user = await apiRequest<UserInfo>('/me/profile', {
method: 'PUT',
data: payload,
});
const result = (res?.result as any) || {};
if (result.code !== 0 || !result.data) {
throw new Error(result.message || '更新用户资料失败');
}
return syncUserState(result.data as UserInfo);
if (getAuthContext().key !== context.key)
throw new Error('接口环境已变化,请重新操作');
return updateSessionUser(user, context);
}
export type { UserInfo };
+5 -7
View File
@@ -1,7 +1,5 @@
import type {
WorksheetCloudCategory,
WorksheetStatus,
} from '../core/models/worksheet';
import type { CategoryId } from '../core/models/category';
import type { WorksheetConfig } from '../core/models/worksheet';
/**
* 调试发布时的裁剪模式。
@@ -19,7 +17,7 @@ export interface DebugPublishMeta {
id: string;
title: string;
subtitle: string;
category: WorksheetCloudCategory;
category: CategoryId;
subcategory?: string;
path: string;
ageMin: number;
@@ -31,7 +29,7 @@ export interface DebugPublishMeta {
isNew: boolean;
isHot: boolean;
sortOrder?: number;
status: WorksheetStatus;
status: WorksheetConfig['status'];
}
/** 发布弹窗里可调的图片处理参数。 */
@@ -118,7 +116,7 @@ export function getCropRatios(mode: DebugCropMode): {
/** 统一构造预览图在云存储中的路径。 */
export function buildWorksheetPreviewCloudPath(
category: WorksheetCloudCategory,
category: CategoryId,
id: string,
): string {
return `assets/previews/${category}/${id}.jpg`;
+27 -48
View File
@@ -1,10 +1,16 @@
import { getUser } from './auth';
import { apiRequest } from '../services/api-request';
import {
legacyWorksheet,
memberStats,
type MemberPage,
} from '../services/member-api';
import { generateUUID } from './uuid';
export interface DownloadLogRecord {
_id: string;
userId: string;
worksheetId: string;
exportId?: string;
exportId?: string | null;
createdAt: string;
worksheet?: {
_id: string;
@@ -20,66 +26,39 @@ export interface DownloadLogRecord {
} | null;
}
/**
* 记录下载日志
*/
/** 一次成功保存沿用同一个 exportId,服务端保证记录及下载计数幂等。 */
export async function addDownloadLog(
worksheetId: string,
exportId?: string,
exportId = generateUUID(),
expectedUserId?: string,
): Promise<boolean> {
if (!worksheetId) return false;
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userDownloadLogs',
data: { action: 'add', worksheetId, exportId },
const user = await getUser();
if (expectedUserId && user.id !== expectedUserId) return false;
await apiRequest('/me/downloads', {
method: 'POST',
data: { worksheetId, exportId },
});
const result = (res?.result as any) || {};
return result.code === 0;
} catch (e) {
console.error('addDownloadLog failed', e);
return true;
} catch {
return false;
}
}
/** 收藏页下载历史展示条数上限 */
export const DOWNLOAD_HISTORY_MAX = 30;
/**
* 获取下载历史(默认最近 30 条)
*/
export async function getDownloadLogs(
page = 1,
pageSize = DOWNLOAD_HISTORY_MAX,
): 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 [];
}
const result = await apiRequest<MemberPage>('/me/downloads', {
data: { page, pageSize },
});
return result.items.map((row) => ({
...row,
_id: row.id,
worksheet: legacyWorksheet(row.worksheet),
}));
}
/**
* 获取当前用户下载(打印)总数
*/
export async function getDownloadLogCount(): Promise<number> {
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userDownloadLogs',
data: { action: 'count' },
});
const result = (res?.result as any) || {};
return result.code === 0 ? Number(result.data?.count || 0) : 0;
} catch (e) {
console.error('getDownloadLogCount failed', e);
return 0;
}
return (await memberStats()).downloadCount;
}
+10 -2
View File
@@ -1,3 +1,4 @@
import { getCachedUser } from './auth';
/**
* 下载打印通用工具
* 支持下载次数限制和激励广告
@@ -67,7 +68,9 @@ function showVideoAd(): Promise<boolean> {
mask: true,
});
const onClose: WechatMiniprogram.RewardedVideoAdOnCloseCallback = (res) => {
const onClose: WechatMiniprogram.RewardedVideoAdOnCloseCallback = (
res,
) => {
videoAd.offClose(onClose);
wx.hideLoading();
resolve(!!(res && res.isEnded));
@@ -137,6 +140,7 @@ function doDownload(
},
): Promise<boolean> {
const exportId = generateUUID();
const expectedUserId = getCachedUser()?.id;
const trackerPage = options.trackerPage || options.trackerName || '';
const trackerParams = {
export_id: exportId,
@@ -154,7 +158,11 @@ function doDownload(
recordDownloadSuccess();
tracker.reportExportResult(trackerPage, true, trackerParams);
if (options.worksheetId) {
await addDownloadLog(options.worksheetId, exportId);
await addDownloadLog(
options.worksheetId,
exportId,
expectedUserId,
);
}
} else {
tracker.reportExportResult(trackerPage, false, trackerParams);
+38 -102
View File
@@ -1,14 +1,13 @@
import { getUser } from './auth';
import { apiRequest } from '../services/api-request';
import {
legacyWorksheet,
memberStats,
type MemberPage,
} from '../services/member-api';
import tracker from './tracker';
function getCurrentPageRoute(): string {
const pages = getCurrentPages();
return pages[pages.length - 1]?.route || '';
}
export interface FavoriteRecord {
_id: string;
userId: string;
worksheetId: string;
createdAt: string;
worksheet?: {
@@ -26,122 +25,59 @@ export interface FavoriteRecord {
} | null;
}
/**
* 添加收藏
*/
/** 收藏操作使用新后端当前账户;调用失败不能向其他用户补发。 */
export async function addFavorite(worksheetId: string): Promise<boolean> {
try {
await getUser(); // 确保已登录
const res = await wx.cloud.callFunction({
name: 'userFavorites',
data: { action: 'add', worksheetId },
await apiRequest(`/me/favorites/${encodeURIComponent(worksheetId)}`, {
method: 'PUT',
});
const result = (res?.result as any) || {};
const success = result.code === 0;
if (success) tracker.reportFavorite(getCurrentPageRoute(), worksheetId, true);
return success;
} catch (e) {
console.error('addFavorite failed', e);
tracker.reportFavorite(currentRoute(), worksheetId, true);
return true;
} catch {
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 },
await apiRequest(`/me/favorites/${encodeURIComponent(worksheetId)}`, {
method: 'DELETE',
});
const result = (res?.result as any) || {};
const success = result.code === 0;
if (success) {
tracker.reportFavorite(getCurrentPageRoute(), worksheetId, false);
}
return success;
} catch (e) {
console.error('removeFavorite failed', e);
tracker.reportFavorite(currentRoute(), worksheetId, false);
return true;
} catch {
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 [];
}
const result = await apiRequest<MemberPage>('/me/favorites', {
data: { page, pageSize },
});
return result.items.map((row) => ({
...row,
_id: row.id,
worksheet: legacyWorksheet(row.worksheet),
}));
}
/**
* 获取当前用户收藏总数
*/
export async function getFavoriteCount(): Promise<number> {
try {
await getUser();
const res = await wx.cloud.callFunction({
name: 'userFavorites',
data: { action: 'count' },
});
const result = (res?.result as any) || {};
return result.code === 0 ? Number(result.data?.count || 0) : 0;
} catch (e) {
console.error('getFavoriteCount failed', e);
return 0;
}
return (await memberStats()).favoriteCount;
}
/**
* 检查是否已收藏
*/
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 {};
}
return apiRequest('/me/favorites/check', {
method: 'POST',
data: { worksheetIds },
});
}
export async function checkFavorited(worksheetId: string): Promise<boolean> {
return (await batchCheckFavorited([worksheetId]))[worksheetId] === true;
}
function currentRoute() {
const pages = getCurrentPages();
return pages[pages.length - 1]?.route || '';
}
-31
View File
@@ -1,31 +0,0 @@
type WorksheetStatsField = 'likes' | 'downloads';
export async function updateWorksheetStats(
id: string,
field: WorksheetStatsField,
): Promise<boolean> {
const worksheetId = String(id || '').trim();
if (!worksheetId) return false;
try {
const { result } = await wx.cloud.callFunction({
name: 'worksheetsStatsUpdate',
data: {
id: worksheetId,
field,
},
});
return !!(result as { success?: boolean } | undefined)?.success;
} catch (error) {
console.error('update worksheet stats failed', field, worksheetId, error);
return false;
}
}
export function incrementWorksheetLikes(id: string): Promise<boolean> {
return updateWorksheetStats(id, 'likes');
}
export function incrementWorksheetDownloads(id: string): Promise<boolean> {
return updateWorksheetStats(id, 'downloads');
}