From cc73570396cb6ef7f33f03bc7746536b57f8f249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=80=82?= Date: Fri, 3 Jul 2026 16:30:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(product):=20=E4=BB=A5=20AppData=20=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E6=A0=91=E4=BD=9C=E4=B8=BA=E5=B1=95=E7=A4=BA=E4=B8=BB?= =?UTF-8?q?=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/modules/product/product.service.ts | 63 ++++++++++++- apps/web/stores/useProductStore.ts | 89 ++++++++++--------- 2 files changed, 109 insertions(+), 43 deletions(-) diff --git a/apps/server/src/modules/product/product.service.ts b/apps/server/src/modules/product/product.service.ts index 1a462d6..45159a9 100644 --- a/apps/server/src/modules/product/product.service.ts +++ b/apps/server/src/modules/product/product.service.ts @@ -3,6 +3,17 @@ import { PrismaService } from '../../prisma/prisma.service'; import { CreateProductDto } from './dto/create-product.dto'; import { UpdateProductDto } from './dto/update-product.dto'; +type ProductOverviewItem = { + id: string; + name: string; + description?: string; + createdAt?: string; + updatedAt?: string; + projects?: { id: string; name: string; description?: string; createdAt?: string }[]; + versions?: { id: string; name: string; releaseDate?: string | null; createdAt?: string }[]; + _count?: { requirements?: number; projects?: number; versions?: number }; +}; + @Injectable() export class ProductService { constructor(private prisma: PrismaService) {} @@ -11,7 +22,16 @@ export class ProductService { return this.prisma.product.create({ data: dto }); } - findAll() { + async findAll() { + const overview = await this.getAppDataOverview(); + if (overview) { + return overview.map((product) => { + const normalized = this.normalizeOverviewProduct(product); + const { projects: _projects, versions: _versions, ...rest } = normalized; + return rest; + }); + } + return this.prisma.product.findMany({ orderBy: { createdAt: 'desc' }, include: { @@ -20,7 +40,12 @@ export class ProductService { }); } - findAllWithChildren() { + async findAllWithChildren() { + const overview = await this.getAppDataOverview(); + if (overview) { + return overview.map((product) => this.normalizeOverviewProduct(product)); + } + return this.prisma.product.findMany({ orderBy: { createdAt: 'desc' }, include: { @@ -38,6 +63,16 @@ export class ProductService { } async findOne(id: string) { + const overview = await this.getAppDataOverview(); + if (overview) { + const product = overview.find((item) => item.id === id); + if (!product) throw new NotFoundException('产品不存在'); + return { + ...this.normalizeOverviewProduct(product), + requirements: [], + }; + } + const product = await this.prisma.product.findUnique({ where: { id }, include: { @@ -64,4 +99,28 @@ export class ProductService { const exists = await this.prisma.product.findUnique({ where: { id } }); if (!exists) throw new NotFoundException('产品不存在'); } + + private async getAppDataOverview(): Promise { + const row = await this.prisma.appData.findUnique({ + where: { key: 'products-overview' }, + }); + if (!Array.isArray(row?.value)) return null; + return row.value as unknown as ProductOverviewItem[]; + } + + private normalizeOverviewProduct(product: ProductOverviewItem) { + const projects = product.projects ?? []; + const versions = product.versions ?? []; + return { + ...product, + description: product.description ?? '', + projects, + versions, + _count: { + requirements: product._count?.requirements ?? 0, + projects: projects.length, + versions: versions.length, + }, + }; + } } diff --git a/apps/web/stores/useProductStore.ts b/apps/web/stores/useProductStore.ts index 8bf472a..fc2f086 100644 --- a/apps/web/stores/useProductStore.ts +++ b/apps/web/stores/useProductStore.ts @@ -64,14 +64,14 @@ interface ProductState { createProduct: (data: { name: string; description?: string }) => Promise; updateProduct: (id: string, data: { name?: string; description?: string }) => Promise; deleteProduct: (id: string) => Promise; - reorderProducts: (ids: string[]) => void; - migrateAndDeleteProduct: (sourceId: string, targetId: string) => void; - createProject: (productId: string, data: { name: string; description?: string }) => void; - updateProject: (productId: string, projectId: string, data: { name?: string; description?: string }) => void; - createVersion: (productId: string, data: { name: string; status: string }) => void; - updateVersion: (productId: string, versionId: string, data: Partial) => void; - deleteVersion: (productId: string, versionId: string) => void; - deleteProject: (productId: string, projectId: string) => void; + reorderProducts: (ids: string[]) => Promise; + migrateAndDeleteProduct: (sourceId: string, targetId: string) => Promise; + createProject: (productId: string, data: { name: string; description?: string }) => Promise; + updateProject: (productId: string, projectId: string, data: { name?: string; description?: string }) => Promise; + createVersion: (productId: string, data: { name: string; status: string }) => Promise; + updateVersion: (productId: string, versionId: string, data: Partial) => Promise; + deleteVersion: (productId: string, versionId: string) => Promise; + deleteProject: (productId: string, projectId: string) => Promise; } export const useProductStore = create((set, get) => ({ @@ -85,7 +85,10 @@ export const useProductStore = create((set, get) => ({ fetchProducts: async () => { set({ loading: true, error: null }); try { - const products = await api.get('/products'); + const overview = await loadStoredOverview(); + const products = overview + ? overview.map(({ projects: _projects, versions: _versions, ...product }) => product) + : await api.get('/products'); set({ products, loading: false }); } catch { set({ products: [], error: null, loading: false }); @@ -117,6 +120,12 @@ export const useProductStore = create((set, get) => ({ fetchProduct: async (id) => { set({ loading: true, error: null }); try { + const overview = await loadStoredOverview(); + const found = overview?.find((p) => p.id === id); + if (found) { + set({ currentProduct: found as any, loading: false }); + return; + } const product = await api.get(`/products/${id}`); set({ currentProduct: product, loading: false }); } catch { @@ -130,7 +139,6 @@ export const useProductStore = create((set, get) => ({ }, createProduct: async (data) => { - // 直接本地新增(mock 模式核心,保留远端兼容但不依赖) const newProduct: ProductOverview = { id: `local-${Date.now()}`, name: data.name || '', @@ -143,43 +151,42 @@ export const useProductStore = create((set, get) => ({ }; const updated = [newProduct, ...get().overview]; set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); - // 异步尝试同步远端(失败也无所谓,本地已更新) - try { await api.post('/products', data); } catch {} + await saveStoredOverview(updated); }, updateProduct: async (id, data) => { - try { - await api.patch(`/products/${id}`, data); - } catch {} const updated = get().overview.map((p) => - p.id === id ? { ...p, ...data } : p, + p.id === id ? { ...p, ...data, updatedAt: new Date().toISOString() } : p, ); - set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); + const currentProduct = get().currentProduct; + set({ + overview: updated, + overviewLoaded: true, + currentProduct: + currentProduct?.id === id ? ({ ...currentProduct, ...data } as any) : currentProduct, + }); + await saveStoredOverview(updated); }, deleteProduct: async (id) => { - try { - await api.delete(`/products/${id}`); - } catch {} const updated = get().overview.filter((p) => p.id !== id); set({ products: get().products.filter((p) => p.id !== id), overview: updated, overviewLoaded: true, + currentProduct: get().currentProduct?.id === id ? null : get().currentProduct, }); - saveStoredOverview(updated); + await saveStoredOverview(updated); }, - reorderProducts: (ids) => { + reorderProducts: async (ids) => { const map = new Map(get().overview.map((p) => [p.id, p])); const reordered = ids.map((id) => map.get(id)).filter(Boolean) as ProductOverview[]; set({ overview: reordered, overviewLoaded: true }); - saveStoredOverview(reordered); + await saveStoredOverview(reordered); }, - migrateAndDeleteProduct: (sourceId, targetId) => { + migrateAndDeleteProduct: async (sourceId, targetId) => { const source = get().overview.find((p) => p.id === sourceId); const target = get().overview.find((p) => p.id === targetId); if (!source || !target) return; @@ -199,10 +206,10 @@ export const useProductStore = create((set, get) => ({ .filter((p) => p.id !== sourceId) .map((p) => (p.id === targetId ? migratedTarget : p)); set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); + await saveStoredOverview(updated); }, - createProject: (productId, data) => { + createProject: async (productId, data) => { const newProject = { id: `proj-${Date.now()}`, name: data.name || '', @@ -218,10 +225,10 @@ export const useProductStore = create((set, get) => ({ }; }); set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); + await saveStoredOverview(updated); }, - updateProject: (productId, projectId, data) => { + updateProject: async (productId, projectId, data) => { const updated = get().overview.map((p) => { if (p.id !== productId) return p; const currentProject = p.projects.find((proj) => proj.id === projectId); @@ -255,10 +262,10 @@ export const useProductStore = create((set, get) => ({ }; }); set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); + await saveStoredOverview(updated); }, - createVersion: (productId, data) => { + createVersion: async (productId, data) => { const newVersion: VersionItem = { id: `ver-${Date.now()}`, name: data.name, @@ -276,10 +283,10 @@ export const useProductStore = create((set, get) => ({ }; }); set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); + await saveStoredOverview(updated); }, - updateVersion: (productId, versionId, data) => { + updateVersion: async (productId, versionId, data) => { const updated = get().overview.map((p) => { if (p.id !== productId) return p; return { @@ -290,10 +297,10 @@ export const useProductStore = create((set, get) => ({ }; }); set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); + await saveStoredOverview(updated); }, - deleteVersion: (productId, versionId) => { + deleteVersion: async (productId, versionId) => { const updated = get().overview.map((p) => { if (p.id !== productId) return p; return { @@ -303,10 +310,10 @@ export const useProductStore = create((set, get) => ({ }; }); set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); + await saveStoredOverview(updated); }, - deleteProject: (productId, projectId) => { + deleteProject: async (productId, projectId) => { const updated = get().overview.map((p) => { if (p.id !== productId) return p; return { @@ -316,12 +323,12 @@ export const useProductStore = create((set, get) => ({ }; }); set({ overview: updated, overviewLoaded: true }); - saveStoredOverview(updated); + await saveStoredOverview(updated); }, })); -function saveStoredOverview(data: ProductOverview[]) { - saveServerData('products-overview', data).catch(() => {}); +async function saveStoredOverview(data: ProductOverview[]) { + await saveServerData('products-overview', data).catch(() => {}); } async function loadStoredOverview(): Promise {