fix(product): 以 AppData 产品树作为展示主源
This commit is contained in:
@@ -3,6 +3,17 @@ import { PrismaService } from '../../prisma/prisma.service';
|
|||||||
import { CreateProductDto } from './dto/create-product.dto';
|
import { CreateProductDto } from './dto/create-product.dto';
|
||||||
import { UpdateProductDto } from './dto/update-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()
|
@Injectable()
|
||||||
export class ProductService {
|
export class ProductService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
@@ -11,7 +22,16 @@ export class ProductService {
|
|||||||
return this.prisma.product.create({ data: dto });
|
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({
|
return this.prisma.product.findMany({
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include: {
|
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({
|
return this.prisma.product.findMany({
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
include: {
|
include: {
|
||||||
@@ -38,6 +63,16 @@ export class ProductService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: string) {
|
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({
|
const product = await this.prisma.product.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: {
|
include: {
|
||||||
@@ -64,4 +99,28 @@ export class ProductService {
|
|||||||
const exists = await this.prisma.product.findUnique({ where: { id } });
|
const exists = await this.prisma.product.findUnique({ where: { id } });
|
||||||
if (!exists) throw new NotFoundException('产品不存在');
|
if (!exists) throw new NotFoundException('产品不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getAppDataOverview(): Promise<ProductOverviewItem[] | null> {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ interface ProductState {
|
|||||||
createProduct: (data: { name: string; description?: string }) => Promise<void>;
|
createProduct: (data: { name: string; description?: string }) => Promise<void>;
|
||||||
updateProduct: (id: string, data: { name?: string; description?: string }) => Promise<void>;
|
updateProduct: (id: string, data: { name?: string; description?: string }) => Promise<void>;
|
||||||
deleteProduct: (id: string) => Promise<void>;
|
deleteProduct: (id: string) => Promise<void>;
|
||||||
reorderProducts: (ids: string[]) => void;
|
reorderProducts: (ids: string[]) => Promise<void>;
|
||||||
migrateAndDeleteProduct: (sourceId: string, targetId: string) => void;
|
migrateAndDeleteProduct: (sourceId: string, targetId: string) => Promise<void>;
|
||||||
createProject: (productId: string, data: { name: string; description?: string }) => void;
|
createProject: (productId: string, data: { name: string; description?: string }) => Promise<void>;
|
||||||
updateProject: (productId: string, projectId: string, data: { name?: string; description?: string }) => void;
|
updateProject: (productId: string, projectId: string, data: { name?: string; description?: string }) => Promise<void>;
|
||||||
createVersion: (productId: string, data: { name: string; status: string }) => void;
|
createVersion: (productId: string, data: { name: string; status: string }) => Promise<void>;
|
||||||
updateVersion: (productId: string, versionId: string, data: Partial<VersionItem>) => void;
|
updateVersion: (productId: string, versionId: string, data: Partial<VersionItem>) => Promise<void>;
|
||||||
deleteVersion: (productId: string, versionId: string) => void;
|
deleteVersion: (productId: string, versionId: string) => Promise<void>;
|
||||||
deleteProject: (productId: string, projectId: string) => void;
|
deleteProject: (productId: string, projectId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useProductStore = create<ProductState>((set, get) => ({
|
export const useProductStore = create<ProductState>((set, get) => ({
|
||||||
@@ -85,7 +85,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
fetchProducts: async () => {
|
fetchProducts: async () => {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
try {
|
||||||
const products = await api.get<ProductWithCount[]>('/products');
|
const overview = await loadStoredOverview();
|
||||||
|
const products = overview
|
||||||
|
? overview.map(({ projects: _projects, versions: _versions, ...product }) => product)
|
||||||
|
: await api.get<ProductWithCount[]>('/products');
|
||||||
set({ products, loading: false });
|
set({ products, loading: false });
|
||||||
} catch {
|
} catch {
|
||||||
set({ products: [], error: null, loading: false });
|
set({ products: [], error: null, loading: false });
|
||||||
@@ -117,6 +120,12 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
fetchProduct: async (id) => {
|
fetchProduct: async (id) => {
|
||||||
set({ loading: true, error: null });
|
set({ loading: true, error: null });
|
||||||
try {
|
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<Product>(`/products/${id}`);
|
const product = await api.get<Product>(`/products/${id}`);
|
||||||
set({ currentProduct: product, loading: false });
|
set({ currentProduct: product, loading: false });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -130,7 +139,6 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
createProduct: async (data) => {
|
createProduct: async (data) => {
|
||||||
// 直接本地新增(mock 模式核心,保留远端兼容但不依赖)
|
|
||||||
const newProduct: ProductOverview = {
|
const newProduct: ProductOverview = {
|
||||||
id: `local-${Date.now()}`,
|
id: `local-${Date.now()}`,
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
@@ -143,43 +151,42 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
const updated = [newProduct, ...get().overview];
|
const updated = [newProduct, ...get().overview];
|
||||||
set({ overview: updated, overviewLoaded: true });
|
set({ overview: updated, overviewLoaded: true });
|
||||||
saveStoredOverview(updated);
|
await saveStoredOverview(updated);
|
||||||
// 异步尝试同步远端(失败也无所谓,本地已更新)
|
|
||||||
try { await api.post('/products', data); } catch {}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateProduct: async (id, data) => {
|
updateProduct: async (id, data) => {
|
||||||
try {
|
|
||||||
await api.patch(`/products/${id}`, data);
|
|
||||||
} catch {}
|
|
||||||
const updated = get().overview.map((p) =>
|
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 });
|
const currentProduct = get().currentProduct;
|
||||||
saveStoredOverview(updated);
|
set({
|
||||||
|
overview: updated,
|
||||||
|
overviewLoaded: true,
|
||||||
|
currentProduct:
|
||||||
|
currentProduct?.id === id ? ({ ...currentProduct, ...data } as any) : currentProduct,
|
||||||
|
});
|
||||||
|
await saveStoredOverview(updated);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteProduct: async (id) => {
|
deleteProduct: async (id) => {
|
||||||
try {
|
|
||||||
await api.delete(`/products/${id}`);
|
|
||||||
} catch {}
|
|
||||||
const updated = get().overview.filter((p) => p.id !== id);
|
const updated = get().overview.filter((p) => p.id !== id);
|
||||||
set({
|
set({
|
||||||
products: get().products.filter((p) => p.id !== id),
|
products: get().products.filter((p) => p.id !== id),
|
||||||
overview: updated,
|
overview: updated,
|
||||||
overviewLoaded: true,
|
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 map = new Map(get().overview.map((p) => [p.id, p]));
|
||||||
const reordered = ids.map((id) => map.get(id)).filter(Boolean) as ProductOverview[];
|
const reordered = ids.map((id) => map.get(id)).filter(Boolean) as ProductOverview[];
|
||||||
set({ overview: reordered, overviewLoaded: true });
|
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 source = get().overview.find((p) => p.id === sourceId);
|
||||||
const target = get().overview.find((p) => p.id === targetId);
|
const target = get().overview.find((p) => p.id === targetId);
|
||||||
if (!source || !target) return;
|
if (!source || !target) return;
|
||||||
@@ -199,10 +206,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
.filter((p) => p.id !== sourceId)
|
.filter((p) => p.id !== sourceId)
|
||||||
.map((p) => (p.id === targetId ? migratedTarget : p));
|
.map((p) => (p.id === targetId ? migratedTarget : p));
|
||||||
set({ overview: updated, overviewLoaded: true });
|
set({ overview: updated, overviewLoaded: true });
|
||||||
saveStoredOverview(updated);
|
await saveStoredOverview(updated);
|
||||||
},
|
},
|
||||||
|
|
||||||
createProject: (productId, data) => {
|
createProject: async (productId, data) => {
|
||||||
const newProject = {
|
const newProject = {
|
||||||
id: `proj-${Date.now()}`,
|
id: `proj-${Date.now()}`,
|
||||||
name: data.name || '',
|
name: data.name || '',
|
||||||
@@ -218,10 +225,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
set({ overview: updated, overviewLoaded: true });
|
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) => {
|
const updated = get().overview.map((p) => {
|
||||||
if (p.id !== productId) return p;
|
if (p.id !== productId) return p;
|
||||||
const currentProject = p.projects.find((proj) => proj.id === projectId);
|
const currentProject = p.projects.find((proj) => proj.id === projectId);
|
||||||
@@ -255,10 +262,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
set({ overview: updated, overviewLoaded: true });
|
set({ overview: updated, overviewLoaded: true });
|
||||||
saveStoredOverview(updated);
|
await saveStoredOverview(updated);
|
||||||
},
|
},
|
||||||
|
|
||||||
createVersion: (productId, data) => {
|
createVersion: async (productId, data) => {
|
||||||
const newVersion: VersionItem = {
|
const newVersion: VersionItem = {
|
||||||
id: `ver-${Date.now()}`,
|
id: `ver-${Date.now()}`,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -276,10 +283,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
set({ overview: updated, overviewLoaded: true });
|
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) => {
|
const updated = get().overview.map((p) => {
|
||||||
if (p.id !== productId) return p;
|
if (p.id !== productId) return p;
|
||||||
return {
|
return {
|
||||||
@@ -290,10 +297,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
set({ overview: updated, overviewLoaded: true });
|
set({ overview: updated, overviewLoaded: true });
|
||||||
saveStoredOverview(updated);
|
await saveStoredOverview(updated);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteVersion: (productId, versionId) => {
|
deleteVersion: async (productId, versionId) => {
|
||||||
const updated = get().overview.map((p) => {
|
const updated = get().overview.map((p) => {
|
||||||
if (p.id !== productId) return p;
|
if (p.id !== productId) return p;
|
||||||
return {
|
return {
|
||||||
@@ -303,10 +310,10 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
set({ overview: updated, overviewLoaded: true });
|
set({ overview: updated, overviewLoaded: true });
|
||||||
saveStoredOverview(updated);
|
await saveStoredOverview(updated);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteProject: (productId, projectId) => {
|
deleteProject: async (productId, projectId) => {
|
||||||
const updated = get().overview.map((p) => {
|
const updated = get().overview.map((p) => {
|
||||||
if (p.id !== productId) return p;
|
if (p.id !== productId) return p;
|
||||||
return {
|
return {
|
||||||
@@ -316,12 +323,12 @@ export const useProductStore = create<ProductState>((set, get) => ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
set({ overview: updated, overviewLoaded: true });
|
set({ overview: updated, overviewLoaded: true });
|
||||||
saveStoredOverview(updated);
|
await saveStoredOverview(updated);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function saveStoredOverview(data: ProductOverview[]) {
|
async function saveStoredOverview(data: ProductOverview[]) {
|
||||||
saveServerData('products-overview', data).catch(() => {});
|
await saveServerData('products-overview', data).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStoredOverview(): Promise<ProductOverview[] | null> {
|
async function loadStoredOverview(): Promise<ProductOverview[] | null> {
|
||||||
|
|||||||
Reference in New Issue
Block a user