feat(gerp/app): 功能/应用体系——App/TenantApp/UserApp 三表 + 应用中心服务层/API + 菜单按安装状态过滤
This commit is contained in:
1 parent
b468ffac99
commit
7bc96892fd
7 files changed
+770
No files matched your search
@@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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 和关联属性访问
|
||||
|
||||
@@ -144,3 +144,14 @@ from .gitea import (
|
||||
GiteaAdminListView,
|
||||
GiteaDisableView,
|
||||
)
|
||||
from .app import (
|
||||
AppMarketView,
|
||||
AppMyAppsView,
|
||||
AppInstallView,
|
||||
AppUninstallView,
|
||||
AppUserEnableView,
|
||||
AppUserDisableView,
|
||||
AppApplyView,
|
||||
AppApproveView,
|
||||
AppPendingView,
|
||||
)
|
||||
@@ -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)}})
|
||||
@@ -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():
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in new issue
Block a user