ERP 采购链
ERP 采购链
业务用途
ERP 是平台「低代码配置 + 自研工作流落地」的完整范例。它覆盖了一条典型的采购闭环:采购需求 -> 采购计划 -> 采购询价 -> 采购订单 -> 送货 -> 到货 -> 验收 / 质检 -> 退货,外加合同变更延期、供应商管理、仓库管理。
整个系统没有手写一行业务表 SQL,也没有为每张表手写前端页面。它的表、字段、表单、列表页、菜单全部由 ErpAppSeeder 在应用启动时用声明式 Schema 驱动 SchemaService 动态生成;6 类核心单据(采购需求 / 询价 / 订单 / 验收 / 退货 / 合同变更,外加供应商入驻共 7 张可审批表)的审批复用平台 BPM 引擎,单据表的 approval_status 列由流程回调自动回写,与流程实例状态保持一致。
为什么把它当作范例
读懂 ErpAppSeeder 这一个文件,你就能掌握平台「数据驱动建表 -> 数据驱动建页 -> 工作流联动」的完整方法论。后续要落地新的行业应用(如 OA、CRM、MES),照搬这套 Seeder 模式即可。
涉及文件
Seeder(数据驱动 seeding 全流程)
| 文件 | 路径 | 职责 |
|---|---|---|
| 应用 Seeder | backend/src/main/java/com/lowcode/seeder/erp/ErpAppSeeder.java | CommandLineRunner,启动时建应用 + 16 表 + 字典 + BPM + 表单 + 页面 + 菜单 + mock 数据,支持增量补丁 |
| 表结构定义 | backend/src/main/java/com/lowcode/seeder/erp/ErpSchema.java | 16 个 TableDef + APPROVABLE_TABLES 列表 + toLcTableMeta 转换 |
| 字典定义 | backend/src/main/java/com/lowcode/seeder/erp/ErpDictionaries.java | 15 个字典类型及其字典项 |
| 演示数据 | backend/src/main/java/com/lowcode/seeder/erp/ErpMockData.java | 少量 mock 数据(税种 / 税率 / 仓库 / 供应商 / 采购需求) |
薄 CRUD 控制器(手写实体层)
| 控制器 | 路径 | 基础路径 | 实体表 |
|---|---|---|---|
| 采购计划 | controller/erp/ErpPurchasePlanController.java | /api/erp/purchasePlan | erp_purchase_plan |
| 采购订单 | controller/erp/ErpPurchaseOrderController.java | /api/erp/purchaseOrder | erp_purchase_order |
| 供应商 | controller/erp/ErpSupplierController.java | /api/erp/supplier | erp_supplier |
| 合同 | controller/erp/ErpContractController.java | /api/erp/contract | erp_contract |
| 客户 | controller/erp/ErpCustomerController.java | /api/erp/customer | erp_customer |
| 商品 | controller/erp/ErpProductController.java | /api/erp/product | erp_product |
| 库存 | controller/erp/ErpInventoryController.java | /api/erp/inventory | erp_inventory |
| 仓库 | controller/erp/ErpWarehouseController.java | /api/erp/warehouse | erp_warehouse |
| 销售订单 | controller/erp/ErpSaleOrderController.java | /api/erp/saleOrder | erp_sale_order |
对应服务在 service/erp/(接口 IErp*Service)与 service/impl/erp/(实现 Erp*ServiceImpl),实体在 entity/erp/。
工作流联动
| 文件 | 路径 | 职责 |
|---|---|---|
| 业务审批入口 | controller/BizApprovalController.java | /api/biz-approval/submit/{formId} 提交审批并启动流程 |
| 状态回写回调 | service/bpm/BpmBusinessStatusCallback.java | 流程启停时把 approval_status 回写到业务表 |
BPM 审批入口是平台统一的
ERP 自身没有为可审批表另写审批接口,所有 7 张可审批表都通过平台统一的 BizApprovalController(/api/biz-approval)提交审批、查询状态,详见 。
两套表的关系
ERP 模块同时存在两套表:(1) Seeder 动态建的 16 张 pms_* / srm_* / wms_* 表,走低代码运行时(列表页 + SchemaService),是 BPM 审批闭环的主角;(2) 手写实体的 erp_* 表(如 erp_purchase_order),由薄 CRUD 控制器直接操作。两者各自独立,文档后续会明确区分。审批 approval_status 联动只发生在第 (1) 套表上。
数据库表
Seeder 动态创建的 16 张表
通用基字段(id / tenant_id / create_time / update_time / create_by / update_by / deleted)由 SchemaService.generateCreateTableSql 自动补齐,下表只列业务字段。
采购组(businessGroup = purchase)
| 表名 | 注释 | 是否可审批 | 关键字段 |
|---|---|---|---|
pms_purchase_requirement | 采购需求 | ✅ | requirement_no、urgency_level(URGENCY_LEVEL)、approval_status、purchase_status、requirement_source、purchase_amount |
pms_purchase_plan | 采购计划 | — | plan_no、inquiry_status、purchase_status、approval_status、purchase_method、purchase_amount |
pms_purchase_result | 采购询价 | ✅ | result_no、plan_id、approval_status、supplier_id、final_amount |
pms_purchase_order | 采购订单 | ✅ | order_no、supplier_id、approval_status、order_status、total_quantity、total_price、delivery_date |
pms_delivery | 送货管理 | — | delivery_no、order_id、supplier_id、delivery_status、tracking_number、logistics_company |
pms_arrival | 到货管理 | — | arrival_no、order_id、arrival_status、quality_check_status、total_amount、arrival_time |
pms_check_accept | 验收单 | ✅ | check_accept_no、arrival_id、requirement_id、check_accept_status、approval_status、checker_id |
pms_quality_inspection | 质检信息 | — | quality_inspection_no、arrival_id、inspection_quantity、qualified_quantity、result |
pms_return | 退货管理 | ✅ | return_no、order_id、return_type、approval_status、total_quantity、total_price |
合同变更(businessGroup = contract)
| 表名 | 注释 | 是否可审批 | 关键字段 |
|---|---|---|---|
pms_change_extension | 变更延期 | ✅ | extension_no、contract_no、project_name、approval_status、original_finish_date、requested_finish_date、extension_reason |
供应商(businessGroup = supplier)
| 表名 | 注释 | 是否可审批 | 关键字段 |
|---|---|---|---|
srm_supplier_info | 供应商信息 | ✅ | supplier_code、supplier_name、approval_status、supplier_level、contact_person、tax_category_id、tax_rate_id |
srm_supplier_tax_category | 供应商税种类别 | — | code、name、rate |
srm_supplier_tax_rate | 供应商税率 | — | code、name、rate |

