From f6aab37d252a8a27a1280b4f3cf71b6f15d85b82 Mon Sep 17 00:00:00 2001 From: feie9456 Date: Fri, 14 Nov 2025 17:38:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9prisma=E9=80=82=E9=85=8D?= =?UTF-8?q?=E5=8E=9F=E6=9C=89=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.ts | 4 +- .../migration.sql | 4 + .../20251008_remove_catalog/migration.sql | 8 + prisma/schema.prisma | 124 ++++++++------- src/openapi/catalog.openapi.ts | 146 ------------------ src/openapi/goods.openapi.ts | 67 ++++++++ src/openapi/registry.ts | 17 +- src/routes/catalog.routes.ts | 50 ------ src/routes/goods.routes.ts | 138 +++++++++++++++++ src/routes/items.routes.ts | 3 +- src/routes/requests.routes.ts | 12 +- src/schemas/catalog.schemas.ts | 25 --- src/schemas/goods.schemas.ts | 30 ++++ src/schemas/request.schemas.ts | 3 +- 14 files changed, 330 insertions(+), 301 deletions(-) create mode 100644 prisma/migrations/20251008_add_consumable_goods_sku/migration.sql create mode 100644 prisma/migrations/20251008_remove_catalog/migration.sql delete mode 100644 src/openapi/catalog.openapi.ts create mode 100644 src/openapi/goods.openapi.ts delete mode 100644 src/routes/catalog.routes.ts create mode 100644 src/routes/goods.routes.ts delete mode 100644 src/schemas/catalog.schemas.ts create mode 100644 src/schemas/goods.schemas.ts diff --git a/index.ts b/index.ts index 085e5f1..48d17b4 100644 --- a/index.ts +++ b/index.ts @@ -8,7 +8,7 @@ import { prisma } from './src/utils/prisma.js'; import { createOpenAPIDocument } from './src/openapi/registry.js'; import requestsRouter from './src/routes/requests.routes.js'; import itemsRouter from './src/routes/items.routes.js'; -import catalogRouter from './src/routes/catalog.routes.js'; +import goodsRouter from './src/routes/goods.routes.js'; // ---------- App ---------- const app = express(); @@ -21,7 +21,7 @@ app.get('/health', (_req, res) => res.json({ ok: true, time: new Date().toISOStr // ---------- Routes ---------- app.use('/api/consumable-temp-requests', requestsRouter); app.use('/api/consumable-temp-items', itemsRouter); -app.use('/api/consumable-temp-catalog', catalogRouter); +app.use('/api/consumable-goods', goodsRouter); // ---------- OpenAPI Documentation ---------- const openApiDocument = createOpenAPIDocument(); diff --git a/prisma/migrations/20251008_add_consumable_goods_sku/migration.sql b/prisma/migrations/20251008_add_consumable_goods_sku/migration.sql new file mode 100644 index 0000000..14021ea --- /dev/null +++ b/prisma/migrations/20251008_add_consumable_goods_sku/migration.sql @@ -0,0 +1,4 @@ +-- This migration is a placeholder because the tables already exist in the database +-- The tables tb_consumable_goods and tb_consumable_sku were created outside of Prisma + +-- No actual schema changes needed diff --git a/prisma/migrations/20251008_remove_catalog/migration.sql b/prisma/migrations/20251008_remove_catalog/migration.sql new file mode 100644 index 0000000..6db8533 --- /dev/null +++ b/prisma/migrations/20251008_remove_catalog/migration.sql @@ -0,0 +1,8 @@ +-- DropForeignKey +ALTER TABLE `易耗品临时申请_易耗品条目` DROP FOREIGN KEY `易耗品临时申请_易耗品条目_catalogId_fkey`; + +-- AlterTable +ALTER TABLE `易耗品临时申请_易耗品条目` DROP COLUMN `catalogId`; + +-- DropTable +DROP TABLE `易耗品目录`; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b00a341..50fe6ff 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,78 +10,92 @@ datasource db { /// 申请单(“易耗品临时申请”) model v2_ConsumableTempRequest { - id String @id @default(cuid()) - applicantName String? // 申请人姓名(可选) - applicantPhone String // 申请人手机号 - department String? // 部门(可选) - reason String? // 申请事由(可选) - neededAt DateTime? // 期望领用时间(可选) - - status v2_ConsumableTempRequestStatus @default(PENDING) - approverId String? // 审批人(可选:员工ID/账号) - approvedAt DateTime? // 审批时间(可选) - remark String? // 备注(可选) - - // 一对多:申请单包含多条物料明细 + id String @id @default(cuid()) + applicantName String? + applicantPhone String + department String? + reason String? + neededAt DateTime? + status v2_ConsumableTempRequestStatus @default(PENDING) + approverId String? + approvedAt DateTime? + remark String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt items v2_ConsumableTempRequestItem[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - @@index([applicantPhone, createdAt]) @@index([status, createdAt]) - - // 如需将表名映射为中文(或你既有库的真实表名),取消下一行注释: @@map("易耗品临时申请") } /// 申请明细(易耗品条目) model v2_ConsumableTempRequestItem { - id String @id @default(cuid()) - - requestId String - request v2_ConsumableTempRequest @relation(fields: [requestId], references: [id], onDelete: Cascade) - - name String // 物料名称(如“打印纸”) - spec String? // 规格/型号(如“A4 70g”) - quantity Int // 数量 - unit String? // 单位(如“包/箱/个”) - estimatedUnitCost Decimal? // 预估单价(可选) - remark String? // 明细备注(可选) - - // 如果你有物料目录表,可在此做可选关联(下面给了 Catalog 模型) - catalogId String? - catalog v2_ConsumableTempCatalog? @relation(fields: [catalogId], references: [id]) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(cuid()) + requestId String + name String + spec String? + quantity Int + unit String? + estimatedUnitCost Decimal? + remark String? + catalogId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + request v2_ConsumableTempRequest @relation(fields: [requestId], references: [id], onDelete: Cascade) @@index([requestId]) @@index([name]) - // 如需把表名映射为既有库命名,取消注释并替换: + @@index([catalogId], map: "易耗品临时申请_易耗品条目_catalogId_fkey") @@map("易耗品临时申请_易耗品条目") } -/// 可选:物料目录(如你已有标准物料清单,便于下拉选择) -model v2_ConsumableTempCatalog { - id String @id @default(cuid()) - name String - spec String? - unit String? - isActive Boolean @default(true) +model tb_consumable_goods { + id BigInt @id @default(autoincrement()) + goods_name String @db.VarChar(100) + category_id BigInt? + brand String? @db.VarChar(50) + status Int? @default(0) @db.TinyInt + create_time DateTime? @default(now()) @db.Timestamp(0) + update_time DateTime? @default(now()) @db.Timestamp(0) + delete_flag Boolean? @default(false) + hosp_id String? @db.VarChar(32) - // 反向关系:哪些申请明细引用了该条目录 - requestItems v2_ConsumableTempRequestItem[] + // 关联 SKU + tb_consumable_sku tb_consumable_sku[] - @@unique([name, spec, unit]) - @@index([isActive]) - @@map("易耗品目录") + @@index([category_id], map: "idx_goods_category") +} + +model tb_consumable_sku { + id BigInt @id @default(autoincrement()) + goods_id BigInt + sku_code String? @unique(map: "uk_sku_code") @db.VarChar(50) + specification String? @db.VarChar(200) + barcode String @unique(map: "uk_sku_barcode") @db.VarChar(50) + supplier_id BigInt? + purchase_price Decimal? @db.Decimal(10, 2) + selling_price Decimal? @db.Decimal(10, 2) + unit String? @db.VarChar(20) + min_stock Int? @default(0) + max_stock Int? + status Int? @default(0) @db.TinyInt + create_time DateTime? @default(now()) @db.Timestamp(0) + update_time DateTime? @default(now()) @db.Timestamp(0) + delete_flag Boolean? @default(false) + hosp_id String? @db.VarChar(32) + + // 关联商品 + tb_consumable_goods tb_consumable_goods @relation(fields: [goods_id], references: [id]) + + @@index([goods_id], map: "idx_sku_goods") + @@index([supplier_id], map: "idx_sku_supplier") } enum v2_ConsumableTempRequestStatus { - PENDING // 待审批 - APPROVED // 通过 - REJECTED // 驳回 - CANCELLED // 取消 - COMPLETED // 已完成(已领用) -} \ No newline at end of file + PENDING + APPROVED + REJECTED + CANCELLED + COMPLETED +} diff --git a/src/openapi/catalog.openapi.ts b/src/openapi/catalog.openapi.ts deleted file mode 100644 index 58d3102..0000000 --- a/src/openapi/catalog.openapi.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; -import { z } from 'zod'; -import { catalogCreateSchema, catalogUpdateSchema, catalogResponseSchema } from '../schemas/catalog.schemas.js'; -import { errorResponseSchema, paginatedResponseSchema } from '../schemas/common.js'; - -export function registerCatalogPaths(registry: OpenAPIRegistry) { - // POST /api/consumable-temp-catalog - registry.registerPath({ - method: 'post', - path: '/api/consumable-temp-catalog', - tags: ['Consumable Catalog'], - summary: '创建耗材目录', - request: { - body: { - content: { - 'application/json': { - schema: catalogCreateSchema - } - } - } - }, - responses: { - 201: { - description: '创建成功', - content: { - 'application/json': { - schema: z.object({ data: catalogResponseSchema }) - } - } - } - } - }); - - // GET /api/consumable-temp-catalog - registry.registerPath({ - method: 'get', - path: '/api/consumable-temp-catalog', - tags: ['Consumable Catalog'], - summary: '查询耗材目录列表', - request: { - query: z.object({ - page: z.string().optional().openapi({ description: '页码' }), - pageSize: z.string().optional().openapi({ description: '每页数量' }), - isActive: z.string().optional().openapi({ description: '是否激活' }), - q: z.string().optional().openapi({ description: '搜索关键词' }) - }) - }, - responses: { - 200: { - description: '查询成功', - content: { - 'application/json': { - schema: paginatedResponseSchema(catalogResponseSchema) - } - } - } - } - }); - - // GET /api/consumable-temp-catalog/:id - registry.registerPath({ - method: 'get', - path: '/api/consumable-temp-catalog/{id}', - tags: ['Consumable Catalog'], - summary: '获取耗材目录详情', - request: { - params: z.object({ - id: z.string().openapi({ description: '目录ID' }) - }) - }, - responses: { - 200: { - description: '查询成功', - content: { - 'application/json': { - schema: z.object({ data: catalogResponseSchema }) - } - } - }, - 404: { - description: '未找到', - content: { - 'application/json': { - schema: errorResponseSchema - } - } - } - } - }); - - // PUT /api/consumable-temp-catalog/:id - registry.registerPath({ - method: 'put', - path: '/api/consumable-temp-catalog/{id}', - tags: ['Consumable Catalog'], - summary: '更新耗材目录', - request: { - params: z.object({ - id: z.string().openapi({ description: '目录ID' }) - }), - body: { - content: { - 'application/json': { - schema: catalogUpdateSchema - } - } - } - }, - responses: { - 200: { - description: '更新成功', - content: { - 'application/json': { - schema: z.object({ data: catalogResponseSchema }) - } - } - }, - 404: { - description: '未找到', - content: { - 'application/json': { - schema: errorResponseSchema - } - } - } - } - }); - - // DELETE /api/consumable-temp-catalog/:id - registry.registerPath({ - method: 'delete', - path: '/api/consumable-temp-catalog/{id}', - tags: ['Consumable Catalog'], - summary: '停用耗材目录', - request: { - params: z.object({ - id: z.string().openapi({ description: '目录ID' }) - }) - }, - responses: { - 204: { - description: '停用成功' - } - } - }); -} diff --git a/src/openapi/goods.openapi.ts b/src/openapi/goods.openapi.ts new file mode 100644 index 0000000..7efaa66 --- /dev/null +++ b/src/openapi/goods.openapi.ts @@ -0,0 +1,67 @@ +import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi'; +import { z } from 'zod'; +import { goodsWithSkuResponseSchema } from '../schemas/goods.schemas.js'; +import { errorResponseSchema, paginatedResponseSchema } from '../schemas/common.js'; + +export function registerGoodsPaths(registry: OpenAPIRegistry) { + // GET /api/consumable-goods + registry.registerPath({ + method: 'get', + path: '/api/consumable-goods', + tags: ['Consumable Goods'], + summary: '查询易耗品种类列表(含规格/价格)', + description: '联合查询商品和 SKU 信息,返回商品及其所有规格、价格等详细信息', + request: { + query: z.object({ + page: z.string().optional().openapi({ description: '页码', example: '1' }), + pageSize: z.string().optional().openapi({ description: '每页数量', example: '20' }), + status: z.string().optional().openapi({ description: '状态:0-正常,1-停用', example: '0' }), + hospId: z.string().optional().openapi({ description: '医院ID' }), + categoryId: z.string().optional().openapi({ description: '分类ID' }), + q: z.string().optional().openapi({ description: '搜索关键词(商品名称)', example: '打印纸' }) + }) + }, + responses: { + 200: { + description: '查询成功', + content: { + 'application/json': { + schema: paginatedResponseSchema(goodsWithSkuResponseSchema) + } + } + } + } + }); + + // GET /api/consumable-goods/:id + registry.registerPath({ + method: 'get', + path: '/api/consumable-goods/{id}', + tags: ['Consumable Goods'], + summary: '获取易耗品种类详情(含规格/价格)', + description: '获取单个商品及其所有 SKU 规格、价格等详细信息', + request: { + params: z.object({ + id: z.string().openapi({ description: '商品ID' }) + }) + }, + responses: { + 200: { + description: '查询成功', + content: { + 'application/json': { + schema: z.object({ data: goodsWithSkuResponseSchema }) + } + } + }, + 404: { + description: '未找到', + content: { + 'application/json': { + schema: errorResponseSchema + } + } + } + } + }); +} diff --git a/src/openapi/registry.ts b/src/openapi/registry.ts index ca6fb32..ee8faa5 100644 --- a/src/openapi/registry.ts +++ b/src/openapi/registry.ts @@ -7,15 +7,11 @@ import { requestItemResponseSchema, requestResponseSchema } from '../schemas/request.schemas.js'; -import { - catalogCreateSchema, - catalogUpdateSchema, - catalogResponseSchema -} from '../schemas/catalog.schemas.js'; +import { goodsWithSkuResponseSchema, skuResponseSchema } from '../schemas/goods.schemas.js'; import { errorResponseSchema } from '../schemas/common.js'; import { registerRequestPaths } from './request.openapi.js'; import { registerItemPaths } from './item.openapi.js'; -import { registerCatalogPaths } from './catalog.openapi.js'; +import { registerGoodsPaths } from './goods.openapi.js'; export function createOpenAPIDocument() { const registry = new OpenAPIRegistry(); @@ -25,17 +21,16 @@ export function createOpenAPIDocument() { registry.register('CreateRequest', createRequestSchema); registry.register('UpdateRequest', updateRequestSchema); registry.register('UpdateStatus', updateStatusSchema); - registry.register('CatalogCreate', catalogCreateSchema); - registry.register('CatalogUpdate', catalogUpdateSchema); registry.register('RequestItemResponse', requestItemResponseSchema); registry.register('RequestResponse', requestResponseSchema); - registry.register('CatalogResponse', catalogResponseSchema); + registry.register('SKUResponse', skuResponseSchema); + registry.register('GoodsWithSKUResponse', goodsWithSkuResponseSchema); registry.register('ErrorResponse', errorResponseSchema); // 注册路由 registerRequestPaths(registry); registerItemPaths(registry); - registerCatalogPaths(registry); + registerGoodsPaths(registry); // 生成文档 const generator = new OpenApiGeneratorV3(registry.definitions); @@ -44,7 +39,7 @@ export function createOpenAPIDocument() { info: { title: '智能配送耗材临时申请 API', version: '1.0.0', - description: '智能配送系统的耗材临时申请管理 API,支持申请创建、审批、物料明细管理和目录管理' + description: '智能配送系统的耗材临时申请管理 API,支持申请创建、审批、物料明细管理和易耗品种类查询' }, servers: [ { diff --git a/src/routes/catalog.routes.ts b/src/routes/catalog.routes.ts deleted file mode 100644 index 69df46d..0000000 --- a/src/routes/catalog.routes.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Router } from 'express'; -import { asyncHandler } from '../utils/asyncHandler.js'; -import { prisma } from '../utils/prisma.js'; -import { paginateParamsSchema } from '../schemas/common.js'; -import { catalogCreateSchema, catalogUpdateSchema } from '../schemas/catalog.schemas.js'; - -const router = Router(); - -// Create catalog -router.post('/', asyncHandler(async (req, res) => { - const parsed = catalogCreateSchema.parse(req.body); - const data = await prisma.v2_ConsumableTempCatalog.create({ data: parsed }); - res.status(201).json({ data }); -})); - -// List catalogs -router.get('/', asyncHandler(async (req, res) => { - const { page, pageSize } = paginateParamsSchema.parse({ page: req.query.page, pageSize: req.query.pageSize }); - const where: any = {}; - if (req.query.isActive !== undefined) where.isActive = req.query.isActive === 'true'; - if (req.query.q) where.name = { contains: String(req.query.q) }; - const [total, rows] = await Promise.all([ - prisma.v2_ConsumableTempCatalog.count({ where }), - prisma.v2_ConsumableTempCatalog.findMany({ where, orderBy: { name: 'asc' }, skip: (page - 1) * pageSize, take: pageSize }) - ]); - res.json({ data: rows, meta: { total, page, pageSize } }); -})); - -// Get catalog detail -router.get('/:id', asyncHandler(async (req, res) => { - const data = await prisma.v2_ConsumableTempCatalog.findUnique({ where: { id: req.params.id } }); - if (!data) return res.status(404).json({ error: 'Not Found' }); - res.json({ data }); -})); - -// Update catalog -router.put('/:id', asyncHandler(async (req, res) => { - const parsed = catalogUpdateSchema.parse(req.body); - const data = await prisma.v2_ConsumableTempCatalog.update({ where: { id: req.params.id }, data: parsed }).catch(e => { if ((e as any).code === 'P2025') return null; throw e; }); - if (!data) return res.status(404).json({ error: 'Not Found' }); - res.json({ data }); -})); - -// Deactivate instead of delete -router.delete('/:id', asyncHandler(async (req, res) => { - await prisma.v2_ConsumableTempCatalog.update({ where: { id: req.params.id }, data: { isActive: false } }).catch(e => { if ((e as any).code === 'P2025') return null; throw e; }); - res.status(204).end(); -})); - -export default router; diff --git a/src/routes/goods.routes.ts b/src/routes/goods.routes.ts new file mode 100644 index 0000000..926f0b2 --- /dev/null +++ b/src/routes/goods.routes.ts @@ -0,0 +1,138 @@ +import { Router } from 'express'; +import { asyncHandler } from '../utils/asyncHandler.js'; +import { prisma } from '../utils/prisma.js'; +import { paginateParamsSchema } from '../schemas/common.js'; + +const router = Router(); + +// List goods with SKU information (联合查询) +router.get('/', asyncHandler(async (req, res) => { + const { page, pageSize } = paginateParamsSchema.parse({ + page: req.query.page, + pageSize: req.query.pageSize + }); + + // 构建查询条件 + const where: any = { + delete_flag: false + }; + + // 过滤条件 + if (req.query.status !== undefined) { + where.status = Number(req.query.status); + } + if (req.query.hospId) { + where.hosp_id = String(req.query.hospId); + } + if (req.query.categoryId) { + where.category_id = BigInt(String(req.query.categoryId)); + } + if (req.query.q) { + where.goods_name = { contains: String(req.query.q) }; + } + + // 查询商品列表(带 SKU) + const [total, goods] = await Promise.all([ + prisma.tb_consumable_goods.count({ where }), + prisma.tb_consumable_goods.findMany({ + where, + include: { + // 联合查询 SKU 信息 + tb_consumable_sku: { + where: { + delete_flag: false + }, + orderBy: { + sku_code: 'asc' + } + } + }, + orderBy: { + create_time: 'desc' + }, + skip: (page - 1) * pageSize, + take: pageSize + }) + ]); + + // 转换数据格式 + const data = goods.map(item => ({ + id: Number(item.id), + goodsName: item.goods_name, + categoryId: item.category_id ? Number(item.category_id) : null, + brand: item.brand, + status: item.status, + hospId: item.hosp_id, + skus: item.tb_consumable_sku.map(sku => ({ + id: Number(sku.id), + skuCode: sku.sku_code, + specification: sku.specification, + barcode: sku.barcode, + supplierId: sku.supplier_id ? Number(sku.supplier_id) : null, + purchasePrice: sku.purchase_price ? Number(sku.purchase_price) : null, + sellingPrice: sku.selling_price ? Number(sku.selling_price) : null, + unit: sku.unit, + minStock: sku.min_stock, + maxStock: sku.max_stock, + status: sku.status + })) + })); + + res.json({ + data, + meta: { total: Number(total), page, pageSize } + }); +})); + +// Get single goods with SKU detail +router.get('/:id', asyncHandler(async (req, res) => { + const goodsId = BigInt(req.params.id!); + + const goods = await prisma.tb_consumable_goods.findUnique({ + where: { + id: goodsId, + delete_flag: false + }, + include: { + tb_consumable_sku: { + where: { + delete_flag: false + }, + orderBy: { + sku_code: 'asc' + } + } + } + }); + + if (!goods) { + return res.status(404).json({ error: 'Not Found' }); + } + + // 转换数据格式 + const data = { + id: Number(goods.id), + goodsName: goods.goods_name, + categoryId: goods.category_id ? Number(goods.category_id) : null, + brand: goods.brand, + status: goods.status, + hospId: goods.hosp_id, + skus: goods.tb_consumable_sku.map(sku => ({ + id: Number(sku.id), + skuCode: sku.sku_code, + specification: sku.specification, + barcode: sku.barcode, + supplierId: sku.supplier_id ? Number(sku.supplier_id) : null, + purchasePrice: sku.purchase_price ? Number(sku.purchase_price) : null, + sellingPrice: sku.selling_price ? Number(sku.selling_price) : null, + unit: sku.unit, + minStock: sku.min_stock, + maxStock: sku.max_stock, + status: sku.status + })) + }; + + res.json({ data }); +})); + +export default router; diff --git a/src/routes/items.routes.ts b/src/routes/items.routes.ts index eda0aeb..07ea638 100644 --- a/src/routes/items.routes.ts +++ b/src/routes/items.routes.ts @@ -17,8 +17,7 @@ router.put('/:itemId', asyncHandler(async (req, res) => { quantity: parsed.quantity, unit: parsed.unit, estimatedUnitCost: parsed.estimatedUnitCost !== undefined ? new Prisma.Decimal(parsed.estimatedUnitCost) : undefined, - remark: parsed.remark, - catalogId: parsed.catalogId + remark: parsed.remark } }).catch(e => { if ((e as any).code === 'P2025') return null; throw e; }); if (!item) return res.status(404).json({ error: 'Not Found' }); diff --git a/src/routes/requests.routes.ts b/src/routes/requests.routes.ts index a94b540..e793e15 100644 --- a/src/routes/requests.routes.ts +++ b/src/routes/requests.routes.ts @@ -29,8 +29,7 @@ router.post('/', asyncHandler(async (req, res) => { quantity: i.quantity, unit: i.unit, estimatedUnitCost: i.estimatedUnitCost !== undefined ? new Prisma.Decimal(i.estimatedUnitCost) : undefined, - remark: i.remark, - catalogId: i.catalogId ?? null + remark: i.remark })) } }, include: { items: true } @@ -109,8 +108,7 @@ router.put('/:id', asyncHandler(async (req, res) => { quantity: item.quantity ?? undefined, unit: item.unit, estimatedUnitCost: item.estimatedUnitCost !== undefined ? new Prisma.Decimal(item.estimatedUnitCost) : undefined, - remark: item.remark, - catalogId: item.catalogId + remark: item.remark } }); } else { @@ -121,8 +119,7 @@ router.put('/:id', asyncHandler(async (req, res) => { quantity: item.quantity, unit: item.unit, estimatedUnitCost: item.estimatedUnitCost !== undefined ? new Prisma.Decimal(item.estimatedUnitCost) : undefined, - remark: item.remark, - catalogId: item.catalogId + remark: item.remark }}); } } @@ -168,8 +165,7 @@ router.post('/:id/items', asyncHandler(async (req, res) => { quantity: parsed.quantity, unit: parsed.unit, estimatedUnitCost: parsed.estimatedUnitCost !== undefined ? new Prisma.Decimal(parsed.estimatedUnitCost) : undefined, - remark: parsed.remark, - catalogId: parsed.catalogId ?? null + remark: parsed.remark }}); res.status(201).json({ data: item }); })); diff --git a/src/schemas/catalog.schemas.ts b/src/schemas/catalog.schemas.ts deleted file mode 100644 index 804197d..0000000 --- a/src/schemas/catalog.schemas.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { z } from 'zod'; -import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; - -extendZodWithOpenApi(z); - -// ---------- Input Schemas ---------- -export const catalogCreateSchema = z.object({ - name: z.string().min(1).openapi({ example: '办公用品', description: '目录名称' }), - spec: z.string().optional().openapi({ example: '标准', description: '规格' }), - unit: z.string().optional().openapi({ example: '个', description: '单位' }), - isActive: z.boolean().optional().default(true).openapi({ description: '是否激活' }) -}); - -export const catalogUpdateSchema = catalogCreateSchema.partial(); - -// ---------- Response Schemas ---------- -export const catalogResponseSchema = z.object({ - id: z.string().openapi({ description: '目录ID' }), - name: z.string().openapi({ description: '目录名称' }), - spec: z.string().nullable().openapi({ description: '规格' }), - unit: z.string().nullable().openapi({ description: '单位' }), - isActive: z.boolean().openapi({ description: '是否激活' }), - createdAt: z.string().openapi({ description: '创建时间' }), - updatedAt: z.string().openapi({ description: '更新时间' }) -}); diff --git a/src/schemas/goods.schemas.ts b/src/schemas/goods.schemas.ts new file mode 100644 index 0000000..977d7da --- /dev/null +++ b/src/schemas/goods.schemas.ts @@ -0,0 +1,30 @@ +import { z } from 'zod'; +import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi'; + +extendZodWithOpenApi(z); + +// ---------- SKU Schema (规格信息) ---------- +export const skuResponseSchema = z.object({ + id: z.number().openapi({ description: 'SKU ID' }), + skuCode: z.string().nullable().openapi({ description: 'SKU编码' }), + specification: z.string().nullable().openapi({ description: '规格型号', example: 'A4 70g' }), + barcode: z.string().openapi({ description: '条形码' }), + supplierId: z.number().nullable().openapi({ description: '供应商ID' }), + purchasePrice: z.number().nullable().openapi({ description: '采购价格', example: 25.50 }), + sellingPrice: z.number().nullable().openapi({ description: '销售价格', example: 30.00 }), + unit: z.string().nullable().openapi({ description: '单位', example: '包' }), + minStock: z.number().nullable().openapi({ description: '最小库存' }), + maxStock: z.number().nullable().openapi({ description: '最大库存' }), + status: z.number().nullable().openapi({ description: '状态:0-正常,1-停用' }) +}); + +// ---------- Goods with SKU Schema (种类+规格) ---------- +export const goodsWithSkuResponseSchema = z.object({ + id: z.number().openapi({ description: '商品ID' }), + goodsName: z.string().openapi({ description: '商品名称', example: '打印纸' }), + categoryId: z.number().nullable().openapi({ description: '分类ID' }), + brand: z.string().nullable().openapi({ description: '品牌' }), + status: z.number().nullable().openapi({ description: '状态:0-正常,1-停用' }), + hospId: z.string().nullable().openapi({ description: '医院ID' }), + skus: z.array(skuResponseSchema).openapi({ description: 'SKU列表(规格/价格等信息)' }) +}); diff --git a/src/schemas/request.schemas.ts b/src/schemas/request.schemas.ts index c7b09de..6a683fc 100644 --- a/src/schemas/request.schemas.ts +++ b/src/schemas/request.schemas.ts @@ -11,8 +11,7 @@ export const requestItemInputSchema = z.object({ quantity: z.number().int().positive().openapi({ example: 10, description: '数量' }), unit: z.string().optional().openapi({ example: '箱', description: '单位' }), estimatedUnitCost: z.number().positive().optional().openapi({ example: 25.5, description: '预估单价' }), - remark: z.string().optional().openapi({ description: '备注' }), - catalogId: z.string().optional().openapi({ description: '关联的目录ID' }) + remark: z.string().optional().openapi({ description: '备注' }) }); export const createRequestSchema = z.object({