Skip to content

用户要用 d:/VOL源码(VOL Pro 3.5.8)开发一套完整的 ERP 系统。2026-10-19 已对其关注的能力做源码级评估,结论:17 项中 12 项开箱即用、3 项需定制、1 项未实现、1 项属业务开发。

Why: 这是当前项目的核心目标,后续所有开发讨论都围绕"这套框架能否承载 ERP"展开,重复评估会浪费大量时间。

How to apply:

  • 唯一硬缺口是文件存储 OSS:src/components/basic/VolUpload.vue 里 import OSS from 'ali-oss' 被注释、uploadOSS() 是空函数(保留扩展点),后端 ServiceBase.Upload() 只写本地磁盘。用户若要云存储,需自建(建议 MinIO,私有化友好)。
  • 两个需提前决策:打印(hiprint 依赖 socket.io-client,静默/连续打印需本地客户端,可能要改服务端 PDF);Quartz 是内存调度器无持久化 JobStore,多实例部署会重复执行任务。
  • 不要高估框架自带 ERP 模块:VolPro.ERP 的 42 个实体是演示级,库存计价(加权平均/移动平均)、成本结转、往来核销、账期账龄等真实 ERP 核心都不完整,必须重写。
  • 用户关注的能力清单:主从表一起保存、表格筛选/排序/合计/单元格合并、导入导出、打印、审核流、自定义列格式、自定义查询条件、上下游数据关联加载、OSS、消息推送、审核流推送、邮件推送、定时任务。
  • 完整评估记录在 d:/VOL源码/VOL_Pro_ERP_能力评估.md(含每项的文件路径与代码证据),需要细节直接查该文件。

2026-10-19 的成本估算(前提:1 名熟悉 Vue3+.NET 的中高级全栈):进销存+往来约 70–100 人日;完整 ERP(含生产与总账)约 150–220 人日;纯自研约 300–450 人日,即框架节省 55%–65%。

Why: 框架砍掉的是 CRUD 那 60%,而 ERP 真正的成本在库存事务、成本核算、单据流转、往来核销——这些框架帮不上,且比重高于直觉。

How to apply:

  • 排期按「单据复杂度」而非「单据数量」估:简单档案 0.5 人日/个,带明细单据 2.5–3.5 人日/个,带上下游流转 4–5 人日/个。
  • 三个最高价值的架构建议(违反会大面积返工):①先抽 ErpBillService<TMaster,TDetail> 公共基类再写第一个单据;②库存操作收敛到唯一入口 InventoryService.UpdateStock,严禁各单据自己写 SQL 改库存;③建通用 Erp_BillRelation 单据关系表,避免每对上下游写一套反写逻辑。
  • 金额精度/税率规则、多租户 TenancyId、库存扣减时机(保存时 vs 审核通过时)必须在建模前定死,后期改是灾难。
  • 建议先做两周垂直切片(物料→采购订单→采购入库→审批→打印→库存查询带合计)验证误差,再铺开。
  • 完整清单见 d:/VOL源码/ERP开发成本评估与流程清单.md。

2026-10-20 已落地的关键设计决策(切片地基,写代码前必须知道):

Why: 这几条是为了避免"两个字段表达同一件事"和"库存对不上"这两类必然返工的问题,当时花了很多时间比对库表与实体才定下来。

How to apply:

  • 建模路线定为"改造现有 ERP_ 表"(用户授权在远程库 vol_pro_service 上改),建模脚本 sqlserver/erp_slice_01_ddl.sql,幂等可重复执行。
  • 库存扣减时机 = 审核通过时(AuditWorkFlowExecuted 且 lastAudit=true),草稿不扣,否则作废单污染库存。
  • 实体扩展放独立 partial 文件 VolPro.Entity/DomainModels/erp/ErpBillExt.cs,不写进生成器产出的主实体文件。
  • ERP_Inventory.Cost 与 Amount 都存库存金额(Cost 是非空 decimal,旧页面在显示它,两者必须同步写,否则页面看到错值)。
  • 上下游关联复用已有的 ERP_PurchaseInDetail.PurchaseOrderDetailId,不要新增 SourceOrderId 之类的平行字段(我曾加了又删掉)。
  • 手写服务用 new 传入 Repository(与示例 ErpInventoryStockService 一致),不走 Autofac 注册;这样与调用方共用 DbContext,自然处于同一事务。
  • 框架按 GetInterfaces()[0] 注册 IDependency 实现类,所以手写 Repository 的类声明顺序必须照抄生成器模板:class X : RepositoryBase<T>, IXRepository。
  • 冲销(反审)同样不允许负库存,失败就报错让人介入,不悄悄冲成负数。

