1
0
forked from erp-dev/erp
Files
erpnew/basic_info/management/commands/sync_frontend_pages.py

165 lines
6.3 KiB
Python

"""
同步前端页面数据
从 docs/menu_keys.json 文件同步前端页面到数据库。
用法:
python manage.py sync_frontend_pages
python manage.py sync_frontend_pages --dry-run # 仅预览,不实际写入
python manage.py sync_frontend_pages --clear # 清空后重新同步
"""
import json
from pathlib import Path
from django.core.management.base import BaseCommand
from django.db import transaction
from basic_info.models import FrontendPage, FrontendPageTypeEnum
class Command(BaseCommand):
help = '从 docs/menu_keys.json 同步前端页面到数据库'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='仅预览变更,不实际写入数据库',
)
parser.add_argument(
'--clear',
action='store_true',
help='清空现有数据后重新同步(谨慎使用)',
)
parser.add_argument(
'--file',
type=str,
default='docs/menu_keys.json',
help='JSON 文件路径(相对于项目根目录)',
)
def handle(self, *args, **options):
dry_run = options['dry_run']
clear = options['clear']
file_path = options['file']
# 读取 JSON 文件
# __file__ = .../basic_info/management/commands/sync_frontend_pages.py
# parents[3] = 项目根目录 (flower/)
json_path = Path(__file__).resolve().parents[3] / file_path
if not json_path.exists():
self.stderr.write(self.style.ERROR(f'文件不存在: {json_path}'))
return
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
menus = data.get('menus', [])
if not menus:
self.stderr.write(self.style.ERROR('JSON 文件中没有 menus 数据'))
return
self.stdout.write(f'读取到 {len(menus)} 个主菜单')
# 统计
created_count = 0
updated_count = 0
skipped_count = 0
try:
with transaction.atomic():
# 清空现有数据
if clear:
if dry_run:
self.stdout.write(self.style.WARNING('[DRY-RUN] 将清空所有前端页面数据'))
else:
deleted_count, _ = FrontendPage.objects.all().delete()
self.stdout.write(self.style.WARNING(f'已清空 {deleted_count} 条记录'))
# 第一轮:创建/更新主菜单
main_menu_map = {} # key -> FrontendPage instance
for sort_order, menu in enumerate(menus):
key = menu['key']
label = menu['label']
if dry_run:
self.stdout.write(f'[DRY-RUN] 主菜单: {key} ({label})')
created_count += 1
else:
page, created = FrontendPage.objects.update_or_create(
key=key,
defaults={
'label': label,
'page_type': FrontendPageTypeEnum.MAIN,
'parent': None,
'sort_order': sort_order * 100, # 主菜单间隔100
'is_active': True,
}
)
main_menu_map[key] = page
if created:
created_count += 1
self.stdout.write(self.style.SUCCESS(f'创建主菜单: {key} ({label})'))
else:
updated_count += 1
self.stdout.write(f'更新主菜单: {key} ({label})')
# 第二轮:创建/更新子菜单
for menu in menus:
parent_key = menu['key']
children = menu.get('children', [])
parent_page = main_menu_map.get(parent_key) if not dry_run else None
for sub_order, child in enumerate(children):
child_key = child['key']
child_label = child['label']
if dry_run:
self.stdout.write(f'[DRY-RUN] 子菜单: {child_key} ({child_label}) <- {parent_key}')
created_count += 1
else:
# 计算排序:父级排序 + 子级偏移
parent_sort = parent_page.sort_order if parent_page else 0
child_sort = parent_sort + sub_order + 1
page, created = FrontendPage.objects.update_or_create(
key=child_key,
defaults={
'label': child_label,
'page_type': FrontendPageTypeEnum.SUB,
'parent': parent_page,
'sort_order': child_sort,
'is_active': True,
}
)
if created:
created_count += 1
self.stdout.write(self.style.SUCCESS(f' 创建子菜单: {child_key} ({child_label})'))
else:
updated_count += 1
self.stdout.write(f' 更新子菜单: {child_key} ({child_label})')
if dry_run:
# 回滚事务
raise DryRunException()
except DryRunException:
pass
# 输出统计
self.stdout.write('')
self.stdout.write(self.style.SUCCESS('=' * 40))
if dry_run:
self.stdout.write(self.style.WARNING(f'[DRY-RUN] 预计创建: {created_count}'))
else:
self.stdout.write(self.style.SUCCESS(f'创建: {created_count}'))
self.stdout.write(f'更新: {updated_count}')
self.stdout.write(f'总计: {FrontendPage.objects.count()}')
class DryRunException(Exception):
"""用于 dry-run 模式回滚事务"""
pass