仓库(businessGroup = warehouse)
| 表名 | 注释 | 是否可审批 | 关键字段 |
|---|---|---|---|
wms_warehouse | 仓库 | — | code、name、type(WAREHOUSE_TYPE)、address、enabled_status、warehouse_administrator_id |
wms_warehouse_area | 库区 | — | warehouse_id、code、name、purpose、enabled_status |
wms_warehouse_location | 库位 | — | warehouse_area_id、code、name、enabled_status |
可审批表清单 APPROVABLE_TABLES
ErpSchema.APPROVABLE_TABLES 定义了需要挂接审批流的表(携带 approval_status 列的审批单据):
public static final List<String> APPROVABLE_TABLES = Arrays.asList(
"pms_purchase_requirement",
"pms_purchase_result",
"pms_purchase_order",
"pms_check_accept",
"pms_return",
"pms_change_extension",
"srm_supplier_info" // 供应商入驻审批
);注意:是 7 张而非 6 张
APPROVABLE_TABLES 实际包含 7 张表:除 5 张采购单据 + 1 张合同变更外,还包含 srm_supplier_info(供应商入驻也走审批)。凡是 isApprovable(tableName) 为 true 的表,Seeder 都会为它创建 LcForm 并绑定 BPM 流程定义。
手写实体表(薄 CRUD 层)
由 entity/erp/* 通过 @TableName 映射,共 8 张 + 1 张采购计划,结构与 Seeder 表相互独立:erp_product、erp_sale_order、erp_inventory、erp_customer、erp_supplier、erp_purchase_order、erp_warehouse、erp_contract、erp_purchase_plan。
平台元数据表(被 Seeder 写入)
| 表 | 用途 |
|---|---|
lc_application | ERP 应用记录(appCode = erp_carbon) |
lc_table_meta / lc_column_meta | 16 张表的表 / 列元数据 |
lc_form | 7 张可审批表的审批表单 |
lc_page | 16 张表对应的列表页 |
lc_app_menu | 4 个分目录 + 16 个叶子菜单 |
sys_dict_type / sys_dict_data | 15 个字典 |
bpm_process_definition_info | 采购审批流程定义 |
实现机制
一、ErpAppSeeder 总体流程
ErpAppSeeder 是一个 @Component + CommandLineRunner,@Order(Ordered.LOWEST_PRECEDENCE - 101),确保在 FoundationSeeder(-100)与 DataInitializer 之后执行。
@Component
@Order(Ordered.LOWEST_PRECEDENCE - 101)
public class ErpAppSeeder implements CommandLineRunner {
static final String APP_CODE = "erp_carbon";
static final String APP_NAME = "ERP";
static final String BPM_DEF_KEY = "PURCHASE_APPROVAL";
private static final Long DEFAULT_TENANT_ID = 1L;
// ...
}run() 方法的决策树:
启动
├─ 默认租户(id=1)不存在? -> 跳过本轮,等下次启动重试(守护)
├─ lc_application 中已有 appCode=erp_carbon?
│ ├─ 是 -> 进入「增量补丁模式」applyIncrementalPatch()
│ └─ 否 -> 进入「首次初始化」:
│ 1. createApplication() 建 lc_application
│ 2. createTables(appId) 建 16 张表(DDL + 元数据)
│ 3. createDictionaries(appId) 建 15 个字典
│ 4. createBpmDefinition(appId) 建采购审批流程定义
│ 5. createForms(...) 为 7 张可审批表建 LcForm
│ 6. createListPages(...) 为 16 张表建列表页
│ 7. createMenus(...) 建菜单(4 目录 + 16 叶子)
│ 8. seedMockData(...) 插入 mock 数据
└─ 任何异常 -> log.error,不阻断启动为什么要设租户上下文
Seeder 在执行前会把 TenantContext.setTenantId(DEFAULT_TENANT_ID)(默认租户 1),执行完在 finally 里恢复。因为后续的 applicationService / schemaService / pageService 等会经过租户拦截器,必须持有正确的 tenantId 才能正确写入带租户隔离的字段。
二、SchemaService.createTable 如何动态建表
这是整个低代码落地的核心。ErpAppSeeder.createTables 遍历 ErpSchema.all(),对每张表:
private Map<String, Long> createTables(Long appId) {
for (FoundationSchema.TableDef def : ErpSchema.all()) {
if (physicalTableExists(def.tableName)) {
// 物理表已存在 -> 只补元数据,不跑 DDL
ids.put(def.tableName, ensureMetadataOnly(def, appId));
continue;
}
LcTableMeta tm = ErpSchema.toLcTableMeta(def); // TableDef -> LcTableMeta
tm.setAppId(appId);
tm.setTenantId(DEFAULT_TENANT_ID);
schemaService.createTable(tm); // ★ DDL + 元数据一把梭
ids.put(def.tableName, tm.getId());
}
return ids;
}SchemaService.createTable(LcTableMeta) 内部做了三件事:
- 生成 DDL:
generateCreateTableSql拼出CREATE TABLE语句,自动加上id BIGINT AUTO_INCREMENT PRIMARY KEY、tenant_id、create_time/update_time/create_by/update_by/deleted等通用字段,并为每列带上COMMENT、NOT NULL、DEFAULT; - 执行 DDL:
jdbcTemplate.execute(sql)在数据库里建出物理表; - 写元数据:
tableMetaMapper.insert(tm)写lc_table_meta,循环columnMetaMapper.insert(col)写lc_column_meta。
生成的 DDL 长什么样
以 pms_purchase_requirement 为例(节选):
CREATE TABLE `pms_purchase_requirement` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'ID',
`tenant_id` BIGINT DEFAULT 1 COMMENT '租户ID',
`requirement_no` VARCHAR(64) NOT NULL COMMENT '需求编号',
`requirement_name` VARCHAR(255) COMMENT '需求名称',
`urgency_level` VARCHAR(50) COMMENT '紧急程度',
`approval_status` VARCHAR(50) DEFAULT 'PENDING' COMMENT '审批状态',
-- ... 其余业务字段
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_by` VARCHAR(50) COMMENT '创建人',
`update_by` VARCHAR(50) COMMENT '更新人',
`deleted` TINYINT DEFAULT 0 COMMENT '删除标记',
INDEX idx_tenant_id (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='采购需求';注意 approval_status 的 DEFAULT 'PENDING' --这是 ErpSchema.toLcColumns 在转换字段时专门给 approval_status 列设置的默认值,保证新建单据初始就是「待提交」状态。
ErpSchema.toLcTableMeta 把声明式 TableDef 转成平台 LcTableMeta:
public static LcTableMeta toLcTableMeta(FoundationSchema.TableDef def) {
LcTableMeta tm = new LcTableMeta();
tm.setTableName(def.tableName);
tm.setTableComment(def.tableComment);
tm.setTableType("business");
tm.setClassName(toClassName(def.tableName)); // pms_purchase_order -> PmsPurchaseOrder
tm.setModuleName("erp");
tm.setBusinessName(stripPrefix(def.tableName));
tm.setFunctionAuthor("erp_seeder");
tm.setColumns(toLcColumns(def.fields));
return tm;
}
public static List<LcColumnMeta> toLcColumns(List<FoundationSchema.Field> fields) {
List<LcColumnMeta> cols = FoundationSchema.toLcColumns(fields);
for (LcColumnMeta c : cols) {
if ("approval_status".equals(c.getColumnName())) {
c.setDefaultValue("PENDING"); // ★ 状态机初值
}
}
return cols;
}字段用 FoundationSchema.Field 的 DSL 声明(req / of / text / datetime / date / dictSel / bool / decimal / intF / bigintF / file 等,详见 ),例如:
list.add(new TableDef("pms_purchase_order", "采购订单", "erp", "purchase", false, Arrays.asList(
Field.req("order_no", "订单编号", "VARCHAR(64)"),
Field.bigintF("supplier_id", "供应商ID"),
Field.of("supplier_name", "供应商名称", "VARCHAR(255)"),
Field.dictSel("approval_status", "审批状态", "APPROVAL_STATUS"),
Field.dictSel("order_status", "订单状态", "ORDER_STATUS"),
Field.of("total_price", "总金额", "DECIMAL(18,2)"),
Field.date("delivery_date", "交货日期"),
Field.file("attachment", "附件"),
Field.text("remark", "备注")
)));三、字典 seeding
ErpDictionaries.all() 返回 15 个字典类型,createDictionaries 逐个写入 sys_dict_type + sys_dict_data,scopeType="APP" 并绑定 appId。写入前先查同租户同 dictType 是否已存在,存在则复用 typeId、跳过已有字典项,保证幂等。
15 个字典码:
| 字典码 | 名称 | 取值示例 |
|---|---|---|
APPROVAL_STATUS | 审批状态 | PENDING / IN_PROGRESS / APPROVED / REJECTED / WITHDRAWN |
URGENCY_LEVEL | 紧急程度 | NORMAL / URGENT / CRITICAL |
PURCHASE_STATUS | 采购状态 | PENDING / IN_PROGRESS / COMPLETED / CANCELED |
INQUIRY_STATUS | 询价状态 | PENDING / IN_PROGRESS / COMPLETED |
ORDER_STATUS | 订单状态 | PENDING / CONFIRMED / PRODUCING / SHIPPING / RECEIVED / COMPLETED / CANCELED |
DELIVERY_STATUS | 送货状态 | PENDING / IN_PROGRESS / DELIVERED |
ARRIVAL_STATUS | 到货状态 | PENDING / PARTIAL / ARRIVED |
QUALITY_CHECK_STATUS | 质检状态 | PENDING / IN_PROGRESS / QUALIFIED / UNQUALIFIED |
CHECK_ACCEPT_STATUS | 验收状态 | PENDING / IN_PROGRESS / ACCEPTED / REJECTED |
RETURN_TYPE | 退货类型 | QUALITY / QUANTITY / OTHER |
PURCHASE_METHOD | 采购方式 | PUBLIC_BIDDING / INVITE_BIDDING / INQUIRY / DIRECT |
REQUIREMENT_SOURCE | 需求来源 | PRODUCTION / REPLENISH / TEMPORARY |
SUPPLIER_LEVEL | 供应商等级 | LEVEL1 / LEVEL2 / LEVEL3 |
WAREHOUSE_TYPE | 仓库类型 | RAW / FINISHED / SEMI |
ENABLED_STATUS | 启用状态 | 1 / 0 |
四、BPM 流程定义创建
createBpmDefinition 创建一个单步审批的流程定义,key = PURCHASE_APPROVAL,所有 7 张可审批表共用这一条流程。
private Long createBpmDefinition(Long appId) {
// 已存在则复用
BpmProcessDefinitionInfo existed = bpmDefMapper.selectOne(...key = BPM_DEF_KEY...);
if (existed != null) return existed.getId();
Long approverId = resolveApproverUserId(); // 默认租户下首个 sys_user
BpmProcessDefinitionInfo def = new BpmProcessDefinitionInfo();
def.setAppId(appId);
def.setKey(BPM_DEF_KEY);
def.setName("采购审批流程");
def.setSimpleModel(buildSimpleModel(approverId)); // 单步:发起人 -> 审批
def.setStatus("deployed");
def.setFormType(10); // ★ 必填,10=自定义表单
def.setModelType(10);
def.setVisible(true);
bpmDefMapper.insert(def);
return def.getId();
}simpleModel 是平台 BPM 的钉钉式流程 JSON,结构为「发起人节点 + 一个审批节点」:
{
"processList": [
{ "type": "start", "nodeId": "start", "nodeName": "发起人" },
{ "type": "approve", "nodeId": "node-approve", "nodeName": "审批", "userIds": [1] }
]
}formType / modelId 必填
BpmProcessDefinitionInfo 的 formType 和 modelId 是必填项(详见平台 BPM 文档)。formType=10 表示自定义表单(由 LcForm 承载),modelId 用 erp_ + 时间戳生成。漏填会导致流程定义无法被 startProcess 正确加载。
五、审批表单绑定(LcForm)
createForms 为 APPROVABLE_TABLES 中的每张表创建一个 LcForm,把「业务表 - 流程定义 - 表单」三者绑定:
for (String tableName : ErpSchema.APPROVABLE_TABLES) {
LcForm form = new LcForm();
form.setFormName(def.tableComment + "审批表单");
form.setFormCode(tableName); // ★ formCode = 业务表名(回调据此找表)
form.setTableId(tableId); // 绑定业务表
form.setFormJson(null); // RuntimeForm 按表字段自动生成
form.setApprovalEnabled(true); // ★ 开启审批
form.setProcessModelId(bpmDefId); // ★ 绑定流程定义
formMapper.insert(form);
}关键约定:
formCode等于业务表名(如pms_purchase_order)。这是BpmBusinessStatusCallback反查tableId的依据(formCode -> LcForm.tableId);formJson = null:运行时表单RuntimeForm会根据lc_column_meta自动渲染,无需手写表单 JSON;approvalEnabled = true+processModelId = bpmDefId:BizApprovalController.submit会校验这两个条件,缺一不可。
六、列表页生成(LcPage)
createListPages 为 16 张表各生成一个列表页,可审批表额外把 formId + approvalEnabled 写入 pageJson 的组件 config,前端列表页据此渲染「提交审批」按钮与审批状态列:
private String buildListPageJson(TableDef def, Long tableId, Long formId, boolean approvalEnabled) {
// 单行单组件,type=list,config.title=表注释,config.tableId=tableId
// 若 approvalEnabled && formId != null:
// config.formId = formId
// config.approvalEnabled = true
}页面路由统一为 /runtime/erp_carbon/{tableName},pageCode 为 erp_carbon_{tableName},状态 published。
七、菜单生成
createMenus 先建 4 个顶层目录菜单(menuType=dir),再按 businessGroup 把每张表的列表页挂到对应目录下(menuType=page):
| 目录 | 图标 | 挂载的表 |
|---|---|---|
| 采购管理 | ShoppingCart | 9 张 pms_ 表 |
| 合同变更 | Document | pms_change_extension |
| 供应商管理 | Goods | 3 张 srm_ 表 |
| 仓库管理 | Box | 3 张 wms_ 表 |
八、approval_status 联动(BPM 回写)
这是「低代码 + 工作流」联动的点睛之笔。整个审批闭环如下:
用户在列表页点「提交审批」
│ POST /api/biz-approval/submit/{formId} (BizApprovalController)
▼
schemaService.insertData(tableId, data) -> 业务表写入新行 (approval_status 默认 PENDING)
│
▼
bpmModelService.startProcess(processModelId, variables)
│ variables.businessKey = formCode + ":" + dataId (如 "pms_purchase_order:42")
│ variables.businessType = formCode
▼
SimpleFlowEngine 启动流程实例 BpmInstance
│
├─▶ BpmBusinessStatusCallback.onStarted(instance)
│ └─ writeBack: UPDATE `pms_purchase_order` SET approval_status='IN_PROGRESS' WHERE id=42
│
│ ... 审批人处理任务(通过 / 驳回 / 撤回)...
│
└─▶ BpmBusinessStatusCallback.onTerminal(instance)
└─ instance.status=completed -> UPDATE ... SET approval_status='APPROVED'
instance.status=rejected -> UPDATE ... SET approval_status='REJECTED'
instance.status=recalled -> UPDATE ... SET approval_status='WITHDRAWN'
BpmBusinessStatusCallback 的安全设计(opt-in):
private void writeBack(BpmInstance instance, String approvalStatus) {
// 1. 从 businessKey 解析 dataId(formCode:dataId)
// 2. 用 businessType(=formCode) 查 LcForm -> tableId
// 3. 检查该 tableId 是否有 approval_status 列,没有则直接 return(opt-in)
// 4. UPDATE `表名` SET approval_status = ? WHERE id = ?
// 任何异常都吞掉 + 记日志,绝不影响审批主流程
}为什么是 opt-in
平台原生审批状态是从 bpm_instance 实时按 businessKey 派生的(见 BizApprovalController.buildApprovalStatus),不落业务表。但 ERP 单据表自身带 approval_status 列,为了让单据表状态与流程实例一致(便于导出与 ERP 状态机对齐),才加了这个回写。回调只在表元数据存在 approval_status 列时才回写,因此对基金会等不带该列的应用完全无副作用。
九、增量补丁模式
当应用已存在时,applyIncrementalPatch 会:
- 补齐新增字典(
createDictionaries幂等); - 复用 / 创建 BPM 定义(
createBpmDefinition幂等); - 为缺失表单的可审批表补
LcForm; - 遍历
ErpSchema.all(),对lc_table_meta里缺失的表逐一补建(DDL + 元数据 + 表单 + 列表页 + 菜单 + mock 数据); - 为已存在但缺列表页 / 菜单的表补齐页面与菜单。
这意味着:升级代码新增了一张表或一个字典,只需重启应用,Seeder 会自动补齐,无需手写迁移脚本。
十、薄 CRUD 控制器层
controller/erp/ 下的 9 个控制器是手写的薄 CRUD,基于 MyBatis-Plus LambdaQueryWrapper + PageHelper.doPage。以 ErpPurchaseOrderController 为例:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/erp/purchaseOrder/list | 列表(支持 keyword / status / supplierId 过滤) |
| GET | /api/erp/purchaseOrder/page | 分页 |
| GET | /api/erp/purchaseOrder/{id} | 详情 |
| POST | /api/erp/purchaseOrder | 新增 |
| PUT | /api/erp/purchaseOrder | 修改 |
| DELETE | /api/erp/purchaseOrder/{id} | 删除 |
| POST | /api/erp/purchaseOrder/submit/{id} | 提交(置 status=pending) |
各控制器差异化的业务动作:
| 控制器 | 额外端点 | 说明 |
|---|---|---|
ErpPurchaseOrderController | POST /submit/{id} | 提交审批(置 status=pending) |
ErpContractController | POST /submit/{id} | 合同提交 |
ErpSaleOrderController | POST /deliver/{id} | 销售订单发货 |
ErpInventoryController | GET /warning | 库存预警 |
ErpSupplierController | DELETE /batch | 批量删除供应商 |
薄 CRUD 与审批的关系
薄 CRUD 控制器操作的是 erp_* 实体表,不直接走 BPM 审批。真正走 BPM 审批闭环的是 Seeder 建的 pms_* 可审批表,通过 BizApprovalController 统一入口提交。两套表分工不同:erp_* 是手写的主数据 / 单据 CRUD;pms_* 是低代码配置 + 工作流联动的审批单据。
操作步骤
1. 启动后端,触发自动 seeding
# 在 backend 目录
mvn spring-boot:run
# 或在 IDE 启动 Application 主类观察日志,首次启动会看到:
[ErpAppSeeder] 开始初始化 ERP 应用...
[ErpAppSeeder] 应用已创建 id=...
[ErpAppSeeder] 表 pms_purchase_requirement 已建 (metaId=...)
...
[ErpAppSeeder] 字典初始化完成
[ErpAppSeeder] BPM 流程定义已创建 id=... approverId=1
[ErpAppSeeder] 表单已创建 pms_purchase_requirement (formId=...)
[ErpAppSeeder] 共生成 16 个列表页
[ErpAppSeeder] 菜单初始化完成
[ErpAppSeeder] mock 数据已插入 N 条
[ErpAppSeeder] 初始化完成。访问 /app/erp_carbon 即可进入 ERP 后台。2. 进入 ERP 后台
浏览器打开前端(默认 http://localhost:3000),在应用列表进入「ERP」,左侧菜单可见「采购管理 / 合同变更 / 供应商管理 / 仓库管理」四个目录。
3. 走一次采购需求审批闭环

# (a) 通过审批入口提交一条采购需求(formId 可在 lc_form 表查 pms_purchase_requirement 的 formCode)
curl -X POST http://localhost:52856/api/biz-approval/submit/{formId} \
-H "Content-Type: application/json" \
-d '{
"requirementNo": "PR20260806001",
"requirementName": "Q3 钢材采购需求",
"urgencyLevel": "URGENT",
"requirementSource": "PRODUCTION",
"purchaseAmount": 150000
}'
# 返回 dataId + instanceId + status=running
# (b) 查业务表,approval_status 已被回写为 IN_PROGRESS
mysql> SELECT id, requirement_no, approval_status FROM pms_purchase_requirement ORDER BY id DESC LIMIT 1;
# +----+----------------+------------------+
# | id | requirement_no | approval_status |
# +----+----------------+------------------+
# | 1 | PR20260806001 | IN_PROGRESS | <- 回写自 onStarted
# +----+----------------+------------------+
# (c) 审批人通过任务后,再查:
# approval_status 变为 APPROVED(回写自 onTerminal,instance.status=completed)4. 查询单据审批状态(不落表也能查)
curl http://localhost:52856/api/biz-approval/status/{formId}/{dataId}
# 返回 { status: "running"/"completed"/"rejected"/"none", instanceId, taskId, ... }5. 重置后重新初始化
-- 删除应用(连带元数据会被识别为缺失,Seeder 下次补建)
DELETE FROM lc_application WHERE app_code = 'erp_carbon';
-- 如需彻底重来,再手动 drop 物理表 + 清 lc_table_meta / lc_form / lc_page / lc_app_menu
-- 然后重启后端即可常见问题
启动日志显示「默认租户尚未初始化,跳过本轮」怎么办?
ErpAppSeeder 有守护逻辑:启动时先查 sys_tenant 是否存在 id=1 的租户,不存在则跳过本轮、等下次启动重试。这说明 DataInitializer 还没跑完或租户表为空。请确保数据库已执行平台基础 SQL、DataInitializer 正常执行,然后重启后端。
表 / 表单 / 页面 / 菜单只建了一部分?
Seeder 对每一步都有 try-catch + 日志,单个表失败不会阻断整体。检查日志里的 WARN [ErpAppSeeder] 行定位失败项。修复后重启,由于已存在的应用会进入「增量补丁模式」,缺失的表 / 表单 / 页面 / 菜单会被自动补齐。
提交审批报「当前表单未启用审批」?
BizApprovalController.submit 会校验 form.getApprovalEnabled() == true。请确认对应表的 lc_form 行 approval_enabled=1 且 process_model_id 已绑定 BPM 定义。Seeder 只为 APPROVABLE_TABLES 里的 7 张表建表单,其它表默认不带审批。
审批通过后 approval_status 没变?
检查三点:(1) 业务表元数据是否有 approval_status 列(lc_column_meta 查),没有则回调直接 return;(2) LcForm.formCode 是否等于业务表名(回调据此反查 tableId);(3) 看 BpmBusinessStatusCallback 的 WARN 日志,回写异常会被吞掉但会记日志。状态码映射:completed->APPROVED、rejected->REJECTED、recalled/canceled/revoked->WITHDRAWN。
想让采购订单走多步审批怎么办?
当前 PURCHASE_APPROVAL 是单步审批(发起人 -> 一个审批节点)。要支持多步串行,需扩展 simpleModel 的 processList,增加多个 approve 节点(平台 BPM 引擎支持会签 / 转办 / 委派,多步串行需在 simpleModel 里配置节点链)。改完 buildSimpleModel 重新初始化即可。
erp_purchase_order 和 pms_purchase_order 是同一张表吗?
不是。erp_purchase_order 是手写实体 ErpPurchaseOrder 映射的表,由 ErpPurchaseOrderController 薄 CRUD 操作;pms_purchase_order 是 Seeder 动态建的采购订单审批单据表,走低代码运行时 + BPM 审批。两者相互独立。