2026-10-20 前端「选单」范式与踩过的坑(写上下游选单必读):

Why: 采购入库页面原本就自带选单代码,但它是坏的——不返回 MaterialId、不扣已入库数量、不过滤未审核订单。这三个缺陷里任何一条都会让库存对不上或直接过账失败。

How to apply:

  • 选单范式:gridRef.getFormOption('字段名') 拿到表单字段配置 → 设 type:'selectTable'、url、columns、onSelect(rows)、loadBefore(param,cb)、single、height/selectWidth、pagination。范例:views/erp/erp/ERP_InventoryIo/purchaseInSetup.js。
  • 明细计算一律放后端,前端只回填。后端返回的结构直接 push 进 gridRef.getTable().rowData。
  • options.js 是生成物不能改:新增明细列在 onInited 里运行时 detail.columns.push({...}),先 find(x => x.field == f) 判重。
  • editFormFields 的隐藏字段必须先声明再赋值(if (editFormFields.X === undefined) editFormFields.X = ''),否则 selectTable 回写的值不进提交数据。这是最隐蔽的坑。
  • extension jsx 里 this 就是 ViewGrid 实例,可用 this.$message、this.detailOptions、this.editFormFields;addBefore(formData) 的明细在 formData.detailData(是数组,不是 { 子表名: [] })。
  • 校验逻辑放 extension/**/*.jsx(生成器不覆盖),选单交互可以放 .vue——即使 .vue 被重新生成,校验仍在,只是失去选单能力。
  • 打印模板存 Sys_PrintOptions.html(hiprint JSON),只能靠可视化设计器拖拽;审批流走 FormWorkFlow 界面配置。两者都是配置工作而非编码,不适合用 SQL 硬写。

2026-10-20 采购入库「多单合并入库」设计 + 两个真踩到的坑:

交互形态(一张入库单可合并同一供应商的多张采购订单,且同一订单支持分批来货): 主表选供应商 → 编辑弹窗内「选择采购订单」按钮 → 弹窗上半是该供应商待入库订单、下半是该单明细 → 跨单累积勾选 → 确认。采购单号记在明细行(ERP_PurchaseInDetail.PurchaseOrderNo),主表 PurchaseOrderNo 只有 nvarchar(50),放不下多张单号,只存概要"PO001 等3张"。

How to apply(组件怎么挂上去):

  • 自定义组件挂 ViewGrid 的 modelBody 插槽(编辑弹窗内、表单下方、明细表上方),在 extension 里 components: { modelBody: Xxx.vue }。
  • 组件与页面通信走框架官方口子:emit('parentCall', ($grid) => { ... }),框架把 gridRef 回传,拿到 $grid 就能 $grid.getTable().rowData 操作明细、$grid.editFormFields 改表单。
  • 跨订单勾选绝不能存在表格上:切换订单时下方表格整体重载,勾选会丢。必须存 Map<orderDetailId, row>,切回时用 toggleRowSelection 恢复;onSelectionChange 里要先清掉当前单的旧 key 再写回,否则取消勾选会残留。
  • 确认时只传勾选的明细 Id,数量一律后端重算 —— 信任前端传的数量,两人并发操作同一订单就能把 100 件收成 200 件。

