feat(msync-app): App 表 Source/ServiceName/ServiceDomain 字段 + 远端清单同步(SyncRemoteAppManifests,幂等 upsert + 本地冲突检测)

This commit is contained in:
TermiNexus committed 2026-08-13 23:57:58 +08:00
1 parent 345abe88bb
commit 6e16c0d3b7
3 files changed
+142

No files matched your search

+106
View File
@@ -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}
@@ -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='提供方服务域名'),
),
]
+4
View File
@@ -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: