feat: 授权管理页面+路由,IM状态30s轮询,移除接入文档,ops去除appKey/平台列,Jenkinsfile固定main分支

这个提交包含在:
Dev 2026-05-16 11:31:21 +08:00
父节点 b153521a91
当前提交 88e5a70d87
共有 8 个文件被更改,包括 183 次插入63 次删除

20
Jenkinsfile vendored
查看文件

@ -2,7 +2,6 @@ pipeline {
agent any
parameters {
string(name: 'BRANCH', defaultValue: 'main', description: 'Git 分支名')
choice(name: 'APP', choices: ['tenant-platform', 'ops-platform'], description: '要构建的 Web 应用')
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag')
booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署')
@ -18,12 +17,19 @@ pipeline {
DOCKER_BUILDKIT = '1'
}
options {
timeout(time: 20, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps {
checkout([$class: 'GitSCM',
branches: [[name: "*/${params.BRANCH}"]],
extensions: [],
checkout([
$class: 'GitSCM',
branches: [[name: 'main']],
extensions: [[$class: 'CleanBeforeCheckout']],
userRemoteConfigs: scm.userRemoteConfigs
])
}
@ -59,8 +65,8 @@ pipeline {
def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}"
bat """
docker login ${env.ACR_REGISTRY} -u ${env.ACR_USERNAME} -p %ACR_PASS%
docker pull ${fullImage} || exit 0
docker build -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${fullImage} -t ${fullImage} .
docker pull --platform=linux/amd64 ${fullImage} || echo Pull failed, will build fresh
docker build --platform=linux/amd64 -f ${env.DOCKERFILE} ${env.BUILD_ARGS} --build-arg BUILDKIT_INLINE_CACHE=1 --cache-from ${fullImage} -t ${fullImage} .
docker push ${fullImage}
docker rmi ${fullImage} || exit 0
"""
@ -76,7 +82,7 @@ pipeline {
script {
def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}"
bat """
ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps ${env.DEPLOY_SERVICE} && docker image prune -f"
ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps --force-recreate ${env.DEPLOY_SERVICE} && docker image prune -f"
"""
}
}

查看文件

@ -2,7 +2,6 @@ pipeline {
agent any
parameters {
string(name: 'BRANCH', defaultValue: 'main', description: 'Git 分支名')
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag')
booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署')
}
@ -21,9 +20,22 @@ pipeline {
BUILD_ARGS = '--build-arg OPS_APP_BASE=/ --build-arg OPS_API_BASE_URL=/api'
}
options {
timeout(time: 20, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps { checkout scm }
steps {
checkout([
$class: 'GitSCM',
branches: [[name: 'main']],
extensions: [[$class: 'CleanBeforeCheckout']],
userRemoteConfigs: scm.userRemoteConfigs
])
}
}
stage('Docker Build & Push') {
@ -50,7 +62,7 @@ pipeline {
script {
def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}"
bat """
ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps ${env.DEPLOY_SERVICE} && docker image prune -f"
ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps --force-recreate ${env.DEPLOY_SERVICE} && docker image prune -f"
"""
}
}

查看文件

@ -2,7 +2,6 @@ pipeline {
agent any
parameters {
string(name: 'BRANCH', defaultValue: 'main', description: 'Git 分支名')
string(name: 'IMAGE_TAG', defaultValue: 'latest', description: '镜像 Tag')
booleanParam(name: 'DEPLOY', defaultValue: true, description: '构建后是否自动部署')
}
@ -21,9 +20,22 @@ pipeline {
BUILD_ARGS = '--build-arg TENANT_APP_BASE=/ --build-arg TENANT_API_BASE_URL=/api'
}
options {
timeout(time: 20, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps { checkout scm }
steps {
checkout([
$class: 'GitSCM',
branches: [[name: 'main']],
extensions: [[$class: 'CleanBeforeCheckout']],
userRemoteConfigs: scm.userRemoteConfigs
])
}
}
stage('Docker Build & Push') {
@ -50,7 +62,7 @@ pipeline {
script {
def fullImage = "${env.ACR_REGISTRY}/${env.ACR_NAMESPACE}/${env.IMAGE_NAME}:${params.IMAGE_TAG}"
bat """
ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps ${env.DEPLOY_SERVICE} && docker image prune -f"
ssh -i "%SSH_KEY%" -o StrictHostKeyChecking=no ${env.PROD_USER}@${env.PROD_HOST} "docker pull ${fullImage} && docker compose -f ${env.COMPOSE_FILE} up -d --no-deps --force-recreate ${env.DEPLOY_SERVICE} && docker image prune -f"
"""
}
}

查看文件

@ -10,7 +10,6 @@
</div>
<el-table :data="requests" v-loading="loading">
<el-table-column prop="appKey" label="AppKey" width="180" />
<el-table-column label="租户信息" min-width="220" show-overflow-tooltip>
<template #default="{ row }">
<div>
@ -27,11 +26,6 @@
</div>
</template>
</el-table-column>
<el-table-column prop="platform" label="平台" width="100">
<template #default="{ row }">
<el-tag size="small">{{ row.platform }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="serviceType" label="服务类型" width="100">
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.serviceType }}</el-tag>

查看文件

@ -97,6 +97,10 @@ const router = createRouter({
path: 'services/update/:appKey?',
component: () => import('@/views/update/VersionManagementView.vue'),
},
{
path: 'services/license/:appKey?',
component: () => import('@/views/license/LicenseManagementView.vue'),
},
{
path: 'accounts',
component: () => import('@/views/accounts/SubAccountView.vue'),

查看文件

@ -814,7 +814,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { appApi, type App } from '@/api/app'
import { ElMessage, ElMessageBox } from 'element-plus'
@ -850,6 +850,14 @@ const checkingService = ref(false)
const showActivationDialog = ref(false)
const activationReason = ref('')
const submittingActivation = ref(false)
let pollingTimer: ReturnType<typeof setInterval> | null = null
function clearPolling() {
if (pollingTimer !== null) {
clearInterval(pollingTimer)
pollingTimer = null
}
}
async function checkServiceEnabled(key: string) {
checkingService.value = true
@ -859,8 +867,21 @@ async function checkServiceEnabled(key: string) {
const enabled = res.data.data.some(s => s.serviceType === 'IM' && s.enabled)
serviceEnabled.value = enabled
if (enabled) {
clearPolling()
loadStats()
loadUsers()
} else if (pollingTimer === null) {
pollingTimer = setInterval(async () => {
const currentKey = appKey.value
if (!currentKey) { clearPolling(); return }
const r = await appApi.getServices(currentKey).catch(() => null)
if (r && r.data.data.some(s => s.serviceType === 'IM' && s.enabled)) {
serviceEnabled.value = true
clearPolling()
loadStats()
loadUsers()
}
}, 30000)
}
} catch {
serviceEnabled.value = false
@ -1976,8 +1997,10 @@ function handleOperationLogPageChange(page: number) {
}
watch(appKey, (key) => {
if (isServicesPortal.value && key) {
checkServiceEnabled(key)
if (isServicesPortal.value) {
clearPolling()
serviceEnabled.value = null
if (key) checkServiceEnabled(key)
}
})
@ -1992,6 +2015,10 @@ onMounted(() => {
loadUsers()
})
onBeforeUnmount(() => {
clearPolling()
})
function logActionLabel(action: string) {
return {
REGISTER_USER: '注册用户',

查看文件

@ -20,9 +20,9 @@
<el-menu-item index="/services/im"><el-icon><ChatDotRound /></el-icon><span></span></el-menu-item>
<el-menu-item index="/services/push"><el-icon><Bell /></el-icon><span>线</span></el-menu-item>
<el-menu-item index="/services/update"><el-icon><Upload /></el-icon><span></span></el-menu-item>
<el-menu-item index="/services/license"><el-icon><Key /></el-icon><span></span></el-menu-item>
</el-sub-menu>
<el-menu-item index="/security"><el-icon><Lock /></el-icon><span></span></el-menu-item>
<el-menu-item index="/docs"><el-icon><Document /></el-icon><span></span></el-menu-item>
<el-menu-item index="/operation-logs"><el-icon><Document /></el-icon><span></span></el-menu-item>
<el-menu-item v-if="auth.user?.type === 'MAIN'" index="/accounts"><el-icon><User /></el-icon><span></span></el-menu-item>
</el-menu>
@ -55,9 +55,9 @@
<el-menu-item index="/services/im"><el-icon><ChatDotRound /></el-icon><span></span></el-menu-item>
<el-menu-item index="/services/push"><el-icon><Bell /></el-icon><span>线</span></el-menu-item>
<el-menu-item index="/services/update"><el-icon><Upload /></el-icon><span></span></el-menu-item>
<el-menu-item index="/services/license"><el-icon><Key /></el-icon><span></span></el-menu-item>
</el-sub-menu>
<el-menu-item index="/security"><el-icon><Lock /></el-icon><span></span></el-menu-item>
<el-menu-item index="/docs"><el-icon><Document /></el-icon><span></span></el-menu-item>
<el-menu-item index="/operation-logs"><el-icon><Document /></el-icon><span></span></el-menu-item>
<el-menu-item v-if="auth.user?.type === 'MAIN'" index="/accounts"><el-icon><User /></el-icon><span></span></el-menu-item>
</el-menu>
@ -106,7 +106,7 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useRoute, useRouter } from 'vue-router'
import { Bell, ChatDotRound, Document, Grid, List, Lock, Menu, Odometer, Upload, User } from '@element-plus/icons-vue'
import { Bell, ChatDotRound, Document, Grid, Key, List, Lock, Menu, Odometer, Upload, User } from '@element-plus/icons-vue'
const auth = useAuthStore()
const route = useRoute()

查看文件

@ -9,7 +9,27 @@
<el-page-header v-else @back="$router.back()" :content="`授权管理 - ${appName}`" style="margin-bottom:20px" />
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
<template v-if="!isServicesPortal || appKey">
<div v-if="isServicesPortal && appKey && checkingService" v-loading="true" style="min-height:200px" />
<template v-if="isServicesPortal && appKey && serviceEnabled === false">
<el-empty :image-size="80" description="当前应用未开通授权管理服务" style="margin-top:60px">
<el-button type="primary" @click="showActivationDialog = true">申请开通</el-button>
</el-empty>
<el-dialog v-model="showActivationDialog" title="申请开通授权管理" width="460px">
<el-form label-width="80px">
<el-form-item label="服务">授权管理</el-form-item>
<el-form-item label="申请理由">
<el-input v-model="activationReason" type="textarea" :rows="3" placeholder="请描述您的业务场景" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showActivationDialog = false">取消</el-button>
<el-button type="primary" :loading="submittingActivation" @click="submitActivation">提交申请</el-button>
</template>
</el-dialog>
</template>
<template v-if="!isServicesPortal || (appKey && serviceEnabled === true)">
<el-row :gutter="16" class="stat-grid">
<el-col :xs="24" :sm="12" :md="6" v-for="item in statCards" :key="item.label">
<el-card shadow="never">
@ -111,9 +131,15 @@ const route = useRoute()
const router = useRouter()
const isMobile = ref(false)
const appKey = computed(() => route.query.appKey as string || route.params.appKey as string)
const appKey = computed(() => (route.params.appKey as string) || (route.query.appKey as string) || '')
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
const serviceEnabled = ref<boolean | null>(null)
const checkingService = ref(false)
const showActivationDialog = ref(false)
const activationReason = ref('')
const submittingActivation = ref(false)
const portalApps = ref<App[]>([])
const currentApp = ref<App | null>(null)
const license = ref<AppLicense | null>(null)
@ -189,8 +215,41 @@ function parseFilename(disposition?: string) {
return plain ? decodeURIComponent(plain) : null
}
async function checkServiceEnabled(key: string) {
checkingService.value = true
serviceEnabled.value = null
try {
const res = await appApi.getServices(key)
const enabled = res.data.data.some(s => s.serviceType === 'LICENSE' && s.enabled)
serviceEnabled.value = enabled
if (enabled) loadData()
} catch {
serviceEnabled.value = false
} finally {
checkingService.value = false
}
}
async function submitActivation() {
if (!activationReason.value.trim()) {
ElMessage.warning('请填写申请理由')
return
}
submittingActivation.value = true
try {
await appApi.requestActivation(appKey.value, 'LICENSE', activationReason.value.trim())
ElMessage.success('申请已提交,等待运营审核')
showActivationDialog.value = false
activationReason.value = ''
} catch {
ElMessage.error('提交失败')
} finally {
submittingActivation.value = false
}
}
function switchApp(key: string) {
router.push({ path: route.path, query: { ...route.query, appKey: key } })
router.push(`/services/license/${key}`)
}
function formatDate(d: string | number) {
@ -202,20 +261,26 @@ function updateViewport() {
isMobile.value = window.innerWidth < 768
}
watch(appKey, () => {
watch(appKey, (key) => {
if (isServicesPortal.value) {
if (key) checkServiceEnabled(key)
} else {
loadData()
}
})
onMounted(() => {
updateViewport()
window.addEventListener('resize', updateViewport)
loadData()
if (isServicesPortal.value) {
appApi.list().then(res => {
portalApps.value = res.data.data || []
currentApp.value = portalApps.value.find(item => item.appKey === appKey.value) ?? currentApp.value
})
if (appKey.value) checkServiceEnabled(appKey.value)
return
}
loadData()
})
onBeforeUnmount(() => {