feat(account): add client account api foundation

This commit is contained in:
2026-05-18 22:02:39 +08:00
parent 960bababc5
commit 881aceb78c
6 changed files with 562 additions and 1 deletions
+336
View File
@@ -0,0 +1,336 @@
import axios from "axios";
import { SPARK_BACKEND_BASE_URL } from "@/global/storeConfig";
import type {
AppReview,
AuthSession,
DownloadedAppList,
DownloadedAppRecord,
FavoriteFolder,
FavoriteItem,
RatingSummary,
ReviewTags,
SparkUser,
SyncedAppList,
SyncedAppListItem,
} from "@/global/typedefinition";
const backend = axios.create({
baseURL: SPARK_BACKEND_BASE_URL,
timeout: 10000,
});
type ApiRecord = Record<string, unknown>;
const asApiRecord = (value: unknown): ApiRecord => {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as ApiRecord;
}
return {};
};
const asApiRecordArray = (value: unknown): ApiRecord[] => {
if (!Array.isArray(value)) return [];
return value.map(asApiRecord);
};
export const parseForumGroups = (raw: unknown): string[] => {
if (Array.isArray(raw)) {
return raw.filter((item): item is string => typeof item === "string");
}
if (typeof raw !== "string" || raw.length === 0) return [];
try {
const parsed: unknown = JSON.parse(raw);
return Array.isArray(parsed)
? parsed.filter((item): item is string => typeof item === "string")
: [];
} catch {
return [];
}
};
const toUser = (raw: ApiRecord): SparkUser => ({
id: Number(raw.id),
flarumUserId: String(raw.flarum_user_id || ""),
username: String(raw.username || ""),
displayName: String(raw.display_name || raw.username || ""),
avatarUrl: String(raw.avatar_url || ""),
forumLevel: String(raw.forum_level || "论坛用户"),
forumGroups: parseForumGroups(raw.forum_groups),
});
const toReview = (raw: ApiRecord): AppReview => ({
id: Number(raw.id),
rating: Number(raw.rating),
content: String(raw.content || ""),
version: String(raw.version || "unknown"),
packageArch: String(raw.package_arch || "unknown"),
clientArch: String(raw.client_arch || "unknown"),
distro: String(raw.distro || "unknown"),
origin: raw.origin === "spark" ? "spark" : "apm",
category: String(raw.category || ""),
createdAt: String(raw.created_at || ""),
updatedAt: String(raw.updated_at || ""),
userDisplayName: String(raw.user_display_name || ""),
userAvatarUrl: String(raw.user_avatar_url || ""),
});
const toFavoriteFolder = (raw: ApiRecord): FavoriteFolder => ({
id: Number(raw.id),
name: String(raw.name || ""),
itemCount: Number(raw.item_count || 0),
createdAt: String(raw.created_at || ""),
updatedAt: String(raw.updated_at || ""),
});
const toFavoriteItem = (raw: ApiRecord): FavoriteItem => ({
id: Number(raw.id),
appKey: String(raw.app_key || ""),
pkgname: String(raw.pkgname || ""),
name: String(raw.name || ""),
category: String(raw.category || ""),
iconUrl: String(raw.icon_url || ""),
createdAt: String(raw.created_at || ""),
});
const toDownloadedApp = (raw: ApiRecord): DownloadedAppRecord => ({
id: Number(raw.id),
appKey: String(raw.app_key || ""),
pkgname: String(raw.pkgname || ""),
name: String(raw.name || ""),
category: String(raw.category || ""),
selectedOrigin: raw.selected_origin === "spark" ? "spark" : "apm",
version: String(raw.version || ""),
packageArch: String(raw.package_arch || "unknown"),
downloadedAt: String(raw.downloaded_at || ""),
});
const toSyncedAppListItem = (raw: ApiRecord): SyncedAppListItem => ({
id: raw.id === undefined ? undefined : Number(raw.id),
pkgname: String(raw.pkgname || ""),
origin: raw.origin === "spark" ? "spark" : "apm",
category: String(raw.category || ""),
version: String(raw.version || ""),
packageArch: String(raw.package_arch || "unknown"),
appName: String(raw.app_name || ""),
iconUrl: String(raw.icon_url || ""),
});
const toSyncedAppList = (
raw: ApiRecord,
fallback?: { clientArch: string; distro: string; items: SyncedAppListItem[] },
): SyncedAppList => ({
snapshotName: String(raw.snapshot_name || "默认列表"),
clientArch: String(raw.client_arch || fallback?.clientArch || "unknown"),
distro: String(raw.distro || fallback?.distro || "unknown"),
updatedAt: String(raw.updated_at || ""),
items: raw.items
? asApiRecordArray(raw.items).map(toSyncedAppListItem)
: fallback?.items || [],
});
export const setBackendToken = (token: string | null): void => {
if (token) backend.defaults.headers.common.Authorization = `Bearer ${token}`;
else delete backend.defaults.headers.common.Authorization;
};
export const exchangeFlarumToken = async (payload: {
flarumUserId: string;
flarumToken: string;
}): Promise<AuthSession> => {
const response = await backend.post("/auth/flarum", {
flarum_user_id: payload.flarumUserId,
flarum_token: payload.flarumToken,
});
const data = asApiRecord(response.data);
return {
accessToken: String(data.access_token || ""),
tokenType: "bearer",
user: toUser(asApiRecord(data.user)),
};
};
export const fetchMe = async (): Promise<SparkUser> => {
const response = await backend.get("/me");
return toUser(asApiRecord(response.data));
};
export const fetchRatingSummary = async (
appKey: string,
): Promise<RatingSummary> => {
const response = await backend.get(
`/apps/${encodeURIComponent(appKey)}/rating-summary`,
);
const data = asApiRecord(response.data);
return {
averageRating: Number(data.average_rating || 0),
reviewCount: Number(data.review_count || 0),
starCounts: Object.fromEntries(
Object.entries(asApiRecord(data.star_counts)).map(([key, value]) => [
Number(key),
Number(value),
]),
),
};
};
export const fetchReviews = async (appKey: string): Promise<AppReview[]> => {
const response = await backend.get(
`/apps/${encodeURIComponent(appKey)}/reviews`,
);
return asApiRecordArray(response.data).map(toReview);
};
export const submitReview = async (
appKey: string,
payload: { rating: number; content: string; tags: ReviewTags },
): Promise<AppReview> => {
const response = await backend.post(
`/apps/${encodeURIComponent(appKey)}/reviews`,
{
rating: payload.rating,
content: payload.content,
tags: {
origin: payload.tags.origin,
category: payload.tags.category,
pkgname: payload.tags.pkgname,
version: payload.tags.version,
package_arch: payload.tags.packageArch,
client_arch: payload.tags.clientArch,
distro: payload.tags.distro,
},
},
);
return toReview(asApiRecord(response.data));
};
export const listFavoriteFolders = async (): Promise<FavoriteFolder[]> => {
const response = await backend.get("/me/favorite-folders");
return asApiRecordArray(response.data).map(toFavoriteFolder);
};
export const createFavoriteFolder = async (
name: string,
): Promise<FavoriteFolder> => {
const response = await backend.post("/me/favorite-folders", { name });
return toFavoriteFolder(asApiRecord(response.data));
};
export const renameFavoriteFolder = async (
folderId: number,
name: string,
): Promise<FavoriteFolder> => {
const response = await backend.patch(`/me/favorite-folders/${folderId}`, {
name,
});
return toFavoriteFolder(asApiRecord(response.data));
};
export const deleteFavoriteFolder = async (folderId: number): Promise<void> => {
await backend.delete(`/me/favorite-folders/${folderId}`);
};
export const listFavoriteItems = async (
folderId: number,
): Promise<FavoriteItem[]> => {
const response = await backend.get(`/me/favorite-folders/${folderId}/items`);
return asApiRecordArray(response.data).map(toFavoriteItem);
};
export const addFavoriteItem = async (
folderId: number | "default",
item: Omit<FavoriteItem, "id" | "createdAt">,
): Promise<FavoriteItem> => {
const response = await backend.post(
`/me/favorite-folders/${folderId}/items`,
{
app_key: item.appKey,
pkgname: item.pkgname,
name: item.name,
category: item.category,
icon_url: item.iconUrl,
},
);
return toFavoriteItem(asApiRecord(response.data));
};
export const deleteFavoriteItem = async (
folderId: number,
itemId: number,
): Promise<void> => {
await backend.delete(`/me/favorite-folders/${folderId}/items/${itemId}`);
};
export const bulkDeleteFavoriteItems = async (
folderId: number,
itemIds: number[],
): Promise<number> => {
const response = await backend.post(
`/me/favorite-folders/${folderId}/items/bulk-delete`,
{ item_ids: itemIds },
);
return Number(asApiRecord(response.data).deleted_count || 0);
};
export const listDownloadedApps = async (
page = 1,
pageSize = 20,
): Promise<DownloadedAppList> => {
const response = await backend.get("/me/downloaded-apps", {
params: { page, page_size: pageSize },
});
const data = asApiRecord(response.data);
return {
items: asApiRecordArray(data.items).map(toDownloadedApp),
total: Number(data.total || 0),
page: Number(data.page || page),
pageSize: Number(data.page_size || pageSize),
};
};
export const recordDownloadedApp = async (
item: Omit<DownloadedAppRecord, "id" | "downloadedAt">,
): Promise<DownloadedAppRecord> => {
const response = await backend.post("/me/downloaded-apps", {
app_key: item.appKey,
pkgname: item.pkgname,
name: item.name,
category: item.category,
selected_origin: item.selectedOrigin,
version: item.version,
package_arch: item.packageArch,
});
return toDownloadedApp(asApiRecord(response.data));
};
export const fetchSyncedAppList = async (): Promise<SyncedAppList | null> => {
const response = await backend.get("/me/app-list");
if (!response.data) return null;
return toSyncedAppList(asApiRecord(response.data));
};
export const uploadSyncedAppList = async (payload: {
clientArch: string;
distro: string;
items: SyncedAppListItem[];
}): Promise<SyncedAppList> => {
const response = await backend.put("/me/app-list", {
client_arch: payload.clientArch,
distro: payload.distro,
items: payload.items.map((item) => ({
pkgname: item.pkgname,
origin: item.origin,
category: item.category,
version: item.version,
package_arch: item.packageArch,
app_name: item.appName,
icon_url: item.iconUrl,
})),
});
return toSyncedAppList(asApiRecord(response.data), payload);
};