1. 纺织行业进销存系统的架构选型与整体设计思路
1.1 为什么纺织行业的进销存不能用通用模板
做过纺织行业信息化的人都知道,这个行业的进销存跟普通商贸企业完全不是一个量级。普通商贸的SKU可能就是“A款红色M码”,一个商品对应一个条码,入库出库扫一下就行。但纺织行业呢?同一匹布,要同时记录缸号、色号、匹号、克重、门幅、批号,还要区分正品、次品、尾布。一个订单可能涉及几十个缸号,每个缸号下面又有几十匹布,每匹布的米数还不一样。这种多维度的属性组合,用通用进销存模板去套,基本上第二周就会崩溃。
我接触过好几个纺织厂的老板,他们最开始都是买通用ERP,结果用不到三个月就退回Excel了。原因很简单:通用系统处理不了“同一物料多批次混放”的场景。比如客户下单要5000米某色号的面料,仓库里可能有三个缸号凑得出来,但每个缸号的色差等级不同,需要按客户要求的色差范围去匹配。这种逻辑通用系统根本没法配。
所以这套系统的核心设计目标就三个:第一,支持多维度批次管理;第二,AI辅助匹配库存与订单需求;第三,前后端分离,方便后续扩展移动端和PDA扫码。技术选型上,前端用Vue3 + TypeScript + Element Plus,后端用NestJS + TypeORM + PostgreSQL,AI部分用Python微服务单独跑模型推理,通过HTTP跟主服务通信。
1.2 前后端技术栈的取舍逻辑
先说前端为什么选Vue3而不是React。团队之前维护过一个React的老项目,class组件和hooks混用,状态管理用Redux,一个简单的表单联动要写一堆action和reducer。Vue3的Composition API在组织逻辑上更符合直觉,尤其是进销存这种表单密集、表格密集的系统,用<script setup>写起来代码量能少三成。而且Element Plus的表格组件对中文排版支持很好,虚拟滚动、固定列、合计行这些开箱即用,省了很多造轮子的时间。
后端选NestJS的原因更直接:它自带依赖注入和模块化机制,天然适合按业务域拆分模块。进销存系统至少涉及采购、销售、库存、财务、报表五个大模块,用NestJS的Module系统可以做到模块间低耦合。比如库存模块只暴露Service接口给销售模块调用,不直接操作对方的Repository。这种约束在项目后期多人协作时特别重要,否则很容易出现一个模块直接改另一个模块数据库表的情况。
数据库选PostgreSQL而不是MySQL,主要是看中它的JSONB字段类型和窗口函数。纺织品的属性是动态的,不同面料需要记录的参数不一样,用JSONB存扩展属性比建一堆稀疏列要灵活得多。窗口函数在生成库存周转率、库龄分析这类报表时也比MySQL写起来顺手。
1.3 AI能力在进销存中的实际落点
很多文章一谈AI进销存就扯“智能预测”“自动补货”,但实际落地时,纺织厂最需要的AI能力其实很具体。我梳理了三个真正能产生价值的场景:
第一个是色号匹配。客户来样下单时,系统需要从现有库存中找出色差在可接受范围内的缸号。传统做法是人工翻色卡比对,效率低且依赖老师傅经验。我们用Python训练了一个轻量级的颜色相似度模型,输入是客户来样的Lab色值,输出是库存中每个缸号的匹配分数,按分数排序推荐。模型本身不复杂,就是一个带注意力机制的双塔结构,但效果比人工比对稳定得多。
第二个是用量预测。纺织厂裁布时会有损耗,不同面料、不同款式的损耗率差异很大。系统会根据历史裁剪记录,用时间序列模型预测下一批订单的建议领料量,减少余料积压。
第三个是异常检测。库存数据录入时难免有笔误,比如把1000米录成10000米。系统用孤立森林算法对每日库存变动做异常检测,发现偏离正常波动范围的操作就推送给主管复核。
这三个场景的共同点是:AI只做辅助建议,最终决策权还在人手里。这样既降低了AI出错带来的风险,也让一线员工更容易接受。
2. 核心模块的数据库设计与关键字段解析
2.1 物料主数据表的字段设计
物料表是整个系统的基石,设计不好后面全是坑。我的做法是把物料信息拆成三层:物料基础表、物料属性表、物料批次表。
物料基础表只存最核心的字段:物料编码、物料名称、物料分类、基本单位、创建时间。物料编码用“分类码+流水号”的规则生成,比如“FAB-0001”表示面料类第一个物料。这里有个经验:编码规则一旦确定就不要改,因为所有关联表都存了物料编码作为外键,改一次要动几十张表。
物料属性表用JSONB存扩展属性,结构大概是这样:
{ "成分": "涤纶65% 棉35%", "克重": 220, "门幅": 150, "组织": "斜纹", "色牢度": 4, "缩水率": 2.5 }用JSONB的好处是不同品类可以有不同的属性集,面料和辅料的属性字段完全不用统一。查询时用PostgreSQL的->>操作符提取字段,配合GIN索引性能也够用。
物料批次表是纺织行业特有的,每个批次记录缸号、色号、匹号、实际米数、色差等级、入库时间、库位。这里的关键设计是批次号生成规则:物料编码+入库日期+缸号+序号。比如“FAB-0001-20250315-G001-01”表示面料0001在2025年3月15日入库的G001缸第一匹。这个规则保证了批次号的唯一性和可追溯性。
2.2 库存流水表的设计要点
库存流水表记录每一次库存变动的明细,是进销存系统的核心表。字段包括:流水号、物料编码、批次号、变动类型(入库/出库/调拨/盘点)、变动数量、变动前数量、变动后数量、关联单据号、操作人、操作时间。
这里有个设计决策:要不要存变动前和变动后数量。存的好处是查历史库存时不用从头累加,直接取最近一条流水的“变动后数量”就行。坏处是每次操作都要读一次当前库存,增加了一次查询。我的选择是存,因为纺织厂的库存查询频率远高于写入频率,用空间换时间是划算的。
另一个关键点是并发控制。同一批次可能同时被多个出库单扣减,如果不加锁会出现超卖。NestJS里用TypeORM的悲观锁实现:
await this.dataSource.transaction(async (manager) => { const batch = await manager.findOne(Batch, { where: { batchNo }, lock: { mode: 'pessimistic_write' }, }); if (batch.quantity < deductQty) { throw new BadRequestException('库存不足'); } batch.quantity -= deductQty; await manager.save(batch); // 写入流水记录 });悲观锁在并发量不大的内部系统里完全够用,比乐观锁重试逻辑简单得多。
2.3 订单与库存的关联设计
销售订单和库存的关联是进销存最复杂的部分。纺织行业的订单通常不是“一个订单对应一个批次”,而是“一个订单行对应多个批次”。比如客户要5000米,系统需要从三个缸号里分别扣减2000米、1800米、1200米。
我的设计是加一张订单批次关联表,记录订单行号、批次号、扣减数量、匹配分数(AI推荐时用)。这样既保留了订单和批次的对应关系,又不会让订单表变得臃肿。查询时通过订单行号关联,可以快速知道某个订单用了哪些批次。
AI色号匹配的分数也存这张表里,方便后续复盘。如果客户反馈色差问题,可以回溯当时匹配的分数和实际色差,用来优化模型。
3. Vue3前端架构与核心交互实现
3.1 项目目录结构与模块划分
Vue3项目的目录结构直接决定了后期维护成本。我采用的是按业务域划分的目录结构,而不是按文件类型划分:
src/ modules/ purchase/ # 采购模块 api/ components/ views/ stores/ sales/ # 销售模块 inventory/ # 库存模块 report/ # 报表模块 shared/ # 共享组件和工具 router/ stores/每个模块内部再分api、components、views、stores四层。这种结构的好处是:当你要改采购模块时,所有相关文件都在一个目录下,不用在components、views、api之间来回跳。新来的同事也能快速定位代码。
路由配置用动态导入实现懒加载:
const routes = [ { path: '/purchase', component: () => import('@/modules/purchase/views/PurchaseLayout.vue'), children: [ { path: 'order', component: () => import('@/modules/purchase/views/OrderList.vue'), }, ], }, ];这样每个模块的代码只在访问时才加载,首屏体积能控制在200KB以内。
3.2 库存查询页面的性能优化
库存查询是使用频率最高的页面,也是性能最容易出问题的地方。纺织厂的库存记录动辄几十万条,如果一次性加载肯定卡死。我的优化方案分三步:
第一步,后端分页+前端虚拟滚动。后端用TypeORM的skip和take做分页,每页50条。前端用Element Plus的el-table-v2虚拟滚动组件,只渲染可视区域的行。实测下来,10万条数据滚动也很流畅。
第二步,查询条件防抖。用户输入物料编码时,不是每敲一个字符就发请求,而是等300毫秒后再发。用Vue3的watch配合debounce实现:
import { debounce } from 'lodash-es'; const searchKeyword = ref(''); const debouncedSearch = debounce((val: string) => { fetchInventory(val); }, 300); watch(searchKeyword, (val) => { debouncedSearch(val); });第三步,结果缓存。相同的查询条件在5分钟内直接返回缓存结果,用Map存查询条件和结果的映射。这个优化在用户反复调整筛选条件时特别有效。
3.3 表单联动与动态字段处理
进销存系统的表单联动特别多。比如选择物料后,需要自动带出该物料的规格、单位、默认仓库;选择批次后,需要显示该批次的可用数量和库位。用Vue3的computed和watch可以很优雅地处理:
const selectedMaterial = ref<Material | null>(null); const availableBatches = ref<Batch[]>([]); watch(selectedMaterial, async (material) => { if (material) { form.unit = material.unit; form.warehouse = material.defaultWarehouse; availableBatches.value = await fetchBatches(material.code); } });动态字段的处理用v-if配合字段配置数组。比如面料需要显示克重和门幅,辅料不需要,就在字段配置里加一个visible函数:
const fieldConfig = [ { key: 'weight', label: '克重', visible: (type) => type === 'fabric' }, { key: 'width', label: '门幅', visible: (type) => type === 'fabric' }, ];这样新增品类时只需要改配置,不用改模板代码。
4. NestJS后端架构与AI服务集成
4.1 模块划分与依赖注入实践
NestJS的模块划分我遵循“一个业务域一个模块”的原则,但有一个例外:库存模块被多个模块依赖,所以我把库存模块拆成了InventoryCoreModule和InventoryApiModule。CoreModule只暴露Service,ApiModule暴露Controller。其他模块只依赖CoreModule,不直接调ApiModule。
依赖注入的一个实用技巧是用@InjectRepository注入Repository时,配合自定义Provider做数据权限过滤。比如销售员只能看自己负责的客户订单,就在Provider里注入当前用户信息,Repository查询时自动加where条件。
模块间的通信优先用事件而不是直接调用。比如销售出库后需要更新库存,不是销售模块直接调库存模块的Service,而是发一个SalesOutboundEvent,库存模块监听这个事件再处理。这样销售模块不需要知道库存模块的存在,耦合度更低。
4.2 AI微服务的通信与容错设计
AI服务用Python的FastAPI单独部署,NestJS通过HTTP调用。这里的关键是超时和降级。AI推理偶尔会慢,不能让主流程等它。我的做法是设置500毫秒超时,超时后直接返回默认推荐(比如按入库时间排序),同时记录一条日志供后续分析。
调用AI服务的代码大概是这样:
async getColorMatchRecommendation(sampleLab: LabColor): Promise<MatchResult[]> { try { const response = await firstValueFrom( this.httpService.post('/api/color-match', sampleLab, { timeout: 500, }).pipe( timeout(500), catchError(() => of({ data: [] })), ), ); return response.data; } catch { this.logger.warn('AI服务超时,降级为默认排序'); return this.getDefaultRecommendation(sampleLab); } }降级策略保证了AI服务挂掉时主流程不受影响,只是推荐质量下降。这在生产环境特别重要,不能因为AI服务不稳定导致整个系统不可用。
4.3 数据库迁移与版本管理
TypeORM的迁移功能在多人协作时是必须的。我的规范是:任何数据库结构变更都必须通过迁移文件,禁止直接改实体后让TypeORM自动同步。自动同步在开发环境方便,但在生产环境是灾难,因为可能会丢数据。
迁移文件的命名规则是时间戳-描述.ts,比如1710000000000-AddBatchColorGrade.ts。每个迁移文件必须包含up和down两个方法,down方法用于回滚。写迁移时要注意:加字段要给默认值,否则已有数据会报错;删字段前要先确认没有代码引用。
5. 实操部署与常见问题排查
5.1 本地开发环境搭建步骤
本地开发环境用Docker Compose一键启动依赖服务:
version: '3.8' services: postgres: image: postgres:15 environment: POSTGRES_DB: textile_erp POSTGRES_USER: dev POSTGRES_PASSWORD: dev123 ports: - '5432:5432' redis: image: redis:7 ports: - '6379:6379'前端启动:npm run dev,后端启动:npm run start:dev,AI服务启动:uvicorn main:app --reload --port 8000。三个服务都起来后,访问http://localhost:5173就能看到登录页。
这里有个坑:PostgreSQL的时区默认是UTC,而纺织厂的操作时间都是北京时间。需要在连接配置里加timezone: 'Asia/Shanghai',否则入库时间会差8小时。
5.2 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 库存数量对不上 | 并发扣减未加锁 | 查流水表看是否有并发操作 | 加悲观锁或改用队列串行处理 |
| AI推荐结果为空 | AI服务未启动或超时 | 检查AI服务日志和网络连通性 | 确认服务启动,检查超时配置 |
| 前端表格卡顿 | 数据量过大未分页 | 打开DevTools看渲染耗时 | 启用虚拟滚动和分页 |
| 批次号重复 | 生成规则有并发问题 | 查数据库唯一索引冲突日志 | 用数据库序列或Redis原子操作生成 |
| 迁移执行失败 | 字段有非空约束但无默认值 | 看迁移报错信息 | 加默认值或分步迁移 |
5.3 实操心得与避坑经验
第一个心得:库存扣减一定要用事务。我见过一个项目因为没用事务,出库时扣了库存但流水没写进去,导致库存和流水对不上,查了三天才找到原因。用TypeORM的dataSource.transaction包起来,要么全成功要么全回滚。
第二个心得:AI模型的输入要做校验。客户来样的Lab色值如果超出正常范围(比如L值大于100),模型会输出奇怪的结果。在调用AI服务前加一层校验,超出范围直接返回错误提示,比让模型瞎猜要好。
第三个心得:前端表单的防重复提交。进销存系统里用户手快连点两次提交按钮的情况太常见了。我的做法是在提交时给按钮加loading状态,同时后端用请求ID做幂等校验。请求ID由前端生成,放在Header里,后端Redis里存5分钟,重复的请求ID直接返回上次结果。
第四个心得:日志要记录关键业务操作。库存变动、订单状态变更、AI推荐结果这些都要记日志。不是记在文件里,而是记在数据库的operation_log表里,方便按时间、操作人、单据号查询。出问题时能快速定位是谁在什么时候做了什么操作。
6. 系统扩展方向与个人体会
这套架构目前支撑了三个纺织厂的日常运营,日均处理订单200多单,库存记录50多万条。后续扩展我主要考虑两个方向:一是移动端PDA扫码入库,用Vue3的移动端适配方案,复用现有的API;二是把AI色号匹配模型换成更轻量的版本,直接跑在NestJS里用ONNX Runtime推理,省掉Python微服务的部署成本。
我个人在实际操作中的体会是:纺织行业的信息化没有银弹,通用系统解决不了行业特有问题,定制开发又容易陷入需求蔓延。这套架构的价值在于用标准化的技术栈(Vue3+NestJS)承载了行业特有的业务逻辑(批次管理、色号匹配),既保证了开发效率,又满足了业务需求。如果你也在做类似的项目,建议先把批次管理和库存流水这两张表设计好,这两张表设计对了,后面的事情就顺了。