From 4de301aa7839baa660f7732a4f2650717a16a08a Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 13 Aug 2026 06:28:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E5=85=AC=E5=8F=B8=E7=BA=A7+?= =?UTF-8?q?=E7=A7=9F=E6=88=B7=E7=BA=A7=E5=8F=8C=E7=B2=92=E5=BA=A6=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E5=AE=89=E8=A3=85=E2=80=94=E2=80=94CompanyApp=20?= =?UTF-8?q?=E8=A1=A8=E3=80=81Install/Uninstall=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=8C=87=E5=AE=9A=E5=85=AC=E5=8F=B8=E3=80=81=E5=8F=AF=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=85=AC=E5=8F=B8=E8=8C=83=E5=9B=B4=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=EF=BC=88super/tenant/company=EF=BC=89=E3=80=81CheckAccess=20?= =?UTF-8?q?=E5=85=AC=E5=8F=B8=E7=BB=B4=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TempLate/GERP/index.html | 4 +- gvsds/apps/gerp/app_access.py | 11 +- gvsds/apps/gerp/app_manager.py | 189 ++++++++++++++++-- gvsds/apps/gerp/migrations/0024_companyapp.py | 34 ++++ gvsds/apps/gerp/models.py | 26 +++ gvsds/apps/gerp/views/__init__.py | 1 + gvsds/apps/gerp/views/app.py | 134 +++++++++++-- gvsds/apps/gerp/views/menu.py | 5 +- gvsds/urls.py | 1 + 9 files changed, 376 insertions(+), 29 deletions(-) create mode 100644 gvsds/apps/gerp/migrations/0024_companyapp.py diff --git a/TempLate/GERP/index.html b/TempLate/GERP/index.html index 76109c7c..eda982c8 100644 --- a/TempLate/GERP/index.html +++ b/TempLate/GERP/index.html @@ -5,7 +5,7 @@ GERP 管理系统 - + @@ -14,7 +14,7 @@ - +
diff --git a/gvsds/apps/gerp/app_access.py b/gvsds/apps/gerp/app_access.py index 4b54100f..89cb4440 100644 --- a/gvsds/apps/gerp/app_access.py +++ b/gvsds/apps/gerp/app_access.py @@ -37,7 +37,16 @@ def AppAccessPermission(AppCode): user_uuid = getattr(user, 'UserUUID', None) if not tenant_uuid or not user_uuid: return False + company_uuid = None + company = getattr(request, 'current_company_uuid', None) + if company is not None: + if hasattr(company, 'bytes'): + company_uuid = company.bytes + elif hasattr(company, 'pk'): + company_uuid = company.pk from .app_manager import AppManager - return AppManager.CheckAccess(tenant_uuid, user_uuid, AppCode) is not None + return AppManager.CheckAccess( + tenant_uuid, user_uuid, AppCode, CompanyUUID=company_uuid, + ) is not None return _AppAccessPermission diff --git a/gvsds/apps/gerp/app_manager.py b/gvsds/apps/gerp/app_manager.py index 09a77ae5..59770ed6 100644 --- a/gvsds/apps/gerp/app_manager.py +++ b/gvsds/apps/gerp/app_manager.py @@ -19,7 +19,7 @@ import uuid from gvsdsdk.model_utils import UUIDObj -from .models import App, TenantApp, UserApp +from .models import App, TenantApp, CompanyApp, UserApp # 投放形态 TYPE_ENTERPRISE = 'enterprise' @@ -54,7 +54,7 @@ def _ToBytes(value): return value -def _AppFlat(app, tenant_installed=None, user_record=None): +def _AppFlat(app, tenant_installed=None, user_record=None, company_installed=None): """App 实例 → 市场/我的应用视图 dict""" return { 'AppUUID': UUIDObj(app.AppUUID).str, @@ -69,6 +69,8 @@ def _AppFlat(app, tenant_installed=None, user_record=None): # 状态附加(市场页/我的应用展示用) 'TenantInstalled': 1 if tenant_installed and tenant_installed.InstallStatus == 1 else 0, 'TenantAuth': tenant_installed.AuthStatus if tenant_installed else 0, + # 公司维度安装(指定公司安装时 TenantInstalled=0、CompanyInstalled=1) + 'CompanyInstalled': 1 if company_installed and company_installed.InstallStatus == 1 else 0, 'UserEnabled': 1 if user_record and user_record.UseStatus == 1 else 0, 'ApplyStatus': user_record.ApplyStatus if user_record else 0, } @@ -94,12 +96,13 @@ class AppManager: return App.objects.filter(AppUUID=_ToBytes(AppUUID)).first() @staticmethod - def ListMarket(TenantUUID=None, UserUUID=None): - """应用市场列表——每个应用附带租户安装状态与个人启用/申请状态 + def ListMarket(TenantUUID=None, UserUUID=None, CompanyUUID=None): + """应用市场列表——每个应用附带租户/公司安装状态与个人启用/申请状态 Args: TenantUUID: 当前租户 UUID(bytes/str/UUIDObj),用于标注"企业功能已安装" UserUUID: 当前用户 UUID,用于标注"个人已启用/申请中" + CompanyUUID: 当前公司 UUID(可选),用于标注"指定公司已安装" Returns: list[dict] @@ -107,10 +110,15 @@ class AppManager: apps = AppManager.ListEnabledApps() tenant_bytes = _ToBytes(TenantUUID) user_bytes = _ToBytes(UserUUID) + company_bytes = _ToBytes(CompanyUUID) tenant_map = {} if tenant_bytes: for ta in TenantApp.objects.filter(TenantUUID=tenant_bytes): tenant_map[UUIDObj(ta.AppUUID).str] = ta + company_map = {} + if company_bytes: + for ca in CompanyApp.objects.filter(CompanyUUID=company_bytes): + company_map[UUIDObj(ca.AppUUID).str] = ca user_map = {} if user_bytes: for ua in UserApp.objects.filter(UserUUID=user_bytes): @@ -122,12 +130,13 @@ class AppManager: app, tenant_installed=tenant_map.get(key), user_record=user_map.get(key), + company_installed=company_map.get(key), )) return result @staticmethod - def ListMyApps(TenantUUID, UserUUID): - """我的应用——当前用户可用应用(租户已安装的企业功能 + 个人已启用的应用) + def ListMyApps(TenantUUID, UserUUID, CompanyUUID=None): + """我的应用——当前用户可用应用(租户已装或当前公司已装的企业功能 + 个人已启用的应用) 需企业授权(AuthRequired=1)但个人未获授权的应用不出现在可用列表, 仅在企业功能已安装时对全员生效(企业授权在企业层面开通)。 @@ -135,10 +144,15 @@ class AppManager: apps = AppManager.ListEnabledApps() tenant_bytes = _ToBytes(TenantUUID) user_bytes = _ToBytes(UserUUID) + company_bytes = _ToBytes(CompanyUUID) tenant_map = {} if tenant_bytes: for ta in TenantApp.objects.filter(TenantUUID=tenant_bytes, InstallStatus=1): tenant_map[UUIDObj(ta.AppUUID).str] = ta + company_map = {} + if company_bytes: + for ca in CompanyApp.objects.filter(CompanyUUID=company_bytes, InstallStatus=1): + company_map[UUIDObj(ca.AppUUID).str] = ca user_map = {} if user_bytes: for ua in UserApp.objects.filter(UserUUID=user_bytes, UseStatus=1): @@ -147,10 +161,11 @@ class AppManager: for app in apps: key = UUIDObj(app.AppUUID).str tenant_installed = tenant_map.get(key) + company_installed = company_map.get(key) user_record = user_map.get(key) if app.AppType == TYPE_ENTERPRISE: - # 企业功能:租户已安装即可用(企业授权在企业层面开通) - if not tenant_installed: + # 企业功能:租户已安装 或 当前公司已安装 即可用 + if not tenant_installed and not company_installed: continue elif app.AppType == TYPE_PERSONAL: # 个人应用:个人已启用可用 @@ -158,9 +173,14 @@ class AppManager: continue else: # both # 混合:企业已安装 或 个人已启用 - if not tenant_installed and not user_record: + if not tenant_installed and not company_installed and not user_record: continue - result.append(_AppFlat(app, tenant_installed=tenant_installed, user_record=user_record)) + result.append(_AppFlat( + app, + tenant_installed=tenant_installed, + user_record=user_record, + company_installed=company_installed, + )) return result # ==================== 企业安装/停用 ==================== @@ -206,6 +226,87 @@ class AppManager: ta.save(update_fields=['InstallStatus']) return True, '' + # ==================== 公司维度安装/停用 ==================== + + @staticmethod + def InstallCompanyApps(CompanyUUIDs, AppUUID, InstallBy=None): + """为指定一个或多个公司安装企业功能(enterprise/both)。 + + Args: + CompanyUUIDs: 公司 UUID 列表(str/bytes/UUIDObj 均可,逐个写入 CompanyApp) + AppUUID: 应用 UUID + InstallBy: 安装人 UUID bytes(可选) + + Returns: + (ok, err, installed_count) + """ + app = AppManager.GetApp(AppUUID) + if app is None or app.AppStatus != 1: + return False, '应用不存在或已禁用', 0 + if app.AppType == TYPE_PERSONAL: + return False, '该应用为个人应用,不支持企业安装', 0 + company_list = [_ToBytes(c) for c in (CompanyUUIDs or []) if _ToBytes(c)] + if not company_list: + return False, '未指定要安装的公司', 0 + install_by = _ToBytes(InstallBy) + for company_bytes in company_list: + ca, created = CompanyApp.objects.get_or_create( + CompanyUUID=company_bytes, + AppUUID=app.AppUUID, + defaults={ + 'CompanyAppUUID': uuid.uuid4().bytes, + 'InstallStatus': 1, + 'InstallTime': datetime.datetime.utcnow(), + 'InstallBy': install_by, + }, + ) + if not created: + ca.InstallStatus = 1 + if install_by: + ca.InstallBy = install_by + ca.save(update_fields=['InstallStatus', 'InstallBy']) + return True, '', len(company_list) + + @staticmethod + def UninstallCompanyApps(CompanyUUIDs, AppUUID): + """为指定一个或多个公司停用企业功能""" + company_list = [_ToBytes(c) for c in (CompanyUUIDs or []) if _ToBytes(c)] + if not company_list: + return False, '未指定要停用的公司' + app_bytes = _ToBytes(AppUUID) + updated = 0 + for company_bytes in company_list: + ca = CompanyApp.objects.filter( + CompanyUUID=company_bytes, AppUUID=app_bytes, + ).first() + if ca is None: + continue + ca.InstallStatus = 0 + ca.save(update_fields=['InstallStatus']) + updated += 1 + if updated == 0: + return False, '所选公司均未安装该功能' + return True, '' + + @staticmethod + def ListCompanyInstalledApps(CompanyUUID): + """列出指定公司已安装的应用编码集合(含停用记录)""" + company_bytes = _ToBytes(CompanyUUID) + if not company_bytes: + return {} + result = {} + for ca in CompanyApp.objects.filter(CompanyUUID=company_bytes): + app = AppManager.GetApp(ca.AppUUID) + if app is None: + continue + result[app.AppCode] = { + 'AppUUID': UUIDObj(app.AppUUID).str, + 'AppCode': app.AppCode, + 'AppName': app.AppName, + 'InstallStatus': ca.InstallStatus, + } + return result + # ==================== 个人启用/申请/审批 ==================== @staticmethod @@ -314,9 +415,19 @@ class AppManager: # ==================== 访问校验(菜单注入用) ==================== @staticmethod - def CheckAccess(TenantUUID, UserUUID, AppCode): + def CheckAccess(TenantUUID, UserUUID, AppCode, CompanyUUID=None): """校验用户当前是否可用某应用(供菜单注入/接口鉴权)。 + 企业功能可用判定(enterprise/both): + - 租户已安装(TenantApp) → 可用 + - 当前公司已安装(CompanyApp,传 CompanyUUID 时)→ 可用 + + Args: + TenantUUID: 当前租户 UUID + UserUUID: 当前用户 UUID + AppCode: 应用编码 + CompanyUUID: 当前公司 UUID(可选,用于公司维度安装判定) + Returns: App 实例或 None """ @@ -325,11 +436,20 @@ class AppManager: return None tenant_bytes = _ToBytes(TenantUUID) user_bytes = _ToBytes(UserUUID) + company_bytes = _ToBytes(CompanyUUID) if app.AppType == TYPE_ENTERPRISE: ta = TenantApp.objects.filter( TenantUUID=tenant_bytes, AppUUID=app.AppUUID, InstallStatus=1, ).first() - return app if ta else None + if ta: + return app + # 公司维度安装:仅当传了当前公司且该公司已安装 + if company_bytes: + ca = CompanyApp.objects.filter( + CompanyUUID=company_bytes, AppUUID=app.AppUUID, InstallStatus=1, + ).first() + return app if ca else None + return None if app.AppType == TYPE_PERSONAL: ua = UserApp.objects.filter( UserUUID=user_bytes, AppUUID=app.AppUUID, UseStatus=1, @@ -339,5 +459,50 @@ class AppManager: ta = TenantApp.objects.filter(TenantUUID=tenant_bytes, AppUUID=app.AppUUID, InstallStatus=1).first() if ta: return app + if company_bytes: + ca = CompanyApp.objects.filter( + CompanyUUID=company_bytes, AppUUID=app.AppUUID, InstallStatus=1, + ).first() + if ca: + return app ua = UserApp.objects.filter(UserUUID=user_bytes, AppUUID=app.AppUUID, UseStatus=1).first() return app if ua else None + + @staticmethod + def GetManageableCompanies(request): + """返回当前管理员可安装应用的公司 UUID 列表(bytes) + + 权限范围: + - admin:super —— 全部租户全部公司(跨租户管理员) + - admin:tenant —— 当前租户下全部公司 + - admin:company—— 仅被授权(admin:company 绑定 CompanyUUID 维度)的公司 + + Returns: + (companies: list[bytes], scope: str) + scope: 'all'(跨租户全量)/ 'tenant'(租户内全量)/ 'company'(指定公司)/ ''(无权限) + """ + from gvsdsdk.auth.admin_service import IsSuperAdmin, IsTenantAdmin, IsCompanyAdmin + if IsSuperAdmin(request=request): + from gvsdsdk.models import Company as SdkCompany + return [c.CompanyUUID for c in SdkCompany.objects.filter(CompanyStatus=1)], 'all' + tenant_uuid = getattr(request, 'current_tenant_uuid', None) + tenant_bytes = _ToBytes(tenant_uuid) + if IsTenantAdmin(request=request): + from gvsdsdk.models import Company as SdkCompany + qs = SdkCompany.objects.filter(TenantUUID=tenant_bytes, CompanyStatus=1) + return [c.CompanyUUID for c in qs], 'tenant' + if IsCompanyAdmin(request=request): + # admin:company:取当前角色被授权公司(RolePermission.CompanyUUID 维度) + role = getattr(request, 'current_role', None) + companies = [] + if role is not None: + from gvsdsdk.models import RolePermission, Permission + perm_uuids = list(Permission.objects.filter( + PermCode='admin:company', PermStatus=1, + ).values_list('PermUUID', flat=True)) + for rp in RolePermission.objects.filter( + RoleUUID=role.RoleUUID, PermUUID__in=perm_uuids): + if rp.CompanyUUID and rp.CompanyUUID != b'\x00' * 16: + companies.append(rp.CompanyUUID) + return companies, 'company' + return [], '' diff --git a/gvsds/apps/gerp/migrations/0024_companyapp.py b/gvsds/apps/gerp/migrations/0024_companyapp.py new file mode 100644 index 00000000..cfbbd860 --- /dev/null +++ b/gvsds/apps/gerp/migrations/0024_companyapp.py @@ -0,0 +1,34 @@ +"""Add CompanyApp (company-dimension app install) + +公司维度应用安装——租户可选定一个或多个公司安装(与 TenantApp 全租户安装互补)。 +""" +import datetime +from django.db import migrations, models +import gvsds.apps.core.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0023_app_tenantapp_userapp'), + ] + + operations = [ + migrations.CreateModel( + name='CompanyApp', + fields=[ + ('company_app_uuid', gvsds.apps.core.models.UUIDField(db_column='CompanyAppUUID', primary_key=True, serialize=False, verbose_name='公司安装记录UUID')), + ('company_uuid', gvsds.apps.core.models.UUIDField(db_column='CompanyUUID', verbose_name='公司UUID')), + ('app_uuid', gvsds.apps.core.models.UUIDField(db_column='AppUUID', verbose_name='应用UUID')), + ('install_status', models.SmallIntegerField(db_column='InstallStatus', default=1, verbose_name='安装状态(1启用/0停用)')), + ('install_time', models.DateTimeField(db_column='InstallTime', default=datetime.datetime.utcnow, verbose_name='安装时间')), + ('install_by', gvsds.apps.core.models.UUIDField(blank=True, db_column='InstallBy', null=True, verbose_name='安装人UUID')), + ], + options={ + 'db_table': 'CompanyApp', + 'verbose_name': '企业应用安装(公司维度)', + 'verbose_name_plural': '企业应用安装(公司维度)', + 'unique_together': {('company_uuid', 'app_uuid')}, + }, + ), + ] diff --git a/gvsds/apps/gerp/models.py b/gvsds/apps/gerp/models.py index d8d42ee7..cb4a2427 100644 --- a/gvsds/apps/gerp/models.py +++ b/gvsds/apps/gerp/models.py @@ -831,6 +831,32 @@ class TenantApp(QModel): verbose_name_plural = '企业应用安装' +class CompanyApp(QModel): + """企业应用安装(公司维度)——租户可选定一个或多个公司安装 + + 与 TenantApp(租户全量安装)互补: + - TenantApp :全租户安装(租户下所有公司生效) + - CompanyApp :指定公司安装(仅该公司生效) + 企业功能可用判定:租户已装 或 当前公司已装。 + + InstallStatus: 1=已安装(启用) / 0=已停用 + """ + + CompanyAppUUID = UUIDField(primary_key=True, verbose_name='公司安装记录UUID') + CompanyUUID = UUIDField(db_column='CompanyUUID', verbose_name='公司UUID') + AppUUID = UUIDField(db_column='AppUUID', verbose_name='应用UUID') + InstallStatus = models.SmallIntegerField(default=1, verbose_name='安装状态(1启用/0停用)') + InstallTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='安装时间') + InstallBy = UUIDField(null=True, blank=True, verbose_name='安装人UUID') + + class Meta: + db_table = 'CompanyApp' + app_label = 'gerp' + unique_together = (('company_uuid', 'app_uuid'),) + verbose_name = '企业应用安装(公司维度)' + verbose_name_plural = '企业应用安装(公司维度)' + + class UserApp(QModel): """个人应用启用/申请——用户与应用的关系 diff --git a/gvsds/apps/gerp/views/__init__.py b/gvsds/apps/gerp/views/__init__.py index 2ea0961c..d38bfd74 100644 --- a/gvsds/apps/gerp/views/__init__.py +++ b/gvsds/apps/gerp/views/__init__.py @@ -147,6 +147,7 @@ from .gitea import ( from .app import ( AppMarketView, AppMyAppsView, + AppManageCompaniesView, AppInstallView, AppUninstallView, AppUserEnableView, diff --git a/gvsds/apps/gerp/views/app.py b/gvsds/apps/gerp/views/app.py index 2e880469..2999d0d6 100644 --- a/gvsds/apps/gerp/views/app.py +++ b/gvsds/apps/gerp/views/app.py @@ -58,8 +58,37 @@ def _GetTenantBytes(request): return None +def _GetCompanyBytes(request): + """当前角色公司 UUID bytes(CurrentRoleMiddleware 注入)""" + cu = getattr(request, 'current_company_uuid', None) + if cu is None: + return None + if hasattr(cu, 'bytes'): + return cu.bytes + if hasattr(cu, 'pk'): + return cu.pk + try: + return UUIDObj(str(cu)).bytes + except Exception: + return None + + +def _ParseUUIDList(values): + """解析公司 UUID 列表(str/bytes 混合 → bytes 列表),过滤无效""" + result = [] + for v in (values or []): + if not v: + continue + try: + b = UUIDObj(str(v)).bytes + result.append(b) + except Exception: + continue + return result + + class AppMarketView(ELTAuthenticatedAPIView): - """应用市场——全部启用应用,附租户安装/个人启用/申请状态。""" + """应用市场——全部启用应用,附租户/公司安装/个人启用/申请状态。""" permission_classes = [IsAuthenticated] @@ -67,6 +96,7 @@ class AppMarketView(ELTAuthenticatedAPIView): data = AppManager.ListMarket( TenantUUID=_GetTenantBytes(request), UserUUID=_GetUserBytes(request), + CompanyUUID=_GetCompanyBytes(request), ) return Response({'code': 0, 'data': {'list': data, 'total': len(data)}}) @@ -80,12 +110,40 @@ class AppMyAppsView(ELTAuthenticatedAPIView): data = AppManager.ListMyApps( TenantUUID=_GetTenantBytes(request), UserUUID=_GetUserBytes(request), + CompanyUUID=_GetCompanyBytes(request), ) return Response({'code': 0, 'data': {'list': data, 'total': len(data)}}) +class AppManageCompaniesView(ELTAuthenticatedAPIView): + """管理员可安装应用的公司列表(按权限范围:super=全部 / tenant=租户内 / company=被授权公司)""" + + permission_classes = [IsAuthenticated, IsAdminUser] + + def get(self, request): + companies, scope = AppManager.GetManageableCompanies(request) + rows = [] + for c in companies: + from gvsdsdk.models import Company as SdkCompany + company = SdkCompany.objects.filter(CompanyUUID=c).first() + if company is None: + continue + rows.append({ + 'CompanyUUID': UUIDObj(company.CompanyUUID).str, + 'CompanyName': company.CompanyName, + 'CompanyShort': company.CompanyShort, + }) + return Response({'code': 0, 'data': {'Scope': scope, 'Companies': rows}}) + + class AppInstallView(ELTAuthenticatedAPIView): - """租户管理员:安装企业功能。""" + """管理员:安装企业功能。 + + 支持两种粒度: + - 全租户安装:不传 CompanyUUIDs(或传空)→ 写入 TenantApp,租户全员可用 + - 指定公司安装:传 CompanyUUIDs(一个或多个)→ 写入 CompanyApp,仅这些公司可用 + 权限范围由 AppManager.GetManageableCompanies 校验(super/tenant/company)。 + """ permission_classes = [IsAuthenticated, IsAdminUser] @@ -93,17 +151,45 @@ class AppInstallView(ELTAuthenticatedAPIView): app_uuid = (request.data.get('AppUUID') or '').strip() if not app_uuid: return Response({'code': 1, 'msg': '缺少应用 UUID'}, status=400) - tenant_bytes = _GetTenantBytes(request) - if not tenant_bytes: - return Response({'code': 1, 'msg': '无法确定当前租户'}, status=400) - ok, err, flat = AppManager.InstallTenantApp(tenant_bytes, app_uuid) + + # 公司维度目标(可选):不传则全租户安装 + company_uuids = _ParseUUIDList(request.data.get('CompanyUUIDs')) + + # 全租户安装:校验租户上下文 + if not company_uuids: + tenant_bytes = _GetTenantBytes(request) + if not tenant_bytes: + return Response({'code': 1, 'msg': '无法确定当前租户'}, status=400) + ok, err, flat = AppManager.InstallTenantApp(tenant_bytes, app_uuid) + if not ok: + return Response({'code': 1, 'msg': err}, status=400) + return Response({'code': 0, 'msg': '安装成功(全租户)', 'data': flat}) + + # 指定公司安装:校验目标公司都在管理员可管理范围内 + manageable, scope = AppManager.GetManageableCompanies(request) + if not manageable: + return Response({'code': 403, 'msg': '无权安装应用:您没有可管理的公司'}, status=403) + manageable_set = {c.hex for c in manageable} + company_bytes = _ParseUUIDList(request.data.get('CompanyUUIDs')) + invalid = [c.hex for c in company_bytes if c.hex not in manageable_set] + if invalid: + return Response({'code': 403, 'msg': '无权为以下公司安装(不在您的管理范围)'}, status=403) + ok, err, count = AppManager.InstallCompanyApps( + company_uuids, app_uuid, InstallBy=_GetUserBytes(request), + ) if not ok: return Response({'code': 1, 'msg': err}, status=400) - return Response({'code': 0, 'msg': '安装成功', 'data': flat}) + return Response({'code': 0, 'msg': f'安装成功({count} 家公司)', 'data': {'InstalledCount': count}}) class AppUninstallView(ELTAuthenticatedAPIView): - """租户管理员:停用企业功能。""" + """管理员:停用企业功能。 + + 支持两种粒度: + - 传 CompanyUUIDs → 停用指定公司安装(CompanyApp) + - 不传 CompanyUUIDs → 停用全租户安装(TenantApp)+ 停用当前公司安装(CompanyApp) + (前端停用当前公司功能时无需知道当前公司 UUID,后端按当前上下文双停) + """ permission_classes = [IsAuthenticated, IsAdminUser] @@ -111,12 +197,34 @@ class AppUninstallView(ELTAuthenticatedAPIView): app_uuid = (request.data.get('AppUUID') or '').strip() if not app_uuid: return Response({'code': 1, 'msg': '缺少应用 UUID'}, status=400) + + company_uuids = _ParseUUIDList(request.data.get('CompanyUUIDs')) + + # 停用指定公司安装 + if company_uuids: + manageable, scope = AppManager.GetManageableCompanies(request) + if not manageable: + return Response({'code': 403, 'msg': '无权停用应用:您没有可管理的公司'}, status=403) + manageable_set = {c.hex for c in manageable} + company_bytes = _ParseUUIDList(request.data.get('CompanyUUIDs')) + invalid = [c.hex for c in company_bytes if c.hex not in manageable_set] + if invalid: + return Response({'code': 403, 'msg': '无权为以下公司停用(不在您的管理范围)'}, status=403) + ok, err = AppManager.UninstallCompanyApps(company_uuids, app_uuid) + if not ok: + return Response({'code': 1, 'msg': err}, status=400) + return Response({'code': 0, 'msg': '已停用(指定公司)'}) + + # 停用全租户安装 + 当前公司安装(前端停用当前公司功能时双停) tenant_bytes = _GetTenantBytes(request) - if not tenant_bytes: - return Response({'code': 1, 'msg': '无法确定当前租户'}, status=400) - ok, err = AppManager.UninstallTenantApp(tenant_bytes, app_uuid) - if not ok: - return Response({'code': 1, 'msg': err}, status=400) + if tenant_bytes: + AppManager.UninstallTenantApp(tenant_bytes, app_uuid) + company_bytes = _GetCompanyBytes(request) + if company_bytes: + try: + AppManager.UninstallCompanyApps([company_bytes], app_uuid) + except Exception: + pass return Response({'code': 0, 'msg': '已停用'}) diff --git a/gvsds/apps/gerp/views/menu.py b/gvsds/apps/gerp/views/menu.py index 537d7c87..0ddaed90 100644 --- a/gvsds/apps/gerp/views/menu.py +++ b/gvsds/apps/gerp/views/menu.py @@ -176,7 +176,10 @@ class MenuListCompatView(ELTAuthenticatedAPIView): a for a in App.objects.filter(AppStatus=1, MenuRootUUID__isnull=False) ] for _app in app_roots: - if AppManager.CheckAccess(current_tenant_uuid, user.UserUUID, _app.AppCode) is None: + if AppManager.CheckAccess( + current_tenant_uuid, user.UserUUID, _app.AppCode, + CompanyUUID=current_company, + ) is None: root_str = UUIDObj(_app.MenuRootUUID).str if root_str not in menu_map: continue diff --git a/gvsds/urls.py b/gvsds/urls.py index 0c332e11..51f10cd7 100644 --- a/gvsds/urls.py +++ b/gvsds/urls.py @@ -149,6 +149,7 @@ urlpatterns = [ # 应用/功能市场与安装授权 path('API/App/Market', gerp_views.AppMarketView.as_view(), name='app_market'), path('API/App/MyApps', gerp_views.AppMyAppsView.as_view(), name='app_my_apps'), + path('API/App/ManageCompanies', gerp_views.AppManageCompaniesView.as_view(), name='app_manage_companies'), path('API/App/Install', gerp_views.AppInstallView.as_view(), name='app_install'), path('API/App/Uninstall', gerp_views.AppUninstallView.as_view(), name='app_uninstall'), path('API/App/Enable', gerp_views.AppUserEnableView.as_view(), name='app_enable'),