diff --git a/gvsds/apps/gerp/app_manifests.py b/gvsds/apps/gerp/app_manifests.py index 1a5c2244..fc35b4e8 100644 --- a/gvsds/apps/gerp/app_manifests.py +++ b/gvsds/apps/gerp/app_manifests.py @@ -375,3 +375,109 @@ def RemoveAppMenus(AppCode: str) -> bool: if names: SdkMenu.objects.filter(MenuName__in=names).delete() return True + + +# ==================== MSYNC 远端清单上报同步 ==================== + +def _ParseRemoteManifest(raw: dict) -> Optional[AppManifest]: + """协议 dict → AppManifest(远端清单结构同本地,Menus/ConfigKeys 嵌套解析)""" + if not isinstance(raw, dict) or not raw.get('AppCode'): + return None + menus = [] + for m in raw.get('Menus') or []: + if not isinstance(m, dict) or not m.get('MenuName'): + continue + menus.append(AppMenuSpec( + MenuName=str(m.get('MenuName', '')), + MenuTitle=str(m.get('MenuTitle', '') or m.get('MenuName', '')), + MenuLink=str(m.get('MenuLink', '') or ''), + MenuIcon=str(m.get('MenuIcon', '') or ''), + MenuSort=int(m.get('MenuSort', 0) or 0), + PermissionLevel=float(m.get('PermissionLevel', 1.0) or 1.0), + PermissionAttr=str(m.get('PermissionAttr', '') or ''), + ParentMenuName=str(m.get('ParentMenuName', '') or None) or None, + )) + config_keys = [] + for c in raw.get('ConfigKeys') or []: + if not isinstance(c, dict) or not c.get('Key'): + continue + config_keys.append(AppConfigSpec( + Key=str(c.get('Key', '')), + Label=str(c.get('Label', '') or ''), + Default=str(c.get('Default', '') or ''), + Secret=bool(c.get('Secret', False)), + Placeholder=str(c.get('Placeholder', '') or ''), + )) + return AppManifest( + AppCode=str(raw.get('AppCode', '')), + AppName=str(raw.get('AppName', '') or raw.get('AppCode', '')), + AppIcon=str(raw.get('AppIcon', '') or ''), + AppDesc=str(raw.get('AppDesc', '') or ''), + AppType=str(raw.get('AppType', '') or 'enterprise'), + AuthRequired=int(raw.get('AuthRequired', 0) or 0), + AppSort=int(raw.get('AppSort', 0) or 0), + DefaultPerms=str(raw.get('DefaultPerms', '') or ''), + FrontendRoute=str(raw.get('FrontendRoute', '') or ''), + FrontendType=str(raw.get('FrontendType', '') or 'legacy'), + LegacySrc=str(raw.get('LegacySrc', '') or ''), + Menus=menus, + ConfigKeys=config_keys, + Source='msync', + ) + + +def _SyncRemoteAppRecord(manifest: AppManifest, service_name: str, service_domain: str) -> App: + """远端清单 → App 表 upsert(Source=msync + 提供方服务标记)""" + app = App.objects.filter(AppCode=manifest.AppCode).first() + if app is None: + app = App(AppUUID=_uuid.uuid4().bytes, AppCode=manifest.AppCode) + app.AppName = manifest.AppName + app.AppIcon = manifest.AppIcon + app.AppDesc = manifest.AppDesc + app.AppType = manifest.AppType + app.AuthRequired = manifest.AuthRequired + app.AppSort = manifest.AppSort + app.DefaultPerms = manifest.DefaultPerms + app.FrontendRoute = manifest.FrontendRoute + app.FrontendType = manifest.FrontendType + app.LegacySrc = manifest.LegacySrc + app.Source = 'msync' + app.ServiceName = service_name or '' + app.ServiceDomain = service_domain or '' + app.save() + return app + + +def SyncRemoteAppManifests(Manifests: List[dict], ServiceName: str, ServiceDomain: str) -> dict: + """同步 MSYNC 子服务器上报的应用清单(幂等 upsert) + + Args: + Manifests: 清单协议 dict 列表(结构见方案文档 3.2) + ServiceName: 上报服务名(子服务器 MSYNC 注册名) + ServiceDomain: 上报服务域名(iframe 页面 base) + + Returns: + dict:{Registered: [AppCode], Failed: [{AppCode, Reason}]} + """ + registered = [] + failed = [] + local_codes = {m.AppCode for m in APP_MANIFESTS} + for raw in Manifests or []: + app_code = (raw.get('AppCode') or '').strip() if isinstance(raw, dict) else '' + if not app_code: + failed.append({'AppCode': app_code, 'Reason': '缺少 AppCode'}) + continue + if app_code in local_codes: + failed.append({'AppCode': app_code, 'Reason': 'AppCode 与本地清单冲突'}) + continue + manifest = _ParseRemoteManifest(raw) + if manifest is None: + failed.append({'AppCode': app_code, 'Reason': '清单格式无效'}) + continue + try: + RegisterRemoteManifest(manifest) + _SyncRemoteAppRecord(manifest, ServiceName, ServiceDomain) + registered.append(app_code) + except Exception as e: + failed.append({'AppCode': app_code, 'Reason': str(e)}) + return {'Registered': registered, 'Failed': failed} diff --git a/gvsds/apps/gerp/migrations/0031_app_remote_fields.py b/gvsds/apps/gerp/migrations/0031_app_remote_fields.py new file mode 100644 index 00000000..b7be287f --- /dev/null +++ b/gvsds/apps/gerp/migrations/0031_app_remote_fields.py @@ -0,0 +1,32 @@ +"""App 表增加远端注入字段(MSYNC 来源标记) + +Source:local=本地清单 / msync=子服务器上报注册 +ServiceName:提供方服务名(msync 服务注册名) +ServiceDomain:提供方服务域名(iframe 页面 base) +""" +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0030_ramgrant'), + ] + + operations = [ + migrations.AddField( + model_name='app', + name='Source', + field=models.CharField(default='local', max_length=16, verbose_name='来源(local/msync)'), + ), + migrations.AddField( + model_name='app', + name='ServiceName', + field=models.CharField(blank=True, default='', max_length=128, verbose_name='提供方服务名'), + ), + migrations.AddField( + model_name='app', + name='ServiceDomain', + field=models.CharField(blank=True, default='', max_length=256, verbose_name='提供方服务域名'), + ), + ] diff --git a/gvsds/apps/gerp/models.py b/gvsds/apps/gerp/models.py index 76a568f7..163aa171 100644 --- a/gvsds/apps/gerp/models.py +++ b/gvsds/apps/gerp/models.py @@ -804,6 +804,10 @@ class App(QModel): LegacySrc = models.CharField(max_length=256, default='', verbose_name='旧版页面路径(legacy)') AppSort = models.IntegerField(default=0, verbose_name='排序') AppStatus = models.SmallIntegerField(default=1, verbose_name='状态(1启用/0禁用)') + # 远端注入(MSYNC):Source=msync 的应用由子服务器上报注册 + Source = models.CharField(max_length=16, default='local', verbose_name='来源(local/msync)') + ServiceName = models.CharField(max_length=128, default='', blank=True, verbose_name='提供方服务名') + ServiceDomain = models.CharField(max_length=256, default='', blank=True, verbose_name='提供方服务域名') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') class Meta: