feat(gerp/app): 功能/应用体系——App/TenantApp/UserApp 三表 + 应用中心服务层/API + 菜单按安装状态过滤

This commit is contained in:
TermiNexus committed 2026-08-13 04:06:15 +08:00
1 parent b468ffac99
commit 7bc96892fd
7 files changed
+770

No files matched your search

+343
View File
@@ -0,0 +1,343 @@
"""模块:应用/功能服务层(AppManager)
功能/应用体系——企业功能(租户安装)与个人应用(个人启用)的统一实体:
- 企业视角叫"功能":租户管理员在应用中心安装后,该租户全员可见可用
- 个人视角叫"应用":个人启用后仅本人可见可用
- 企业授权(AuthRequired=1):个人使用须经租户管理员审批授权
核心概念:
App.AppType enterprise=企业功能 / personal=个人应用 / both=混合
TenantApp 租户安装记录(InstallStatus 1启用/0停用;AuthStatus 企业授权 0/1/2)
UserApp 个人记录(UseStatus 1启用/0停用;ApplyStatus 0未申请/1申请中/2已授权/3已拒绝)
菜单注入(配合 MenuList 扩展):
- enterprise/both:租户 TenantApp.InstallStatus=1 → 注入 App.MenuRootUUID 菜单根
- personal/both :用户 UserApp.UseStatus=1 → 注入菜单根
"""
import datetime
import uuid
from gvsdsdk.model_utils import UUIDObj
from .models import App, TenantApp, UserApp
# 投放形态
TYPE_ENTERPRISE = 'enterprise'
TYPE_PERSONAL = 'personal'
TYPE_BOTH = 'both'
# 企业授权状态
AUTH_NONE = 0
AUTH_OK = 1
AUTH_DENIED = 2
# 个人申请状态
APPLY_NONE = 0
APPLY_PENDING = 1
APPLY_APPROVED = 2
APPLY_DENIED = 3
def _ToBytes(value):
"""统一转 bytes(兼容 bytes/UUIDObj/uuid.UUID/str)"""
if value is None:
return None
if isinstance(value, bytes):
return value
if hasattr(value, 'bytes'):
return value.bytes
if isinstance(value, str):
try:
return UUIDObj(value).bytes
except Exception:
return value
return value
def _AppFlat(app, tenant_installed=None, user_record=None):
"""App 实例 → 市场/我的应用视图 dict"""
return {
'AppUUID': UUIDObj(app.AppUUID).str,
'AppCode': app.AppCode,
'AppName': app.AppName,
'AppIcon': app.AppIcon or '',
'AppDesc': app.AppDesc or '',
'AppType': app.AppType,
'AuthRequired': app.AuthRequired,
'MenuRootUUID': UUIDObj(app.MenuRootUUID).str if app.MenuRootUUID else '',
'AppSort': app.AppSort or 0,
# 状态附加(市场页/我的应用展示用)
'TenantInstalled': 1 if tenant_installed and tenant_installed.InstallStatus == 1 else 0,
'TenantAuth': tenant_installed.AuthStatus if tenant_installed else 0,
'UserEnabled': 1 if user_record and user_record.UseStatus == 1 else 0,
'ApplyStatus': user_record.ApplyStatus if user_record else 0,
}
class AppManager:
"""应用/功能统一服务层"""
# ==================== 查询 ====================
@staticmethod
def ListEnabledApps():
"""全部启用中的应用(AppStatus=1)"""
return list(App.objects.filter(AppStatus=1).order_by('AppSort', 'CreateTime'))
@staticmethod
def GetAppByCode(AppCode):
"""按应用编码查应用(禁用也返回,供访问校验)"""
return App.objects.filter(AppCode=AppCode).first()
@staticmethod
def GetApp(AppUUID):
return App.objects.filter(AppUUID=_ToBytes(AppUUID)).first()
@staticmethod
def ListMarket(TenantUUID=None, UserUUID=None):
"""应用市场列表——每个应用附带租户安装状态与个人启用/申请状态
Args:
TenantUUID: 当前租户 UUID(bytes/str/UUIDObj),用于标注"企业功能已安装"
UserUUID: 当前用户 UUID,用于标注"个人已启用/申请中"
Returns:
list[dict]
"""
apps = AppManager.ListEnabledApps()
tenant_bytes = _ToBytes(TenantUUID)
user_bytes = _ToBytes(UserUUID)
tenant_map = {}
if tenant_bytes:
for ta in TenantApp.objects.filter(TenantUUID=tenant_bytes):
tenant_map[UUIDObj(ta.AppUUID).str] = ta
user_map = {}
if user_bytes:
for ua in UserApp.objects.filter(UserUUID=user_bytes):
user_map[UUIDObj(ua.AppUUID).str] = ua
result = []
for app in apps:
key = UUIDObj(app.AppUUID).str
result.append(_AppFlat(
app,
tenant_installed=tenant_map.get(key),
user_record=user_map.get(key),
))
return result
@staticmethod
def ListMyApps(TenantUUID, UserUUID):
"""我的应用——当前用户可用应用(租户已安装的企业功能 + 个人已启用的应用)
需企业授权(AuthRequired=1)但个人未获授权的应用不出现在可用列表,
仅在企业功能已安装时对全员生效(企业授权在企业层面开通)。
"""
apps = AppManager.ListEnabledApps()
tenant_bytes = _ToBytes(TenantUUID)
user_bytes = _ToBytes(UserUUID)
tenant_map = {}
if tenant_bytes:
for ta in TenantApp.objects.filter(TenantUUID=tenant_bytes, InstallStatus=1):
tenant_map[UUIDObj(ta.AppUUID).str] = ta
user_map = {}
if user_bytes:
for ua in UserApp.objects.filter(UserUUID=user_bytes, UseStatus=1):
user_map[UUIDObj(ua.AppUUID).str] = ua
result = []
for app in apps:
key = UUIDObj(app.AppUUID).str
tenant_installed = tenant_map.get(key)
user_record = user_map.get(key)
if app.AppType == TYPE_ENTERPRISE:
# 企业功能:租户已安装即可用(企业授权在企业层面开通)
if not tenant_installed:
continue
elif app.AppType == TYPE_PERSONAL:
# 个人应用:个人已启用可用
if not user_record:
continue
else: # both
# 混合:企业已安装 或 个人已启用
if not tenant_installed and not user_record:
continue
result.append(_AppFlat(app, tenant_installed=tenant_installed, user_record=user_record))
return result
# ==================== 企业安装/停用 ====================
@staticmethod
def InstallTenantApp(TenantUUID, AppUUID):
"""租户安装企业功能(enterprise/both)。
Returns:
(ok, err, app_flat)
"""
tenant_bytes = _ToBytes(TenantUUID)
app = AppManager.GetApp(AppUUID)
if app is None or app.AppStatus != 1:
return False, '应用不存在或已禁用', None
if app.AppType == TYPE_PERSONAL:
return False, '该应用为个人应用,不支持企业安装', None
ta, created = TenantApp.objects.get_or_create(
TenantUUID=tenant_bytes,
AppUUID=app.AppUUID,
defaults={
'TenantAppUUID': uuid.uuid4().bytes,
'InstallStatus': 1,
'AuthStatus': AUTH_OK if app.AuthRequired == 1 else AUTH_NONE,
'InstallTime': datetime.datetime.utcnow(),
},
)
if not created:
ta.InstallStatus = 1
if app.AuthRequired == 1 and ta.AuthStatus != AUTH_OK:
ta.AuthStatus = AUTH_OK
ta.save(update_fields=['InstallStatus', 'AuthStatus'])
return True, '', _AppFlat(app, tenant_installed=ta)
@staticmethod
def UninstallTenantApp(TenantUUID, AppUUID):
"""租户停用企业功能"""
tenant_bytes = _ToBytes(TenantUUID)
ta = TenantApp.objects.filter(TenantUUID=tenant_bytes, AppUUID=_ToBytes(AppUUID)).first()
if ta is None:
return False, '该功能未安装'
ta.InstallStatus = 0
ta.save(update_fields=['InstallStatus'])
return True, ''
# ==================== 个人启用/申请/审批 ====================
@staticmethod
def EnableUserApp(UserUUID, AppUUID):
"""个人启用应用。
企业功能(enterprise)不参与个人启用(由租户安装控制);
AuthRequired=1 的应用必须先申请授权(ApplyStatus=2 才可启用)。
Returns:
(ok, err, app_flat)
"""
user_bytes = _ToBytes(UserUUID)
app = AppManager.GetApp(AppUUID)
if app is None or app.AppStatus != 1:
return False, '应用不存在或已禁用', None
if app.AppType == TYPE_ENTERPRISE:
return False, '该应用为企业功能,由租户管理员统一开通', None
ua, created = UserApp.objects.get_or_create(
UserUUID=user_bytes,
AppUUID=app.AppUUID,
defaults={'UserAppUUID': uuid.uuid4().bytes},
)
if app.AuthRequired == 1:
if ua.ApplyStatus != APPLY_APPROVED:
return False, '该应用需企业授权,请先申请(待租户管理员审批)', None
ua.UseStatus = 1
ua.save(update_fields=['UseStatus'])
return True, '', _AppFlat(app, user_record=ua)
ua.UseStatus = 1
ua.save(update_fields=['UseStatus'])
return True, '', _AppFlat(app, user_record=ua)
@staticmethod
def DisableUserApp(UserUUID, AppUUID):
"""个人停用应用"""
user_bytes = _ToBytes(UserUUID)
ua = UserApp.objects.filter(UserUUID=user_bytes, AppUUID=_ToBytes(AppUUID)).first()
if ua is None:
return False, '未启用该应用'
ua.UseStatus = 0
ua.save(update_fields=['UseStatus'])
return True, ''
@staticmethod
def ApplyUserApp(UserUUID, AppUUID):
"""个人申请企业授权(AuthRequired=1 的应用)"""
user_bytes = _ToBytes(UserUUID)
app = AppManager.GetApp(AppUUID)
if app is None or app.AppStatus != 1:
return False, '应用不存在或已禁用'
if app.AuthRequired != 1:
return False, '该应用无需企业授权,可直接启用'
ua, created = UserApp.objects.get_or_create(
UserUUID=user_bytes,
AppUUID=app.AppUUID,
defaults={'UserAppUUID': uuid.uuid4().bytes},
)
if ua.ApplyStatus == APPLY_APPROVED:
return False, '该应用已授权,可直接启用'
ua.ApplyStatus = APPLY_PENDING
ua.ApplyTime = datetime.datetime.utcnow()
ua.save(update_fields=['ApplyStatus', 'ApplyTime'])
return True, ''
@staticmethod
def ApproveUserApp(OperatorUUID, UserAppUUID, Approved=True):
"""租户管理员审批个人授权申请。
Returns:
(ok, err, flat)
"""
ua = UserApp.objects.filter(UserAppUUID=_ToBytes(UserAppUUID)).first()
if ua is None:
return False, '申请记录不存在'
if ua.ApplyStatus != APPLY_PENDING:
return False, '该申请已处理(非待审批状态)'
ua.ApplyStatus = APPLY_APPROVED if Approved else APPLY_DENIED
ua.UseStatus = 1 if Approved else 0
ua.ApproveBy = _ToBytes(OperatorUUID)
ua.ApproveTime = datetime.datetime.utcnow()
ua.save(update_fields=['ApplyStatus', 'UseStatus', 'ApproveBy', 'ApproveTime'])
app = AppManager.GetApp(ua.AppUUID)
return True, '', _AppFlat(app, user_record=ua)
@staticmethod
def ListPendingApplications(TenantUUID):
"""租户内待审批的个人授权申请(供租户管理员审批台)"""
tenant_bytes = _ToBytes(TenantUUID)
rows = []
for ua in UserApp.objects.filter(ApplyStatus=APPLY_PENDING).order_by('-ApplyTime'):
app = AppManager.GetApp(ua.AppUUID)
if app is None:
continue
rows.append({
'UserAppUUID': UUIDObj(ua.UserAppUUID).str,
'UserUUID': UUIDObj(ua.UserUUID).str,
'AppUUID': UUIDObj(ua.AppUUID).str,
'AppName': app.AppName,
'AppCode': app.AppCode,
'AppIcon': app.AppIcon or '',
'ApplyTime': ua.ApplyTime.isoformat() if ua.ApplyTime else None,
})
return rows
# ==================== 访问校验(菜单注入用) ====================
@staticmethod
def CheckAccess(TenantUUID, UserUUID, AppCode):
"""校验用户当前是否可用某应用(供菜单注入/接口鉴权)。
Returns:
App 实例或 None
"""
app = AppManager.GetAppByCode(AppCode)
if app is None or app.AppStatus != 1:
return None
tenant_bytes = _ToBytes(TenantUUID)
user_bytes = _ToBytes(UserUUID)
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 app.AppType == TYPE_PERSONAL:
ua = UserApp.objects.filter(
UserUUID=user_bytes, AppUUID=app.AppUUID, UseStatus=1,
).first()
return app if ua else None
# both
ta = TenantApp.objects.filter(TenantUUID=tenant_bytes, AppUUID=app.AppUUID, InstallStatus=1).first()
if ta:
return app
ua = UserApp.objects.filter(UserUUID=user_bytes, AppUUID=app.AppUUID, UseStatus=1).first()
return app if ua else None
@@ -0,0 +1,82 @@
"""Add App, TenantApp, UserApp
功能/应用体系——企业功能(租户安装)与个人应用(个人启用)的统一实体:
- App:应用注册表(AppType: enterprise/personal/both,AuthRequired 企业授权标记)
- TenantApp:企业安装/授权(InstallStatus/AuthStatus)
- UserApp:个人启用/申请(UseStatus/ApplyStatus 审批状态机)
"""
import datetime
from django.db import migrations, models
import gvsds.apps.core.models
class Migration(migrations.Migration):
dependencies = [
('gerp', '0022_user_gitea_email'),
]
operations = [
migrations.CreateModel(
name='App',
fields=[
('app_uuid', gvsds.apps.core.models.UUIDField(db_column='AppUUID', primary_key=True, serialize=False)),
('app_code', models.CharField(db_column='AppCode', max_length=64, unique=True)),
('app_name', models.CharField(db_column='AppName', max_length=128)),
('app_icon', models.CharField(db_column='AppIcon', default='', max_length=256)),
('app_desc', models.TextField(db_column='AppDesc', default='')),
('app_type', models.CharField(db_column='AppType', default='both', max_length=16)),
('auth_required', models.SmallIntegerField(db_column='AuthRequired', default=0)),
('menu_root_uuid', gvsds.apps.core.models.UUIDField(blank=True, db_column='MenuRootUUID', null=True)),
('default_perms', models.TextField(db_column='DefaultPerms', default='')),
('app_sort', models.IntegerField(db_column='AppSort', default=0)),
('app_status', models.SmallIntegerField(db_column='AppStatus', default=1)),
('create_time', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
],
options={
'verbose_name': '应用/功能',
'verbose_name_plural': '应用/功能',
'db_table': 'App',
'ordering': ['AppSort', 'CreateTime'],
},
),
migrations.CreateModel(
name='TenantApp',
fields=[
('tenant_app_uuid', gvsds.apps.core.models.UUIDField(db_column='TenantAppUUID', primary_key=True, serialize=False)),
('tenant_uuid', gvsds.apps.core.models.UUIDField(db_column='TenantUUID')),
('app_uuid', gvsds.apps.core.models.UUIDField(db_column='AppUUID')),
('install_status', models.SmallIntegerField(db_column='InstallStatus', default=1)),
('auth_status', models.SmallIntegerField(db_column='AuthStatus', default=0)),
('install_time', models.DateTimeField(db_column='InstallTime', default=datetime.datetime.utcnow)),
('auth_by', gvsds.apps.core.models.UUIDField(blank=True, db_column='AuthBy', null=True)),
('auth_time', models.DateTimeField(blank=True, db_column='AuthTime', null=True)),
],
options={
'verbose_name': '企业应用安装',
'verbose_name_plural': '企业应用安装',
'db_table': 'TenantApp',
'unique_together': {('tenant_uuid', 'app_uuid')},
},
),
migrations.CreateModel(
name='UserApp',
fields=[
('user_app_uuid', gvsds.apps.core.models.UUIDField(db_column='UserAppUUID', primary_key=True, serialize=False)),
('user_uuid', gvsds.apps.core.models.UUIDField(db_column='UserUUID')),
('app_uuid', gvsds.apps.core.models.UUIDField(db_column='AppUUID')),
('use_status', models.SmallIntegerField(db_column='UseStatus', default=1)),
('apply_status', models.SmallIntegerField(db_column='ApplyStatus', default=0)),
('apply_time', models.DateTimeField(blank=True, db_column='ApplyTime', null=True)),
('approve_by', gvsds.apps.core.models.UUIDField(blank=True, db_column='ApproveBy', null=True)),
('approve_time', models.DateTimeField(blank=True, db_column='ApproveTime', null=True)),
('create_time', models.DateTimeField(db_column='CreateTime', default=datetime.datetime.utcnow)),
],
options={
'verbose_name': '个人应用',
'verbose_name_plural': '个人应用',
'db_table': 'UserApp',
'unique_together': {('user_uuid', 'app_uuid')},
},
),
]
+84
View File
@@ -772,6 +772,90 @@ class UserGiteaAccount(QModel):
return self.GiteaLogin
class App(QModel):
"""应用/功能注册表——企业功能与个人应用的统一实体
企业视角叫"功能"(租户安装后全员可用),个人视角叫"应用"(个人启用后自用)。
投放形态 AppType:
enterprise —— 企业功能:仅租户管理员安装后该租户可见
personal —— 个人应用:个人启用后仅本人可见
both —— 混合:企业装=全员,个人装=自用
企业授权 AuthRequired=1:个人使用须经租户管理员审批授权(UserApp.ApplyStatus 状态机)。
"""
AppUUID = UUIDField(primary_key=True, verbose_name='应用UUID')
AppCode = models.CharField(max_length=64, unique=True, verbose_name='应用编码')
AppName = models.CharField(max_length=128, verbose_name='应用名称')
AppIcon = models.CharField(max_length=256, default='', verbose_name='应用图标')
AppDesc = models.TextField(default='', verbose_name='应用描述')
AppType = models.CharField(max_length=16, default='both', verbose_name='投放形态(enterprise/personal/both)')
AuthRequired = models.SmallIntegerField(default=0, verbose_name='是否需企业授权(0/1)')
MenuRootUUID = UUIDField(null=True, blank=True, verbose_name='关联菜单根UUID(安装后注入)')
DefaultPerms = models.TextField(default='', verbose_name='默认权限码集(;分隔)')
AppSort = models.IntegerField(default=0, verbose_name='排序')
AppStatus = models.SmallIntegerField(default=1, verbose_name='状态(1启用/0禁用)')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'App'
app_label = 'gerp'
ordering = ['AppSort', 'CreateTime']
verbose_name = '应用/功能'
verbose_name_plural = '应用/功能'
def __str__(self):
return self.AppName
class TenantApp(QModel):
"""企业安装/授权——租户与应用的关系
InstallStatus: 1=已安装(启用) / 0=已停用
AuthStatus: 0=未授权 / 1=已授权 / 2=已拒绝(AuthRequired 应用的企业级开通状态)
"""
TenantAppUUID = UUIDField(primary_key=True, verbose_name='企业安装记录UUID')
TenantUUID = UUIDField(db_column='TenantUUID', verbose_name='租户UUID')
AppUUID = UUIDField(db_column='AppUUID', verbose_name='应用UUID')
InstallStatus = models.SmallIntegerField(default=1, verbose_name='安装状态(1启用/0停用)')
AuthStatus = models.SmallIntegerField(default=0, verbose_name='企业授权状态(0/1/2)')
InstallTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='安装时间')
AuthBy = UUIDField(null=True, blank=True, verbose_name='企业授权人UUID')
AuthTime = models.DateTimeField(null=True, blank=True, verbose_name='企业授权时间')
class Meta:
db_table = 'TenantApp'
app_label = 'gerp'
unique_together = (('tenant_uuid', 'app_uuid'),)
verbose_name = '企业应用安装'
verbose_name_plural = '企业应用安装'
class UserApp(QModel):
"""个人应用启用/申请——用户与应用的关系
UseStatus: 1=已启用 / 0=已停用
ApplyStatus: 0=未申请 / 1=申请中 / 2=已授权 / 3=已拒绝(AuthRequired 应用的审批状态机)
"""
UserAppUUID = UUIDField(primary_key=True, verbose_name='个人应用记录UUID')
UserUUID = UUIDField(db_column='UserUUID', verbose_name='用户UUID')
AppUUID = UUIDField(db_column='AppUUID', verbose_name='应用UUID')
UseStatus = models.SmallIntegerField(default=1, verbose_name='使用状态(1启用/0停用)')
ApplyStatus = models.SmallIntegerField(default=0, verbose_name='授权申请状态(0/1/2/3)')
ApplyTime = models.DateTimeField(null=True, blank=True, verbose_name='申请时间')
ApproveBy = UUIDField(null=True, blank=True, verbose_name='审批人UUID')
ApproveTime = models.DateTimeField(null=True, blank=True, verbose_name='审批时间')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
class Meta:
db_table = 'UserApp'
app_label = 'gerp'
unique_together = (('user_uuid', 'app_uuid'),)
verbose_name = '个人应用'
verbose_name_plural = '个人应用'
# ==================== 从 gvsdsdk 导入重复模型 ====================
# 以下模型原在 gerp/models.py 定义,现统一依赖 gvsdsdk.models(映射 SAAS 主库)
# 注意:gvsdsdk 模型用 UUIDField(非 ForeignKey),不支持 select_related 和关联属性访问
+11
View File
@@ -144,3 +144,14 @@ from .gitea import (
GiteaAdminListView,
GiteaDisableView,
)
from .app import (
AppMarketView,
AppMyAppsView,
AppInstallView,
AppUninstallView,
AppUserEnableView,
AppUserDisableView,
AppApplyView,
AppApproveView,
AppPendingView,
)
+206
View File
@@ -0,0 +1,206 @@
"""模块:GERP 应用/功能视图(应用市场与安装授权)
提供应用市场的浏览、企业功能安装/停用、个人应用启用/停用、
企业授权申请/审批,以及"我的应用"查询。
URL 路径最后一段作为 action 名称:
GET /API/App/Market - 应用市场列表(含租户安装/个人启用/申请状态)
GET /API/App/MyApps - 我的应用(当前用户可用)
POST /API/App/Install - 租户管理员:安装企业功能(enterprise/both)
POST /API/App/Uninstall - 租户管理员:停用企业功能
POST /API/App/Enable - 个人:启用应用(AuthRequired=1 需先获授权)
POST /API/App/Disable - 个人:停用应用
POST /API/App/Apply - 个人:申请企业授权
POST /API/App/Approve - 租户管理员:审批个人授权申请
GET /API/App/Pending - 租户管理员:待审批申请列表
"""
import logging
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from gvsdsdk.auth.permissions import IsAdminUser
from gvsdsdk.model_utils import UUIDObj
from . import ELTAuthenticatedAPIView
from ..app_manager import AppManager
logger = logging.getLogger(__name__)
def _GetUserBytes(request):
"""当前用户 UUID bytes(兼容 core User)"""
user = request.user
if not user:
return None
uu = getattr(user, 'UserUUID', None)
if uu is None:
return None
if hasattr(uu, 'bytes'):
return uu.bytes
try:
return UUIDObj(str(uu)).bytes
except Exception:
return None
def _GetTenantBytes(request):
"""当前角色租户 UUID bytes(CurrentRoleMiddleware/ResolveRoleContext 注入)"""
tu = getattr(request, 'current_tenant_uuid', None)
if tu is None:
return None
if hasattr(tu, 'bytes'):
return tu.bytes
try:
return UUIDObj(str(tu)).bytes
except Exception:
return None
class AppMarketView(ELTAuthenticatedAPIView):
"""应用市场——全部启用应用,附租户安装/个人启用/申请状态。"""
permission_classes = [IsAuthenticated]
def get(self, request):
data = AppManager.ListMarket(
TenantUUID=_GetTenantBytes(request),
UserUUID=_GetUserBytes(request),
)
return Response({'code': 0, 'data': {'list': data, 'total': len(data)}})
class AppMyAppsView(ELTAuthenticatedAPIView):
"""我的应用——当前用户可用应用列表。"""
permission_classes = [IsAuthenticated]
def get(self, request):
data = AppManager.ListMyApps(
TenantUUID=_GetTenantBytes(request),
UserUUID=_GetUserBytes(request),
)
return Response({'code': 0, 'data': {'list': data, 'total': len(data)}})
class AppInstallView(ELTAuthenticatedAPIView):
"""租户管理员:安装企业功能。"""
permission_classes = [IsAuthenticated, IsAdminUser]
def post(self, request):
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)
if not ok:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': '安装成功', 'data': flat})
class AppUninstallView(ELTAuthenticatedAPIView):
"""租户管理员:停用企业功能。"""
permission_classes = [IsAuthenticated, IsAdminUser]
def post(self, request):
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 = AppManager.UninstallTenantApp(tenant_bytes, app_uuid)
if not ok:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': '已停用'})
class AppUserEnableView(ELTAuthenticatedAPIView):
"""个人:启用应用(AuthRequired=1 需先获企业授权)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
app_uuid = (request.data.get('AppUUID') or '').strip()
if not app_uuid:
return Response({'code': 1, 'msg': '缺少应用 UUID'}, status=400)
user_bytes = _GetUserBytes(request)
if not user_bytes:
return Response({'code': 1, 'msg': '无法识别当前用户'}, status=401)
ok, err, flat = AppManager.EnableUserApp(user_bytes, app_uuid)
if not ok:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': '已启用', 'data': flat})
class AppUserDisableView(ELTAuthenticatedAPIView):
"""个人:停用应用。"""
permission_classes = [IsAuthenticated]
def post(self, request):
app_uuid = (request.data.get('AppUUID') or '').strip()
if not app_uuid:
return Response({'code': 1, 'msg': '缺少应用 UUID'}, status=400)
user_bytes = _GetUserBytes(request)
if not user_bytes:
return Response({'code': 1, 'msg': '无法识别当前用户'}, status=401)
ok, err = AppManager.DisableUserApp(user_bytes, app_uuid)
if not ok:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': '已停用'})
class AppApplyView(ELTAuthenticatedAPIView):
"""个人:申请企业授权(AuthRequired=1 的应用)。"""
permission_classes = [IsAuthenticated]
def post(self, request):
app_uuid = (request.data.get('AppUUID') or '').strip()
if not app_uuid:
return Response({'code': 1, 'msg': '缺少应用 UUID'}, status=400)
user_bytes = _GetUserBytes(request)
if not user_bytes:
return Response({'code': 1, 'msg': '无法识别当前用户'}, status=401)
ok, err = AppManager.ApplyUserApp(user_bytes, app_uuid)
if not ok:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': '申请已提交,等待租户管理员审批'})
class AppApproveView(ELTAuthenticatedAPIView):
"""租户管理员:审批个人授权申请。"""
permission_classes = [IsAuthenticated, IsAdminUser]
def post(self, request):
user_app_uuid = (request.data.get('UserAppUUID') or '').strip()
if not user_app_uuid:
return Response({'code': 1, 'msg': '缺少申请记录 UUID'}, status=400)
approved = request.data.get('Approved', True)
if isinstance(approved, str):
approved = approved.lower() in ('1', 'true', 'yes')
user_bytes = _GetUserBytes(request)
ok, err, flat = AppManager.ApproveUserApp(user_bytes, user_app_uuid, Approved=bool(approved))
if not ok:
return Response({'code': 1, 'msg': err}, status=400)
return Response({'code': 0, 'msg': '已通过' if approved else '已拒绝', 'data': flat})
class AppPendingView(ELTAuthenticatedAPIView):
"""租户管理员:待审批的个人授权申请列表。"""
permission_classes = [IsAuthenticated, IsAdminUser]
def get(self, request):
tenant_bytes = _GetTenantBytes(request)
if not tenant_bytes:
return Response({'code': 0, 'data': {'list': [], 'total': 0}})
rows = AppManager.ListPendingApplications(tenant_bytes)
return Response({'code': 0, 'data': {'list': rows, 'total': len(rows)}})
+33
View File
@@ -146,6 +146,39 @@ class MenuListCompatView(ELTAuthenticatedAPIView):
for uuid_str in no_perm_uuids:
menu_map.pop(uuid_str, None)
# 应用/功能菜单过滤(仅普通用户):企业功能按租户安装状态、个人应用按个人启用状态
# 管理员(admin:super/tenant)跳过过滤——管理视角需全览,便于在应用中心管理
if not is_admin:
try:
from ..models import App
from ..app_manager import AppManager
# 当前租户 UUID:与 ResolveRoleContext 一致,取自当前角色 Role.TenantUUID
current_tenant_uuid = None
if role_uuid_bytes:
from gvsdsdk.models import Role as _SdkRole
_role_obj = _SdkRole.objects.filter(RoleUUID=role_uuid_bytes).first()
if _role_obj is not None:
current_tenant_uuid = _role_obj.TenantUUID
app_roots = [
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:
root_str = UUIDObj(_app.MenuRootUUID).str
if root_str not in menu_map:
continue
_invalid = set()
def _CollectAppSub(uuid_str, acc):
acc.add(uuid_str)
for uid, item in menu_map.items():
if item['ParentUUID'] == uuid_str and uid not in acc:
_CollectAppSub(uid, acc)
_CollectAppSub(root_str, _invalid)
for _u in _invalid:
menu_map.pop(_u, None)
except Exception:
logger.exception('MenuListCompatView 应用菜单过滤失败')
# 构建树
root_menus = []
for menu_uuid_str, item in menu_map.items():
+11
View File
@@ -146,6 +146,17 @@ urlpatterns = [
path('API/Tenant/Approve/<uuid:tenant_uuid>', gerp_views.TenantApproveView.as_view(), name='tenant_approve'),
path('API/Tenant/Reject/<uuid:tenant_uuid>', gerp_views.TenantRejectView.as_view(), name='tenant_reject'),
# 应用/功能市场与安装授权
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/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'),
path('API/App/Disable', gerp_views.AppUserDisableView.as_view(), name='app_disable'),
path('API/App/Apply', gerp_views.AppApplyView.as_view(), name='app_apply'),
path('API/App/Approve', gerp_views.AppApproveView.as_view(), name='app_approve'),
path('API/App/Pending', gerp_views.AppPendingView.as_view(), name='app_pending'),
# 角色权限分配(权限树勾选)
path('API/Role/PermissionsTree', gerp_views.RolePermissionsTreeView.as_view(), name='role_permissions_tree'),
path('API/Role/SavePermissions', gerp_views.RoleSavePermissionsView.as_view(), name='role_save_permissions'),