权限:移除业务物理删除入口

This commit is contained in:
焦龙言
2026-09-24 08:13:16 +08:00
parent 4706c4afeb
commit ae8c4c8c38
35 changed files with 668 additions and 445 deletions
@@ -446,6 +446,7 @@ EXPECTED_CHECKS.update(
)
EXPECTED_PAGES = {
519: ('测试数据批次', 1, 7, 'testBatch', 'system/testBatch/index', 'system:testbatch:list'),
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'),
@@ -476,47 +477,46 @@ 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
1109 519 1 批次查询 system:testbatch:query
1110 519 2 批次新增 system:testbatch:add
1111 519 3 批次修改 system:testbatch:edit
1112 519 4 批次授权 system:testbatch:assign
1113 519 5 批次关闭 system:testbatch:close
1114 519 6 数据纳入 system:testbatch:adopt
1115 519 7 批次清理 system:testbatch:purge
"""
EXPECTED_BUTTONS = {
int(menu_id): (name, int(parent_id), int(order_num), permission)
@@ -526,6 +526,7 @@ EXPECTED_BUTTONS = {
}
EXPECTED_PAGE_PERMISSIONS = frozenset(
[
'system:testbatch:list',
'master:strain:list',
'master:customer:list',
'master:site:list',
@@ -550,41 +551,40 @@ EXPECTED_BUTTON_PERMISSIONS = frozenset(
'master:strain:query',
'master:strain:add',
'master:strain:edit',
'master:strain:remove',
'master:customer:query',
'master:customer:add',
'master:customer:edit',
'master:customer:remove',
'master:site:query',
'master:site:add',
'master:site:edit',
'master:site:remove',
'master:site:assign',
'master:line:query',
'master:line:add',
'master:line:edit',
'master:line:remove',
'master:warehouse:query',
'master:warehouse:add',
'master:warehouse:edit',
'master:warehouse:remove',
'master:warehouse:assign',
'master:pallet:query',
'master:pallet:add',
'master:pallet:edit',
'master:pallet:remove',
'master:pallet:print',
'business:inbound:query',
'business:inbound:add',
'business:inbound:edit',
'business:inbound:remove',
'business:inbound:confirm',
'business:filling:query',
'business:filling:add',
'business:shipment:query',
'business:shipment:add',
'business:shipment:edit',
'business:shipment:remove',
'system:testbatch:query',
'system:testbatch:add',
'system:testbatch:edit',
'system:testbatch:assign',
'system:testbatch:close',
'system:testbatch:adopt',
'system:testbatch:purge',
'business:shipment:confirm',
'business:usage:query',
'business:usage:add',
@@ -595,6 +595,7 @@ EXPECTED_BUTTON_PERMISSIONS = frozenset(
)
EXPECTED_PERMISSIONS = EXPECTED_PAGE_PERMISSIONS | EXPECTED_BUTTON_PERMISSIONS
BUSINESS_MENU_SPECS = """
519|1|测试数据批次|testBatch||7|system:testbatch:list|system/testBatch/index
121|0|基础资料维护|businessMaster||1||<NULL>
122|0|业务管理|businessOperation||2||<NULL>
123|0|报表查询|businessReport||3||<NULL>
@@ -618,47 +619,46 @@ BUSINESS_MENU_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|
1109|519|批次查询|||1|system:testbatch:query|
1110|519|批次新增|||2|system:testbatch:add|
1111|519|批次修改|||3|system:testbatch:edit|
1112|519|批次授权|||4|system:testbatch:assign|
1113|519|批次关闭|||5|system:testbatch:close|
1114|519|数据纳入|||6|system:testbatch:adopt|
1115|519|批次清理|||7|system:testbatch:purge|
""".strip()
@@ -679,7 +679,33 @@ def _expected_business_menu_fields() -> dict[int, tuple]:
EXPECTED_BUSINESS_MENU_FIELDS = _expected_business_menu_fields()
LEGACY_PATCH_BUSINESS_MENU_FIELDS = EXPECTED_BUSINESS_MENU_FIELDS | {
LEGACY_REMOVED_BUTTON_SPECS = {
1068: ('菌种删除', 502, 4, 'master:strain:remove'),
1072: ('客户删除', 503, 4, 'master:customer:remove'),
1076: ('现场删除', 504, 4, 'master:site:remove'),
1081: ('产线删除', 505, 4, 'master:line:remove'),
1085: ('仓库删除', 506, 4, 'master:warehouse:remove'),
1090: ('托盘删除', 507, 4, 'master:pallet:remove'),
1095: ('入库删除', 508, 4, 'business:inbound:remove'),
1102: ('发货删除', 510, 4, 'business:shipment:remove'),
}
LEGACY_BUSINESS_MENU_IDS = set(range(121, 124)) | set(range(502, 519)) | set(range(1065, 1109))
LEGACY_EXPECTED_BUSINESS_MENU_FIELDS = {
menu_id: fields for menu_id, fields in EXPECTED_BUSINESS_MENU_FIELDS.items() if menu_id in LEGACY_BUSINESS_MENU_IDS
}
LEGACY_EXPECTED_BUSINESS_MENU_FIELDS.update(
{
menu_id: (parent_id, name, '', '', order_num, permission, '')
for menu_id, (name, parent_id, order_num, permission) in LEGACY_REMOVED_BUTTON_SPECS.items()
}
)
LEGACY_EXPECTED_PAGE_PERMISSIONS = EXPECTED_PAGE_PERMISSIONS - {'system:testbatch:list'}
LEGACY_EXPECTED_BUTTON_PERMISSIONS = EXPECTED_BUTTON_PERMISSIONS - {
permission for permission in EXPECTED_BUTTON_PERMISSIONS if permission.startswith('system:testbatch:')
} | {spec[3] for spec in LEGACY_REMOVED_BUTTON_SPECS.values()}
LEGACY_EXPECTED_PERMISSIONS = LEGACY_EXPECTED_PAGE_PERMISSIONS | LEGACY_EXPECTED_BUTTON_PERMISSIONS
LEGACY_PATCH_BUSINESS_MENU_FIELDS = LEGACY_EXPECTED_BUSINESS_MENU_FIELDS | {
121: (1, '基础资料维护', 'businessMaster', '', 10, '', None),
122: (1, '业务管理', 'businessOperation', '', 11, '', None),
123: (1, '报表查询', 'businessReport', '', 12, '', None),
@@ -706,7 +732,28 @@ EXPECTED_TOP_LEVEL_MENU_STABLE_FIELDS = {
3: ('系统工具', 'tool', None, '', '', '1', '0', 'M', '0', '0', '', 'tool'),
}
MENU_PROMOTION_IDS = set(EXPECTED_TOP_LEVEL_MENU_POSITIONS)
BUSINESS_MENU_IDS = set(range(121, 124)) | set(range(502, 519)) | set(range(1065, 1109))
BUSINESS_MENU_IDS = (
set(range(121, 124))
| set(range(502, 519))
| {519}
| set(range(1065, 1068))
| set(range(1069, 1072))
| set(range(1073, 1076))
| {1077}
| set(range(1078, 1081))
| set(range(1082, 1085))
| {1086}
| set(range(1087, 1090))
| {1091}
| set(range(1092, 1095))
| {1096}
| set(range(1097, 1099))
| set(range(1099, 1102))
| {1103}
| set(range(1104, 1109))
| set(range(1109, 1116))
)
TEST_BATCH_MENU_IDS = {519, 1109, 1110, 1111, 1112, 1113, 1114, 1115}
MYSQL_MENU_STABLE_COLUMNS = (
'menu_name',
'parent_id',
@@ -1022,13 +1069,11 @@ def _assert_test_batch_sql_contract(
test_case.assertRegex(normalized, index_pattern)
test_case.assertIn("comment '测试数据批次id;null表示正式数据'", normalized)
else:
index_pattern = (
rf'\bcreate\s+index\s+{index_name}\s+on\s+{table_name}\s*\(\s*test_batch_id\s*\)'
)
index_pattern = rf'\bcreate\s+index\s+{index_name}\s+on\s+{table_name}\s*\(\s*test_batch_id\s*\)'
test_case.assertRegex(normalized, index_pattern)
test_case.assertRegex(
normalized,
rf"comment\s+on\s+column\s+{table_name}\.test_batch_id\s+is\s+"
rf'comment\s+on\s+column\s+{table_name}\.test_batch_id\s+is\s+'
r"'测试数据批次id;null表示正式数据'",
)
@@ -1049,8 +1094,8 @@ def _menu_rows(sql: str, menu_ids: set[int]) -> dict[int, list]:
return {int(row[0]): row for row in selected}
def _business_menu_rows(sql: str) -> dict[int, list]:
return _menu_rows(sql, BUSINESS_MENU_IDS)
def _business_menu_rows(sql: str, menu_ids: set[int] | None = None) -> dict[int, list]:
return _menu_rows(sql, BUSINESS_MENU_IDS if menu_ids is None else menu_ids)
def _assert_top_level_menu_fields(test_case: unittest.TestCase, sql: str) -> None:
@@ -1067,7 +1112,7 @@ def _assert_business_menu_fields(
sql: str,
expected_fields: dict[int, tuple] = EXPECTED_BUSINESS_MENU_FIELDS,
) -> None:
rows = _business_menu_rows(sql)
rows = _business_menu_rows(sql, set(expected_fields))
invalid_widths = {menu_id: len(row) for menu_id, row in rows.items() if len(row) != SYS_MENU_VALUE_COUNT}
test_case.assertEqual({}, invalid_widths)
actual = {
@@ -1085,17 +1130,21 @@ def _assert_business_menu_fields(
test_case.assertEqual(expected_fields, actual)
def _assert_business_permission_inventory(test_case: unittest.TestCase, sql: str) -> None:
def _assert_business_permission_inventory(
test_case: unittest.TestCase,
sql: str,
expected_permissions: frozenset[str] = EXPECTED_PERMISSIONS,
) -> None:
permission_rows = [
row
for row in _insert_rows(sql, 'sys_menu')
if len(row) > SYS_MENU_PERMS_INDEX
and row[SYS_MENU_PERMS_INDEX]
and re.match(r'^(?:master|business|report):', row[SYS_MENU_PERMS_INDEX])
and re.match(r'^(?:master|business|report|system:testbatch):', row[SYS_MENU_PERMS_INDEX])
]
test_case.assertTrue(all(len(row) == SYS_MENU_VALUE_COUNT for row in permission_rows))
actual_permissions = Counter(row[SYS_MENU_PERMS_INDEX] for row in permission_rows)
test_case.assertEqual(Counter(EXPECTED_PERMISSIONS), actual_permissions)
test_case.assertEqual(Counter(expected_permissions), actual_permissions)
MENU_PROMOTION_LOCK_STATEMENT = (
@@ -1310,6 +1359,57 @@ class SqlContractTest(unittest.TestCase):
is_patch=is_patch,
)
def test_test_batch_patches_migrate_legacy_menu_permissions_without_role_grants(self) -> None:
legacy_removed_ids = set(LEGACY_REMOVED_BUTTON_SPECS)
for path in (TEST_BATCH_MYSQL_PATCH_PATH, TEST_BATCH_PG_PATCH_PATH):
with self.subTest(path=path.name):
raw = path.read_text(encoding='utf-8')
statements = _sql_statements(raw)
normalized = [_normalized_sql_statement(statement) for statement in statements]
role_delete_indexes = [
index
for index, statement in enumerate(normalized)
if re.fullmatch(r'delete from sys_role_menu where menu_id in \([^)]+\)', statement)
]
menu_delete_indexes = [
index
for index, statement in enumerate(normalized)
if re.fullmatch(r'delete from sys_menu where menu_id in \([^)]+\)', statement)
]
menu_insert_indexes = [
index
for index, statement in enumerate(normalized)
if statement.startswith('insert into sys_menu values ')
]
self.assertEqual(1, len(role_delete_indexes))
self.assertEqual(1, len(menu_delete_indexes))
self.assertEqual(1, len(menu_insert_indexes))
role_delete = normalized[role_delete_indexes[0]]
menu_delete = normalized[menu_delete_indexes[0]]
role_delete_ids = {
int(value.strip()) for value in re.search(r'\(([^)]+)\)', role_delete).group(1).split(',')
}
menu_delete_ids = {
int(value.strip()) for value in re.search(r'\(([^)]+)\)', menu_delete).group(1).split(',')
}
self.assertEqual(legacy_removed_ids, role_delete_ids)
self.assertEqual(legacy_removed_ids, menu_delete_ids)
self.assertLess(role_delete_indexes[0], menu_delete_indexes[0])
self.assertLess(menu_delete_indexes[0], menu_insert_indexes[0])
inserted_rows = _insert_rows(raw, 'sys_menu')
inserted_by_id = {int(row[0]): row for row in inserted_rows}
self.assertEqual(TEST_BATCH_MENU_IDS, set(inserted_by_id))
self.assertTrue(all(len(row) == SYS_MENU_VALUE_COUNT for row in inserted_by_id.values()))
self.assertEqual('测试数据批次', inserted_by_id[519][1])
self.assertEqual('1', inserted_by_id[519][2])
self.assertEqual('7', inserted_by_id[519][3])
self.assertEqual('system:testbatch:list', inserted_by_id[519][13])
self.assertEqual(519, int(inserted_by_id[1109][2]))
self.assertEqual('system:testbatch:purge', inserted_by_id[1115][13])
self.assertNotRegex('\n'.join(normalized), r'\binsert into sys_role_menu\b')
def test_initializers_define_exact_business_table_set(self) -> None:
for filename in INITIALIZERS:
with self.subTest(filename=filename):
@@ -1357,8 +1457,8 @@ class SqlContractTest(unittest.TestCase):
self.assertFalse(_is_postgres_naive_timestamp_with_default_precision('occurred_at timestamp with time zone'))
def test_initializers_define_fixed_business_menus_and_permissions(self) -> None:
self.assertEqual(17, len(EXPECTED_PAGE_PERMISSIONS))
self.assertEqual(44, len(EXPECTED_BUTTON_PERMISSIONS))
self.assertEqual(18, len(EXPECTED_PAGE_PERMISSIONS))
self.assertEqual(43, len(EXPECTED_BUTTON_PERMISSIONS))
self.assertFalse(EXPECTED_PAGE_PERMISSIONS & EXPECTED_BUTTON_PERMISSIONS)
self.assertEqual(61, len(EXPECTED_PERMISSIONS))
self.assertEqual(64, len(EXPECTED_BUSINESS_MENU_FIELDS))
@@ -1969,20 +2069,26 @@ class SqlContractTest(unittest.TestCase):
self.assertEqual(LEGACY_EXPECTED_FOREIGN_KEYS[table], foreign_keys)
self.assertEqual(LEGACY_EXPECTED_CHECKS[table], checks)
_assert_business_menu_fields(self, raw, LEGACY_PATCH_BUSINESS_MENU_FIELDS)
_assert_business_permission_inventory(self, raw)
patch_rows = _business_menu_rows(raw)
_assert_business_permission_inventory(self, raw, LEGACY_EXPECTED_PERMISSIONS)
patch_rows = _business_menu_rows(raw, LEGACY_BUSINESS_MENU_IDS)
mysql_rows = _business_menu_rows((SQL_ROOT / 'shierfeng-fastapi.sql').read_text(encoding='utf-8'))
compared_indexes = (0, 1, *range(4, 15))
common_menu_ids = LEGACY_BUSINESS_MENU_IDS & BUSINESS_MENU_IDS
expected_legacy_rows = {
menu_id: tuple(
'#' if index == SYS_MENU_ICON_INDEX and menu_id in EXPECTED_BUSINESS_ROOT_ICONS else row[index]
for index in compared_indexes
)
for menu_id, row in mysql_rows.items()
if menu_id in common_menu_ids
}
self.assertEqual(
expected_legacy_rows,
{menu_id: tuple(row[index] for index in compared_indexes) for menu_id, row in patch_rows.items()},
{
menu_id: tuple(row[index] for index in compared_indexes)
for menu_id, row in patch_rows.items()
if menu_id in common_menu_ids
},
)
self.assertRegex(lower, r'\bon\s+duplicate\s+key\s+update\b')
self.assertNotRegex(lower, r'\binsert\s+ignore\b')
@@ -2017,7 +2123,7 @@ class SqlContractTest(unittest.TestCase):
flags=re.S,
)
self.assertIsNotNone(menu_insert, 'sys_menu seed must use a fail-closed upsert')
self.assertEqual(len(BUSINESS_MENU_IDS), len(_insert_rows(menu_insert_statement, 'sys_menu')))
self.assertEqual(len(LEGACY_BUSINESS_MENU_IDS), len(_insert_rows(menu_insert_statement, 'sys_menu')))
expected_guard = ' and '.join(f'{column} <=> values({column})' for column in MYSQL_MENU_STABLE_COLUMNS)
actual_guard = re.sub(r'\s+', ' ', menu_insert.group('guard')).strip()
self.assertEqual(expected_guard, actual_guard)
@@ -2039,8 +2145,8 @@ class SqlContractTest(unittest.TestCase):
verification_columns = tuple(column.strip() for column in verification.group('columns').split(','))
self.assertEqual(('menu_id', *MYSQL_MENU_STABLE_COLUMNS), verification_columns)
verification_ids = [int(value.strip()) for value in verification.group('ids').split(',')]
self.assertEqual(len(BUSINESS_MENU_IDS), len(verification_ids))
self.assertEqual(BUSINESS_MENU_IDS, set(verification_ids))
self.assertEqual(len(LEGACY_BUSINESS_MENU_IDS), len(verification_ids))
self.assertEqual(LEGACY_BUSINESS_MENU_IDS, set(verification_ids))
self.assertRegex(raw, r'查询结果必须为\s*64')
normalized = [_normalized_sql_statement(statement) for statement in statements]
@@ -2149,11 +2255,12 @@ class SqlContractTest(unittest.TestCase):
self.assertEqual(('menu_id', *MYSQL_MENU_STABLE_COLUMNS), expected_columns)
expected_rows = _insert_rows(statements[expected_inserts[0]], expected_table)
self.assertEqual(len(BUSINESS_MENU_IDS), len(expected_rows))
self.assertEqual(len(LEGACY_BUSINESS_MENU_IDS), len(expected_rows))
expected_rows_by_id = {int(row[0]): tuple(row) for row in expected_rows}
self.assertEqual(BUSINESS_MENU_IDS, set(expected_rows_by_id))
self.assertEqual(LEGACY_BUSINESS_MENU_IDS, set(expected_rows_by_id))
seed_rows_by_id = {
menu_id: tuple(row[: len(expected_columns)]) for menu_id, row in _business_menu_rows(raw).items()
menu_id: tuple(row[: len(expected_columns)])
for menu_id, row in _business_menu_rows(raw, LEGACY_BUSINESS_MENU_IDS).items()
}
self.assertEqual(seed_rows_by_id, expected_rows_by_id)