feat(msync-app): App 表 Source/ServiceName/ServiceDomain 字段 + 远端清单同步(SyncRemoteAppManifests,幂等 upsert + 本地冲突检测)
This commit is contained in:
1 parent
345abe88bb
commit
6e16c0d3b7
3 files changed
+142
No files matched your search
@@ -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='提供方服务域名'),
|
||||
),
|
||||
]
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user