修改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 { createOpenAPIDocument } from './src/openapi/registry.js';
|
||||||
import requestsRouter from './src/routes/requests.routes.js';
|
import requestsRouter from './src/routes/requests.routes.js';
|
||||||
import itemsRouter from './src/routes/items.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 ----------
|
// ---------- App ----------
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -21,7 +21,7 @@ app.get('/health', (_req, res) => res.json({ ok: true, time: new Date().toISOStr
|
|||||||
// ---------- Routes ----------
|
// ---------- Routes ----------
|
||||||
app.use('/api/consumable-temp-requests', requestsRouter);
|
app.use('/api/consumable-temp-requests', requestsRouter);
|
||||||
app.use('/api/consumable-temp-items', itemsRouter);
|
app.use('/api/consumable-temp-items', itemsRouter);
|
||||||
app.use('/api/consumable-temp-catalog', catalogRouter);
|
app.use('/api/consumable-goods', goodsRouter);
|
||||||
|
|
||||||
// ---------- OpenAPI Documentation ----------
|
// ---------- OpenAPI Documentation ----------
|
||||||
const openApiDocument = createOpenAPIDocument();
|
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 {
|
model v2_ConsumableTempRequest {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
applicantName String? // 申请人姓名(可选)
|
applicantName String?
|
||||||
applicantPhone String // 申请人手机号
|
applicantPhone String
|
||||||
department String? // 部门(可选)
|
department String?
|
||||||
reason String? // 申请事由(可选)
|
reason String?
|
||||||
neededAt DateTime? // 期望领用时间(可选)
|
neededAt DateTime?
|
||||||
|
status v2_ConsumableTempRequestStatus @default(PENDING)
|
||||||
status v2_ConsumableTempRequestStatus @default(PENDING)
|
approverId String?
|
||||||
approverId String? // 审批人(可选:员工ID/账号)
|
approvedAt DateTime?
|
||||||
approvedAt DateTime? // 审批时间(可选)
|
remark String?
|
||||||
remark String? // 备注(可选)
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
// 一对多:申请单包含多条物料明细
|
|
||||||
items v2_ConsumableTempRequestItem[]
|
items v2_ConsumableTempRequestItem[]
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@index([applicantPhone, createdAt])
|
@@index([applicantPhone, createdAt])
|
||||||
@@index([status, createdAt])
|
@@index([status, createdAt])
|
||||||
|
|
||||||
// 如需将表名映射为中文(或你既有库的真实表名),取消下一行注释:
|
|
||||||
@@map("易耗品临时申请")
|
@@map("易耗品临时申请")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 申请明细(易耗品条目)
|
/// 申请明细(易耗品条目)
|
||||||
model v2_ConsumableTempRequestItem {
|
model v2_ConsumableTempRequestItem {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
requestId String
|
||||||
requestId String
|
name String
|
||||||
request v2_ConsumableTempRequest @relation(fields: [requestId], references: [id], onDelete: Cascade)
|
spec String?
|
||||||
|
quantity Int
|
||||||
name String // 物料名称(如“打印纸”)
|
unit String?
|
||||||
spec String? // 规格/型号(如“A4 70g”)
|
estimatedUnitCost Decimal?
|
||||||
quantity Int // 数量
|
remark String?
|
||||||
unit String? // 单位(如“包/箱/个”)
|
catalogId String?
|
||||||
estimatedUnitCost Decimal? // 预估单价(可选)
|
createdAt DateTime @default(now())
|
||||||
remark String? // 明细备注(可选)
|
updatedAt DateTime @updatedAt
|
||||||
|
request v2_ConsumableTempRequest @relation(fields: [requestId], references: [id], onDelete: Cascade)
|
||||||
// 如果你有物料目录表,可在此做可选关联(下面给了 Catalog 模型)
|
|
||||||
catalogId String?
|
|
||||||
catalog v2_ConsumableTempCatalog? @relation(fields: [catalogId], references: [id])
|
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@index([requestId])
|
@@index([requestId])
|
||||||
@@index([name])
|
@@index([name])
|
||||||
// 如需把表名映射为既有库命名,取消注释并替换:
|
@@index([catalogId], map: "易耗品临时申请_易耗品条目_catalogId_fkey")
|
||||||
@@map("易耗品临时申请_易耗品条目")
|
@@map("易耗品临时申请_易耗品条目")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 可选:物料目录(如你已有标准物料清单,便于下拉选择)
|
model tb_consumable_goods {
|
||||||
model v2_ConsumableTempCatalog {
|
id BigInt @id @default(autoincrement())
|
||||||
id String @id @default(cuid())
|
goods_name String @db.VarChar(100)
|
||||||
name String
|
category_id BigInt?
|
||||||
spec String?
|
brand String? @db.VarChar(50)
|
||||||
unit String?
|
status Int? @default(0) @db.TinyInt
|
||||||
isActive Boolean @default(true)
|
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)
|
||||||
|
|
||||||
// 反向关系:哪些申请明细引用了该条目录
|
// 关联 SKU
|
||||||
requestItems v2_ConsumableTempRequestItem[]
|
tb_consumable_sku tb_consumable_sku[]
|
||||||
|
|
||||||
@@unique([name, spec, unit])
|
@@index([category_id], map: "idx_goods_category")
|
||||||
@@index([isActive])
|
}
|
||||||
@@map("易耗品目录")
|
|
||||||
|
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 {
|
enum v2_ConsumableTempRequestStatus {
|
||||||
PENDING // 待审批
|
PENDING
|
||||||
APPROVED // 通过
|
APPROVED
|
||||||
REJECTED // 驳回
|
REJECTED
|
||||||
CANCELLED // 取消
|
CANCELLED
|
||||||
COMPLETED // 已完成(已领用)
|
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,
|
requestItemResponseSchema,
|
||||||
requestResponseSchema
|
requestResponseSchema
|
||||||
} from '../schemas/request.schemas.js';
|
} from '../schemas/request.schemas.js';
|
||||||
import {
|
import { goodsWithSkuResponseSchema, skuResponseSchema } from '../schemas/goods.schemas.js';
|
||||||
catalogCreateSchema,
|
|
||||||
catalogUpdateSchema,
|
|
||||||
catalogResponseSchema
|
|
||||||
} from '../schemas/catalog.schemas.js';
|
|
||||||
import { errorResponseSchema } from '../schemas/common.js';
|
import { errorResponseSchema } from '../schemas/common.js';
|
||||||
import { registerRequestPaths } from './request.openapi.js';
|
import { registerRequestPaths } from './request.openapi.js';
|
||||||
import { registerItemPaths } from './item.openapi.js';
|
import { registerItemPaths } from './item.openapi.js';
|
||||||
import { registerCatalogPaths } from './catalog.openapi.js';
|
import { registerGoodsPaths } from './goods.openapi.js';
|
||||||
|
|
||||||
export function createOpenAPIDocument() {
|
export function createOpenAPIDocument() {
|
||||||
const registry = new OpenAPIRegistry();
|
const registry = new OpenAPIRegistry();
|
||||||
@ -25,17 +21,16 @@ export function createOpenAPIDocument() {
|
|||||||
registry.register('CreateRequest', createRequestSchema);
|
registry.register('CreateRequest', createRequestSchema);
|
||||||
registry.register('UpdateRequest', updateRequestSchema);
|
registry.register('UpdateRequest', updateRequestSchema);
|
||||||
registry.register('UpdateStatus', updateStatusSchema);
|
registry.register('UpdateStatus', updateStatusSchema);
|
||||||
registry.register('CatalogCreate', catalogCreateSchema);
|
|
||||||
registry.register('CatalogUpdate', catalogUpdateSchema);
|
|
||||||
registry.register('RequestItemResponse', requestItemResponseSchema);
|
registry.register('RequestItemResponse', requestItemResponseSchema);
|
||||||
registry.register('RequestResponse', requestResponseSchema);
|
registry.register('RequestResponse', requestResponseSchema);
|
||||||
registry.register('CatalogResponse', catalogResponseSchema);
|
registry.register('SKUResponse', skuResponseSchema);
|
||||||
|
registry.register('GoodsWithSKUResponse', goodsWithSkuResponseSchema);
|
||||||
registry.register('ErrorResponse', errorResponseSchema);
|
registry.register('ErrorResponse', errorResponseSchema);
|
||||||
|
|
||||||
// 注册路由
|
// 注册路由
|
||||||
registerRequestPaths(registry);
|
registerRequestPaths(registry);
|
||||||
registerItemPaths(registry);
|
registerItemPaths(registry);
|
||||||
registerCatalogPaths(registry);
|
registerGoodsPaths(registry);
|
||||||
|
|
||||||
// 生成文档
|
// 生成文档
|
||||||
const generator = new OpenApiGeneratorV3(registry.definitions);
|
const generator = new OpenApiGeneratorV3(registry.definitions);
|
||||||
@ -44,7 +39,7 @@ export function createOpenAPIDocument() {
|
|||||||
info: {
|
info: {
|
||||||
title: '智能配送耗材临时申请 API',
|
title: '智能配送耗材临时申请 API',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
description: '智能配送系统的耗材临时申请管理 API,支持申请创建、审批、物料明细管理和目录管理'
|
description: '智能配送系统的耗材临时申请管理 API,支持申请创建、审批、物料明细管理和易耗品种类查询'
|
||||||
},
|
},
|
||||||
servers: [
|
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,
|
quantity: parsed.quantity,
|
||||||
unit: parsed.unit,
|
unit: parsed.unit,
|
||||||
estimatedUnitCost: parsed.estimatedUnitCost !== undefined ? new Prisma.Decimal(parsed.estimatedUnitCost) : undefined,
|
estimatedUnitCost: parsed.estimatedUnitCost !== undefined ? new Prisma.Decimal(parsed.estimatedUnitCost) : undefined,
|
||||||
remark: parsed.remark,
|
remark: parsed.remark
|
||||||
catalogId: parsed.catalogId
|
|
||||||
}
|
}
|
||||||
}).catch(e => { if ((e as any).code === 'P2025') return null; throw e; });
|
}).catch(e => { if ((e as any).code === 'P2025') return null; throw e; });
|
||||||
if (!item) return res.status(404).json({ error: 'Not Found' });
|
if (!item) return res.status(404).json({ error: 'Not Found' });
|
||||||
|
|||||||
@ -29,8 +29,7 @@ router.post('/', asyncHandler(async (req, res) => {
|
|||||||
quantity: i.quantity,
|
quantity: i.quantity,
|
||||||
unit: i.unit,
|
unit: i.unit,
|
||||||
estimatedUnitCost: i.estimatedUnitCost !== undefined ? new Prisma.Decimal(i.estimatedUnitCost) : undefined,
|
estimatedUnitCost: i.estimatedUnitCost !== undefined ? new Prisma.Decimal(i.estimatedUnitCost) : undefined,
|
||||||
remark: i.remark,
|
remark: i.remark
|
||||||
catalogId: i.catalogId ?? null
|
|
||||||
})) }
|
})) }
|
||||||
},
|
},
|
||||||
include: { items: true }
|
include: { items: true }
|
||||||
@ -109,8 +108,7 @@ router.put('/:id', asyncHandler(async (req, res) => {
|
|||||||
quantity: item.quantity ?? undefined,
|
quantity: item.quantity ?? undefined,
|
||||||
unit: item.unit,
|
unit: item.unit,
|
||||||
estimatedUnitCost: item.estimatedUnitCost !== undefined ? new Prisma.Decimal(item.estimatedUnitCost) : undefined,
|
estimatedUnitCost: item.estimatedUnitCost !== undefined ? new Prisma.Decimal(item.estimatedUnitCost) : undefined,
|
||||||
remark: item.remark,
|
remark: item.remark
|
||||||
catalogId: item.catalogId
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -121,8 +119,7 @@ router.put('/:id', asyncHandler(async (req, res) => {
|
|||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
unit: item.unit,
|
unit: item.unit,
|
||||||
estimatedUnitCost: item.estimatedUnitCost !== undefined ? new Prisma.Decimal(item.estimatedUnitCost) : undefined,
|
estimatedUnitCost: item.estimatedUnitCost !== undefined ? new Prisma.Decimal(item.estimatedUnitCost) : undefined,
|
||||||
remark: item.remark,
|
remark: item.remark
|
||||||
catalogId: item.catalogId
|
|
||||||
}});
|
}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -168,8 +165,7 @@ router.post('/:id/items', asyncHandler(async (req, res) => {
|
|||||||
quantity: parsed.quantity,
|
quantity: parsed.quantity,
|
||||||
unit: parsed.unit,
|
unit: parsed.unit,
|
||||||
estimatedUnitCost: parsed.estimatedUnitCost !== undefined ? new Prisma.Decimal(parsed.estimatedUnitCost) : undefined,
|
estimatedUnitCost: parsed.estimatedUnitCost !== undefined ? new Prisma.Decimal(parsed.estimatedUnitCost) : undefined,
|
||||||
remark: parsed.remark,
|
remark: parsed.remark
|
||||||
catalogId: parsed.catalogId ?? null
|
|
||||||
}});
|
}});
|
||||||
res.status(201).json({ data: item });
|
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: '数量' }),
|
quantity: z.number().int().positive().openapi({ example: 10, description: '数量' }),
|
||||||
unit: z.string().optional().openapi({ example: '箱', description: '单位' }),
|
unit: z.string().optional().openapi({ example: '箱', description: '单位' }),
|
||||||
estimatedUnitCost: z.number().positive().optional().openapi({ example: 25.5, description: '预估单价' }),
|
estimatedUnitCost: z.number().positive().optional().openapi({ example: 25.5, description: '预估单价' }),
|
||||||
remark: z.string().optional().openapi({ description: '备注' }),
|
remark: z.string().optional().openapi({ description: '备注' })
|
||||||
catalogId: z.string().optional().openapi({ description: '关联的目录ID' })
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createRequestSchema = z.object({
|
export const createRequestSchema = z.object({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user