diff --git a/gvsds/apps/gerp/app_manifests.py b/gvsds/apps/gerp/app_manifests.py index fc35b4e8..acd2c97b 100644 --- a/gvsds/apps/gerp/app_manifests.py +++ b/gvsds/apps/gerp/app_manifests.py @@ -444,10 +444,43 @@ def _SyncRemoteAppRecord(manifest: AppManifest, service_name: str, service_domai app.Source = 'msync' app.ServiceName = service_name or '' app.ServiceDomain = service_domain or '' + app.ServiceOnline = 1 # 上报即在线 app.save() return app +def MarkServiceAppsOffline(ServiceName: str) -> int: + """子服务器下线:标记该服务提供的全部远端 App 为离线(ServiceOnline=0) + + Args: + ServiceName: 服务注册名(App.ServiceName) + + Returns: + 受影响 App 数量 + """ + if not ServiceName: + return 0 + return App.objects.filter(Source='msync', ServiceName=ServiceName, ServiceOnline=1).update( + ServiceOnline=0, + ) + + +def MarkServiceAppsOnline(ServiceName: str) -> int: + """子服务器重新上线:恢复该服务提供的远端 App 在线状态(ServiceOnline=1) + + Args: + ServiceName: 服务注册名(App.ServiceName) + + Returns: + 受影响 App 数量 + """ + if not ServiceName: + return 0 + return App.objects.filter(Source='msync', ServiceName=ServiceName, ServiceOnline=0).update( + ServiceOnline=1, + ) + + def SyncRemoteAppManifests(Manifests: List[dict], ServiceName: str, ServiceDomain: str) -> dict: """同步 MSYNC 子服务器上报的应用清单(幂等 upsert) diff --git a/gvsds/apps/gerp/migrations/0032_app_service_online.py b/gvsds/apps/gerp/migrations/0032_app_service_online.py new file mode 100644 index 00000000..f6f4c795 --- /dev/null +++ b/gvsds/apps/gerp/migrations/0032_app_service_online.py @@ -0,0 +1,21 @@ +"""App 表增加 ServiceOnline 字段——MSYNC 提供方服务在线状态 + +子服务器下线(heartbeat 超时/offline/注销)时标记 0,重新上报恢复 1; +菜单保留但前端显示"服务离线"提示(或按策略停用)。 +""" +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0031_app_remote_fields'), + ] + + operations = [ + migrations.AddField( + model_name='app', + name='ServiceOnline', + field=models.SmallIntegerField(default=1, verbose_name='提供方服务在线(1在线/0离线)'), + ), + ] diff --git a/gvsds/apps/gerp/models.py b/gvsds/apps/gerp/models.py index 163aa171..61a1c78a 100644 --- a/gvsds/apps/gerp/models.py +++ b/gvsds/apps/gerp/models.py @@ -808,6 +808,7 @@ class App(QModel): 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='提供方服务域名') + ServiceOnline = models.SmallIntegerField(default=1, verbose_name='提供方服务在线(1在线/0离线)') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间') class Meta: diff --git a/gvsds/apps/gerp/views/app.py b/gvsds/apps/gerp/views/app.py index 18128434..9122d470 100644 --- a/gvsds/apps/gerp/views/app.py +++ b/gvsds/apps/gerp/views/app.py @@ -104,6 +104,30 @@ def _ToBytes(value): return None +def _GrantRolesToApp(grant_roles, app_uuid, tenant_bytes, company_bytes, operator_bytes): + """RAM 角色粒度授权:安装时把 App 的 view 权限授予指定角色 + + Args: + grant_roles: 角色 UUID bytes 列表 + app_uuid: App UUID(str/bytes) + tenant_bytes: 租户 UUID bytes(作用域) + company_bytes: 公司 UUID bytes(None=租户级授权) + operator_bytes: 授权人 UUID bytes + """ + if not grant_roles: + return + from gvsdsdk.auth import RAMManager + app_bytes = _ToBytes(app_uuid) + for role in grant_roles: + try: + RAMManager.GrantAccess( + 'role', role, 'app', app_bytes, 'view', + tenant_bytes, company_bytes, grant_by=operator_bytes, + ) + except Exception as e: + logger.warning('RAM 角色授权失败 role=%s: %s', role, e) + + def _ResolveConfigCompany(request, company_uuid_str): """解析应用配置目标范围并校验权限(租户默认/公司级) @@ -216,6 +240,10 @@ class AppInstallView(ELTAuthenticatedAPIView): if not app_uuid: return Response({'code': 1, 'msg': '缺少应用 UUID'}, status=400) + # RAM 角色粒度授权(可选):安装时指定角色 → 该角色对安装范围可见该 App + grant_roles = _ParseUUIDList(request.data.get('GrantRoles')) + user_bytes = _GetUserBytes(request) + # 公司维度目标(可选):不传则全租户安装 company_uuids = _ParseUUIDList(request.data.get('CompanyUUIDs')) @@ -227,6 +255,7 @@ class AppInstallView(ELTAuthenticatedAPIView): ok, err, flat = AppManager.InstallTenantApp(tenant_bytes, app_uuid) if not ok: return Response({'code': 1, 'msg': err}, status=400) + _GrantRolesToApp(grant_roles, app_uuid, tenant_bytes, None, user_bytes) return Response({'code': 0, 'msg': '安装成功(全租户)', 'data': flat}) # 指定公司安装:校验目标公司都在管理员可管理范围内 @@ -239,10 +268,14 @@ class AppInstallView(ELTAuthenticatedAPIView): if invalid: return Response({'code': 403, 'msg': '无权为以下公司安装(不在您的管理范围)'}, status=403) ok, err, count = AppManager.InstallCompanyApps( - company_uuids, app_uuid, InstallBy=_GetUserBytes(request), + company_uuids, app_uuid, InstallBy=user_bytes, ) if not ok: return Response({'code': 1, 'msg': err}, status=400) + if grant_roles: + tenant_bytes = _GetTenantBytes(request) + for company in company_uuids: + _GrantRolesToApp(grant_roles, app_uuid, tenant_bytes, company, user_bytes) return Response({'code': 0, 'msg': f'安装成功({count} 家公司)', 'data': {'InstalledCount': count}})