From 92eb1ed539a2f2a3e59532d3b4a2a2c1b2122221 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Thu, 30 Apr 2026 11:47:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20=E6=B7=BB=E5=8A=A0=E7=94=9F?= =?UTF-8?q?=E4=BA=A7=E7=8E=AF=E5=A2=83=E9=83=A8=E7=BD=B2=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=92=8C=E8=81=94=E8=B0=83=E7=8E=AF=E5=A2=83=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 .env.production.example 配置文件,包含所有微服务的数据库和Redis配置 - 添加 compose.production.yaml Docker Compose部署文件,定义web和各服务容器 - 实现Android SDK环境切换功能,支持外部服务和本地联调模式切换 - 添加推送注册状态管理和接收开关设置界面 - 集成演示服务的应用密钥客户端和认证服务实现 - 完善文档说明各SDK模块的集成和使用方法 --- Jenkinsfile | 1 + ops-platform/vite.config.ts | 2 +- tenant-platform/components.d.ts | 1 + tenant-platform/src/api/app.ts | 15 +- tenant-platform/src/api/dashboard.ts | 11 + tenant-platform/src/api/file.ts | 52 +++- tenant-platform/src/api/im.ts | 41 ++- tenant-platform/src/api/update.ts | 31 +- .../src/views/dashboard/DashboardView.vue | 17 +- .../src/views/logs/OperationLogView.vue | 283 +++++++++++++----- .../views/update/VersionManagementView.vue | 83 ++++- tenant-platform/vite.config.ts | 2 +- 12 files changed, 428 insertions(+), 111 deletions(-) create mode 100644 tenant-platform/src/api/dashboard.ts diff --git a/Jenkinsfile b/Jenkinsfile index 1eb40c8..bc844c5 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,6 +2,7 @@ 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: '构建后是否自动部署') } diff --git a/ops-platform/vite.config.ts b/ops-platform/vite.config.ts index ca0ef5c..d513d99 100644 --- a/ops-platform/vite.config.ts +++ b/ops-platform/vite.config.ts @@ -36,7 +36,7 @@ export default defineConfig(({ mode }) => { server: { port: 5174, proxy: { - '/api': { target: 'http://192.168.116.9:8081', changeOrigin: true }, + '/api': { target: 'http://127.0.0.1:8081', changeOrigin: true }, }, }, } diff --git a/tenant-platform/components.d.ts b/tenant-platform/components.d.ts index 6e2bb39..756ae21 100644 --- a/tenant-platform/components.d.ts +++ b/tenant-platform/components.d.ts @@ -42,6 +42,7 @@ declare module 'vue' { ElOption: typeof import('element-plus/es')['ElOption'] ElPageHeader: typeof import('element-plus/es')['ElPageHeader'] ElPagination: typeof import('element-plus/es')['ElPagination'] + ElProgress: typeof import('element-plus/es')['ElProgress'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] diff --git a/tenant-platform/src/api/app.ts b/tenant-platform/src/api/app.ts index 6d33a9b..3642ea4 100644 --- a/tenant-platform/src/api/app.ts +++ b/tenant-platform/src/api/app.ts @@ -95,10 +95,17 @@ export const appApi = { getServices: (appId: string) => client.get<{ data: FeatureService[] }>(`/apps/${appId}/services`), - getService: (appId: string, platform: string, serviceType: string) => - client.get<{ data: FeatureService }>(`/apps/${appId}/services/item`, { - params: { platform, serviceType }, - }), + getService: async (appId: string, platform: string, serviceType: string) => { + const res = await client.get<{ data: FeatureService[] }>(`/apps/${appId}/services`) + const service = res.data.data.find(item => item.platform === platform && item.serviceType === serviceType) + if (!service) { + throw new Error('服务不存在') + } + return { + ...res, + data: { data: service }, + } as typeof res & { data: { data: FeatureService } } + }, toggleService: (appId: string, platform: string, serviceType: string, enable: boolean) => client.post<{ data: FeatureService }>(`/apps/${appId}/services/toggle`, null, { diff --git a/tenant-platform/src/api/dashboard.ts b/tenant-platform/src/api/dashboard.ts new file mode 100644 index 0000000..de7ac23 --- /dev/null +++ b/tenant-platform/src/api/dashboard.ts @@ -0,0 +1,11 @@ +import client from './client' + +export interface DashboardStats { + appCount: number + serviceCount: number + subAccountCount: number +} + +export const dashboardApi = { + stats: () => client.get<{ data: DashboardStats }>('/dashboard/stats'), +} diff --git a/tenant-platform/src/api/file.ts b/tenant-platform/src/api/file.ts index d09cb99..051418a 100644 --- a/tenant-platform/src/api/file.ts +++ b/tenant-platform/src/api/file.ts @@ -1,7 +1,12 @@ import axios from 'axios' +import { ElMessage } from 'element-plus' +import router from '@/router' +import { isJwtExpired } from '@/utils/jwt' + +export type UploadProgressHandler = (percent: number) => void const fileClient = axios.create({ - baseURL: import.meta.env.VITE_FILE_SERVICE_URL ?? 'http://192.168.116.9:8086', + baseURL: import.meta.env.VITE_FILE_SERVICE_URL ?? '', timeout: 30000, }) @@ -25,12 +30,44 @@ if (import.meta.env.DEV) { fileClient.interceptors.request.use((config) => { const token = localStorage.getItem('token') - if (token) { + if (token && !isJwtExpired(token)) { config.headers.Authorization = `Bearer ${token}` + } else if (token && isJwtExpired(token)) { + localStorage.removeItem('token') + if (router.currentRoute.value.path !== '/login') { + router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录')) + } + return Promise.reject(new Error('登录已失效,请重新登录')) } return config }) +fileClient.interceptors.response.use( + (res) => res, + (error) => { + const status = error.response?.status + if (status === 401) { + localStorage.removeItem('token') + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } + ElMessage.error('登录已失效,请重新登录') + return Promise.reject(error) + } + if (status === 403) { + localStorage.removeItem('token') + if (router.currentRoute.value.path !== '/login') { + router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录')) + } + ElMessage.error(error.response?.data?.message ?? '登录已失效,请重新登录') + return Promise.reject(error) + } + const msg = error.response?.data?.message ?? '文件请求失败' + ElMessage.error(msg) + return Promise.reject(error) + }, +) + export interface FileUploadResult { url: string thumbnailUrl?: string @@ -42,12 +79,19 @@ export interface FileUploadResult { } export const fileApi = { - uploadFile(file: File, thumbnail?: File | null) { + uploadFile(file: File, thumbnail?: File | null, onProgress?: UploadProgressHandler) { const formData = new FormData() formData.append('file', file) if (thumbnail) { formData.append('thumbnail', thumbnail) } - return fileClient.post<{ data: FileUploadResult }>('/api/file/upload', formData) + return fileClient.post<{ data: FileUploadResult }>('/api/file/upload', formData, { + onUploadProgress: (event) => { + if (!onProgress) return + const total = event.total ?? 0 + if (!total) return + onProgress(Math.min(100, Math.round((event.loaded * 100) / total))) + }, + }) }, } diff --git a/tenant-platform/src/api/im.ts b/tenant-platform/src/api/im.ts index 5f6ec0c..944ac62 100644 --- a/tenant-platform/src/api/im.ts +++ b/tenant-platform/src/api/im.ts @@ -1,7 +1,10 @@ import axios from 'axios' +import { ElMessage } from 'element-plus' +import router from '@/router' +import { isJwtExpired } from '@/utils/jwt' const imClient = axios.create({ - baseURL: import.meta.env.VITE_IM_API_BASE_URL ?? 'http://192.168.116.9:8082', + baseURL: import.meta.env.VITE_IM_API_BASE_URL ?? '', timeout: 15000, }) @@ -25,10 +28,44 @@ if (import.meta.env.DEV) { imClient.interceptors.request.use((config) => { const token = localStorage.getItem('token') - if (token) config.headers.Authorization = `Bearer ${token}` + if (token && !isJwtExpired(token)) { + config.headers.Authorization = `Bearer ${token}` + } else if (token && isJwtExpired(token)) { + localStorage.removeItem('token') + if (router.currentRoute.value.path !== '/login') { + router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录')) + } + return Promise.reject(new Error('登录已失效,请重新登录')) + } return config }) +imClient.interceptors.response.use( + (res) => res, + (error) => { + const status = error.response?.status + if (status === 401) { + localStorage.removeItem('token') + if (router.currentRoute.value.path !== '/login') { + router.push('/login') + } + ElMessage.error('登录已失效,请重新登录') + return Promise.reject(error) + } + if (status === 403) { + localStorage.removeItem('token') + if (router.currentRoute.value.path !== '/login') { + router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录')) + } + ElMessage.error(error.response?.data?.message ?? '登录已失效,请重新登录') + return Promise.reject(error) + } + const msg = error.response?.data?.message ?? 'IM 请求失败' + ElMessage.error(msg) + return Promise.reject(error) + }, +) + export interface ImUser { id: string appId: string diff --git a/tenant-platform/src/api/update.ts b/tenant-platform/src/api/update.ts index a0cb2c4..aa35666 100644 --- a/tenant-platform/src/api/update.ts +++ b/tenant-platform/src/api/update.ts @@ -1,4 +1,4 @@ -import axios from 'axios' +import axios, { type AxiosProgressEvent } from 'axios' import { isJwtExpired } from '@/utils/jwt' const updateClient = axios.create({ @@ -6,6 +6,19 @@ const updateClient = axios.create({ timeout: 30000, }) +type UploadProgressHandler = (percent: number) => void + +function uploadProgressConfig(onProgress?: UploadProgressHandler) { + if (!onProgress) return {} + return { + onUploadProgress: (event: AxiosProgressEvent) => { + const total = event.total ?? 0 + if (!total) return + onProgress(Math.min(100, Math.round((event.loaded * 100) / total))) + }, + } +} + if (import.meta.env.DEV) { updateClient.interceptors.request.use((config) => { console.debug('[tenant-platform][UPDATE] request', { @@ -232,8 +245,8 @@ export const updateAdminApi = { return updateClient.post(`/api/v1/updates/app/${id}/gray`, body) }, - uploadAppVersion(formData: FormData) { - return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData) + uploadAppVersion(formData: FormData, onProgress?: UploadProgressHandler) { + return updateClient.post<{ data: AppVersion }>('/api/v1/updates/app/upload', formData, uploadProgressConfig(onProgress)) }, inspectAppPackage(apkUrl: string) { @@ -266,16 +279,16 @@ export const updateAdminApi = { return updateClient.post(`/api/v1/rn/${id}/gray`, body) }, - uploadRnBundle(formData: FormData) { - return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData) + uploadRnBundle(formData: FormData, onProgress?: UploadProgressHandler) { + return updateClient.post<{ data: RnBundle }>('/api/v1/rn/upload', formData, uploadProgressConfig(onProgress)) }, - inspectRnBundle(formData: FormData) { - return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData) + inspectRnBundle(formData: FormData, onProgress?: UploadProgressHandler) { + return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData, uploadProgressConfig(onProgress)) }, - uploadUnifiedRelease(formData: FormData) { - return updateClient.post('/api/v1/updates/unified/upload', formData) + uploadUnifiedRelease(formData: FormData, onProgress?: UploadProgressHandler) { + return updateClient.post('/api/v1/updates/unified/upload', formData, uploadProgressConfig(onProgress)) }, // ── Store config ──────────────────────────────────────────────────────── diff --git a/tenant-platform/src/views/dashboard/DashboardView.vue b/tenant-platform/src/views/dashboard/DashboardView.vue index 5d92c67..e860e75 100644 --- a/tenant-platform/src/views/dashboard/DashboardView.vue +++ b/tenant-platform/src/views/dashboard/DashboardView.vue @@ -45,25 +45,14 @@ diff --git a/tenant-platform/src/views/logs/OperationLogView.vue b/tenant-platform/src/views/logs/OperationLogView.vue index ba142c0..f6d7bca 100644 --- a/tenant-platform/src/views/logs/OperationLogView.vue +++ b/tenant-platform/src/views/logs/OperationLogView.vue @@ -3,98 +3,206 @@

操作日志

-
- - - - - - - - - 刷新 -
+ + +
+ + + + + + + + + + 刷新 +
-
- - - - - - - - - - - - - - - -
+
+ + + + + + + + + + + + + + + +
- + +
+ + +
+ + + + + 刷新 +
+ +
+ + + + + + + + + + + + + + + + +
+
+