From b8583408903b5154bd8308c39b7ef64b2fb2d4f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A6=E9=BE=99=E8=A8=80?= Date: Mon, 27 Jul 2026 13:12:19 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=89=98=E7=9B=98=E4=B8=9A=E5=8A=A1=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../20260724_pallet_workflow_mysql.sql | 489 ++++++++++ .../sql/shierfeng-fastapi-pg.sql | 463 ++++++++++ .../sql/shierfeng-fastapi.sql | 490 ++++++++++ .../module_business/test_sql_contract.py | 841 ++++++++++++++++++ 4 files changed, 2283 insertions(+) create mode 100644 shierfeng-fastapi-backend/sql/patches/20260724_pallet_workflow_mysql.sql create mode 100644 shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py diff --git a/shierfeng-fastapi-backend/sql/patches/20260724_pallet_workflow_mysql.sql b/shierfeng-fastapi-backend/sql/patches/20260724_pallet_workflow_mysql.sql new file mode 100644 index 0000000..cc26429 --- /dev/null +++ b/shierfeng-fastapi-backend/sql/patches/20260724_pallet_workflow_mysql.sql @@ -0,0 +1,489 @@ +-- 警告:MySQL DDL 会隐式提交,不能依赖事务整体回滚。 +-- 应用前必须完成数据库备份并由 DBA 人工审查;请在维护窗口内分节执行和验证。 +-- 本文件仅作为人工增量补丁交付,未自动执行。 + +-- ==================== DDL:托盘流转业务表 ==================== + +-- 菌种主数据表 +create table if not exists biz_strain ( + strain_id bigint(20) not null auto_increment comment '菌种ID', + strain_code varchar(64) not null comment '菌种编码', + strain_name varchar(100) not null comment '菌种名称', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (strain_id), + constraint uq_biz_strain_code unique (strain_code), + constraint ck_biz_strain_status check (status in ('0', '1')) +) engine=innodb comment = '菌种主数据表'; + +-- 仓库主数据表 +create table if not exists biz_warehouse ( + warehouse_id bigint(20) not null auto_increment comment '仓库ID', + warehouse_code varchar(64) not null comment '仓库编码', + warehouse_name varchar(100) not null comment '仓库名称', + address varchar(500) comment '仓库地址', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (warehouse_id), + constraint uq_biz_warehouse_code unique (warehouse_code), + constraint ck_biz_warehouse_status check (status in ('0', '1')) +) engine=innodb comment = '仓库主数据表'; + +-- 客户主数据表 +create table if not exists biz_customer ( + customer_id bigint(20) not null auto_increment comment '客户ID', + customer_code varchar(64) not null comment '客户编码', + customer_name varchar(100) not null comment '客户名称', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (customer_id), + constraint uq_biz_customer_code unique (customer_code), + constraint ck_biz_customer_status check (status in ('0', '1')) +) engine=innodb comment = '客户主数据表'; + +-- 客户现场主数据表 +create table if not exists biz_customer_site ( + site_id bigint(20) not null auto_increment comment '现场ID', + customer_id bigint(20) not null comment '客户ID', + site_code varchar(64) not null comment '现场编码', + site_name varchar(100) not null comment '现场名称', + address varchar(500) comment '现场地址', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (site_id), + constraint uq_biz_customer_site_customer_code unique (customer_id, site_code), + constraint fk_biz_customer_site_customer foreign key (customer_id) references biz_customer (customer_id), + constraint ck_biz_customer_site_status check (status in ('0', '1')) +) engine=innodb comment = '客户现场主数据表'; + +-- 客户现场产线主数据表 +create table if not exists biz_production_line ( + line_id bigint(20) not null auto_increment comment '产线ID', + site_id bigint(20) not null comment '现场ID', + line_code varchar(64) not null comment '产线编码', + line_name varchar(100) not null comment '产线名称', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (line_id), + constraint uq_biz_production_line_site_code unique (site_id, line_code), + constraint fk_biz_production_line_site foreign key (site_id) references biz_customer_site (site_id), + constraint ck_biz_production_line_status check (status in ('0', '1')) +) engine=innodb comment = '客户现场产线主数据表'; + +-- 托盘资产主数据表 +create table if not exists biz_pallet ( + pallet_id bigint(20) not null auto_increment comment '托盘ID', + pallet_code varchar(64) not null comment '托盘编号', + qr_token varchar(128) not null comment '二维码固定标识', + capacity_packages int not null comment '容量包数', + asset_status char(1) not null default '0' comment '资产状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (pallet_id), + constraint uq_biz_pallet_code unique (pallet_code), + constraint uq_biz_pallet_qr_token unique (qr_token), + constraint ck_biz_pallet_capacity_packages check (capacity_packages > 0), + constraint ck_biz_pallet_asset_status check (asset_status in ('0', '1')) +) engine=innodb comment = '托盘资产主数据表'; + +-- 用户与仓库关联表 +create table if not exists biz_user_warehouse ( + user_id bigint(20) not null comment '用户ID', + warehouse_id bigint(20) not null comment '仓库ID', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (user_id, warehouse_id), + constraint fk_biz_user_warehouse_user foreign key (user_id) references sys_user (user_id), + constraint fk_biz_user_warehouse_warehouse foreign key (warehouse_id) references biz_warehouse (warehouse_id) +) engine=innodb comment = '用户与仓库关联表'; + +-- 用户与客户现场关联表 +create table if not exists biz_user_site ( + user_id bigint(20) not null comment '用户ID', + site_id bigint(20) not null comment '现场ID', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (user_id, site_id), + constraint fk_biz_user_site_user foreign key (user_id) references sys_user (user_id), + constraint fk_biz_user_site_site foreign key (site_id) references biz_customer_site (site_id) +) engine=innodb comment = '用户与客户现场关联表'; + +-- 首次入库单 +create table if not exists biz_inbound_order ( + inbound_id bigint(20) not null auto_increment comment '入库单ID', + inbound_no varchar(64) not null comment '入库单号', + warehouse_id bigint(20) not null comment '仓库ID', + status varchar(32) not null default 'DRAFT' comment '单据状态', + confirm_idempotency_key varchar(64) comment '确认幂等键', + confirm_request_hash varchar(64) comment '确认请求摘要', + confirmed_by bigint(20) comment '确认人ID', + confirmed_at datetime comment '确认时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (inbound_id), + constraint uq_biz_inbound_order_no unique (inbound_no), + constraint uq_biz_inbound_order_confirm_idempotency unique (confirm_idempotency_key), + constraint fk_biz_inbound_order_warehouse foreign key (warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_inbound_order_confirmed_by foreign key (confirmed_by) references sys_user (user_id), + constraint ck_biz_inbound_order_status check (status in ('DRAFT', 'CONFIRMED')) +) engine=innodb comment = '首次入库单'; + +-- 首次入库单托盘明细 +create table if not exists biz_inbound_item ( + inbound_item_id bigint(20) not null auto_increment comment '入库明细ID', + inbound_id bigint(20) not null comment '入库单ID', + pallet_id bigint(20) not null comment '托盘ID', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (inbound_item_id), + constraint uq_biz_inbound_item_order_pallet unique (inbound_id, pallet_id), + constraint fk_biz_inbound_item_order foreign key (inbound_id) references biz_inbound_order (inbound_id), + constraint fk_biz_inbound_item_pallet foreign key (pallet_id) references biz_pallet (pallet_id) +) engine=innodb comment = '首次入库单托盘明细'; + +-- 托盘装填记录 +create table if not exists biz_filling_record ( + filling_id bigint(20) not null auto_increment comment '装填记录ID', + filling_no varchar(64) not null comment '装填单号', + pallet_id bigint(20) not null comment '托盘ID', + warehouse_id bigint(20) not null comment '仓库ID', + strain_id bigint(20) not null comment '菌种ID', + strain_code_snapshot varchar(64) not null comment '菌种编码快照', + strain_name_snapshot varchar(100) not null comment '菌种名称快照', + batch_no varchar(64) not null comment '生产批次', + package_spec varchar(64) not null comment '包装规格', + initial_packages int not null comment '初始包数', + active_slot varchar(16) comment '活动装填槽', + closed_at datetime comment '装填周期关闭时间', + idempotency_key varchar(64) not null comment '幂等键', + request_hash varchar(64) not null comment '请求摘要', + operator_id bigint(20) not null comment '操作人ID', + operated_at datetime not null comment '操作时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (filling_id), + constraint uq_biz_filling_record_no unique (filling_no), + constraint uq_biz_filling_record_idempotency unique (idempotency_key), + constraint uq_biz_filling_record_pallet_active_slot unique (pallet_id, active_slot), + constraint fk_biz_filling_record_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_filling_record_warehouse foreign key (warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_filling_record_strain foreign key (strain_id) references biz_strain (strain_id), + constraint fk_biz_filling_record_operator foreign key (operator_id) references sys_user (user_id), + constraint ck_biz_filling_record_initial_packages check (initial_packages > 0), + constraint ck_biz_filling_record_active_slot check (active_slot is null or active_slot = 'ACTIVE') +) engine=innodb comment = '托盘装填记录'; + +-- 整托发货单 +create table if not exists biz_shipment_order ( + shipment_id bigint(20) not null auto_increment comment '发货单ID', + shipment_no varchar(64) not null comment '发货单号', + source_warehouse_id bigint(20) not null comment '来源仓库ID', + target_site_id bigint(20) not null comment '目标现场ID', + status varchar(32) not null default 'DRAFT' comment '单据状态', + confirm_idempotency_key varchar(64) comment '确认幂等键', + confirm_request_hash varchar(64) comment '确认请求摘要', + confirmed_by bigint(20) comment '确认人ID', + confirmed_at datetime comment '确认时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (shipment_id), + constraint uq_biz_shipment_order_no unique (shipment_no), + constraint uq_biz_shipment_order_confirm_idempotency unique (confirm_idempotency_key), + constraint fk_biz_shipment_order_warehouse foreign key (source_warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_shipment_order_site foreign key (target_site_id) references biz_customer_site (site_id), + constraint fk_biz_shipment_order_confirmed_by foreign key (confirmed_by) references sys_user (user_id), + constraint ck_biz_shipment_order_status check (status in ('DRAFT', 'CONFIRMED')) +) engine=innodb comment = '整托发货单'; + +-- 整托发货明细 +create table if not exists biz_shipment_item ( + shipment_item_id bigint(20) not null auto_increment comment '发货明细ID', + shipment_id bigint(20) not null comment '发货单ID', + pallet_id bigint(20) not null comment '托盘ID', + active_filling_id bigint(20) comment '确认时活动装填记录ID', + package_qty int comment '确认时包数快照', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (shipment_item_id), + constraint uq_biz_shipment_item_order_pallet unique (shipment_id, pallet_id), + constraint fk_biz_shipment_item_order foreign key (shipment_id) references biz_shipment_order (shipment_id), + constraint fk_biz_shipment_item_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_shipment_item_filling foreign key (active_filling_id) references biz_filling_record (filling_id), + constraint ck_biz_shipment_item_package_qty check (package_qty is null or package_qty > 0) +) engine=innodb comment = '整托发货明细'; + +-- 客户现场使用事实记录 +create table if not exists biz_usage_record ( + usage_id bigint(20) not null auto_increment comment '使用记录ID', + usage_no varchar(64) not null comment '使用记录号', + pallet_id bigint(20) not null comment '托盘ID', + active_filling_id bigint(20) not null comment '活动装填记录ID', + site_id bigint(20) not null comment '现场ID', + line_id bigint(20) not null comment '产线ID', + used_packages int not null comment '本次使用包数', + remaining_packages_after int not null comment '使用后剩余包数', + idempotency_key varchar(64) not null comment '幂等键', + request_hash varchar(64) not null comment '请求摘要', + operator_id bigint(20) not null comment '操作人ID', + occurred_at datetime not null comment '发生时间', + primary key (usage_id), + constraint uq_biz_usage_record_no unique (usage_no), + constraint uq_biz_usage_record_idempotency unique (idempotency_key), + constraint fk_biz_usage_record_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_usage_record_filling foreign key (active_filling_id) references biz_filling_record (filling_id), + constraint fk_biz_usage_record_site foreign key (site_id) references biz_customer_site (site_id), + constraint fk_biz_usage_record_line foreign key (line_id) references biz_production_line (line_id), + constraint fk_biz_usage_record_operator foreign key (operator_id) references sys_user (user_id), + constraint ck_biz_usage_record_used_packages check (used_packages > 0), + constraint ck_biz_usage_record_remaining_packages_after check (remaining_packages_after >= 0) +) engine=innodb comment = '客户现场使用事实记录'; + +-- 托盘退回单 +create table if not exists biz_return_order ( + return_id bigint(20) not null auto_increment comment '退回单ID', + return_no varchar(64) not null comment '退回单号', + source_site_id bigint(20) not null comment '来源现场ID', + target_warehouse_id bigint(20) not null comment '目标仓库ID', + status varchar(32) not null default 'REQUESTED' comment '退回状态', + request_idempotency_key varchar(64) not null comment '申请幂等键', + request_hash varchar(64) not null comment '申请请求摘要', + confirm_idempotency_key varchar(64) comment '确认幂等键', + confirm_request_hash varchar(64) comment '确认请求摘要', + requested_by bigint(20) not null comment '申请人ID', + requested_at datetime not null comment '申请时间', + confirmed_by bigint(20) comment '确认人ID', + confirmed_at datetime comment '确认时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (return_id), + constraint uq_biz_return_order_no unique (return_no), + constraint uq_biz_return_order_request_idempotency unique (request_idempotency_key), + constraint uq_biz_return_order_confirm_idempotency unique (confirm_idempotency_key), + constraint fk_biz_return_order_site foreign key (source_site_id) references biz_customer_site (site_id), + constraint fk_biz_return_order_warehouse foreign key (target_warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_return_order_requested_by foreign key (requested_by) references sys_user (user_id), + constraint fk_biz_return_order_confirmed_by foreign key (confirmed_by) references sys_user (user_id), + constraint ck_biz_return_order_status check (status in ('REQUESTED', 'CONFIRMED')) +) engine=innodb comment = '托盘退回单'; + +-- 托盘退回明细 +create table if not exists biz_return_item ( + return_item_id bigint(20) not null auto_increment comment '退回明细ID', + return_id bigint(20) not null comment '退回单ID', + pallet_id bigint(20) not null comment '托盘ID', + active_filling_id_snapshot bigint(20) comment '申请时活动装填记录快照', + remaining_packages_snapshot int not null comment '申请时剩余包数快照', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (return_item_id), + constraint uq_biz_return_item_order_pallet unique (return_id, pallet_id), + constraint fk_biz_return_item_order foreign key (return_id) references biz_return_order (return_id), + constraint fk_biz_return_item_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_return_item_filling foreign key (active_filling_id_snapshot) references biz_filling_record (filling_id), + constraint ck_biz_return_item_remaining_packages_snapshot check (remaining_packages_snapshot >= 0) +) engine=innodb comment = '托盘退回明细'; + +-- 托盘只追加流转事件;必须先于库存快照创建 +create table if not exists biz_pallet_event ( + event_id bigint(20) not null auto_increment comment '事件ID', + pallet_id bigint(20) not null comment '托盘ID', + event_seq int not null comment '托盘内事件序号', + event_type varchar(32) not null comment '事件类型', + source_type varchar(32) not null comment '来源类型', + source_id bigint(20) not null comment '来源记录ID', + before_state varchar(32) not null comment '变更前状态', + after_state varchar(32) not null comment '变更后状态', + before_warehouse_id bigint(20) comment '变更前仓库ID', + before_site_id bigint(20) comment '变更前现场ID', + before_line_id bigint(20) comment '变更前产线ID', + after_warehouse_id bigint(20) comment '变更后仓库ID', + after_site_id bigint(20) comment '变更后现场ID', + after_line_id bigint(20) comment '变更后产线ID', + before_remaining_packages int not null comment '变更前剩余包数', + after_remaining_packages int not null comment '变更后剩余包数', + operator_id bigint(20) not null comment '操作人ID', + occurred_at datetime not null comment '发生时间', + idempotency_key varchar(64) not null comment '幂等键', + request_hash varchar(64) not null comment '请求摘要', + primary key (event_id), + constraint uq_biz_pallet_event_pallet_seq unique (pallet_id, event_seq), + constraint uq_biz_pallet_event_pallet_idempotency unique (pallet_id, idempotency_key), + constraint fk_biz_pallet_event_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_pallet_event_before_warehouse foreign key (before_warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_pallet_event_before_site foreign key (before_site_id) references biz_customer_site (site_id), + constraint fk_biz_pallet_event_before_line foreign key (before_line_id) references biz_production_line (line_id), + constraint fk_biz_pallet_event_after_warehouse foreign key (after_warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_pallet_event_after_site foreign key (after_site_id) references biz_customer_site (site_id), + constraint fk_biz_pallet_event_after_line foreign key (after_line_id) references biz_production_line (line_id), + constraint fk_biz_pallet_event_operator foreign key (operator_id) references sys_user (user_id), + constraint ck_biz_pallet_event_event_type check ( + event_type in ( + 'INBOUND_CONFIRMED', + 'FILLING_CONFIRMED', + 'SHIPMENT_CONFIRMED', + 'USAGE_RECORDED', + 'RETURN_REQUESTED', + 'RETURN_CONFIRMED' + ) + ) +) engine=innodb comment = '托盘只追加流转事件'; + +-- 托盘当前库存、位置和流转状态 +create table if not exists biz_pallet_inventory ( + pallet_id bigint(20) not null comment '托盘ID', + active_filling_id bigint(20) comment '当前活动装填记录ID', + warehouse_id bigint(20) comment '当前仓库ID', + site_id bigint(20) comment '当前现场ID', + current_line_id bigint(20) comment '当前驻场周期绑定产线ID', + flow_state varchar(32) not null default 'UNSTOCKED' comment '托盘流转状态', + remaining_packages int not null default 0 comment '当前剩余包数', + last_event_id bigint(20) comment '最后事件ID', + version int not null default 0 comment '并发版本号', + updated_at datetime not null comment '更新时间', + primary key (pallet_id), + constraint fk_biz_pallet_inventory_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_pallet_inventory_filling foreign key (active_filling_id) references biz_filling_record (filling_id), + constraint fk_biz_pallet_inventory_warehouse foreign key (warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_pallet_inventory_site foreign key (site_id) references biz_customer_site (site_id), + constraint fk_biz_pallet_inventory_line foreign key (current_line_id) references biz_production_line (line_id), + constraint fk_biz_pallet_inventory_event foreign key (last_event_id) references biz_pallet_event (event_id), + constraint ck_biz_pallet_inventory_flow_state check ( + flow_state in ( + 'UNSTOCKED', + 'EMPTY_IN_WAREHOUSE', + 'FILLED_IN_WAREHOUSE', + 'AT_SITE_UNUSED', + 'AT_SITE_PARTIAL', + 'AT_SITE_EMPTY', + 'RETURNING' + ) + ), + constraint ck_biz_pallet_inventory_remaining_packages check (remaining_packages >= 0), + constraint ck_biz_pallet_inventory_version check (version >= 0), + constraint ck_biz_pallet_inventory_location check (not (warehouse_id is not null and site_id is not null)) +) engine=innodb comment = '托盘当前库存、位置和流转状态'; + +-- ==================== DML:清理旧 seed 并写入托盘业务菜单 ==================== + +-- 先解除所有待清理菜单的历史角色关联。 +delete from sys_role_menu +where menu_id in (4, 99, 118, 119, 1061, 1062, 1063, 1064); + +-- 再清理原 AI 菜单;外链菜单 99 按要求在字典清理后单独删除。 +delete from sys_menu +where menu_id in (4, 118, 119, 1061, 1062, 1063, 1064); + +delete from sys_dict_data +where dict_code between 33 and 69 + or dict_type = 'ai_provider_type'; + +delete from sys_dict_type +where dict_id = 12 + or dict_type = 'ai_provider_type'; + +delete from sys_menu +where menu_id = 99; + +-- INSERT IGNORE 使明确 ID 的菜单 seed 可重复应用而不会生成重复记录。 +insert ignore into sys_menu values +(121, '基础资料维护', 1, '10', 'businessMaster', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', sysdate(), '', null, ''), +(122, '业务管理', 1, '11', 'businessOperation', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', sysdate(), '', null, ''), +(123, '报表查询', 1, '12', 'businessReport', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', sysdate(), '', null, ''), +(502, '菌种资料', 121, '1', 'strain', 'master/strain/index', '', '', 1, 0, 'C', '0', '0', 'master:strain:list', '#', 'admin', sysdate(), '', null, ''), +(503, '客户资料', 121, '2', 'customer', 'master/customer/index', '', '', 1, 0, 'C', '0', '0', 'master:customer:list', '#', 'admin', sysdate(), '', null, ''), +(504, '客户现场', 121, '3', 'site', 'master/site/index', '', '', 1, 0, 'C', '0', '0', 'master:site:list', '#', 'admin', sysdate(), '', null, ''), +(505, '产线资料', 121, '4', 'line', 'master/line/index', '', '', 1, 0, 'C', '0', '0', 'master:line:list', '#', 'admin', sysdate(), '', null, ''), +(506, '仓库资料', 121, '5', 'warehouse', 'master/warehouse/index', '', '', 1, 0, 'C', '0', '0', 'master:warehouse:list', '#', 'admin', sysdate(), '', null, ''), +(507, '托盘资料', 121, '6', 'pallet', 'master/pallet/index', '', '', 1, 0, 'C', '0', '0', 'master:pallet:list', '#', 'admin', sysdate(), '', null, ''), +(508, '仓库入库', 122, '1', 'inbound', 'business/inbound/index', '', '', 1, 0, 'C', '0', '0', 'business:inbound:list', '#', 'admin', sysdate(), '', null, ''), +(509, '工厂装填', 122, '2', 'filling', 'business/filling/index', '', '', 1, 0, 'C', '0', '0', 'business:filling:list', '#', 'admin', sysdate(), '', null, ''), +(510, '发货出库', 122, '3', 'shipment', 'business/shipment/index', '', '', 1, 0, 'C', '0', '0', 'business:shipment:list', '#', 'admin', sysdate(), '', null, ''), +(511, '客户现场使用', 122, '4', 'usage', 'business/usage/index', '', '', 1, 0, 'C', '0', '0', 'business:usage:list', '#', 'admin', sysdate(), '', null, ''), +(512, '退回仓库', 122, '5', 'return', 'business/return/index', '', '', 1, 0, 'C', '0', '0', 'business:return:list', '#', 'admin', sysdate(), '', null, ''), +(513, '托盘库存', 123, '1', 'palletInventory', 'report/palletInventory/index', '', '', 1, 0, 'C', '0', '0', 'report:palletinventory:list', '#', 'admin', sysdate(), '', null, ''), +(514, '装填记录', 123, '2', 'reportFilling', 'report/filling/index', '', '', 1, 0, 'C', '0', '0', 'report:filling:list', '#', 'admin', sysdate(), '', null, ''), +(515, '发货出库', 123, '3', 'reportShipment', 'report/shipment/index', '', '', 1, 0, 'C', '0', '0', 'report:shipment:list', '#', 'admin', sysdate(), '', null, ''), +(516, '产线消耗', 123, '4', 'consumption', 'report/consumption/index', '', '', 1, 0, 'C', '0', '0', 'report:consumption:list', '#', 'admin', sysdate(), '', null, ''), +(517, '退回仓库', 123, '5', 'reportReturn', 'report/return/index', '', '', 1, 0, 'C', '0', '0', 'report:return:list', '#', 'admin', sysdate(), '', null, ''), +(518, '托盘追溯', 123, '6', 'palletTrace', 'report/palletTrace/index', '', '', 1, 0, 'C', '0', '0', 'report:pallettrace:list', '#', 'admin', sysdate(), '', null, ''), +(1065, '菌种查询', 502, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:query', '#', 'admin', sysdate(), '', null, ''), +(1066, '菌种新增', 502, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:add', '#', 'admin', sysdate(), '', null, ''), +(1067, '菌种修改', 502, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:edit', '#', 'admin', sysdate(), '', null, ''), +(1068, '菌种删除', 502, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:remove', '#', 'admin', sysdate(), '', null, ''), +(1069, '客户查询', 503, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:query', '#', 'admin', sysdate(), '', null, ''), +(1070, '客户新增', 503, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:add', '#', 'admin', sysdate(), '', null, ''), +(1071, '客户修改', 503, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:edit', '#', 'admin', sysdate(), '', null, ''), +(1072, '客户删除', 503, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:remove', '#', 'admin', sysdate(), '', null, ''), +(1073, '现场查询', 504, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:query', '#', 'admin', sysdate(), '', null, ''), +(1074, '现场新增', 504, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:add', '#', 'admin', sysdate(), '', null, ''), +(1075, '现场修改', 504, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:edit', '#', 'admin', sysdate(), '', null, ''), +(1076, '现场删除', 504, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:remove', '#', 'admin', sysdate(), '', null, ''), +(1077, '分配用户', 504, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:assign', '#', 'admin', sysdate(), '', null, ''), +(1078, '产线查询', 505, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:query', '#', 'admin', sysdate(), '', null, ''), +(1079, '产线新增', 505, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:add', '#', 'admin', sysdate(), '', null, ''), +(1080, '产线修改', 505, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:edit', '#', 'admin', sysdate(), '', null, ''), +(1081, '产线删除', 505, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:remove', '#', 'admin', sysdate(), '', null, ''), +(1082, '仓库查询', 506, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:query', '#', 'admin', sysdate(), '', null, ''), +(1083, '仓库新增', 506, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:add', '#', 'admin', sysdate(), '', null, ''), +(1084, '仓库修改', 506, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:edit', '#', 'admin', sysdate(), '', null, ''), +(1085, '仓库删除', 506, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:remove', '#', 'admin', sysdate(), '', null, ''), +(1086, '分配用户', 506, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:assign', '#', 'admin', sysdate(), '', null, ''), +(1087, '托盘查询', 507, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:query', '#', 'admin', sysdate(), '', null, ''), +(1088, '托盘新增', 507, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:add', '#', 'admin', sysdate(), '', null, ''), +(1089, '托盘修改', 507, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:edit', '#', 'admin', sysdate(), '', null, ''), +(1090, '托盘删除', 507, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:remove', '#', 'admin', sysdate(), '', null, ''), +(1091, '托盘打印', 507, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:print', '#', 'admin', sysdate(), '', null, ''), +(1092, '入库查询', 508, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:query', '#', 'admin', sysdate(), '', null, ''), +(1093, '入库新增', 508, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:add', '#', 'admin', sysdate(), '', null, ''), +(1094, '入库修改', 508, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:edit', '#', 'admin', sysdate(), '', null, ''), +(1095, '入库删除', 508, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:remove', '#', 'admin', sysdate(), '', null, ''), +(1096, '入库确认', 508, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:confirm', '#', 'admin', sysdate(), '', null, ''), +(1097, '装填查询', 509, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:filling:query', '#', 'admin', sysdate(), '', null, ''), +(1098, '装填新增', 509, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:filling:add', '#', 'admin', sysdate(), '', null, ''), +(1099, '发货查询', 510, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:query', '#', 'admin', sysdate(), '', null, ''), +(1100, '发货新增', 510, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:add', '#', 'admin', sysdate(), '', null, ''), +(1101, '发货修改', 510, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:edit', '#', 'admin', sysdate(), '', null, ''), +(1102, '发货删除', 510, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:remove', '#', 'admin', sysdate(), '', null, ''), +(1103, '发货确认', 510, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:confirm', '#', 'admin', sysdate(), '', null, ''), +(1104, '使用查询', 511, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:usage:query', '#', 'admin', sysdate(), '', null, ''), +(1105, '使用新增', 511, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:usage:add', '#', 'admin', sysdate(), '', null, ''), +(1106, '退回查询', 512, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:query', '#', 'admin', sysdate(), '', null, ''), +(1107, '退回申请', 512, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:request', '#', 'admin', sysdate(), '', null, ''), +(1108, '退回确认', 512, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:confirm', '#', 'admin', sysdate(), '', null, ''); + +-- 应用完成后仅人工清理 /getRouters、用户信息以及 sys_dict:ai_provider_type 相关缓存;本补丁不执行缓存命令。 diff --git a/shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql b/shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql index 6b3efe4..6f91314 100644 --- a/shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql +++ b/shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql @@ -1,3 +1,25 @@ +-- ---------------------------- +-- 托盘业务表(须先于被引用的系统表按反向依赖删除) +-- ---------------------------- +drop table if exists biz_pallet_inventory; +drop table if exists biz_pallet_event; +drop table if exists biz_return_item; +drop table if exists biz_return_order; +drop table if exists biz_usage_record; +drop table if exists biz_shipment_item; +drop table if exists biz_shipment_order; +drop table if exists biz_filling_record; +drop table if exists biz_inbound_item; +drop table if exists biz_inbound_order; +drop table if exists biz_user_site; +drop table if exists biz_user_warehouse; +drop table if exists biz_pallet; +drop table if exists biz_production_line; +drop table if exists biz_customer_site; +drop table if exists biz_customer; +drop table if exists biz_warehouse; +drop table if exists biz_strain; + -- ---------------------------- -- 1、部门表 -- ---------------------------- @@ -1050,3 +1072,444 @@ RETURN pg_catalog.array_to_string(tokens[indexnum:length], $2); END IF; END; $$ IMMUTABLE STRICT LANGUAGE PLPGSQL; + +-- ---------------------------- +-- 20、托盘流转业务表 +-- ---------------------------- +create table biz_strain ( + strain_id bigserial not null, + strain_code varchar(64) not null, + strain_name varchar(100) not null, + status char(1) not null default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (strain_id), + constraint uq_biz_strain_code unique (strain_code), + constraint ck_biz_strain_status check (status in ('0', '1')) +); + +create table biz_warehouse ( + warehouse_id bigserial not null, + warehouse_code varchar(64) not null, + warehouse_name varchar(100) not null, + address varchar(500), + status char(1) not null default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (warehouse_id), + constraint uq_biz_warehouse_code unique (warehouse_code), + constraint ck_biz_warehouse_status check (status in ('0', '1')) +); + +create table biz_customer ( + customer_id bigserial not null, + customer_code varchar(64) not null, + customer_name varchar(100) not null, + status char(1) not null default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (customer_id), + constraint uq_biz_customer_code unique (customer_code), + constraint ck_biz_customer_status check (status in ('0', '1')) +); + +create table biz_customer_site ( + site_id bigserial not null, + customer_id bigint not null, + site_code varchar(64) not null, + site_name varchar(100) not null, + address varchar(500), + status char(1) not null default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (site_id), + constraint uq_biz_customer_site_customer_code unique (customer_id, site_code), + constraint ck_biz_customer_site_status check (status in ('0', '1')), + constraint fk_biz_customer_site_customer foreign key (customer_id) references biz_customer(customer_id) +); + +create table biz_production_line ( + line_id bigserial not null, + site_id bigint not null, + line_code varchar(64) not null, + line_name varchar(100) not null, + status char(1) not null default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (line_id), + constraint uq_biz_production_line_site_code unique (site_id, line_code), + constraint ck_biz_production_line_status check (status in ('0', '1')), + constraint fk_biz_production_line_site foreign key (site_id) references biz_customer_site(site_id) +); + +create table biz_pallet ( + pallet_id bigserial not null, + pallet_code varchar(64) not null, + qr_token varchar(128) not null, + capacity_packages integer not null, + asset_status char(1) not null default '0', + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (pallet_id), + constraint uq_biz_pallet_code unique (pallet_code), + constraint uq_biz_pallet_qr_token unique (qr_token), + constraint ck_biz_pallet_capacity_packages check (capacity_packages > 0), + constraint ck_biz_pallet_asset_status check (asset_status in ('0', '1')) +); + +create table biz_user_warehouse ( + user_id bigint not null, + warehouse_id bigint not null, + create_by varchar(64) default '', + create_time timestamp(0), + primary key (user_id, warehouse_id), + constraint fk_biz_user_warehouse_user foreign key (user_id) references sys_user(user_id), + constraint fk_biz_user_warehouse_warehouse foreign key (warehouse_id) references biz_warehouse(warehouse_id) +); + +create table biz_user_site ( + user_id bigint not null, + site_id bigint not null, + create_by varchar(64) default '', + create_time timestamp(0), + primary key (user_id, site_id), + constraint fk_biz_user_site_user foreign key (user_id) references sys_user(user_id), + constraint fk_biz_user_site_site foreign key (site_id) references biz_customer_site(site_id) +); + +create table biz_inbound_order ( + inbound_id bigserial not null, + inbound_no varchar(64) not null, + warehouse_id bigint not null, + status varchar(32) not null default 'DRAFT', + confirm_idempotency_key varchar(64), + confirm_request_hash varchar(64), + confirmed_by bigint, + confirmed_at timestamp(0), + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (inbound_id), + constraint uq_biz_inbound_order_no unique (inbound_no), + constraint uq_biz_inbound_order_confirm_key unique (confirm_idempotency_key), + constraint ck_biz_inbound_order_status check (status in ('DRAFT', 'CONFIRMED')), + constraint fk_biz_inbound_order_warehouse foreign key (warehouse_id) references biz_warehouse(warehouse_id), + constraint fk_biz_inbound_order_confirmer foreign key (confirmed_by) references sys_user(user_id) +); + +create table biz_inbound_item ( + inbound_item_id bigserial not null, + inbound_id bigint not null, + pallet_id bigint not null, + create_by varchar(64) default '', + create_time timestamp(0), + primary key (inbound_item_id), + constraint uq_biz_inbound_item_order_pallet unique (inbound_id, pallet_id), + constraint fk_biz_inbound_item_order foreign key (inbound_id) references biz_inbound_order(inbound_id), + constraint fk_biz_inbound_item_pallet foreign key (pallet_id) references biz_pallet(pallet_id) +); + +create table biz_filling_record ( + filling_id bigserial not null, + filling_no varchar(64) not null, + pallet_id bigint not null, + warehouse_id bigint not null, + strain_id bigint not null, + strain_code_snapshot varchar(64) not null, + strain_name_snapshot varchar(100) not null, + batch_no varchar(64) not null, + package_spec varchar(64) not null, + initial_packages integer not null, + active_slot varchar(16), + closed_at timestamp(0), + idempotency_key varchar(64) not null, + request_hash varchar(64) not null, + operator_id bigint not null, + operated_at timestamp(0) not null, + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (filling_id), + constraint uq_biz_filling_record_no unique (filling_no), + constraint uq_biz_filling_record_idempotency unique (idempotency_key), + constraint uq_biz_filling_record_pallet_active_slot unique (pallet_id, active_slot), + constraint ck_biz_filling_record_initial_packages check (initial_packages > 0), + constraint ck_biz_filling_record_active_slot check (active_slot is null or active_slot = 'ACTIVE'), + constraint fk_biz_filling_record_pallet foreign key (pallet_id) references biz_pallet(pallet_id), + constraint fk_biz_filling_record_warehouse foreign key (warehouse_id) references biz_warehouse(warehouse_id), + constraint fk_biz_filling_record_strain foreign key (strain_id) references biz_strain(strain_id), + constraint fk_biz_filling_record_operator foreign key (operator_id) references sys_user(user_id) +); + +create table biz_shipment_order ( + shipment_id bigserial not null, + shipment_no varchar(64) not null, + source_warehouse_id bigint not null, + target_site_id bigint not null, + status varchar(32) not null default 'DRAFT', + confirm_idempotency_key varchar(64), + confirm_request_hash varchar(64), + confirmed_by bigint, + confirmed_at timestamp(0), + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (shipment_id), + constraint uq_biz_shipment_order_no unique (shipment_no), + constraint uq_biz_shipment_order_confirm_key unique (confirm_idempotency_key), + constraint ck_biz_shipment_order_status check (status in ('DRAFT', 'CONFIRMED')), + constraint fk_biz_shipment_order_warehouse foreign key (source_warehouse_id) references biz_warehouse(warehouse_id), + constraint fk_biz_shipment_order_site foreign key (target_site_id) references biz_customer_site(site_id), + constraint fk_biz_shipment_order_confirmer foreign key (confirmed_by) references sys_user(user_id) +); + +create table biz_shipment_item ( + shipment_item_id bigserial not null, + shipment_id bigint not null, + pallet_id bigint not null, + active_filling_id bigint, + package_qty integer, + create_by varchar(64) default '', + create_time timestamp(0), + primary key (shipment_item_id), + constraint uq_biz_shipment_item_order_pallet unique (shipment_id, pallet_id), + constraint ck_biz_shipment_item_package_qty check (package_qty is null or package_qty > 0), + constraint fk_biz_shipment_item_order foreign key (shipment_id) references biz_shipment_order(shipment_id), + constraint fk_biz_shipment_item_pallet foreign key (pallet_id) references biz_pallet(pallet_id), + constraint fk_biz_shipment_item_filling foreign key (active_filling_id) references biz_filling_record(filling_id) +); + +create table biz_usage_record ( + usage_id bigserial not null, + usage_no varchar(64) not null, + pallet_id bigint not null, + active_filling_id bigint not null, + site_id bigint not null, + line_id bigint not null, + used_packages integer not null, + remaining_packages_after integer not null, + idempotency_key varchar(64) not null, + request_hash varchar(64) not null, + operator_id bigint not null, + occurred_at timestamp(0) not null, + primary key (usage_id), + constraint uq_biz_usage_record_no unique (usage_no), + constraint uq_biz_usage_record_idempotency unique (idempotency_key), + constraint ck_biz_usage_record_used_packages check (used_packages > 0), + constraint ck_biz_usage_record_remaining_packages_after check (remaining_packages_after >= 0), + constraint fk_biz_usage_record_pallet foreign key (pallet_id) references biz_pallet(pallet_id), + constraint fk_biz_usage_record_filling foreign key (active_filling_id) references biz_filling_record(filling_id), + constraint fk_biz_usage_record_site foreign key (site_id) references biz_customer_site(site_id), + constraint fk_biz_usage_record_line foreign key (line_id) references biz_production_line(line_id), + constraint fk_biz_usage_record_operator foreign key (operator_id) references sys_user(user_id) +); + +create table biz_return_order ( + return_id bigserial not null, + return_no varchar(64) not null, + source_site_id bigint not null, + target_warehouse_id bigint not null, + status varchar(32) not null default 'REQUESTED', + request_idempotency_key varchar(64) not null, + request_hash varchar(64) not null, + confirm_idempotency_key varchar(64), + confirm_request_hash varchar(64), + requested_by bigint not null, + requested_at timestamp(0) not null, + confirmed_by bigint, + confirmed_at timestamp(0), + create_by varchar(64) default '', + create_time timestamp(0), + update_by varchar(64) default '', + update_time timestamp(0), + remark varchar(500) default null, + primary key (return_id), + constraint uq_biz_return_order_no unique (return_no), + constraint uq_biz_return_order_request_key unique (request_idempotency_key), + constraint uq_biz_return_order_confirm_key unique (confirm_idempotency_key), + constraint ck_biz_return_order_status check (status in ('REQUESTED', 'CONFIRMED')), + constraint fk_biz_return_order_site foreign key (source_site_id) references biz_customer_site(site_id), + constraint fk_biz_return_order_warehouse foreign key (target_warehouse_id) references biz_warehouse(warehouse_id), + constraint fk_biz_return_order_requester foreign key (requested_by) references sys_user(user_id), + constraint fk_biz_return_order_confirmer foreign key (confirmed_by) references sys_user(user_id) +); + +create table biz_return_item ( + return_item_id bigserial not null, + return_id bigint not null, + pallet_id bigint not null, + active_filling_id_snapshot bigint, + remaining_packages_snapshot integer not null, + create_by varchar(64) default '', + create_time timestamp(0), + primary key (return_item_id), + constraint uq_biz_return_item_order_pallet unique (return_id, pallet_id), + constraint ck_biz_return_item_remaining_packages_snapshot check (remaining_packages_snapshot >= 0), + constraint fk_biz_return_item_order foreign key (return_id) references biz_return_order(return_id), + constraint fk_biz_return_item_pallet foreign key (pallet_id) references biz_pallet(pallet_id), + constraint fk_biz_return_item_filling foreign key (active_filling_id_snapshot) references biz_filling_record(filling_id) +); + +create table biz_pallet_event ( + event_id bigserial not null, + pallet_id bigint not null, + event_seq integer not null, + event_type varchar(32) not null, + source_type varchar(32) not null, + source_id bigint not null, + before_state varchar(32) not null, + after_state varchar(32) not null, + before_warehouse_id bigint, + before_site_id bigint, + before_line_id bigint, + after_warehouse_id bigint, + after_site_id bigint, + after_line_id bigint, + before_remaining_packages integer not null, + after_remaining_packages integer not null, + operator_id bigint not null, + occurred_at timestamp(0) not null, + idempotency_key varchar(64) not null, + request_hash varchar(64) not null, + primary key (event_id), + constraint uq_biz_pallet_event_pallet_seq unique (pallet_id, event_seq), + constraint uq_biz_pallet_event_pallet_idempotency unique (pallet_id, idempotency_key), + constraint ck_biz_pallet_event_event_type check ( + event_type in ( + 'INBOUND_CONFIRMED', 'FILLING_CONFIRMED', 'SHIPMENT_CONFIRMED', + 'USAGE_RECORDED', 'RETURN_REQUESTED', 'RETURN_CONFIRMED' + ) + ), + constraint fk_biz_pallet_event_pallet foreign key (pallet_id) references biz_pallet(pallet_id), + constraint fk_biz_pallet_event_before_warehouse foreign key (before_warehouse_id) references biz_warehouse(warehouse_id), + constraint fk_biz_pallet_event_before_site foreign key (before_site_id) references biz_customer_site(site_id), + constraint fk_biz_pallet_event_before_line foreign key (before_line_id) references biz_production_line(line_id), + constraint fk_biz_pallet_event_after_warehouse foreign key (after_warehouse_id) references biz_warehouse(warehouse_id), + constraint fk_biz_pallet_event_after_site foreign key (after_site_id) references biz_customer_site(site_id), + constraint fk_biz_pallet_event_after_line foreign key (after_line_id) references biz_production_line(line_id), + constraint fk_biz_pallet_event_operator foreign key (operator_id) references sys_user(user_id) +); + +create table biz_pallet_inventory ( + pallet_id bigint not null, + active_filling_id bigint, + warehouse_id bigint, + site_id bigint, + current_line_id bigint, + flow_state varchar(32) not null default 'UNSTOCKED', + remaining_packages integer not null default 0, + last_event_id bigint, + version integer not null default 0, + updated_at timestamp(0) not null, + primary key (pallet_id), + constraint ck_biz_pallet_inventory_flow_state check ( + flow_state in ( + 'UNSTOCKED', 'EMPTY_IN_WAREHOUSE', 'FILLED_IN_WAREHOUSE', 'AT_SITE_UNUSED', + 'AT_SITE_PARTIAL', 'AT_SITE_EMPTY', 'RETURNING' + ) + ), + constraint ck_biz_pallet_inventory_remaining_packages check (remaining_packages >= 0), + constraint ck_biz_pallet_inventory_version check (version >= 0), + constraint ck_biz_pallet_inventory_location check ( + not (warehouse_id is not null and site_id is not null) + ), + constraint fk_biz_pallet_inventory_pallet foreign key (pallet_id) references biz_pallet(pallet_id), + constraint fk_biz_pallet_inventory_filling foreign key (active_filling_id) references biz_filling_record(filling_id), + constraint fk_biz_pallet_inventory_warehouse foreign key (warehouse_id) references biz_warehouse(warehouse_id), + constraint fk_biz_pallet_inventory_site foreign key (site_id) references biz_customer_site(site_id), + constraint fk_biz_pallet_inventory_line foreign key (current_line_id) references biz_production_line(line_id), + constraint fk_biz_pallet_inventory_event foreign key (last_event_id) references biz_pallet_event(event_id) +); + +-- ---------------------------- +-- 初始化-托盘业务菜单和权限 +-- ---------------------------- +insert into sys_menu values +(121, '基础资料维护', 1, '10', 'businessMaster', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', current_timestamp, '', null, ''), +(122, '业务管理', 1, '11', 'businessOperation', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', current_timestamp, '', null, ''), +(123, '报表查询', 1, '12', 'businessReport', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', current_timestamp, '', null, ''), +(502, '菌种资料', 121, '1', 'strain', 'master/strain/index', '', '', 1, 0, 'C', '0', '0', 'master:strain:list', '#', 'admin', current_timestamp, '', null, ''), +(503, '客户资料', 121, '2', 'customer', 'master/customer/index', '', '', 1, 0, 'C', '0', '0', 'master:customer:list', '#', 'admin', current_timestamp, '', null, ''), +(504, '客户现场', 121, '3', 'site', 'master/site/index', '', '', 1, 0, 'C', '0', '0', 'master:site:list', '#', 'admin', current_timestamp, '', null, ''), +(505, '产线资料', 121, '4', 'line', 'master/line/index', '', '', 1, 0, 'C', '0', '0', 'master:line:list', '#', 'admin', current_timestamp, '', null, ''), +(506, '仓库资料', 121, '5', 'warehouse', 'master/warehouse/index', '', '', 1, 0, 'C', '0', '0', 'master:warehouse:list', '#', 'admin', current_timestamp, '', null, ''), +(507, '托盘资料', 121, '6', 'pallet', 'master/pallet/index', '', '', 1, 0, 'C', '0', '0', 'master:pallet:list', '#', 'admin', current_timestamp, '', null, ''), +(508, '仓库入库', 122, '1', 'inbound', 'business/inbound/index', '', '', 1, 0, 'C', '0', '0', 'business:inbound:list', '#', 'admin', current_timestamp, '', null, ''), +(509, '工厂装填', 122, '2', 'filling', 'business/filling/index', '', '', 1, 0, 'C', '0', '0', 'business:filling:list', '#', 'admin', current_timestamp, '', null, ''), +(510, '发货出库', 122, '3', 'shipment', 'business/shipment/index', '', '', 1, 0, 'C', '0', '0', 'business:shipment:list', '#', 'admin', current_timestamp, '', null, ''), +(511, '客户现场使用', 122, '4', 'usage', 'business/usage/index', '', '', 1, 0, 'C', '0', '0', 'business:usage:list', '#', 'admin', current_timestamp, '', null, ''), +(512, '退回仓库', 122, '5', 'return', 'business/return/index', '', '', 1, 0, 'C', '0', '0', 'business:return:list', '#', 'admin', current_timestamp, '', null, ''), +(513, '托盘库存', 123, '1', 'palletInventory', 'report/palletInventory/index', '', '', 1, 0, 'C', '0', '0', 'report:palletinventory:list', '#', 'admin', current_timestamp, '', null, ''), +(514, '装填记录', 123, '2', 'reportFilling', 'report/filling/index', '', '', 1, 0, 'C', '0', '0', 'report:filling:list', '#', 'admin', current_timestamp, '', null, ''), +(515, '发货出库', 123, '3', 'reportShipment', 'report/shipment/index', '', '', 1, 0, 'C', '0', '0', 'report:shipment:list', '#', 'admin', current_timestamp, '', null, ''), +(516, '产线消耗', 123, '4', 'consumption', 'report/consumption/index', '', '', 1, 0, 'C', '0', '0', 'report:consumption:list', '#', 'admin', current_timestamp, '', null, ''), +(517, '退回仓库', 123, '5', 'reportReturn', 'report/return/index', '', '', 1, 0, 'C', '0', '0', 'report:return:list', '#', 'admin', current_timestamp, '', null, ''), +(518, '托盘追溯', 123, '6', 'palletTrace', 'report/palletTrace/index', '', '', 1, 0, 'C', '0', '0', 'report:pallettrace:list', '#', 'admin', current_timestamp, '', null, ''), +(1065, '菌种查询', 502, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:query', '#', 'admin', current_timestamp, '', null, ''), +(1066, '菌种新增', 502, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:add', '#', 'admin', current_timestamp, '', null, ''), +(1067, '菌种修改', 502, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:edit', '#', 'admin', current_timestamp, '', null, ''), +(1068, '菌种删除', 502, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:remove', '#', 'admin', current_timestamp, '', null, ''), +(1069, '客户查询', 503, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:query', '#', 'admin', current_timestamp, '', null, ''), +(1070, '客户新增', 503, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:add', '#', 'admin', current_timestamp, '', null, ''), +(1071, '客户修改', 503, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:edit', '#', 'admin', current_timestamp, '', null, ''), +(1072, '客户删除', 503, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:remove', '#', 'admin', current_timestamp, '', null, ''), +(1073, '现场查询', 504, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:query', '#', 'admin', current_timestamp, '', null, ''), +(1074, '现场新增', 504, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:add', '#', 'admin', current_timestamp, '', null, ''), +(1075, '现场修改', 504, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:edit', '#', 'admin', current_timestamp, '', null, ''), +(1076, '现场删除', 504, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:remove', '#', 'admin', current_timestamp, '', null, ''), +(1077, '分配用户', 504, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:assign', '#', 'admin', current_timestamp, '', null, ''), +(1078, '产线查询', 505, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:query', '#', 'admin', current_timestamp, '', null, ''), +(1079, '产线新增', 505, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:add', '#', 'admin', current_timestamp, '', null, ''), +(1080, '产线修改', 505, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:edit', '#', 'admin', current_timestamp, '', null, ''), +(1081, '产线删除', 505, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:remove', '#', 'admin', current_timestamp, '', null, ''), +(1082, '仓库查询', 506, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:query', '#', 'admin', current_timestamp, '', null, ''), +(1083, '仓库新增', 506, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:add', '#', 'admin', current_timestamp, '', null, ''), +(1084, '仓库修改', 506, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:edit', '#', 'admin', current_timestamp, '', null, ''), +(1085, '仓库删除', 506, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:remove', '#', 'admin', current_timestamp, '', null, ''), +(1086, '分配用户', 506, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:assign', '#', 'admin', current_timestamp, '', null, ''), +(1087, '托盘查询', 507, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:query', '#', 'admin', current_timestamp, '', null, ''), +(1088, '托盘新增', 507, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:add', '#', 'admin', current_timestamp, '', null, ''), +(1089, '托盘修改', 507, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:edit', '#', 'admin', current_timestamp, '', null, ''), +(1090, '托盘删除', 507, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:remove', '#', 'admin', current_timestamp, '', null, ''), +(1091, '托盘打印', 507, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:print', '#', 'admin', current_timestamp, '', null, ''), +(1092, '入库查询', 508, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:query', '#', 'admin', current_timestamp, '', null, ''), +(1093, '入库新增', 508, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:add', '#', 'admin', current_timestamp, '', null, ''), +(1094, '入库修改', 508, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:edit', '#', 'admin', current_timestamp, '', null, ''), +(1095, '入库删除', 508, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:remove', '#', 'admin', current_timestamp, '', null, ''), +(1096, '入库确认', 508, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:confirm', '#', 'admin', current_timestamp, '', null, ''), +(1097, '装填查询', 509, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:filling:query', '#', 'admin', current_timestamp, '', null, ''), +(1098, '装填新增', 509, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:filling:add', '#', 'admin', current_timestamp, '', null, ''), +(1099, '发货查询', 510, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:query', '#', 'admin', current_timestamp, '', null, ''), +(1100, '发货新增', 510, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:add', '#', 'admin', current_timestamp, '', null, ''), +(1101, '发货修改', 510, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:edit', '#', 'admin', current_timestamp, '', null, ''), +(1102, '发货删除', 510, '4', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:remove', '#', 'admin', current_timestamp, '', null, ''), +(1103, '发货确认', 510, '5', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:confirm', '#', 'admin', current_timestamp, '', null, ''), +(1104, '使用查询', 511, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:usage:query', '#', 'admin', current_timestamp, '', null, ''), +(1105, '使用新增', 511, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:usage:add', '#', 'admin', current_timestamp, '', null, ''), +(1106, '退回查询', 512, '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:query', '#', 'admin', current_timestamp, '', null, ''), +(1107, '退回申请', 512, '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:request', '#', 'admin', current_timestamp, '', null, ''), +(1108, '退回确认', 512, '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:confirm', '#', 'admin', current_timestamp, '', null, ''); diff --git a/shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql b/shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql index d3937ef..c4b24c9 100644 --- a/shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql +++ b/shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql @@ -1,3 +1,25 @@ +-- ---------------------------- +-- 托盘业务表(须先于被引用的系统表按反向依赖删除) +-- ---------------------------- +drop table if exists biz_pallet_inventory; +drop table if exists biz_pallet_event; +drop table if exists biz_return_item; +drop table if exists biz_return_order; +drop table if exists biz_usage_record; +drop table if exists biz_shipment_item; +drop table if exists biz_shipment_order; +drop table if exists biz_filling_record; +drop table if exists biz_inbound_item; +drop table if exists biz_inbound_order; +drop table if exists biz_user_site; +drop table if exists biz_user_warehouse; +drop table if exists biz_pallet; +drop table if exists biz_production_line; +drop table if exists biz_customer_site; +drop table if exists biz_customer; +drop table if exists biz_warehouse; +drop table if exists biz_strain; + -- ---------------------------- -- 1、部门表 -- ---------------------------- @@ -260,6 +282,76 @@ insert into sys_menu values('1058', '导入代码', '116', '4', '#', '', '', '', insert into sys_menu values('1059', '预览代码', '116', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:preview', '#', 'admin', sysdate(), '', null, ''); insert into sys_menu values('1060', '生成代码', '116', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'tool:gen:code', '#', 'admin', sysdate(), '', null, ''); +-- 托盘业务目录 +insert into sys_menu values('121', '基础资料维护', '1', '10', 'businessMaster', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', sysdate(), '', null, '基础资料维护目录'); +insert into sys_menu values('122', '业务管理', '1', '11', 'businessOperation', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', sysdate(), '', null, '业务管理目录'); +insert into sys_menu values('123', '报表查询', '1', '12', 'businessReport', null, '', '', 1, 0, 'M', '0', '0', '', '#', 'admin', sysdate(), '', null, '报表查询目录'); + +-- 托盘业务页面 +insert into sys_menu values('502', '菌种资料', '121', '1', 'strain', 'master/strain/index', '', '', 1, 0, 'C', '0', '0', 'master:strain:list', '#', 'admin', sysdate(), '', null, '菌种资料菜单'); +insert into sys_menu values('503', '客户资料', '121', '2', 'customer', 'master/customer/index', '', '', 1, 0, 'C', '0', '0', 'master:customer:list', '#', 'admin', sysdate(), '', null, '客户资料菜单'); +insert into sys_menu values('504', '客户现场', '121', '3', 'site', 'master/site/index', '', '', 1, 0, 'C', '0', '0', 'master:site:list', '#', 'admin', sysdate(), '', null, '客户现场菜单'); +insert into sys_menu values('505', '产线资料', '121', '4', 'line', 'master/line/index', '', '', 1, 0, 'C', '0', '0', 'master:line:list', '#', 'admin', sysdate(), '', null, '产线资料菜单'); +insert into sys_menu values('506', '仓库资料', '121', '5', 'warehouse', 'master/warehouse/index', '', '', 1, 0, 'C', '0', '0', 'master:warehouse:list', '#', 'admin', sysdate(), '', null, '仓库资料菜单'); +insert into sys_menu values('507', '托盘资料', '121', '6', 'pallet', 'master/pallet/index', '', '', 1, 0, 'C', '0', '0', 'master:pallet:list', '#', 'admin', sysdate(), '', null, '托盘资料菜单'); +insert into sys_menu values('508', '仓库入库', '122', '1', 'inbound', 'business/inbound/index', '', '', 1, 0, 'C', '0', '0', 'business:inbound:list', '#', 'admin', sysdate(), '', null, '仓库入库菜单'); +insert into sys_menu values('509', '工厂装填', '122', '2', 'filling', 'business/filling/index', '', '', 1, 0, 'C', '0', '0', 'business:filling:list', '#', 'admin', sysdate(), '', null, '工厂装填菜单'); +insert into sys_menu values('510', '发货出库', '122', '3', 'shipment', 'business/shipment/index', '', '', 1, 0, 'C', '0', '0', 'business:shipment:list', '#', 'admin', sysdate(), '', null, '发货出库菜单'); +insert into sys_menu values('511', '客户现场使用', '122', '4', 'usage', 'business/usage/index', '', '', 1, 0, 'C', '0', '0', 'business:usage:list', '#', 'admin', sysdate(), '', null, '客户现场使用菜单'); +insert into sys_menu values('512', '退回仓库', '122', '5', 'return', 'business/return/index', '', '', 1, 0, 'C', '0', '0', 'business:return:list', '#', 'admin', sysdate(), '', null, '退回仓库菜单'); +insert into sys_menu values('513', '托盘库存', '123', '1', 'palletInventory', 'report/palletInventory/index', '', '', 1, 0, 'C', '0', '0', 'report:palletinventory:list', '#', 'admin', sysdate(), '', null, '托盘库存菜单'); +insert into sys_menu values('514', '装填记录', '123', '2', 'reportFilling', 'report/filling/index', '', '', 1, 0, 'C', '0', '0', 'report:filling:list', '#', 'admin', sysdate(), '', null, '装填记录菜单'); +insert into sys_menu values('515', '发货出库', '123', '3', 'reportShipment', 'report/shipment/index', '', '', 1, 0, 'C', '0', '0', 'report:shipment:list', '#', 'admin', sysdate(), '', null, '发货出库报表菜单'); +insert into sys_menu values('516', '产线消耗', '123', '4', 'consumption', 'report/consumption/index', '', '', 1, 0, 'C', '0', '0', 'report:consumption:list', '#', 'admin', sysdate(), '', null, '产线消耗菜单'); +insert into sys_menu values('517', '退回仓库', '123', '5', 'reportReturn', 'report/return/index', '', '', 1, 0, 'C', '0', '0', 'report:return:list', '#', 'admin', sysdate(), '', null, '退回仓库报表菜单'); +insert into sys_menu values('518', '托盘追溯', '123', '6', 'palletTrace', 'report/palletTrace/index', '', '', 1, 0, 'C', '0', '0', 'report:pallettrace:list', '#', 'admin', sysdate(), '', null, '托盘追溯菜单'); + +-- 托盘业务按钮 +insert into sys_menu values('1065', '菌种查询', '502', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1066', '菌种新增', '502', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1067', '菌种修改', '502', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1068', '菌种删除', '502', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:strain:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1069', '客户查询', '503', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1070', '客户新增', '503', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1071', '客户修改', '503', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1072', '客户删除', '503', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:customer:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1073', '现场查询', '504', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1074', '现场新增', '504', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1075', '现场修改', '504', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1076', '现场删除', '504', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1077', '分配用户', '504', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:site:assign', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1078', '产线查询', '505', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1079', '产线新增', '505', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1080', '产线修改', '505', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1081', '产线删除', '505', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:line:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1082', '仓库查询', '506', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1083', '仓库新增', '506', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1084', '仓库修改', '506', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1085', '仓库删除', '506', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1086', '分配用户', '506', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:warehouse:assign', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1087', '托盘查询', '507', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1088', '托盘新增', '507', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1089', '托盘修改', '507', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1090', '托盘删除', '507', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1091', '托盘打印', '507', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'master:pallet:print', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1092', '入库查询', '508', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1093', '入库新增', '508', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1094', '入库修改', '508', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1095', '入库删除', '508', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1096', '入库确认', '508', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'business:inbound:confirm', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1097', '装填查询', '509', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:filling:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1098', '装填新增', '509', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:filling:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1099', '发货查询', '510', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1100', '发货新增', '510', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1101', '发货修改', '510', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:edit', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1102', '发货删除', '510', '4', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:remove', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1103', '发货确认', '510', '5', '', '', '', '', 1, 0, 'F', '0', '0', 'business:shipment:confirm', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1104', '使用查询', '511', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:usage:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1105', '使用新增', '511', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:usage:add', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1106', '退回查询', '512', '1', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:query', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1107', '退回申请', '512', '2', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:request', '#', 'admin', sysdate(), '', null, ''); +insert into sys_menu values('1108', '退回确认', '512', '3', '', '', '', '', 1, 0, 'F', '0', '0', 'business:return:confirm', '#', 'admin', sysdate(), '', null, ''); + -- ---------------------------- -- 6、用户和角色关联表 用户N-1角色 @@ -714,3 +806,401 @@ create table gen_table_column ( update_time datetime comment '更新时间', primary key (column_id) ) engine=innodb auto_increment=1 comment = '代码生成业务表字段'; + + +-- ---------------------------- +-- 20、托盘业务表(按依赖创建) +-- ---------------------------- +-- 菌种主数据表 +create table biz_strain ( + strain_id bigint(20) not null auto_increment comment '菌种ID', + strain_code varchar(64) not null comment '菌种编码', + strain_name varchar(100) not null comment '菌种名称', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (strain_id), + constraint uq_biz_strain_code unique (strain_code), + constraint ck_biz_strain_status check (status in ('0', '1')) +) engine=innodb comment = '菌种主数据表'; + +-- 仓库主数据表 +create table biz_warehouse ( + warehouse_id bigint(20) not null auto_increment comment '仓库ID', + warehouse_code varchar(64) not null comment '仓库编码', + warehouse_name varchar(100) not null comment '仓库名称', + address varchar(500) comment '仓库地址', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (warehouse_id), + constraint uq_biz_warehouse_code unique (warehouse_code), + constraint ck_biz_warehouse_status check (status in ('0', '1')) +) engine=innodb comment = '仓库主数据表'; + +-- 客户主数据表 +create table biz_customer ( + customer_id bigint(20) not null auto_increment comment '客户ID', + customer_code varchar(64) not null comment '客户编码', + customer_name varchar(100) not null comment '客户名称', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (customer_id), + constraint uq_biz_customer_code unique (customer_code), + constraint ck_biz_customer_status check (status in ('0', '1')) +) engine=innodb comment = '客户主数据表'; + +-- 客户现场主数据表 +create table biz_customer_site ( + site_id bigint(20) not null auto_increment comment '现场ID', + customer_id bigint(20) not null comment '客户ID', + site_code varchar(64) not null comment '现场编码', + site_name varchar(100) not null comment '现场名称', + address varchar(500) comment '现场地址', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (site_id), + constraint uq_biz_customer_site_customer_code unique (customer_id, site_code), + constraint fk_biz_customer_site_customer foreign key (customer_id) references biz_customer (customer_id), + constraint ck_biz_customer_site_status check (status in ('0', '1')) +) engine=innodb comment = '客户现场主数据表'; + +-- 客户现场产线主数据表 +create table biz_production_line ( + line_id bigint(20) not null auto_increment comment '产线ID', + site_id bigint(20) not null comment '现场ID', + line_code varchar(64) not null comment '产线编码', + line_name varchar(100) not null comment '产线名称', + status char(1) not null default '0' comment '状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (line_id), + constraint uq_biz_production_line_site_code unique (site_id, line_code), + constraint fk_biz_production_line_site foreign key (site_id) references biz_customer_site (site_id), + constraint ck_biz_production_line_status check (status in ('0', '1')) +) engine=innodb comment = '客户现场产线主数据表'; + +-- 托盘资产主数据表 +create table biz_pallet ( + pallet_id bigint(20) not null auto_increment comment '托盘ID', + pallet_code varchar(64) not null comment '托盘编号', + qr_token varchar(128) not null comment '二维码固定标识', + capacity_packages int not null comment '容量包数', + asset_status char(1) not null default '0' comment '资产状态(0正常 1停用)', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (pallet_id), + constraint uq_biz_pallet_code unique (pallet_code), + constraint uq_biz_pallet_qr_token unique (qr_token), + constraint ck_biz_pallet_capacity_packages check (capacity_packages > 0), + constraint ck_biz_pallet_asset_status check (asset_status in ('0', '1')) +) engine=innodb comment = '托盘资产主数据表'; + +-- 用户与仓库关联表 +create table biz_user_warehouse ( + user_id bigint(20) not null comment '用户ID', + warehouse_id bigint(20) not null comment '仓库ID', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (user_id, warehouse_id), + constraint fk_biz_user_warehouse_user foreign key (user_id) references sys_user (user_id), + constraint fk_biz_user_warehouse_warehouse foreign key (warehouse_id) references biz_warehouse (warehouse_id) +) engine=innodb comment = '用户与仓库关联表'; + +-- 用户与客户现场关联表 +create table biz_user_site ( + user_id bigint(20) not null comment '用户ID', + site_id bigint(20) not null comment '现场ID', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (user_id, site_id), + constraint fk_biz_user_site_user foreign key (user_id) references sys_user (user_id), + constraint fk_biz_user_site_site foreign key (site_id) references biz_customer_site (site_id) +) engine=innodb comment = '用户与客户现场关联表'; + +-- 首次入库单 +create table biz_inbound_order ( + inbound_id bigint(20) not null auto_increment comment '入库单ID', + inbound_no varchar(64) not null comment '入库单号', + warehouse_id bigint(20) not null comment '仓库ID', + status varchar(32) not null default 'DRAFT' comment '单据状态', + confirm_idempotency_key varchar(64) comment '确认幂等键', + confirm_request_hash varchar(64) comment '确认请求摘要', + confirmed_by bigint(20) comment '确认人ID', + confirmed_at datetime comment '确认时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (inbound_id), + constraint uq_biz_inbound_order_no unique (inbound_no), + constraint uq_biz_inbound_order_confirm_idempotency unique (confirm_idempotency_key), + constraint fk_biz_inbound_order_warehouse foreign key (warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_inbound_order_confirmed_by foreign key (confirmed_by) references sys_user (user_id), + constraint ck_biz_inbound_order_status check (status in ('DRAFT', 'CONFIRMED')) +) engine=innodb comment = '首次入库单'; + +-- 首次入库单托盘明细 +create table biz_inbound_item ( + inbound_item_id bigint(20) not null auto_increment comment '入库明细ID', + inbound_id bigint(20) not null comment '入库单ID', + pallet_id bigint(20) not null comment '托盘ID', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (inbound_item_id), + constraint uq_biz_inbound_item_order_pallet unique (inbound_id, pallet_id), + constraint fk_biz_inbound_item_order foreign key (inbound_id) references biz_inbound_order (inbound_id), + constraint fk_biz_inbound_item_pallet foreign key (pallet_id) references biz_pallet (pallet_id) +) engine=innodb comment = '首次入库单托盘明细'; + +-- 托盘装填记录 +create table biz_filling_record ( + filling_id bigint(20) not null auto_increment comment '装填记录ID', + filling_no varchar(64) not null comment '装填单号', + pallet_id bigint(20) not null comment '托盘ID', + warehouse_id bigint(20) not null comment '仓库ID', + strain_id bigint(20) not null comment '菌种ID', + strain_code_snapshot varchar(64) not null comment '菌种编码快照', + strain_name_snapshot varchar(100) not null comment '菌种名称快照', + batch_no varchar(64) not null comment '生产批次', + package_spec varchar(64) not null comment '包装规格', + initial_packages int not null comment '初始包数', + active_slot varchar(16) comment '活动装填槽', + closed_at datetime comment '装填周期关闭时间', + idempotency_key varchar(64) not null comment '幂等键', + request_hash varchar(64) not null comment '请求摘要', + operator_id bigint(20) not null comment '操作人ID', + operated_at datetime not null comment '操作时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (filling_id), + constraint uq_biz_filling_record_no unique (filling_no), + constraint uq_biz_filling_record_idempotency unique (idempotency_key), + constraint uq_biz_filling_record_pallet_active_slot unique (pallet_id, active_slot), + constraint fk_biz_filling_record_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_filling_record_warehouse foreign key (warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_filling_record_strain foreign key (strain_id) references biz_strain (strain_id), + constraint fk_biz_filling_record_operator foreign key (operator_id) references sys_user (user_id), + constraint ck_biz_filling_record_initial_packages check (initial_packages > 0), + constraint ck_biz_filling_record_active_slot check (active_slot is null or active_slot = 'ACTIVE') +) engine=innodb comment = '托盘装填记录'; + +-- 整托发货单 +create table biz_shipment_order ( + shipment_id bigint(20) not null auto_increment comment '发货单ID', + shipment_no varchar(64) not null comment '发货单号', + source_warehouse_id bigint(20) not null comment '来源仓库ID', + target_site_id bigint(20) not null comment '目标现场ID', + status varchar(32) not null default 'DRAFT' comment '单据状态', + confirm_idempotency_key varchar(64) comment '确认幂等键', + confirm_request_hash varchar(64) comment '确认请求摘要', + confirmed_by bigint(20) comment '确认人ID', + confirmed_at datetime comment '确认时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (shipment_id), + constraint uq_biz_shipment_order_no unique (shipment_no), + constraint uq_biz_shipment_order_confirm_idempotency unique (confirm_idempotency_key), + constraint fk_biz_shipment_order_warehouse foreign key (source_warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_shipment_order_site foreign key (target_site_id) references biz_customer_site (site_id), + constraint fk_biz_shipment_order_confirmed_by foreign key (confirmed_by) references sys_user (user_id), + constraint ck_biz_shipment_order_status check (status in ('DRAFT', 'CONFIRMED')) +) engine=innodb comment = '整托发货单'; + +-- 整托发货明细 +create table biz_shipment_item ( + shipment_item_id bigint(20) not null auto_increment comment '发货明细ID', + shipment_id bigint(20) not null comment '发货单ID', + pallet_id bigint(20) not null comment '托盘ID', + active_filling_id bigint(20) comment '确认时活动装填记录ID', + package_qty int comment '确认时包数快照', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (shipment_item_id), + constraint uq_biz_shipment_item_order_pallet unique (shipment_id, pallet_id), + constraint fk_biz_shipment_item_order foreign key (shipment_id) references biz_shipment_order (shipment_id), + constraint fk_biz_shipment_item_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_shipment_item_filling foreign key (active_filling_id) references biz_filling_record (filling_id), + constraint ck_biz_shipment_item_package_qty check (package_qty is null or package_qty > 0) +) engine=innodb comment = '整托发货明细'; + +-- 客户现场使用事实记录 +create table biz_usage_record ( + usage_id bigint(20) not null auto_increment comment '使用记录ID', + usage_no varchar(64) not null comment '使用记录号', + pallet_id bigint(20) not null comment '托盘ID', + active_filling_id bigint(20) not null comment '活动装填记录ID', + site_id bigint(20) not null comment '现场ID', + line_id bigint(20) not null comment '产线ID', + used_packages int not null comment '本次使用包数', + remaining_packages_after int not null comment '使用后剩余包数', + idempotency_key varchar(64) not null comment '幂等键', + request_hash varchar(64) not null comment '请求摘要', + operator_id bigint(20) not null comment '操作人ID', + occurred_at datetime not null comment '发生时间', + primary key (usage_id), + constraint uq_biz_usage_record_no unique (usage_no), + constraint uq_biz_usage_record_idempotency unique (idempotency_key), + constraint fk_biz_usage_record_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_usage_record_filling foreign key (active_filling_id) references biz_filling_record (filling_id), + constraint fk_biz_usage_record_site foreign key (site_id) references biz_customer_site (site_id), + constraint fk_biz_usage_record_line foreign key (line_id) references biz_production_line (line_id), + constraint fk_biz_usage_record_operator foreign key (operator_id) references sys_user (user_id), + constraint ck_biz_usage_record_used_packages check (used_packages > 0), + constraint ck_biz_usage_record_remaining_packages_after check (remaining_packages_after >= 0) +) engine=innodb comment = '客户现场使用事实记录'; + +-- 托盘退回单 +create table biz_return_order ( + return_id bigint(20) not null auto_increment comment '退回单ID', + return_no varchar(64) not null comment '退回单号', + source_site_id bigint(20) not null comment '来源现场ID', + target_warehouse_id bigint(20) not null comment '目标仓库ID', + status varchar(32) not null default 'REQUESTED' comment '退回状态', + request_idempotency_key varchar(64) not null comment '申请幂等键', + request_hash varchar(64) not null comment '申请请求摘要', + confirm_idempotency_key varchar(64) comment '确认幂等键', + confirm_request_hash varchar(64) comment '确认请求摘要', + requested_by bigint(20) not null comment '申请人ID', + requested_at datetime not null comment '申请时间', + confirmed_by bigint(20) comment '确认人ID', + confirmed_at datetime comment '确认时间', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + update_by varchar(64) default '' comment '更新者', + update_time datetime comment '更新时间', + remark varchar(500) default null comment '备注', + primary key (return_id), + constraint uq_biz_return_order_no unique (return_no), + constraint uq_biz_return_order_request_idempotency unique (request_idempotency_key), + constraint uq_biz_return_order_confirm_idempotency unique (confirm_idempotency_key), + constraint fk_biz_return_order_site foreign key (source_site_id) references biz_customer_site (site_id), + constraint fk_biz_return_order_warehouse foreign key (target_warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_return_order_requested_by foreign key (requested_by) references sys_user (user_id), + constraint fk_biz_return_order_confirmed_by foreign key (confirmed_by) references sys_user (user_id), + constraint ck_biz_return_order_status check (status in ('REQUESTED', 'CONFIRMED')) +) engine=innodb comment = '托盘退回单'; + +-- 托盘退回明细 +create table biz_return_item ( + return_item_id bigint(20) not null auto_increment comment '退回明细ID', + return_id bigint(20) not null comment '退回单ID', + pallet_id bigint(20) not null comment '托盘ID', + active_filling_id_snapshot bigint(20) comment '申请时活动装填记录快照', + remaining_packages_snapshot int not null comment '申请时剩余包数快照', + create_by varchar(64) default '' comment '创建者', + create_time datetime comment '创建时间', + primary key (return_item_id), + constraint uq_biz_return_item_order_pallet unique (return_id, pallet_id), + constraint fk_biz_return_item_order foreign key (return_id) references biz_return_order (return_id), + constraint fk_biz_return_item_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_return_item_filling foreign key (active_filling_id_snapshot) references biz_filling_record (filling_id), + constraint ck_biz_return_item_remaining_packages_snapshot check (remaining_packages_snapshot >= 0) +) engine=innodb comment = '托盘退回明细'; + +-- 托盘只追加流转事件;必须先于库存快照创建 +create table biz_pallet_event ( + event_id bigint(20) not null auto_increment comment '事件ID', + pallet_id bigint(20) not null comment '托盘ID', + event_seq int not null comment '托盘内事件序号', + event_type varchar(32) not null comment '事件类型', + source_type varchar(32) not null comment '来源类型', + source_id bigint(20) not null comment '来源记录ID', + before_state varchar(32) not null comment '变更前状态', + after_state varchar(32) not null comment '变更后状态', + before_warehouse_id bigint(20) comment '变更前仓库ID', + before_site_id bigint(20) comment '变更前现场ID', + before_line_id bigint(20) comment '变更前产线ID', + after_warehouse_id bigint(20) comment '变更后仓库ID', + after_site_id bigint(20) comment '变更后现场ID', + after_line_id bigint(20) comment '变更后产线ID', + before_remaining_packages int not null comment '变更前剩余包数', + after_remaining_packages int not null comment '变更后剩余包数', + operator_id bigint(20) not null comment '操作人ID', + occurred_at datetime not null comment '发生时间', + idempotency_key varchar(64) not null comment '幂等键', + request_hash varchar(64) not null comment '请求摘要', + primary key (event_id), + constraint uq_biz_pallet_event_pallet_seq unique (pallet_id, event_seq), + constraint uq_biz_pallet_event_pallet_idempotency unique (pallet_id, idempotency_key), + constraint fk_biz_pallet_event_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_pallet_event_before_warehouse foreign key (before_warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_pallet_event_before_site foreign key (before_site_id) references biz_customer_site (site_id), + constraint fk_biz_pallet_event_before_line foreign key (before_line_id) references biz_production_line (line_id), + constraint fk_biz_pallet_event_after_warehouse foreign key (after_warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_pallet_event_after_site foreign key (after_site_id) references biz_customer_site (site_id), + constraint fk_biz_pallet_event_after_line foreign key (after_line_id) references biz_production_line (line_id), + constraint fk_biz_pallet_event_operator foreign key (operator_id) references sys_user (user_id), + constraint ck_biz_pallet_event_event_type check ( + event_type in ( + 'INBOUND_CONFIRMED', + 'FILLING_CONFIRMED', + 'SHIPMENT_CONFIRMED', + 'USAGE_RECORDED', + 'RETURN_REQUESTED', + 'RETURN_CONFIRMED' + ) + ) +) engine=innodb comment = '托盘只追加流转事件'; + +-- 托盘当前库存、位置和流转状态 +create table biz_pallet_inventory ( + pallet_id bigint(20) not null comment '托盘ID', + active_filling_id bigint(20) comment '当前活动装填记录ID', + warehouse_id bigint(20) comment '当前仓库ID', + site_id bigint(20) comment '当前现场ID', + current_line_id bigint(20) comment '当前驻场周期绑定产线ID', + flow_state varchar(32) not null default 'UNSTOCKED' comment '托盘流转状态', + remaining_packages int not null default 0 comment '当前剩余包数', + last_event_id bigint(20) comment '最后事件ID', + version int not null default 0 comment '并发版本号', + updated_at datetime not null comment '更新时间', + primary key (pallet_id), + constraint fk_biz_pallet_inventory_pallet foreign key (pallet_id) references biz_pallet (pallet_id), + constraint fk_biz_pallet_inventory_filling foreign key (active_filling_id) references biz_filling_record (filling_id), + constraint fk_biz_pallet_inventory_warehouse foreign key (warehouse_id) references biz_warehouse (warehouse_id), + constraint fk_biz_pallet_inventory_site foreign key (site_id) references biz_customer_site (site_id), + constraint fk_biz_pallet_inventory_line foreign key (current_line_id) references biz_production_line (line_id), + constraint fk_biz_pallet_inventory_event foreign key (last_event_id) references biz_pallet_event (event_id), + constraint ck_biz_pallet_inventory_flow_state check ( + flow_state in ( + 'UNSTOCKED', + 'EMPTY_IN_WAREHOUSE', + 'FILLED_IN_WAREHOUSE', + 'AT_SITE_UNUSED', + 'AT_SITE_PARTIAL', + 'AT_SITE_EMPTY', + 'RETURNING' + ) + ), + constraint ck_biz_pallet_inventory_remaining_packages check (remaining_packages >= 0), + constraint ck_biz_pallet_inventory_version check (version >= 0), + constraint ck_biz_pallet_inventory_location check (not (warehouse_id is not null and site_id is not null)) +) engine=innodb comment = '托盘当前库存、位置和流转状态'; diff --git a/shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py b/shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py new file mode 100644 index 0000000..152dd1b --- /dev/null +++ b/shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py @@ -0,0 +1,841 @@ +import re +import unittest +from pathlib import Path + +SQL_ROOT = Path(__file__).resolve().parents[2] / 'sql' +INITIALIZERS = ('shierfeng-fastapi.sql', 'shierfeng-fastapi-pg.sql') +PATCH_PATH = SQL_ROOT / 'patches/20260724_pallet_workflow_mysql.sql' +SYS_MENU_VALUE_COUNT = 20 +REMOVED_AI_DICT_CODES = set(range(33, 70)) + +EXPECTED_TABLES = { + 'biz_strain', + 'biz_warehouse', + 'biz_customer', + 'biz_customer_site', + 'biz_production_line', + 'biz_pallet', + 'biz_user_warehouse', + 'biz_user_site', + 'biz_inbound_order', + 'biz_inbound_item', + 'biz_filling_record', + 'biz_shipment_order', + 'biz_shipment_item', + 'biz_usage_record', + 'biz_return_order', + 'biz_return_item', + 'biz_pallet_inventory', + 'biz_pallet_event', +} + +AUDIT_COLUMNS = {'create_by', 'create_time', 'update_by', 'update_time', 'remark'} +CREATE_AUDIT_COLUMNS = {'create_by', 'create_time'} +EXPECTED_COLUMNS = { + 'biz_strain': {'strain_id', 'strain_code', 'strain_name', 'status'} | AUDIT_COLUMNS, + 'biz_warehouse': {'warehouse_id', 'warehouse_code', 'warehouse_name', 'address', 'status'} | AUDIT_COLUMNS, + 'biz_customer': {'customer_id', 'customer_code', 'customer_name', 'status'} | AUDIT_COLUMNS, + 'biz_customer_site': {'site_id', 'customer_id', 'site_code', 'site_name', 'address', 'status'} | AUDIT_COLUMNS, + 'biz_production_line': {'line_id', 'site_id', 'line_code', 'line_name', 'status'} | AUDIT_COLUMNS, + 'biz_pallet': {'pallet_id', 'pallet_code', 'qr_token', 'capacity_packages', 'asset_status'} | AUDIT_COLUMNS, + 'biz_user_warehouse': {'user_id', 'warehouse_id'} | CREATE_AUDIT_COLUMNS, + 'biz_user_site': {'user_id', 'site_id'} | CREATE_AUDIT_COLUMNS, + 'biz_inbound_order': { + 'inbound_id', + 'inbound_no', + 'warehouse_id', + 'status', + 'confirm_idempotency_key', + 'confirm_request_hash', + 'confirmed_by', + 'confirmed_at', + } + | AUDIT_COLUMNS, + 'biz_inbound_item': {'inbound_item_id', 'inbound_id', 'pallet_id'} | CREATE_AUDIT_COLUMNS, + 'biz_filling_record': { + 'filling_id', + 'filling_no', + 'pallet_id', + 'warehouse_id', + 'strain_id', + 'strain_code_snapshot', + 'strain_name_snapshot', + 'batch_no', + 'package_spec', + 'initial_packages', + 'active_slot', + 'closed_at', + 'idempotency_key', + 'request_hash', + 'operator_id', + 'operated_at', + } + | AUDIT_COLUMNS, + 'biz_shipment_order': { + 'shipment_id', + 'shipment_no', + 'source_warehouse_id', + 'target_site_id', + 'status', + 'confirm_idempotency_key', + 'confirm_request_hash', + 'confirmed_by', + 'confirmed_at', + } + | AUDIT_COLUMNS, + 'biz_shipment_item': { + 'shipment_item_id', + 'shipment_id', + 'pallet_id', + 'active_filling_id', + 'package_qty', + } + | CREATE_AUDIT_COLUMNS, + 'biz_usage_record': { + 'usage_id', + 'usage_no', + 'pallet_id', + 'active_filling_id', + 'site_id', + 'line_id', + 'used_packages', + 'remaining_packages_after', + 'idempotency_key', + 'request_hash', + 'operator_id', + 'occurred_at', + }, + 'biz_return_order': { + 'return_id', + 'return_no', + 'source_site_id', + 'target_warehouse_id', + 'status', + 'request_idempotency_key', + 'request_hash', + 'confirm_idempotency_key', + 'confirm_request_hash', + 'requested_by', + 'requested_at', + 'confirmed_by', + 'confirmed_at', + } + | AUDIT_COLUMNS, + 'biz_return_item': { + 'return_item_id', + 'return_id', + 'pallet_id', + 'active_filling_id_snapshot', + 'remaining_packages_snapshot', + } + | CREATE_AUDIT_COLUMNS, + 'biz_pallet_inventory': { + 'pallet_id', + 'active_filling_id', + 'warehouse_id', + 'site_id', + 'current_line_id', + 'flow_state', + 'remaining_packages', + 'last_event_id', + 'version', + 'updated_at', + }, + 'biz_pallet_event': { + 'event_id', + 'pallet_id', + 'event_seq', + 'event_type', + 'source_type', + 'source_id', + 'before_state', + 'after_state', + 'before_warehouse_id', + 'before_site_id', + 'before_line_id', + 'after_warehouse_id', + 'after_site_id', + 'after_line_id', + 'before_remaining_packages', + 'after_remaining_packages', + 'operator_id', + 'occurred_at', + 'idempotency_key', + 'request_hash', + }, +} + +EXPECTED_PRIMARY_KEYS = { + 'biz_strain': ('strain_id',), + 'biz_warehouse': ('warehouse_id',), + 'biz_customer': ('customer_id',), + 'biz_customer_site': ('site_id',), + 'biz_production_line': ('line_id',), + 'biz_pallet': ('pallet_id',), + 'biz_user_warehouse': ('user_id', 'warehouse_id'), + 'biz_user_site': ('user_id', 'site_id'), + 'biz_inbound_order': ('inbound_id',), + 'biz_inbound_item': ('inbound_item_id',), + 'biz_filling_record': ('filling_id',), + 'biz_shipment_order': ('shipment_id',), + 'biz_shipment_item': ('shipment_item_id',), + 'biz_usage_record': ('usage_id',), + 'biz_return_order': ('return_id',), + 'biz_return_item': ('return_item_id',), + 'biz_pallet_inventory': ('pallet_id',), + 'biz_pallet_event': ('event_id',), +} + +EXPECTED_UNIQUE_KEYS = { + 'biz_strain': {('strain_code',)}, + 'biz_warehouse': {('warehouse_code',)}, + 'biz_customer': {('customer_code',)}, + 'biz_customer_site': {('customer_id', 'site_code')}, + 'biz_production_line': {('site_id', 'line_code')}, + 'biz_pallet': {('pallet_code',), ('qr_token',)}, + 'biz_user_warehouse': set(), + 'biz_user_site': set(), + 'biz_inbound_order': {('inbound_no',), ('confirm_idempotency_key',)}, + 'biz_inbound_item': {('inbound_id', 'pallet_id')}, + 'biz_filling_record': {('filling_no',), ('idempotency_key',), ('pallet_id', 'active_slot')}, + 'biz_shipment_order': {('shipment_no',), ('confirm_idempotency_key',)}, + 'biz_shipment_item': {('shipment_id', 'pallet_id')}, + 'biz_usage_record': {('usage_no',), ('idempotency_key',)}, + 'biz_return_order': {('return_no',), ('request_idempotency_key',), ('confirm_idempotency_key',)}, + 'biz_return_item': {('return_id', 'pallet_id')}, + 'biz_pallet_inventory': set(), + 'biz_pallet_event': {('pallet_id', 'event_seq'), ('pallet_id', 'idempotency_key')}, +} + +EXPECTED_FOREIGN_KEYS = { + 'biz_strain': set(), + 'biz_warehouse': set(), + 'biz_customer': set(), + 'biz_customer_site': {('customer_id', 'biz_customer.customer_id')}, + 'biz_production_line': {('site_id', 'biz_customer_site.site_id')}, + 'biz_pallet': set(), + 'biz_user_warehouse': {('user_id', 'sys_user.user_id'), ('warehouse_id', 'biz_warehouse.warehouse_id')}, + 'biz_user_site': {('user_id', 'sys_user.user_id'), ('site_id', 'biz_customer_site.site_id')}, + 'biz_inbound_order': { + ('warehouse_id', 'biz_warehouse.warehouse_id'), + ('confirmed_by', 'sys_user.user_id'), + }, + 'biz_inbound_item': { + ('inbound_id', 'biz_inbound_order.inbound_id'), + ('pallet_id', 'biz_pallet.pallet_id'), + }, + 'biz_filling_record': { + ('pallet_id', 'biz_pallet.pallet_id'), + ('warehouse_id', 'biz_warehouse.warehouse_id'), + ('strain_id', 'biz_strain.strain_id'), + ('operator_id', 'sys_user.user_id'), + }, + 'biz_shipment_order': { + ('source_warehouse_id', 'biz_warehouse.warehouse_id'), + ('target_site_id', 'biz_customer_site.site_id'), + ('confirmed_by', 'sys_user.user_id'), + }, + 'biz_shipment_item': { + ('shipment_id', 'biz_shipment_order.shipment_id'), + ('pallet_id', 'biz_pallet.pallet_id'), + ('active_filling_id', 'biz_filling_record.filling_id'), + }, + 'biz_usage_record': { + ('pallet_id', 'biz_pallet.pallet_id'), + ('active_filling_id', 'biz_filling_record.filling_id'), + ('site_id', 'biz_customer_site.site_id'), + ('line_id', 'biz_production_line.line_id'), + ('operator_id', 'sys_user.user_id'), + }, + 'biz_return_order': { + ('source_site_id', 'biz_customer_site.site_id'), + ('target_warehouse_id', 'biz_warehouse.warehouse_id'), + ('requested_by', 'sys_user.user_id'), + ('confirmed_by', 'sys_user.user_id'), + }, + 'biz_return_item': { + ('return_id', 'biz_return_order.return_id'), + ('pallet_id', 'biz_pallet.pallet_id'), + ('active_filling_id_snapshot', 'biz_filling_record.filling_id'), + }, + 'biz_pallet_inventory': { + ('pallet_id', 'biz_pallet.pallet_id'), + ('active_filling_id', 'biz_filling_record.filling_id'), + ('warehouse_id', 'biz_warehouse.warehouse_id'), + ('site_id', 'biz_customer_site.site_id'), + ('current_line_id', 'biz_production_line.line_id'), + ('last_event_id', 'biz_pallet_event.event_id'), + }, + 'biz_pallet_event': { + ('pallet_id', 'biz_pallet.pallet_id'), + ('before_warehouse_id', 'biz_warehouse.warehouse_id'), + ('before_site_id', 'biz_customer_site.site_id'), + ('before_line_id', 'biz_production_line.line_id'), + ('after_warehouse_id', 'biz_warehouse.warehouse_id'), + ('after_site_id', 'biz_customer_site.site_id'), + ('after_line_id', 'biz_production_line.line_id'), + ('operator_id', 'sys_user.user_id'), + }, +} + +EXPECTED_CHECKS = { + 'biz_strain': {"statusin('0','1')"}, + 'biz_warehouse': {"statusin('0','1')"}, + 'biz_customer': {"statusin('0','1')"}, + 'biz_customer_site': {"statusin('0','1')"}, + 'biz_production_line': {"statusin('0','1')"}, + 'biz_pallet': {'capacity_packages>0', "asset_statusin('0','1')"}, + 'biz_user_warehouse': set(), + 'biz_user_site': set(), + 'biz_inbound_order': {"statusin('draft','confirmed')"}, + 'biz_inbound_item': set(), + 'biz_filling_record': {'initial_packages>0', "active_slotisnulloractive_slot='active'"}, + 'biz_shipment_order': {"statusin('draft','confirmed')"}, + 'biz_shipment_item': {'package_qtyisnullorpackage_qty>0'}, + 'biz_usage_record': {'used_packages>0', 'remaining_packages_after>=0'}, + 'biz_return_order': {"statusin('requested','confirmed')"}, + 'biz_return_item': {'remaining_packages_snapshot>=0'}, + 'biz_pallet_inventory': { + "flow_statein('unstocked','empty_in_warehouse','filled_in_warehouse','at_site_unused','at_site_partial'," + "'at_site_empty','returning')", + 'remaining_packages>=0', + 'version>=0', + 'not(warehouse_idisnotnullandsite_idisnotnull)', + }, + 'biz_pallet_event': { + "event_typein('inbound_confirmed','filling_confirmed','shipment_confirmed','usage_recorded'," + "'return_requested','return_confirmed')" + }, +} + +EXPECTED_PAGES = { + 502: ('菌种资料', 121, 1, 'strain', 'master/strain/index', 'master:strain:list'), + 503: ('客户资料', 121, 2, 'customer', 'master/customer/index', 'master:customer:list'), + 504: ('客户现场', 121, 3, 'site', 'master/site/index', 'master:site:list'), + 505: ('产线资料', 121, 4, 'line', 'master/line/index', 'master:line:list'), + 506: ('仓库资料', 121, 5, 'warehouse', 'master/warehouse/index', 'master:warehouse:list'), + 507: ('托盘资料', 121, 6, 'pallet', 'master/pallet/index', 'master:pallet:list'), + 508: ('仓库入库', 122, 1, 'inbound', 'business/inbound/index', 'business:inbound:list'), + 509: ('工厂装填', 122, 2, 'filling', 'business/filling/index', 'business:filling:list'), + 510: ('发货出库', 122, 3, 'shipment', 'business/shipment/index', 'business:shipment:list'), + 511: ('客户现场使用', 122, 4, 'usage', 'business/usage/index', 'business:usage:list'), + 512: ('退回仓库', 122, 5, 'return', 'business/return/index', 'business:return:list'), + 513: ( + '托盘库存', + 123, + 1, + 'palletInventory', + 'report/palletInventory/index', + 'report:palletinventory:list', + ), + 514: ('装填记录', 123, 2, 'reportFilling', 'report/filling/index', 'report:filling:list'), + 515: ('发货出库', 123, 3, 'reportShipment', 'report/shipment/index', 'report:shipment:list'), + 516: ('产线消耗', 123, 4, 'consumption', 'report/consumption/index', 'report:consumption:list'), + 517: ('退回仓库', 123, 5, 'reportReturn', 'report/return/index', 'report:return:list'), + 518: ('托盘追溯', 123, 6, 'palletTrace', 'report/palletTrace/index', 'report:pallettrace:list'), +} + +BUTTON_SPECS = """ +1065 502 1 菌种查询 master:strain:query +1066 502 2 菌种新增 master:strain:add +1067 502 3 菌种修改 master:strain:edit +1068 502 4 菌种删除 master:strain:remove +1069 503 1 客户查询 master:customer:query +1070 503 2 客户新增 master:customer:add +1071 503 3 客户修改 master:customer:edit +1072 503 4 客户删除 master:customer:remove +1073 504 1 现场查询 master:site:query +1074 504 2 现场新增 master:site:add +1075 504 3 现场修改 master:site:edit +1076 504 4 现场删除 master:site:remove +1077 504 5 分配用户 master:site:assign +1078 505 1 产线查询 master:line:query +1079 505 2 产线新增 master:line:add +1080 505 3 产线修改 master:line:edit +1081 505 4 产线删除 master:line:remove +1082 506 1 仓库查询 master:warehouse:query +1083 506 2 仓库新增 master:warehouse:add +1084 506 3 仓库修改 master:warehouse:edit +1085 506 4 仓库删除 master:warehouse:remove +1086 506 5 分配用户 master:warehouse:assign +1087 507 1 托盘查询 master:pallet:query +1088 507 2 托盘新增 master:pallet:add +1089 507 3 托盘修改 master:pallet:edit +1090 507 4 托盘删除 master:pallet:remove +1091 507 5 托盘打印 master:pallet:print +1092 508 1 入库查询 business:inbound:query +1093 508 2 入库新增 business:inbound:add +1094 508 3 入库修改 business:inbound:edit +1095 508 4 入库删除 business:inbound:remove +1096 508 5 入库确认 business:inbound:confirm +1097 509 1 装填查询 business:filling:query +1098 509 2 装填新增 business:filling:add +1099 510 1 发货查询 business:shipment:query +1100 510 2 发货新增 business:shipment:add +1101 510 3 发货修改 business:shipment:edit +1102 510 4 发货删除 business:shipment:remove +1103 510 5 发货确认 business:shipment:confirm +1104 511 1 使用查询 business:usage:query +1105 511 2 使用新增 business:usage:add +1106 512 1 退回查询 business:return:query +1107 512 2 退回申请 business:return:request +1108 512 3 退回确认 business:return:confirm +""" +EXPECTED_BUTTONS = { + int(menu_id): (name, int(parent_id), int(order_num), permission) + for menu_id, parent_id, order_num, name, permission in ( + line.split() for line in BUTTON_SPECS.splitlines() if line.strip() + ) +} +BUSINESS_MENU_IDS = set(range(121, 124)) | set(range(502, 519)) | set(range(1065, 1109)) + + +def _strip_sql_comments(sql: str) -> str: + sql = re.sub(r'/\*.*?\*/', ' ', sql, flags=re.S) + return re.sub(r'(?m)^\s*(?:--|#).*$\n?', ' ', sql) + + +def _matching_parenthesis(text: str, opening: int) -> int: + depth = 0 + quote = None + index = opening + while index < len(text): + char = text[index] + if quote: + if char == quote and index + 1 < len(text) and text[index + 1] == quote: + index += 2 + continue + if char == quote: + quote = None + elif char in '\'"`': + quote = char + elif char == '(': + depth += 1 + elif char == ')': + depth -= 1 + if depth == 0: + return index + index += 1 + raise AssertionError('unbalanced SQL parentheses') + + +def _split_values(values: str) -> list[str]: + parts = [] + start = 0 + depth = 0 + quote = None + index = 0 + while index < len(values): + char = values[index] + if quote: + if char == quote and index + 1 < len(values) and values[index + 1] == quote: + index += 2 + continue + if char == quote: + quote = None + elif char in '\'"`': + quote = char + elif char == '(': + depth += 1 + elif char == ')': + depth -= 1 + elif char == ',' and depth == 0: + parts.append(values[start:index].strip()) + start = index + 1 + index += 1 + parts.append(values[start:].strip()) + return parts + + +def _decode_value(value: str) -> str | None: + value = value.strip() + if value.lower() == 'null': + return None + if value.startswith("'") and value.endswith("'"): + return value[1:-1].replace("''", "'") + return value + + +def _sql_statements(sql: str) -> list[str]: + return [statement.strip() for statement in _strip_sql_comments(sql).split(';') if statement.strip()] + + +def _insert_rows(sql: str, table: str) -> list[list[str | None]]: + rows = [] + pattern = re.compile( + rf'\binsert(?:\s+ignore)?\s+into\s+(?:[`"]?[a-z0-9_]+[`"]?\.)?[`"]?{table}[`"]?' + r'(?:\s*\([^;]*?\))?\s+values\s*', + re.I | re.S, + ) + for statement in _sql_statements(sql): + match = pattern.search(statement) + if not match: + continue + index = match.end() + while index < len(statement): + while index < len(statement) and (statement[index].isspace() or statement[index] == ','): + index += 1 + if index >= len(statement) or statement[index] != '(': + break + closing = _matching_parenthesis(statement, index) + rows.append([_decode_value(value) for value in _split_values(statement[index + 1 : closing])]) + index = closing + 1 + return rows + + +def _create_table_bodies(sql: str) -> dict[str, str]: + clean_sql = _strip_sql_comments(sql) + pattern = re.compile( + r'\bcreate\s+table\s+(?:if\s+not\s+exists\s+)?' + r'(?:[`"]?[a-z0-9_]+[`"]?\.)?[`"]?([a-z_][a-z0-9_]*)[`"]?\s*\(', + re.I, + ) + tables = {} + for match in pattern.finditer(clean_sql): + opening = match.end() - 1 + closing = _matching_parenthesis(clean_sql, opening) + tables[match.group(1).lower()] = clean_sql[opening + 1 : closing] + return tables + + +def _identifiers(value: str) -> tuple[str, ...]: + return tuple(part.strip().strip('`"').lower() for part in value.split(',')) + + +def _normalized_check(value: str) -> str: + normalized = re.sub(r'[\s`"]+', '', value).lower() + while normalized.startswith('(') and normalized.endswith(')'): + try: + if _matching_parenthesis(normalized, 0) != len(normalized) - 1: + break + except AssertionError: + break + normalized = normalized[1:-1] + return normalized + + +def _table_contract( + body: str, +) -> tuple[set[str], tuple[str, ...], set[tuple[str, ...]], set[tuple[str, str]], set[str]]: + columns = set() + primary_key = () + unique_keys = set() + foreign_keys = set() + checks = set() + reserved = {'constraint', 'primary', 'unique', 'foreign', 'check', 'key', 'index'} + for clause in _split_values(body): + clean = clause.strip() + identifier_match = re.match(r'[`"]?([a-z_][a-z0-9_]*)[`"]?', clean, flags=re.I) + first = identifier_match.group(1).lower() if identifier_match else '' + is_column = first and first not in reserved + if is_column: + columns.add(first) + if re.search(r'\bprimary\s+key\b', clean, flags=re.I): + primary_key = (first,) + if re.search(r'\bunique\b', clean, flags=re.I): + unique_keys.add((first,)) + reference = re.search( + r'\breferences\s+[`"]?([a-z_][a-z0-9_]*)[`"]?\s*\(\s*[`"]?([a-z_][a-z0-9_]*)', + clean, + flags=re.I, + ) + if reference: + foreign_keys.add((first, f'{reference.group(1).lower()}.{reference.group(2).lower()}')) + + primary_match = re.search(r'\bprimary\s+key\s*\(', clean, flags=re.I) + if primary_match: + opening = primary_match.end() - 1 + primary_key = _identifiers(clean[opening + 1 : _matching_parenthesis(clean, opening)]) + + unique_match = re.search( + r'\bunique(?:\s+(?:key|index))?(?:\s+[`"]?[a-z_][a-z0-9_]*[`"]?)?\s*\(', + clean, + flags=re.I, + ) + if unique_match: + opening = unique_match.end() - 1 + unique_keys.add(_identifiers(clean[opening + 1 : _matching_parenthesis(clean, opening)])) + + foreign_match = re.search(r'\bforeign\s+key\s*\(', clean, flags=re.I) + if foreign_match: + opening = foreign_match.end() - 1 + local_columns = _identifiers(clean[opening + 1 : _matching_parenthesis(clean, opening)]) + reference = re.search( + r'\breferences\s+[`"]?([a-z_][a-z0-9_]*)[`"]?\s*\((.*?)\)', + clean, + flags=re.I | re.S, + ) + if reference: + target_columns = _identifiers(reference.group(2)) + for local_column, target_column in zip(local_columns, target_columns, strict=False): + foreign_keys.add((local_column, f'{reference.group(1).lower()}.{target_column}')) + + check_match = re.search(r'\bcheck\s*\(', clean, flags=re.I) + if check_match: + opening = check_match.end() - 1 + checks.add(_normalized_check(clean[opening + 1 : _matching_parenthesis(clean, opening)])) + return columns, primary_key, unique_keys, foreign_keys, checks + + +def _business_menu_rows(sql: str) -> dict[int, list]: + rows = _insert_rows(sql, 'sys_menu') + selected = [row for row in rows if row and str(row[0]).isdigit() and int(row[0]) in BUSINESS_MENU_IDS] + counts = {menu_id: sum(int(row[0]) == menu_id for row in selected) for menu_id in BUSINESS_MENU_IDS} + if set(counts) != BUSINESS_MENU_IDS or any(count != 1 for count in counts.values()): + raise AssertionError(f'business menu row counts differ: {counts}') + return {int(row[0]): row for row in selected} + + +class SqlContractTest(unittest.TestCase): + def test_initializers_define_exact_business_table_set(self) -> None: + for filename in INITIALIZERS: + with self.subTest(filename=filename): + sql = (SQL_ROOT / filename).read_text(encoding='utf-8') + actual = {table for table in _create_table_bodies(sql) if table.startswith('biz_')} + self.assertEqual(EXPECTED_TABLES, actual) + + def test_initializers_match_orm_table_contract(self) -> None: + for filename in INITIALIZERS: + tables = _create_table_bodies((SQL_ROOT / filename).read_text(encoding='utf-8')) + for table in sorted(EXPECTED_TABLES): + with self.subTest(filename=filename, table=table): + self.assertIn(table, tables) + columns, primary_key, unique_keys, foreign_keys, checks = _table_contract(tables[table]) + self.assertEqual(EXPECTED_COLUMNS[table], columns) + self.assertEqual(EXPECTED_PRIMARY_KEYS[table], primary_key) + self.assertEqual(EXPECTED_UNIQUE_KEYS[table], unique_keys) + self.assertEqual(EXPECTED_FOREIGN_KEYS[table], foreign_keys) + self.assertEqual(EXPECTED_CHECKS[table], checks) + + def test_initializers_define_fixed_business_menus_and_permissions(self) -> None: + expected_permissions = {page[-1] for page in EXPECTED_PAGES.values()} | { + button[-1] for button in EXPECTED_BUTTONS.values() + } + self.assertEqual(61, len(expected_permissions)) + for filename in INITIALIZERS: + with self.subTest(filename=filename): + rows = _business_menu_rows((SQL_ROOT / filename).read_text(encoding='utf-8')) + self.assertTrue(all(len(row) == SYS_MENU_VALUE_COUNT for row in rows.values())) + self.assertEqual( + ('基础资料维护', '1', '10', 'businessMaster', 'M', ''), + tuple(rows[121][index] for index in (1, 2, 3, 4, 10, 13)), + ) + self.assertEqual( + ('业务管理', '1', '11', 'businessOperation', 'M', ''), + tuple(rows[122][index] for index in (1, 2, 3, 4, 10, 13)), + ) + self.assertEqual( + ('报表查询', '1', '12', 'businessReport', 'M', ''), + tuple(rows[123][index] for index in (1, 2, 3, 4, 10, 13)), + ) + for menu_id in (121, 122, 123): + self.assertEqual( + (None, '', '', '1', '0', '0', '0'), tuple(rows[menu_id][i] for i in (5, 6, 7, 8, 9, 11, 12)) + ) + for menu_id, expected in EXPECTED_PAGES.items(): + row = rows[menu_id] + actual = (row[1], int(row[2]), int(row[3]), row[4], row[5], row[13]) + self.assertEqual(expected, actual, menu_id) + self.assertEqual( + ('', '', '1', '0', 'C', '0', '0'), tuple(row[index] for index in (6, 7, 8, 9, 10, 11, 12)) + ) + for menu_id, expected in EXPECTED_BUTTONS.items(): + row = rows[menu_id] + actual = (row[1], int(row[2]), int(row[3]), row[13]) + self.assertEqual(expected, actual, menu_id) + self.assertEqual( + ('', '', '', '', '1', '0', 'F', '0', '0'), + tuple(row[index] for index in (4, 5, 6, 7, 8, 9, 10, 11, 12)), + ) + all_menu_rows = _insert_rows((SQL_ROOT / filename).read_text(encoding='utf-8'), 'sys_menu') + actual_permissions = { + row[13] + for row in all_menu_rows + if len(row) == SYS_MENU_VALUE_COUNT + and row[13] + and re.match(r'^(?:master|business|report):', row[13]) + } + self.assertEqual(expected_permissions, actual_permissions) + + def test_mysql_and_postgresql_business_menu_fields_are_identical(self) -> None: + mysql_rows = _business_menu_rows((SQL_ROOT / INITIALIZERS[0]).read_text(encoding='utf-8')) + postgres_rows = _business_menu_rows((SQL_ROOT / INITIALIZERS[1]).read_text(encoding='utf-8')) + compared_indexes = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13) + mysql_contract = { + menu_id: tuple(row[index] for index in compared_indexes) for menu_id, row in mysql_rows.items() + } + postgres_contract = { + menu_id: tuple(row[index] for index in compared_indexes) for menu_id, row in postgres_rows.items() + } + self.assertEqual(mysql_contract, postgres_contract) + + def test_initializers_do_not_grant_new_business_menus_to_roles(self) -> None: + for filename in INITIALIZERS: + with self.subTest(filename=filename): + rows = _insert_rows((SQL_ROOT / filename).read_text(encoding='utf-8'), 'sys_role_menu') + granted = {int(row[1]) for row in rows if len(row) > 1 and str(row[1]).isdigit()} + self.assertFalse(BUSINESS_MENU_IDS & granted) + self.assertFalse({4, 99, 118, 119, 1061, 1062, 1063, 1064} & granted) + + def test_initializers_exclude_removed_ai_and_external_link_seeds(self) -> None: + removed_menu_ids = {4, 99, 118, 119, 1061, 1062, 1063, 1064} + for filename in INITIALIZERS: + with self.subTest(filename=filename): + sql = (SQL_ROOT / filename).read_text(encoding='utf-8') + menu_ids = { + int(row[0]) + for row in _insert_rows(sql, 'sys_menu') + if row and row[0] is not None and str(row[0]).isdigit() + } + self.assertFalse(removed_menu_ids & menu_ids) + dict_types = _insert_rows(sql, 'sys_dict_type') + self.assertFalse(any(row[0] == '12' or row[2] == 'ai_provider_type' for row in dict_types)) + dict_data = _insert_rows(sql, 'sys_dict_data') + self.assertFalse( + any(int(row[0]) in REMOVED_AI_DICT_CODES or row[4] == 'ai_provider_type' for row in dict_data) + ) + + def test_initializers_keep_menu_allocator_and_use_native_dialects(self) -> None: + mysql = (SQL_ROOT / 'shierfeng-fastapi.sql').read_text(encoding='utf-8').lower() + postgres = (SQL_ROOT / 'shierfeng-fastapi-pg.sql').read_text(encoding='utf-8').lower() + self.assertRegex(mysql, r'auto_increment\s*=\s*2000') + self.assertNotRegex(mysql, r'\bbigserial\b|\balter\s+sequence\b|\bcomment\s+on\b') + self.assertRegex(postgres, r'alter\s+sequence\s+sys_menu_menu_id_seq\s+restart\s+2000') + self.assertNotIn('`', postgres) + self.assertNotRegex(postgres, r'\bauto_increment\b|\bengine\s*=') + + def test_initializers_create_event_before_inventory_foreign_key(self) -> None: + for filename in INITIALIZERS: + with self.subTest(filename=filename): + sql = _strip_sql_comments((SQL_ROOT / filename).read_text(encoding='utf-8')).lower() + self.assertIn('create table biz_pallet_event', sql) + self.assertIn('create table biz_pallet_inventory', sql) + self.assertLess( + sql.index('create table biz_pallet_event'), sql.index('create table biz_pallet_inventory') + ) + + def test_initializers_drop_business_tables_before_recreating_referenced_system_tables(self) -> None: + reverse_dependency_order = ( + 'biz_pallet_inventory', + 'biz_pallet_event', + 'biz_return_item', + 'biz_return_order', + 'biz_usage_record', + 'biz_shipment_item', + 'biz_shipment_order', + 'biz_filling_record', + 'biz_inbound_item', + 'biz_inbound_order', + 'biz_user_site', + 'biz_user_warehouse', + 'biz_pallet', + 'biz_production_line', + 'biz_customer_site', + 'biz_customer', + 'biz_warehouse', + 'biz_strain', + ) + for filename in INITIALIZERS: + with self.subTest(filename=filename): + sql = _strip_sql_comments((SQL_ROOT / filename).read_text(encoding='utf-8')).lower() + drop_positions = [sql.index(f'drop table if exists {table};') for table in reverse_dependency_order] + self.assertEqual(sorted(drop_positions), drop_positions) + self.assertLess(drop_positions[-1], sql.index('drop table if exists sys_user')) + + def test_patch_exists_and_preserves_ai_business_tables(self) -> None: + self.assertTrue(PATCH_PATH.is_file(), f'missing patch: {PATCH_PATH}') + raw = PATCH_PATH.read_text(encoding='utf-8').lower() + sql = re.sub(r'/\*.*?\*/', '', raw, flags=re.S) + sql = re.sub(r'(?m)^\s*(?:--|#).*$\n?', '', sql).replace('`', '') + for table in ( + 'ai_models', + 'ai_chat_config', + 'ai_sessions', + 'ai_memories', + 'ai_metrics', + 'ai_eval_runs', + 'ai_knowledge', + 'ai_culture', + 'ai_traces', + 'ai_spans', + 'ai_schema_versions', + ): + qualified = rf'(?:[a-z0-9_]+\.)?{table}' + patterns = ( + rf'\bdrop\s+(?:temporary\s+)?table\s+(?:if\s+exists\s+)?{qualified}\b', + rf'\btruncate\s+(?:table\s+)?{qualified}\b', + rf'\bdelete\s+from\s+{qualified}\b', + rf'\balter\s+table\s+(?:if\s+exists\s+)?{qualified}\b', + ) + for pattern in patterns: + self.assertIsNone(re.search(pattern, sql), f'dangerous AI table mutation: {table}') + self.assertNotRegex(sql, r'\b(?:drop\s+(?:temporary\s+)?table|truncate\s+(?:table\s+)?|delete\s+from)\s+biz_') + + def test_patch_is_documented_idempotent_and_contains_full_contract(self) -> None: + self.assertTrue(PATCH_PATH.is_file(), f'missing patch: {PATCH_PATH}') + raw = PATCH_PATH.read_text(encoding='utf-8') + lower = raw.lower() + self.assertIn('隐式提交', raw) + self.assertIn('备份', raw) + self.assertIn('人工审查', raw) + self.assertIn('未自动执行', raw) + self.assertLess(lower.index('ddl'), lower.index('dml')) + self.assertEqual(18, len(re.findall(r'create\s+table\s+if\s+not\s+exists\s+`?biz_', lower))) + tables = _create_table_bodies(raw) + self.assertEqual(EXPECTED_TABLES, {table for table in tables if table.startswith('biz_')}) + for table in EXPECTED_TABLES: + with self.subTest(table=table): + columns, primary_key, unique_keys, foreign_keys, checks = _table_contract(tables[table]) + self.assertEqual(EXPECTED_COLUMNS[table], columns) + self.assertEqual(EXPECTED_PRIMARY_KEYS[table], primary_key) + self.assertEqual(EXPECTED_UNIQUE_KEYS[table], unique_keys) + self.assertEqual(EXPECTED_FOREIGN_KEYS[table], foreign_keys) + self.assertEqual(EXPECTED_CHECKS[table], checks) + patch_rows = _business_menu_rows(raw) + mysql_rows = _business_menu_rows((SQL_ROOT / 'shierfeng-fastapi.sql').read_text(encoding='utf-8')) + compared_indexes = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13) + self.assertEqual( + {menu_id: tuple(row[index] for index in compared_indexes) for menu_id, row in mysql_rows.items()}, + {menu_id: tuple(row[index] for index in compared_indexes) for menu_id, row in patch_rows.items()}, + ) + self.assertRegex(lower, r'\bon\s+duplicate\s+key\s+update\b|\binsert\s+ignore\s+into\s+sys_menu\b') + self.assertNotRegex(_strip_sql_comments(lower), r'\binsert(?:\s+ignore)?\s+into\s+`?sys_role_menu`?\b') + self.assertIn('/getRouters', raw) + self.assertIn('用户信息', raw) + self.assertIn('sys_dict:ai_provider_type', raw) + executable = _strip_sql_comments(raw) + self.assertNotIn('/getRouters', executable) + self.assertNotIn('sys_dict:ai_provider_type', executable) + self.assertNotRegex(lower, r'\bbigserial\b|\balter\s+sequence\b|\bcomment\s+on\b') + + def test_patch_cleanup_is_explicit_and_ordered(self) -> None: + self.assertTrue(PATCH_PATH.is_file(), f'missing patch: {PATCH_PATH}') + sql = _strip_sql_comments(PATCH_PATH.read_text(encoding='utf-8')).replace('`', '').lower() + role_cleanup = re.search(r'delete\s+from\s+sys_role_menu\s+where\s+menu_id\s+in\s*\([^;]+\)', sql) + ai_menu_cleanup = re.search(r'delete\s+from\s+sys_menu\s+where\s+menu_id\s+in\s*\([^;]+\)', sql) + dict_data_cleanup = re.search(r'delete\s+from\s+sys_dict_data\s+where\s+[^;]+', sql) + dict_type_cleanup = re.search(r'delete\s+from\s+sys_dict_type\s+where\s+[^;]+', sql) + external_link_cleanup = re.search(r'delete\s+from\s+sys_menu\s+where\s+menu_id\s*=\s*99\b', sql) + for cleanup in ( + role_cleanup, + ai_menu_cleanup, + dict_data_cleanup, + dict_type_cleanup, + external_link_cleanup, + ): + self.assertIsNotNone(cleanup) + self.assertRegex(role_cleanup.group(), r'\b4\b.*\b99\b.*\b118\b.*\b119\b.*\b1061\b.*\b1064\b') + self.assertRegex(ai_menu_cleanup.group(), r'\b4\b.*\b118\b.*\b119\b.*\b1061\b.*\b1064\b') + self.assertRegex(dict_data_cleanup.group(), r'dict_code\s+between\s+33\s+and\s+69') + self.assertIn('ai_provider_type', dict_data_cleanup.group()) + self.assertRegex(dict_type_cleanup.group(), r'dict_id\s*=\s*12') + self.assertIn('ai_provider_type', dict_type_cleanup.group()) + self.assertLess(role_cleanup.start(), ai_menu_cleanup.start()) + self.assertLess(ai_menu_cleanup.start(), dict_data_cleanup.start()) + self.assertLess(dict_data_cleanup.start(), dict_type_cleanup.start()) + self.assertLess(dict_type_cleanup.start(), external_link_cleanup.start()) + self.assertNotRegex(sql, r'\bdelete\s+from\s+(?:[a-z0-9_]+\.)?(?:biz|ai)_[a-z0-9_]+\b') + + +if __name__ == '__main__': + unittest.main()