fix(account): gate reviews and stale refreshes

This commit is contained in:
2026-05-19 08:16:57 +08:00
parent 341c740ced
commit a8a00d8165
6 changed files with 175 additions and 9 deletions
+13 -3
View File
@@ -1464,6 +1464,16 @@ const clearRestoreState = () => {
showRestoreModal.value = false;
};
const nextFavoriteRequestGeneration = (): number => {
favoriteRequestGeneration.value += 1;
return favoriteRequestGeneration.value;
};
const nextDownloadedRequestGeneration = (): number => {
downloadedRequestGeneration.value += 1;
return downloadedRequestGeneration.value;
};
const isCurrentFavoriteRequest = (generation: number): boolean =>
favoriteRequestGeneration.value === generation && isLoggedIn.value;
@@ -1522,7 +1532,7 @@ const loadDownloadedHistory = async (): Promise<void> => {
if (!requireLogin("请登录后查看和管理账号信息。")) return;
const userId = currentUser.value?.id;
if (userId === undefined) return;
const generation = downloadedRequestGeneration.value;
const generation = nextDownloadedRequestGeneration();
downloadedLoading.value = true;
downloadedError.value = "";
@@ -1741,7 +1751,7 @@ const loadActiveFavoriteItems = async (
};
const refreshFavorites = async (): Promise<void> => {
const generation = favoriteRequestGeneration.value;
const generation = nextFavoriteRequestGeneration();
favoriteLoading.value = true;
favoriteError.value = "";
try {
@@ -1806,7 +1816,7 @@ const openFavoriteManagement = async () => {
};
const selectFavoriteFolder = async (folderId: number) => {
const generation = favoriteRequestGeneration.value;
const generation = nextFavoriteRequestGeneration();
activeFavoriteFolderId.value = folderId;
favoriteLoading.value = true;
favoriteError.value = "";
@@ -462,6 +462,108 @@ describe("App account placeholders", () => {
expect(screen.getByText("暂无下载记录。")).toBeTruthy();
});
it("ignores older downloaded history refreshes for the same user", async () => {
const firstHistory = createDeferred<DownloadedAppList>();
const secondHistory = createDeferred<DownloadedAppList>();
vi.mocked(listDownloadedApps)
.mockReturnValueOnce(firstHistory.promise)
.mockReturnValueOnce(secondHistory.promise);
render(App);
await fireEvent.click(await screen.findByRole("button", { name: /Momen/ }));
await fireEvent.click(screen.getByText("用户管理"));
await fireEvent.click(screen.getByRole("button", { name: "刷新" }));
secondHistory.resolve(
downloadedList([
{
id: 88,
appKey: "app:office:new-app",
pkgname: "new-app",
name: "新下载应用",
category: "office",
selectedOrigin: "apm",
version: "2.0.0",
packageArch: "amd64",
downloadedAt: "2026-05-18T00:00:00Z",
},
]),
);
expect(await screen.findByText("新下载应用")).toBeTruthy();
firstHistory.resolve(
downloadedList([
{
id: 77,
appKey: "app:office:old-app",
pkgname: "old-app",
name: "旧下载应用",
category: "office",
selectedOrigin: "apm",
version: "1.0.0",
packageArch: "amd64",
downloadedAt: "2026-05-18T00:00:00Z",
},
]),
);
await firstHistory.promise;
await Promise.resolve();
await Promise.resolve();
expect(screen.queryByText("旧下载应用")).toBeNull();
expect(screen.getByText("新下载应用")).toBeTruthy();
});
it("ignores older favorite folder refreshes for the same user", async () => {
const firstFolders = createDeferred<FavoriteFolder[]>();
const secondFolders = createDeferred<FavoriteFolder[]>();
vi.mocked(listFavoriteFolders)
.mockReturnValueOnce(firstFolders.promise)
.mockReturnValueOnce(secondFolders.promise);
render(App);
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
await fireEvent.click(screen.getByRole("button", { name: "我的收藏" }));
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
if (!screen.queryByRole("button", { name: "我的收藏" })) {
await fireEvent.click(
await screen.findByRole("button", { name: /^Momen$/ }),
);
}
await fireEvent.click(screen.getByRole("button", { name: "我的收藏" }));
secondFolders.resolve([
{
id: 42,
name: "新收藏夹",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
]);
expect(await screen.findByText("新收藏夹 (0)")).toBeTruthy();
firstFolders.resolve([
{
id: 41,
name: "旧收藏夹",
itemCount: 0,
createdAt: "2026-05-18T00:00:00Z",
updatedAt: "2026-05-18T00:00:00Z",
},
]);
await firstFolders.promise;
await Promise.resolve();
await Promise.resolve();
expect(screen.queryByText("旧收藏夹 (0)")).toBeNull();
expect(screen.getByText("新收藏夹 (0)")).toBeTruthy();
});
it("restores cloud apps by origin and package when category changed", async () => {
vi.mocked(fetchSyncedAppList).mockResolvedValueOnce({
snapshotName: "默认列表",
+23 -1
View File
@@ -91,6 +91,28 @@ describe("AppDetailPage", () => {
);
});
it("gates reviews for anonymous users", async () => {
const rendered = render(AppDetailPage, {
props: {
app,
screenshots: [],
sparkInstalled: false,
apmInstalled: false,
loggedIn: false,
reviewAppKey: "apm:amd64-apm:office:wps",
reviewTags: sparkTags,
},
});
expect(screen.queryByTestId("reviews-panel")).toBeNull();
await fireEvent.click(
screen.getByRole("button", { name: "登录后查看评价" }),
);
expect(rendered.emitted("request-login")?.[0]?.[0]).toBe(
"登录后查看和发表评论。",
);
});
it("updates review identity when switching a merged app origin", async () => {
render(AppDetailPage, {
props: {
@@ -98,7 +120,7 @@ describe("AppDetailPage", () => {
screenshots: [],
sparkInstalled: false,
apmInstalled: false,
loggedIn: false,
loggedIn: true,
reviewAppKey: "spark:amd64-store:office:wps",
reviewTags: sparkTags,
},
+4 -2
View File
@@ -52,6 +52,8 @@ describe("ReviewsPanel", () => {
expect(screen.getByText("登录后发表评论")).toBeTruthy();
expect(screen.getByText("1.0.0")).toBeTruthy();
expect(screen.getByText("deepin 25")).toBeTruthy();
expect(fetchRatingSummary).not.toHaveBeenCalled();
expect(fetchReviews).not.toHaveBeenCalled();
});
it("ignores stale review responses after app key changes", async () => {
@@ -84,10 +86,10 @@ describe("ReviewsPanel", () => {
);
const rendered = render(ReviewsPanel, {
props: { appKey: "first", tags, loggedIn: false },
props: { appKey: "first", tags, loggedIn: true },
});
await rendered.rerender({ appKey: "second", tags, loggedIn: false });
await rendered.rerender({ appKey: "second", tags, loggedIn: true });
resolveSecondSummary({ averageRating: 5, reviewCount: 1, starCounts: {} });
resolveSecondReviews([
+20 -1
View File
@@ -196,12 +196,31 @@
</div>
<ReviewsPanel
v-if="reviewAppKey && reviewTags"
v-if="loggedIn && reviewAppKey && reviewTags"
:app-key="reviewAppKey"
:tags="reviewTags"
:logged-in="loggedIn"
@request-login="$emit('request-login', $event)"
/>
<section
v-else-if="reviewAppKey && reviewTags"
class="rounded-2xl border border-slate-200/60 bg-slate-50/50 p-5 dark:border-slate-800/60 dark:bg-slate-800/30"
>
<h2 class="mb-2 flex items-center gap-2 text-base font-semibold">
<i class="fas fa-comments text-slate-400"></i>
应用评价
</h2>
<p class="text-sm text-slate-500 dark:text-slate-400">
登录星火账号后可查看评价并发表评论
</p>
<button
type="button"
class="mt-4 inline-flex items-center rounded-xl bg-slate-800 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600"
@click="emit('request-login', '登录后查看和发表评论。')"
>
登录后查看评价
</button>
</section>
</div>
</div>
</section>
+13 -2
View File
@@ -152,8 +152,19 @@ const ratingText = computed(() => {
return `${summary.value.averageRating.toFixed(1)} / 5 (${summary.value.reviewCount})`;
});
const clearReviewState = () => {
loadGeneration.value += 1;
reviews.value = [];
summary.value = null;
loading.value = false;
error.value = "";
};
const loadReviews = async () => {
if (!props.appKey) return;
if (!props.loggedIn || !props.appKey) {
clearReviewState();
return;
}
const generation = loadGeneration.value + 1;
loadGeneration.value = generation;
const appKey = props.appKey;
@@ -202,5 +213,5 @@ const submit = async () => {
};
onMounted(loadReviews);
watch(() => props.appKey, loadReviews);
watch(() => [props.appKey, props.loggedIn], loadReviews);
</script>