feat(订阅式软件提供): 包详情支持依赖列表与 README,打通产品官网数据链路

GVSDServiceServer:VenomPackage 增 Dependencies(JSON)/Readme(Text) 与迁移 0002,新增按包名取详情接口 /api/venom/project/<包名>/,定制页配置新增依赖编辑器与 README 编辑。

gvsds:新增 /API/Venom/Project/<包名> 公开只读接口,按 PublicSiteHost 反查产品并回源提供者;SubscriptionGroup 增 PublicSiteHost 字段(迁移 0055)。

Main.py:venom 子域加 SPA fallback,/project/<包名>/ 回退 index.html。
This commit is contained in:
TermiNexus committed 2026-10-07 19:35:46 +08:00
1 parent edecc6435a
commit 4bd5697cff
13 files changed
+394 -17

No files matched your search

@@ -0,0 +1,23 @@
# Generated by Django 6.0.5 on 2026-10-07 18:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('venom', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='venompackage',
name='Dependencies',
field=models.JSONField(blank=True, default=list, verbose_name='依赖列表'),
),
migrations.AddField(
model_name='venompackage',
name='Readme',
field=models.TextField(default='', verbose_name='README(Markdown 原文)'),
),
]
+4
View File
@@ -17,6 +17,10 @@ class VenomPackage(models.Model):
PackageName = models.CharField(max_length=128, unique=True, verbose_name='包名(全局唯一)')
PackageDesc = models.CharField(max_length=512, default='', verbose_name='包说明')
ConfigText = models.TextField(default='', verbose_name='包配置内容')
#: 依赖列表:[{'Name': 'xxx', 'Version': '^1.0.0'}];Version 可为空(无版本约束)
Dependencies = models.JSONField(default=list, blank=True, verbose_name='依赖列表')
#: README(Markdown 原文,由官网前端渲染)
Readme = models.TextField(default='', verbose_name='README(Markdown 原文)')
CreatorName = models.CharField(max_length=128, default='', verbose_name='创建人')
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
+2
View File
@@ -11,4 +11,6 @@ urlpatterns = [
path('packages/<uuid:package_uuid>/', views.PackageDetailView.as_view(), name='package_detail'),
path('packages/<uuid:package_uuid>/config/', views.PackageConfigView.as_view(), name='package_config'),
path('packages/<uuid:package_uuid>/releases/', views.PackageReleaseView.as_view(), name='package_releases'),
# 官网按包名取详情(venom.gvsds.com/project/<包名>/)
path('project/<path:package_name>/', views.PackageByNameView.as_view(), name='package_by_name'),
]
+65 -10
View File
@@ -7,6 +7,7 @@
POST /api/venom/packages/<uuid>/config/ 保存包配置
GET /api/venom/packages/<uuid>/releases/ 发布历史
POST /api/venom/packages/<uuid>/releases/ 发布新版本(写历史)
GET /api/venom/project/<包名>/ 按包名取详情(官网调用:含 README 与依赖列表)
"""
import logging
@@ -60,6 +61,42 @@ def _GetPackage(PackageUUID):
PackageUUID=PackageUUID).first()
def _NormalizeDependencies(value):
"""依赖列表标准化为 [{'Name', 'Version'}];非法项丢弃、同名去重、限 200 条
兼容两种入参:字符串(仅包名)与对象 {'Name','Version'}(Version 可为空 = 无版本约束)。
"""
if not isinstance(value, list):
return []
rows, seen = [], set()
for item in value[:200]:
if isinstance(item, str):
name, version = item.strip(), ''
elif isinstance(item, dict):
name = str(item.get('Name') or '').strip()
version = str(item.get('Version') or '').strip()
else:
continue
if not name or name in seen:
continue
seen.add(name)
rows.append({'Name': name[:128], 'Version': version[:64]})
return rows
def _DetailRow(package):
"""包详情行(含 README/依赖/发布历史;官网与配置页共用)"""
releases = list(package.releases.all())
return {
**_PackageRow(package),
'ConfigText': package.ConfigText or '',
'Readme': package.Readme or '',
'Dependencies': package.Dependencies or [],
'LatestVersion': releases[0].Version if releases else '',
'Releases': [_ReleaseRow(r) for r in releases],
}
class PackageListView(APIView):
"""包列表 / 新建包"""
@@ -83,26 +120,26 @@ class PackageListView(APIView):
PackageName=name,
PackageDesc=(request.data.get('PackageDesc') or '').strip()[:512],
ConfigText=request.data.get('ConfigText') or '',
Dependencies=_NormalizeDependencies(request.data.get('Dependencies')),
Readme=request.data.get('Readme') or '',
CreatorName=(request.data.get('CreatorName') or '').strip()[:128],
)
package = _GetPackage(package.PackageUUID)
return _ok(_PackageRow(package), msg='软件包已创建')
return _ok(_DetailRow(package), msg='软件包已创建')
class PackageDetailView(APIView):
"""包详情(配置页读取)"""
"""包详情(配置页读取;含 README / 依赖列表 / 发布历史)"""
def get(self, request, package_uuid):
package = _GetPackage(package_uuid)
if package is None:
return _err('软件包不存在', 404)
row = _PackageRow(package)
row['ConfigText'] = package.ConfigText or ''
return _ok(row)
return _ok(_DetailRow(package))
class PackageConfigView(APIView):
"""保存包配置"""
"""保存包配置(说明 / 配置内容 / 依赖列表 / README)"""
def post(self, request, package_uuid):
package = VenomPackage.objects.filter(PackageUUID=package_uuid).first()
@@ -112,10 +149,13 @@ class PackageConfigView(APIView):
package.PackageDesc = (request.data.get('PackageDesc') or '').strip()[:512]
if 'ConfigText' in request.data:
package.ConfigText = request.data.get('ConfigText') or ''
package.save(update_fields=['PackageDesc', 'ConfigText', 'UpdateTime'])
row = _PackageRow(_GetPackage(package.PackageUUID))
row['ConfigText'] = package.ConfigText or ''
return _ok(row, msg='配置已保存')
if 'Dependencies' in request.data:
package.Dependencies = _NormalizeDependencies(request.data.get('Dependencies'))
if 'Readme' in request.data:
package.Readme = request.data.get('Readme') or ''
package.save(update_fields=[
'PackageDesc', 'ConfigText', 'Dependencies', 'Readme', 'UpdateTime'])
return _ok(_DetailRow(_GetPackage(package.PackageUUID)), msg='配置已保存')
class PackageReleaseView(APIView):
@@ -144,3 +184,18 @@ class PackageReleaseView(APIView):
PublisherName=(request.data.get('PublisherName') or '').strip()[:128],
)
return _ok(_ReleaseRow(release), msg='已发布')
class PackageByNameView(APIView):
"""按包名取包详情——官网 `venom.gvsds.com/project/<包名>/` 的数据源
GET /api/venom/project/<PackageName>/
"""
def get(self, request, package_name):
name = str(package_name or '').strip().strip('/')
package = VenomPackage.objects.annotate(release_count=Count('releases')).filter(
PackageName=name).first()
if package is None:
return _err(f'包「{name}」不存在', 404, status=404)
return _ok(_DetailRow(package))
+130 -3
View File
@@ -15,6 +15,7 @@ import {
Box,
Clock,
Collection,
Delete,
Finished,
Plus,
Promotion,
@@ -25,6 +26,7 @@ import {
import {
venomApi,
type RemoteContext,
type VenomDependency,
type VenomPackageItem,
type VenomReleaseItem,
} from './api'
@@ -45,11 +47,40 @@ const detail = ref<VenomPackageItem | null>(null)
const releases = ref<VenomReleaseItem[]>([])
const dialogVisible = ref(false)
const createForm = reactive({ PackageName: '', PackageDesc: '', ConfigText: '' })
const createForm = reactive({
PackageName: '',
PackageDesc: '',
ConfigText: '',
Readme: '',
Dependencies: [] as VenomDependency[],
})
const configForm = reactive({ PackageDesc: '', ConfigText: '' })
const configForm = reactive({
PackageDesc: '',
ConfigText: '',
Readme: '',
Dependencies: [] as VenomDependency[],
})
const releaseForm = reactive({ Version: '', Notes: '' })
/** 空依赖行(依赖编辑器用) */
function EmptyDependency(): VenomDependency {
return { Name: '', Version: '' }
}
/** 提交前清洗依赖:丢弃空包名、去空白、保序去重 */
function CleanDependencies(rows: VenomDependency[]): VenomDependency[] {
const seen = new Set<string>()
const result: VenomDependency[] = []
for (const row of rows) {
const name = (row.Name || '').trim()
if (!name || seen.has(name)) continue
seen.add(name)
result.push({ Name: name, Version: (row.Version || '').trim() })
}
return result
}
/** 全部软件包的发布次数合计(列表概览用) */
const totalReleases = computed(() =>
packages.value.reduce((sum, p) => sum + (p.ReleaseCount || 0), 0),
@@ -71,6 +102,8 @@ function openCreate(): void {
createForm.PackageName = ''
createForm.PackageDesc = ''
createForm.ConfigText = ''
createForm.Readme = ''
createForm.Dependencies = [EmptyDependency()]
dialogVisible.value = true
}
@@ -85,6 +118,8 @@ async function submitCreate(): Promise<void> {
PackageName: createForm.PackageName.trim(),
PackageDesc: createForm.PackageDesc,
ConfigText: createForm.ConfigText,
Readme: createForm.Readme,
Dependencies: CleanDependencies(createForm.Dependencies),
CreatorName: operatorName.value,
})
ElMessage.success('软件包已创建')
@@ -109,6 +144,11 @@ async function openConfig(row: VenomPackageItem): Promise<void> {
detail.value = d
configForm.PackageDesc = d.PackageDesc || ''
configForm.ConfigText = d.ConfigText || ''
configForm.Readme = d.Readme || ''
configForm.Dependencies = (d.Dependencies || []).map((dep) => ({
Name: dep.Name || '',
Version: dep.Version || '',
}))
releases.value = r.list || []
releaseForm.Version = ''
releaseForm.Notes = ''
@@ -126,6 +166,17 @@ function backToList(): void {
releases.value = []
}
/** 依赖编辑器:追加空行 */
function addDependency(rows: VenomDependency[]): void {
rows.push(EmptyDependency())
}
/** 依赖编辑器:删除指定行(至少保留一行占位) */
function removeDependency(rows: VenomDependency[], index: number): void {
rows.splice(index, 1)
if (!rows.length) rows.push(EmptyDependency())
}
async function saveConfig(): Promise<void> {
if (!detail.value) return
saving.value = true
@@ -133,6 +184,8 @@ async function saveConfig(): Promise<void> {
detail.value = await venomApi.saveConfig(detail.value.PackageUUID, {
PackageDesc: configForm.PackageDesc,
ConfigText: configForm.ConfigText,
Readme: configForm.Readme,
Dependencies: CleanDependencies(configForm.Dependencies),
})
ElMessage.success('配置已保存')
await loadPackages()
@@ -299,6 +352,32 @@ onMounted(loadPackages)
placeholder="包配置内容(如依赖、构建参数等)"
/>
</el-form-item>
<el-form-item label="依赖列表">
<div class="dep-editor">
<div v-for="(dep, index) in configForm.Dependencies" :key="index" class="dep-row">
<el-input v-model="dep.Name" placeholder="依赖包名" class="dep-name" />
<el-input v-model="dep.Version" placeholder="版本约束,如 ^1.0.0(可留空)" class="dep-version" />
<el-button
:icon="Delete"
circle
plain
type="danger"
@click="removeDependency(configForm.Dependencies, index)"
/>
</div>
<el-button :icon="Plus" text type="primary" @click="addDependency(configForm.Dependencies)">
添加依赖
</el-button>
</div>
</el-form-item>
<el-form-item label="README(Markdown 原文,官网渲染)">
<el-input
v-model="configForm.Readme"
type="textarea"
:rows="10"
placeholder="# 标题&#10;&#10;支持 Markdown 语法,将展示在 venom.gvsds.com/project/<包名>/"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" round :loading="saving" @click="saveConfig">保存配置</el-button>
</el-form-item>
@@ -359,7 +438,13 @@ onMounted(loadPackages)
</template>
<!-- ==================== 新建软件包 ==================== -->
<el-dialog v-model="dialogVisible" title="新建软件包" width="620px" :close-on-click-modal="false">
<el-dialog
v-model="dialogVisible"
title="新建软件包"
width="720px"
top="6vh"
:close-on-click-modal="false"
>
<el-form label-position="top" class="form">
<el-form-item label="包名" required>
<el-input v-model="createForm.PackageName" maxlength="128" placeholder="包名必须全局唯一" />
@@ -370,6 +455,27 @@ onMounted(loadPackages)
<el-form-item label="配置内容">
<el-input v-model="createForm.ConfigText" type="textarea" :rows="5" placeholder="可选" />
</el-form-item>
<el-form-item label="依赖列表">
<div class="dep-editor">
<div v-for="(dep, index) in createForm.Dependencies" :key="index" class="dep-row">
<el-input v-model="dep.Name" placeholder="依赖包名" class="dep-name" />
<el-input v-model="dep.Version" placeholder="版本约束,如 ^1.0.0(可留空)" class="dep-version" />
<el-button
:icon="Delete"
circle
plain
type="danger"
@click="removeDependency(createForm.Dependencies, index)"
/>
</div>
<el-button :icon="Plus" text type="primary" @click="addDependency(createForm.Dependencies)">
添加依赖
</el-button>
</div>
</el-form-item>
<el-form-item label="README(Markdown 原文,可选)">
<el-input v-model="createForm.Readme" type="textarea" :rows="6" placeholder="包文档,将展示在官网站点" />
</el-form-item>
</el-form>
<template #footer>
<el-button round @click="dialogVisible = false">取消</el-button>
@@ -560,4 +666,25 @@ onMounted(loadPackages)
.form {
max-width: 100%;
}
/* ==== 依赖编辑器 ==== */
.dep-editor {
width: 100%;
display: flex;
flex-direction: column;
gap: 8px;
}
.dep-row {
display: flex;
align-items: center;
gap: 8px;
}
.dep-name {
flex: 1 1 42%;
min-width: 0;
}
.dep-version {
flex: 1 1 58%;
min-width: 0;
}
</style>
+22 -2
View File
@@ -27,6 +27,12 @@ const API_BASE = (() => {
}
})()
/** 依赖项:包名 + 版本约束(Version 为空 = 无约束) */
export interface VenomDependency {
Name: string
Version: string
}
export interface VenomPackageItem {
PackageUUID: string
PackageName: string
@@ -36,6 +42,10 @@ export interface VenomPackageItem {
UpdateTime: string
ReleaseCount: number
ConfigText?: string
/** 依赖列表(详情接口返回) */
Dependencies?: VenomDependency[]
/** README(Markdown 原文,官网渲染) */
Readme?: string
}
/** 宿主注入的上下文(当前登录人/公司,用于「创建人」「发布人」) */
@@ -72,16 +82,26 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
return payload.data as T
}
/** 可写的包配置字段(新建/保存共用) */
export interface VenomPackageConfig {
PackageDesc: string
ConfigText: string
/** 依赖列表(名称 + 版本约束) */
Dependencies: VenomDependency[]
/** README(Markdown 原文) */
Readme: string
}
export const venomApi = {
listPackages: (keyword = '') =>
request<{ list: VenomPackageItem[]; total: number }>(
'GET',
`/packages/${keyword ? `?Keyword=${encodeURIComponent(keyword)}` : ''}`,
),
createPackage: (data: { PackageName: string; PackageDesc: string; ConfigText: string; CreatorName: string }) =>
createPackage: (data: VenomPackageConfig & { PackageName: string; CreatorName: string }) =>
request<VenomPackageItem>('POST', '/packages/', data),
getPackage: (uuid: string) => request<VenomPackageItem>('GET', `/packages/${uuid}/`),
saveConfig: (uuid: string, data: { PackageDesc: string; ConfigText: string }) =>
saveConfig: (uuid: string, data: VenomPackageConfig) =>
request<VenomPackageItem>('POST', `/packages/${uuid}/config/`, data),
listReleases: (uuid: string) =>
request<{ PackageName: string; list: VenomReleaseItem[]; total: number }>(
+10
View File
@@ -74,6 +74,10 @@ STATIC_SITES = {
"venom.gvsds.com": Path("TempLate") / "Venom",
}
# 单页应用站点:命中不到文件时回退 index.html,交由前端路由处理
# (如 venom 官网的 /project/<包名>/ 详情页;带扩展名的资源请求仍按 404 处理)
SPA_STATIC_SITES = {"venom.gvsds.com"}
LOG_CONFIG = {
'disable_static_success': False,
'disable_favicon': False,
@@ -458,6 +462,12 @@ class SecurityMiddleware:
# 目录则尝试 index.html
if target.is_dir():
target = target / 'index.html'
# SPA 兜底:客户端路由(无扩展名,如 /project/<包名>/)回退站点入口
if not target.is_file() and host in SPA_STATIC_SITES \
and '.' not in os.path.basename(rel_path):
spa_index = site_root / 'index.html'
if spa_index.is_file():
target = spa_index
if not target.is_file():
start_response('404 Not Found', [('Content-Type', 'text/html')])
try:
+33 -2
View File
@@ -74,6 +74,19 @@ def _ToStr(value):
return str(value)
def _NormalizeHost(value):
"""域名归一化:去掉 scheme / 路径 / 端口并转小写(用于按 Host 反查产品)"""
text = str(value or '').strip().lower()
if not text:
return ''
if '//' in text:
text = text.split('//', 1)[1]
text = text.split('/', 1)[0]
if ':' in text:
text = text.split(':', 1)[0]
return text
class SubscriptionManager:
"""订阅组(产品)与组成员公司"""
@@ -564,12 +577,15 @@ class SubscriptionManager:
# ==================== 产品定制页(提供者声明) + 我的订阅 ====================
@staticmethod
def SetGroupRemotePage(GroupUUID, RemoteEntry, RemoteModule='default', TenantUUID=None):
def SetGroupRemotePage(GroupUUID, RemoteEntry, RemoteModule='default',
PublicSiteHost=None, TenantUUID=None):
"""提供者子服务器声明本产品定制页的远端入口(幂等覆盖)
调用方已通过订阅访问令牌鉴权(令牌里的 SoftwareUUID 即 GroupUUID),
因此这里只做范围与格式校验。
PublicSiteHost:产品官网域名(如 venom.gvsds.com),传 None 表示不改动。
Returns:
(dict, error):{'GroupUUID', 'GroupName', 'RemoteEntry', 'RemoteModule'}
"""
@@ -584,7 +600,9 @@ class SubscriptionManager:
module_name = str(RemoteModule or 'default').strip()[:64] or 'default'
group.RemoteEntry = entry
group.RemoteModule = module_name
group.save(update_fields=['RemoteEntry', 'RemoteModule'])
if PublicSiteHost is not None:
group.PublicSiteHost = _NormalizeHost(PublicSiteHost)[:128]
group.save(update_fields=['RemoteEntry', 'RemoteModule', 'PublicSiteHost'])
# (重新)声明即失效本产品的资源缓存:入口 URL 通常不含内容版本号(如
# /venom/remote-entry.js),若不主动清理,主服务在 TTL 内会一直回旧内容;
# 清理后下一次请求立即回源,配合 SHA256 只换来真正变化的文件。
@@ -601,6 +619,7 @@ class SubscriptionManager:
'GroupName': group.GroupName,
'RemoteEntry': group.RemoteEntry,
'RemoteModule': group.RemoteModule,
'PublicSiteHost': group.PublicSiteHost or '',
'PurgedAssets': purged,
'WarmError': warm_err or '',
}, None
@@ -659,6 +678,18 @@ class SubscriptionManager:
return None, '该产品未声明有效的子服务器远端入口'
return origin, None
@staticmethod
def GroupByPublicHost(Host):
"""按产品官网域名反查订阅组(提供者在声明定制页时登记的 PublicSiteHost)
用途:主服务承接产品官网域名(如 venom.gvsds.com)下的 `/API/...` 读取请求时,
据此确定「是哪个产品」,再回源到对应的提供者子服务器。
"""
host = _NormalizeHost(Host)
if not host:
return None
return SubscriptionGroup.objects.filter(PublicSiteHost=host).first()
@staticmethod
def FetchRemoteAsset(GroupUUID, AssetPath):
"""取远端资源:缓存内且未过期直接命中;否则回源并按 SHA256 判断是否变更
@@ -37,6 +37,10 @@ class SubscriptionGroup(QModel):
# 宿主前端按 RemoteEntry 运行时加载远端组件并原生渲染(微前端,非 iframe)。
RemoteEntry = models.CharField(max_length=512, default='', verbose_name='定制页远端入口URL')
RemoteModule = models.CharField(max_length=64, default='default', verbose_name='定制页远端导出名')
#: 产品官网域名(提供者声明,如 venom.gvsds.com):主服务据此把该域名的
#: `/API/...` 读取请求反查到本产品(见 venom_site.GroupByPublicHost)。
PublicSiteHost = models.CharField(max_length=128, default='', db_index=True,
verbose_name='产品官网域名')
CreatorUUID = UUIDField(null=True, blank=True, db_column='CreatorUUID', verbose_name='创建人UUID')
CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, verbose_name='创建时间')
@@ -0,0 +1,77 @@
"""模块:Venom 产品官网的公开只读接口(`venom.gvsds.com/API/Venom/*`)
定位:包数据由**提供者子服务器(GVSDServiceServer)**存储,主服务只负责「读」——
按请求域名反查产品(`SubscriptionGroup.PublicSiteHost`),再回源到该产品的子服务器。
挂载:`gvsds/urls.py` → `path('API/Venom/', include('gvsds.apps.gerp.apps.subscription.venom_site'))`
GET /API/Venom/Project/<包名> 包详情(含 README / 依赖列表 / 发布历史)
"""
import logging
from urllib.parse import quote
import requests
from django.urls import path
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from .manager import SubscriptionManager
logger = logging.getLogger(__name__)
#: 子服务器上 Venom 包接口的路径前缀
PROVIDER_API_PREFIX = '/api/venom'
#: 回源超时(秒)
PROVIDER_TIMEOUT = 15
class VenomProjectView(APIView):
"""包详情——官网 `venom.gvsds.com/project/<包名>/` 的数据源
GET /API/Venom/Project/<PackageName>
免登录:官网对所有人开放。
"""
authentication_classes = []
permission_classes = [AllowAny]
def get(self, request, package_name):
host = request.get_host()
group = SubscriptionManager.GroupByPublicHost(host)
if group is None:
return Response(
{'code': 1, 'msg': f'域名 {host} 未登记为任何产品的官网'}, status=404)
origin = SubscriptionManager.EntryOrigin(group.RemoteEntry)
if origin is None:
return Response({'code': 1, 'msg': '该产品未声明有效的子服务器远端入口'}, status=502)
name = str(package_name or '').strip().strip('/')
if not name:
return Response({'code': 1, 'msg': '缺少包名'}, status=400)
url = f'{origin}{PROVIDER_API_PREFIX}/project/{quote(name)}/'
try:
upstream = requests.get(url, timeout=PROVIDER_TIMEOUT)
except Exception as exc:
logger.error('Venom 读取提供者失败: %s (%s)', url, exc)
return Response({'code': 1, 'msg': f'读取提供者失败:{exc}'}, status=502)
if upstream.status_code == 404:
return Response({'code': 1, 'msg': f'包「{name}」不存在'}, status=404)
if upstream.status_code != 200:
logger.warning('Venom 提供者返回 %s: %s', upstream.status_code, url)
return Response({'code': 1, 'msg': f'提供者返回 {upstream.status_code}'}, status=502)
try:
payload = upstream.json()
except Exception:
return Response({'code': 1, 'msg': '提供者响应不是 JSON'}, status=502)
return Response({'code': 0, 'data': payload.get('data')})
urlpatterns = [
path('Project/<path:package_name>', VenomProjectView.as_view(), name='venom_public_project'),
]
@@ -420,6 +420,8 @@ class SubscriptionProviderPageSaveView(ELTAuthenticatedAPIView):
getattr(principal, 'GroupUUID', None),
request.data.get('RemoteEntry'),
RemoteModule=request.data.get('RemoteModule') or 'default',
# 可选:产品官网域名(如 venom.gvsds.com),供主服务承接该域名下的读取请求
PublicSiteHost=request.data.get('PublicSiteHost'),
)
if err:
return Response({'code': 1, 'msg': err}, status=400)
@@ -0,0 +1,19 @@
"""订阅式软件提供(subscription):产品官网域名(SubscriptionGroup.PublicSiteHost)
"""
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gerp', '0054_subscription_remote_asset'),
]
operations = [
migrations.AddField(
model_name='subscriptiongroup',
name='PublicSiteHost',
field=models.CharField(
db_index=True, default='', max_length=128, verbose_name='产品官网域名'),
),
]
+3
View File
@@ -210,6 +210,9 @@ urlpatterns = [
# 订阅式软件提供(订阅组=产品:提供者/消费者公司)——路由在子应用内(apps/subscription/urls.py)
path('API/Subscription/', include('gvsds.apps.gerp.apps.subscription.urls')),
# Venom 产品官网(venom.gvsds.com)的公开只读接口——按域名反查产品并回源子服务器
path('API/Venom/', include('gvsds.apps.gerp.apps.subscription.venom_site')),
# GERP系统 运维管理(仅系统管理员 admin:super)
path('API/GERPSystem/Status', gerp_views.GERPSystemStatusView.as_view(), name='gerp_system_status'),
path('API/GERPSystem/Schedules', gerp_views.GERPSystemSchedulesView.as_view(), name='gerp_system_schedules'),