坑1(最严重,差点让功能完全不可用):SQL 三值逻辑。 ERP_PurchaseOrder.Status 是 int?,而现库 7 张订单这一列全是 NULL。在 FindAsIQueryable 里写 x.Status != 4 时,NULL != 4 求值 UNKNOWN,整行被过滤 → 选单永远返回空列表。必须写成 (x.Status == null || (x.Status != 4 && x.Status != 5))。 注意区分:内存里(已 ToList 后的 C# 判断)不受此影响,null != 4 是 true。所以只有 SQL 层过滤要小心。改这类可空状态字段的查询时,先 SELECT Status, COUNT(*) ... GROUP BY Status 看看 NULL 占比。

坑2:ERP_PurchaseOrder.SupplierId 与 SupplierNameId 在 C# 里都是 Guid?,不是字符串(前端 options 里写的 type:'string' 只是渲染用,别被误导)。库里两列存同一个值,判断同供应商时两个字段都要认。

其他:明细/订单的扩展字段统一写在 VolPro.Entity/DomainModels/erp/ErpBillExt.cs(手写区,生成器不覆盖),对应 sqlserver/erp_slice_01_ddl.sql;主实体文件是生成物不能改。

2026-10-20 补充:选单组件的两个修正(用户实测反馈后)

勾选丢失的真实根因:表格在数据被整体替换时会触发一次 selection-change 并传空数组。如果在该事件里无条件按"当前表格里未选中"去清理勾选池,那么切回上一张单时,之前勾的行就被这次空事件误删了。 How to apply:切换数据期间用 switching 标志位把这类事件挡掉,换完数据再按 Map 用 toggleRowSelection 手动恢复。 别指望框架自带的 reserveSelection:它是为分页翻页设计的,靠 isPageLoad 标志跳过清理,只有走 load() 才置位;手动换 table-data 用不上,同样的误删照样发生。

让自定义弹窗融入框架样式的三条:

  • 弹窗用 vol-box(全局注册,有 #footer 插槽),不要用 el-dialog。
  • 表格用 vol-table(需 import @/components/basic/VolTable.vue)::ck 多选、:selectable="(row,index)=>bool"、:column-index、:pagination-hide、:load-key="false"。本地数据必须用 splice 就地更新,别整体替换数组引用(框架约定,注释明文写了"外部只能 splice+push")。
  • 按钮塞进 gridRef.detailOptions.buttons(框架明细表工具栏),和"新增行/删除行"同款渲染,审核态还会自动隐藏 —— 千万别在 modelBody 里自己造一排按钮,会多出一行且风格不一致。

自定义接口传参约定(415 坑,实测踩到): 前端 axios 全局设了 Content-Type: application/json(src/api/http.js 第 7 行)。自定义接口若用 [FromBody] Guid / [FromBody] string 收裸标量,请求体就是一串不带引号的字符,不是合法 JSON → ASP.NET Core 直接 415,且响应体为空,前端表现为 SyntaxError: Unexpected end of JSON input + “服务器处理异常”,很容易误判成接口没注册或后端崩了。 How to apply:自定义接口一律用对象传参并定义请求 DTO;DTO 每个属性显式写 [JsonPropertyName("camelCase")] —— 框架全局命名策略不确定,不显式指定可能绑成 Guid.Empty,表现为“静默查不到数据”而不报错,比 415 更难查。 注意:项目里官方示例(如 voidBill)用的正是 [FromBody] Guid 裸字符串,别照抄。 排查同类问题时可查 swagger:/swagger/v1/swagger.json 里看 requestBody.$ref 和属性名,能立刻确认契约是否对得上(比端到端调用快,且不需要登录)。

parentCall 的生效条件(易踩):只有挂在 extension 的 components.modelBody 上才有效,因为 ViewGrid 用 <component :is> 渲染并监听了 @parentCall。若把组件放进页面 .vue 的 <template #modelBody> 插槽,emit 会发给页面而不是 ViewGrid,拿不到 $grid。当按钮必须在框架工具栏、弹窗必须在 modelBody 时,两者分属两条渲染链,用一个共享 ref 模块(如 poPickerBus.js)桥接即可。