feat(gerp): 订阅应用新增应用级订阅方与业务虚拟职务
- 新增 SubscriptionAppParty(应用级提供者/消费者公司,作为初始化引用)+ 迁移 0051 - 新增接口 GET /API/Subscription/AppParties、POST /API/Subscription/AppParty/Save - 清单新增 RequirePositionBinding、菜单「软件管理」(/subscription/software)、3 个业务虚拟职务(软件提供方/软件消费方/订阅管理员)与「订阅」职务模板
This commit is contained in:
1 parent
e60200b334
commit
02bb7b3852
6 files changed
+233
-4
No files matched your search
@@ -5,16 +5,25 @@
|
|||||||
"AppIcon": "Connection",
|
"AppIcon": "Connection",
|
||||||
"AppDesc": "以「订阅组(产品)」为单位登记软件提供关系:为每个产品指定提供者公司与消费者公司,并汇总订阅关系",
|
"AppDesc": "以「订阅组(产品)」为单位登记软件提供关系:为每个产品指定提供者公司与消费者公司,并汇总订阅关系",
|
||||||
"AppType": "enterprise",
|
"AppType": "enterprise",
|
||||||
|
"RequirePositionBinding": 1,
|
||||||
"AppSort": 13,
|
"AppSort": 13,
|
||||||
"FrontendRoute": "/subscription/group",
|
"FrontendRoute": "/subscription/software",
|
||||||
"FrontendType": "vue",
|
"FrontendType": "vue",
|
||||||
"Menus": [
|
"Menus": [
|
||||||
|
{
|
||||||
|
"MenuName": "subscription-software",
|
||||||
|
"MenuTitle": "软件管理",
|
||||||
|
"MenuLink": "/subscription/software",
|
||||||
|
"MenuIcon": "Management",
|
||||||
|
"MenuSort": 1,
|
||||||
|
"ParentMenuName": "subscription"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"MenuName": "subscription-group",
|
"MenuName": "subscription-group",
|
||||||
"MenuTitle": "订阅组管理",
|
"MenuTitle": "订阅组管理",
|
||||||
"MenuLink": "/subscription/group",
|
"MenuLink": "/subscription/group",
|
||||||
"MenuIcon": "Connection",
|
"MenuIcon": "Connection",
|
||||||
"MenuSort": 1,
|
"MenuSort": 2,
|
||||||
"ParentMenuName": "subscription"
|
"ParentMenuName": "subscription"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -22,8 +31,31 @@
|
|||||||
"MenuTitle": "订阅关系",
|
"MenuTitle": "订阅关系",
|
||||||
"MenuLink": "/subscription/relation",
|
"MenuLink": "/subscription/relation",
|
||||||
"MenuIcon": "Share",
|
"MenuIcon": "Share",
|
||||||
"MenuSort": 2,
|
"MenuSort": 3,
|
||||||
"ParentMenuName": "subscription"
|
"ParentMenuName": "subscription"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"VirtualPositions": [
|
||||||
|
{
|
||||||
|
"Code": "software-provider",
|
||||||
|
"Name": "软件提供方",
|
||||||
|
"MenuNames": ["subscription-software", "subscription-group"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Code": "software-consumer",
|
||||||
|
"Name": "软件消费方",
|
||||||
|
"MenuNames": ["subscription-software"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Code": "subscription-manager",
|
||||||
|
"Name": "订阅管理员",
|
||||||
|
"MenuNames": ["subscription-software", "subscription-group", "subscription-relation"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PositionTemplates": [
|
||||||
|
{
|
||||||
|
"CategoryName": "订阅",
|
||||||
|
"Positions": ["软件提供方", "软件消费方", "订阅管理员"]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,13 @@ from django.db import transaction
|
|||||||
|
|
||||||
from gvsdsdk.model_utils import UUIDObj
|
from gvsdsdk.model_utils import UUIDObj
|
||||||
|
|
||||||
from .models import PARTY_CONSUMER, PARTY_PROVIDER, SubscriptionGroup, SubscriptionGroupCompany
|
from .models import (
|
||||||
|
PARTY_CONSUMER,
|
||||||
|
PARTY_PROVIDER,
|
||||||
|
SubscriptionAppParty,
|
||||||
|
SubscriptionGroup,
|
||||||
|
SubscriptionGroupCompany,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _ToBytes(value):
|
def _ToBytes(value):
|
||||||
@@ -332,3 +338,82 @@ class SubscriptionManager:
|
|||||||
'GroupStatus': group['GroupStatus'],
|
'GroupStatus': group['GroupStatus'],
|
||||||
})
|
})
|
||||||
return list(index.values())
|
return list(index.values())
|
||||||
|
|
||||||
|
# ==================== 应用级订阅方(应用本身的两方公司,初始化引用) ====================
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ListAppParties(AppUUID):
|
||||||
|
"""读取某应用的提供者 / 消费者公司(不分具体产品)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: {'AppUUID': str, 'Providers': [公司展示行], 'Consumers': [公司展示行]}
|
||||||
|
"""
|
||||||
|
app_bytes = _ToBytes(AppUUID)
|
||||||
|
if app_bytes is None:
|
||||||
|
return {'AppUUID': '', 'Providers': [], 'Consumers': []}
|
||||||
|
rows = list(SubscriptionAppParty.objects.filter(AppUUID=app_bytes))
|
||||||
|
info = {
|
||||||
|
row['CompanyUUID']: row
|
||||||
|
for row in SubscriptionManager.CompanyRows([r.CompanyUUID for r in rows])
|
||||||
|
}
|
||||||
|
result = {'AppUUID': _ToStr(app_bytes), 'Providers': [], 'Consumers': []}
|
||||||
|
for r in rows:
|
||||||
|
target = 'Providers' if r.PartyType == PARTY_PROVIDER else 'Consumers'
|
||||||
|
company = info.get(_ToStr(r.CompanyUUID))
|
||||||
|
if company is None:
|
||||||
|
company = {
|
||||||
|
'CompanyUUID': _ToStr(r.CompanyUUID),
|
||||||
|
'CompanyName': '',
|
||||||
|
'CompanyShort': '',
|
||||||
|
'TenantUUID': _ToStr(r.TenantUUID),
|
||||||
|
'TenantName': '',
|
||||||
|
}
|
||||||
|
result[target].append(company)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def SaveAppParties(AppUUID, TenantUUID, Data, AllowedCompanyUUIDs=None):
|
||||||
|
"""整体重写某应用的提供者 / 消费者公司(应用级,不分产品)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
AppUUID: 应用 UUID
|
||||||
|
TenantUUID: 归属租户(公司查不到所属租户时的兜底)
|
||||||
|
Data: {'Providers': [公司UUID...], 'Consumers': [公司UUID...]}(允许为空=清除)
|
||||||
|
AllowedCompanyUUIDs: 允许选用的公司集合(bytes);None=不校验
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(row, error) —— 校验失败时 row 为 None
|
||||||
|
"""
|
||||||
|
app_bytes = _ToBytes(AppUUID)
|
||||||
|
if app_bytes is None:
|
||||||
|
return None, '无效的应用 UUID'
|
||||||
|
provider_bytes = [c for c in (_ToBytes(x) for x in (Data.get('Providers') or [])) if c]
|
||||||
|
consumer_bytes = [c for c in (_ToBytes(x) for x in (Data.get('Consumers') or [])) if c]
|
||||||
|
if (len(set(provider_bytes)) != len(provider_bytes)
|
||||||
|
or len(set(consumer_bytes)) != len(consumer_bytes)):
|
||||||
|
return None, '同一家公司在同一身份下只能出现一次'
|
||||||
|
overlap = {c.hex() for c in provider_bytes} & {c.hex() for c in consumer_bytes}
|
||||||
|
if overlap:
|
||||||
|
return None, '同一家公司不能既是提供者又是消费者'
|
||||||
|
if AllowedCompanyUUIDs is not None:
|
||||||
|
allowed = {c.hex() for c in (_ToBytes(x) for x in AllowedCompanyUUIDs) if c}
|
||||||
|
outside = [c for c in (provider_bytes + consumer_bytes) if c.hex() not in allowed]
|
||||||
|
if outside:
|
||||||
|
return None, '存在不在您管理范围内的公司,请重新选择'
|
||||||
|
|
||||||
|
tenant_bytes = _ToBytes(TenantUUID)
|
||||||
|
with transaction.atomic():
|
||||||
|
SubscriptionAppParty.objects.filter(AppUUID=app_bytes).delete()
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
for party_type, values in ((PARTY_PROVIDER, provider_bytes), (PARTY_CONSUMER, consumer_bytes)):
|
||||||
|
for company_bytes in values:
|
||||||
|
SubscriptionAppParty.objects.create(
|
||||||
|
SubscriptionAppPartyUUID=uuid.uuid4().bytes,
|
||||||
|
AppUUID=app_bytes,
|
||||||
|
# 公司所属租户优先;查不到时回退到调用方租户上下文
|
||||||
|
TenantUUID=_ToBytes(SubscriptionManager._CompanyTenant(company_bytes)) or tenant_bytes,
|
||||||
|
CompanyUUID=company_bytes,
|
||||||
|
PartyType=party_type,
|
||||||
|
CreateTime=now,
|
||||||
|
)
|
||||||
|
return SubscriptionManager.ListAppParties(app_bytes), None
|
||||||
@@ -74,3 +74,33 @@ class SubscriptionGroupCompany(QModel):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'{self.SubscriptionGroupUUID} → {self.CompanyUUID}'
|
return f'{self.SubscriptionGroupUUID} → {self.CompanyUUID}'
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionAppParty(QModel):
|
||||||
|
"""应用级订阅方——「订阅式软件提供」用于初始化引用的提供者/消费者公司
|
||||||
|
|
||||||
|
与 SubscriptionGroupCompany(产品级)不同:这里登记的是**应用本身**的两方公司,
|
||||||
|
不分具体产品,作为订阅关系的初始化引用(应用配置页的注入卡片即读写本表)。
|
||||||
|
|
||||||
|
PartyType:1=提供者 / 0=消费者;同一应用同一公司同一身份只允许一条。
|
||||||
|
TenantUUID 冗余存公司所属租户,便于按租户分组展示。
|
||||||
|
"""
|
||||||
|
|
||||||
|
SubscriptionAppPartyUUID = UUIDField(primary_key=True, verbose_name='应用订阅方UUID')
|
||||||
|
AppUUID = UUIDField(db_column='AppUUID', db_index=True, verbose_name='应用UUID')
|
||||||
|
TenantUUID = UUIDField(null=True, blank=True, db_column='TenantUUID', db_index=True, verbose_name='归属租户UUID')
|
||||||
|
CompanyUUID = UUIDField(db_column='CompanyUUID', db_index=True, verbose_name='公司UUID')
|
||||||
|
PartyType = models.SmallIntegerField(default=PARTY_CONSUMER, verbose_name='一方类型(1提供者/0消费者)')
|
||||||
|
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'SubscriptionAppParty'
|
||||||
|
app_label = 'gerp'
|
||||||
|
ordering = ['PartyType', 'CreateTime']
|
||||||
|
# 同一应用内,同一家公司同一身份只允许一条
|
||||||
|
unique_together = (('AppUUID', 'CompanyUUID', 'PartyType'),)
|
||||||
|
verbose_name = '应用订阅方'
|
||||||
|
verbose_name_plural = '应用订阅方'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{self.AppUUID} → {self.CompanyUUID}'
|
||||||
@@ -13,4 +13,7 @@ urlpatterns = [
|
|||||||
path('Group/Delete', views.SubscriptionGroupDeleteView.as_view(), name='subscription_group_delete'),
|
path('Group/Delete', views.SubscriptionGroupDeleteView.as_view(), name='subscription_group_delete'),
|
||||||
path('Relations', views.SubscriptionRelationView.as_view(), name='subscription_relations'),
|
path('Relations', views.SubscriptionRelationView.as_view(), name='subscription_relations'),
|
||||||
path('Companies', views.SubscriptionCompanyView.as_view(), name='subscription_companies'),
|
path('Companies', views.SubscriptionCompanyView.as_view(), name='subscription_companies'),
|
||||||
|
# 应用级订阅方(应用配置页注入卡片)
|
||||||
|
path('AppParties', views.SubscriptionAppPartyView.as_view(), name='subscription_app_parties'),
|
||||||
|
path('AppParty/Save', views.SubscriptionAppPartySaveView.as_view(), name='subscription_app_party_save'),
|
||||||
]
|
]
|
||||||
@@ -6,6 +6,8 @@ URL 路径最后一段作为 action 名称:
|
|||||||
POST /API/Subscription/Group/Delete - 删除订阅组
|
POST /API/Subscription/Group/Delete - 删除订阅组
|
||||||
GET /API/Subscription/Relations - 订阅关系(按公司汇总其提供/消费的产品)
|
GET /API/Subscription/Relations - 订阅关系(按公司汇总其提供/消费的产品)
|
||||||
GET /API/Subscription/Companies - 可选公司列表(当前管理员全部可管理公司,含所属租户)
|
GET /API/Subscription/Companies - 可选公司列表(当前管理员全部可管理公司,含所属租户)
|
||||||
|
GET /API/Subscription/AppParties - 应用级订阅方(应用配置页注入卡片:提供者/消费者公司)
|
||||||
|
POST /API/Subscription/AppParty/Save - 保存应用级订阅方(整体重写)
|
||||||
|
|
||||||
权限:管理员(admin:super / admin:tenant / admin:company)。
|
权限:管理员(admin:super / admin:tenant / admin:company)。
|
||||||
公司可选范围 = AppManager.GetManageableCompanies:
|
公司可选范围 = AppManager.GetManageableCompanies:
|
||||||
@@ -139,3 +141,45 @@ class SubscriptionCompanyView(ELTAuthenticatedAPIView):
|
|||||||
return Response({'code': 403, 'msg': '无权查看公司列表:您不是管理员'}, status=403)
|
return Response({'code': 403, 'msg': '无权查看公司列表:您不是管理员'}, status=403)
|
||||||
rows = SubscriptionManager.CompanyRows(companies)
|
rows = SubscriptionManager.CompanyRows(companies)
|
||||||
return Response({'code': 0, 'data': {'list': rows, 'total': len(rows), 'Scope': scope}})
|
return Response({'code': 0, 'data': {'list': rows, 'total': len(rows), 'Scope': scope}})
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionAppPartyView(ELTAuthenticatedAPIView):
|
||||||
|
"""应用级订阅方(应用本身的两方公司,不分产品)——应用配置页注入卡片读取
|
||||||
|
|
||||||
|
GET /API/Subscription/AppParties?AppUUID=xxx
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
_companies, scope, _owner_tenant, _all_tenants = _ResolveScope(request)
|
||||||
|
if not scope:
|
||||||
|
return Response({'code': 403, 'msg': '无权查看应用订阅方:您不是管理员'}, status=403)
|
||||||
|
app_uuid = (request.query_params.get('AppUUID') or '').strip()
|
||||||
|
if not app_uuid:
|
||||||
|
return Response({'code': 1, 'msg': '缺少应用 UUID'}, status=400)
|
||||||
|
return Response({'code': 0, 'data': SubscriptionManager.ListAppParties(app_uuid)})
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionAppPartySaveView(ELTAuthenticatedAPIView):
|
||||||
|
"""保存应用级订阅方(整体重写)——应用配置页注入卡片保存
|
||||||
|
|
||||||
|
POST /API/Subscription/AppParty/Save {AppUUID, Providers: [], Consumers: []}
|
||||||
|
"""
|
||||||
|
|
||||||
|
permission_classes = [IsAuthenticated, IsAdminUser]
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
companies, scope, owner_tenant, all_tenants = _ResolveScope(request)
|
||||||
|
if not scope:
|
||||||
|
return Response({'code': 403, 'msg': '无权保存应用订阅方:您不是管理员'}, status=403)
|
||||||
|
row, err = SubscriptionManager.SaveAppParties(
|
||||||
|
AppUUID=request.data.get('AppUUID'),
|
||||||
|
TenantUUID=owner_tenant,
|
||||||
|
Data=request.data,
|
||||||
|
# 跨租户管理员可选任意公司;其余限定在其可管理公司内(防越权指定)
|
||||||
|
AllowedCompanyUUIDs=None if all_tenants else companies,
|
||||||
|
)
|
||||||
|
if err:
|
||||||
|
return Response({'code': 1, 'msg': err}, status=400)
|
||||||
|
return Response({'code': 0, 'msg': '保存成功', 'data': row})
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""订阅式软件提供(subscription):应用级订阅方(Application 提供者/消费者公司,用于初始化引用)
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import gvsds.apps.core.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('gerp', '0050_subscription'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='SubscriptionAppParty',
|
||||||
|
fields=[
|
||||||
|
('SubscriptionAppPartyUUID', gvsds.apps.core.models.UUIDField(primary_key=True, serialize=False, verbose_name='应用订阅方UUID')),
|
||||||
|
('AppUUID', gvsds.apps.core.models.UUIDField(db_column='AppUUID', db_index=True, verbose_name='应用UUID')),
|
||||||
|
('TenantUUID', gvsds.apps.core.models.UUIDField(blank=True, db_column='TenantUUID', db_index=True, null=True, verbose_name='归属租户UUID')),
|
||||||
|
('CompanyUUID', gvsds.apps.core.models.UUIDField(db_column='CompanyUUID', db_index=True, verbose_name='公司UUID')),
|
||||||
|
('PartyType', models.SmallIntegerField(default=0, verbose_name='一方类型(1提供者/0消费者)')),
|
||||||
|
('CreateTime', models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'SubscriptionAppParty',
|
||||||
|
'ordering': ['PartyType', 'CreateTime'],
|
||||||
|
'unique_together': {('AppUUID', 'CompanyUUID', 'PartyType')},
|
||||||
|
'verbose_name': '应用订阅方',
|
||||||
|
'verbose_name_plural': '应用订阅方',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in new issue
Block a user