修改prisma适配原有表
This commit is contained in:
parent
ce4476fd4a
commit
f6aab37d25
4
index.ts
4
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();
|
||||
|
||||
@ -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
|
||||
8
prisma/migrations/20251008_remove_catalog/migration.sql
Normal file
8
prisma/migrations/20251008_remove_catalog/migration.sql
Normal file
@ -0,0 +1,8 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE `易耗品临时申请_易耗品条目` DROP FOREIGN KEY `易耗品临时申请_易耗品条目_catalogId_fkey`;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE `易耗品临时申请_易耗品条目` DROP COLUMN `catalogId`;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE `易耗品目录`;
|
||||
@ -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 // 已完成(已领用)
|
||||
}
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
CANCELLED
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
@ -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: '停用成功'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
67
src/openapi/goods.openapi.ts
Normal file
67
src/openapi/goods.openapi.ts
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
@ -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;
|
||||
138
src/routes/goods.routes.ts
Normal file
138
src/routes/goods.routes.ts
Normal file
@ -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;
|
||||
@ -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' });
|
||||
|
||||
@ -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 });
|
||||
}));
|
||||
|
||||
@ -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: '更新时间' })
|
||||
});
|
||||
30
src/schemas/goods.schemas.ts
Normal file
30
src/schemas/goods.schemas.ts
Normal file
@ -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列表(规格/价格等信息)' })
|
||||
});
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user