""" 同步前端页面数据 从 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