Kaynağa Gözat

update:租户存储桶以及视频上传线路配置

ct 1 hafta önce
ebeveyn
işleme
4391166e86

+ 2 - 19
src/main.js

@@ -31,6 +31,7 @@ import DictTag from '@/components/DictTag'
 import VueMeta from 'vue-meta'
 import * as echarts from "echarts";
 import { getConfigByKey } from '@/api/system/config'
+import { buildRuntimeConfigFromAdminUi } from '@/utils/adminUiConfig'
 Vue.prototype.$runtimeConfig = {}
 
 // 全局方法挂载
@@ -111,25 +112,7 @@ async function initRuntimeConfig() {
     // 后端配置 JSON
     const form = JSON.parse(configValue)
 
-    // 字段映射表(安全改造:移除OBS密钥映射)
-    const mapping = {
-      VUE_APP_OBS_SERVER: 'obsServer',
-      VUE_APP_OBS_BUCKET: 'obsBucket',
-      VUE_APP_VIDEO_LINE_1: 'videoLinePrimary',
-      VUE_APP_VIDEO_LINE_2: 'videoLineSecondary',
-      VUE_APP_VIDEO_URL: 'volcanoVideoDomain',
-      VUE_APP_HSY_SPACE: 'volcanoVodSpace',
-      VUE_APP_LIVE_PATH: 'livePath',
-      VUE_APP_COS_BUCKET: 'cosBucket',
-      VUE_APP_LIVE_WS_URL: 'liveWebSocketUrl',
-      VUE_APP_COURSE_DEFAULT: 'courseDefaultType',
-      VUE_APP_COS_REGION: 'cosRegion'
-    }
-
-    // 写入运行时配置
-    Object.keys(mapping).forEach(key => {
-      Vue.prototype.$runtimeConfig[key] = form[mapping[key]] ?? null
-    })
+    Vue.prototype.$runtimeConfig = buildRuntimeConfigFromAdminUi(form)
   } catch (e) {
     console.error('初始化运行时配置失败', e)
   }

+ 102 - 55
src/views/admin/frontConfig/index.vue

@@ -62,44 +62,86 @@
         <!-- 存储桶配置 -->
         <el-card shadow="never" class="section-card">
           <div slot="header"><span>存储桶配置</span></div>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="OBS Access Key ID" prop="obsAccessKeyId">
-                <el-input v-model="form.obsAccessKeyId" type="password" show-password placeholder="请输入Access Key ID" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="OBS Secret Access Key" prop="obsSecretAccessKey">
-                <el-input v-model="form.obsSecretAccessKey" type="password" show-password placeholder="请输入Secret Access Key" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="OBS服务器地址" prop="obsServer">
-                <el-input v-model="form.obsServer" placeholder="请输入OBS服务器地址">
-                  <template slot="prepend">https://</template>
-                </el-input>
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="OBS存储桶" prop="obsBucket">
-                <el-input v-model="form.obsBucket" placeholder="请输入OBS存储桶名称" />
-              </el-form-item>
-            </el-col>
-          </el-row>
-          <el-row :gutter="20">
-            <el-col :span="12">
-              <el-form-item label="COS存储桶" prop="cosBucket">
-                <el-input v-model="form.cosBucket" placeholder="请输入COS存储桶名称" />
-              </el-form-item>
-            </el-col>
-            <el-col :span="12">
-              <el-form-item label="COS存储区域" prop="cosRegion">
-                <el-input v-model="form.cosRegion" placeholder="请输入COS存储区域" />
-              </el-form-item>
-            </el-col>
-          </el-row>
+
+          <div class="cloud-block">
+            <div class="cloud-block-title">腾讯云配置(tencent_cloud_config)</div>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="Secret ID">
+                  <el-input v-model="form.tencent_cloud_config.secret_id" type="password" show-password placeholder="secret_id" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="Secret Key">
+                  <el-input v-model="form.tencent_cloud_config.secret_key" type="password" show-password placeholder="secret_key" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="Bucket">
+                  <el-input v-model="form.tencent_cloud_config.bucket" placeholder="如 jnmy-1323137866" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="App ID">
+                  <el-input v-model="form.tencent_cloud_config.app_id" placeholder="app_id" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="Region">
+                  <el-input v-model="form.tencent_cloud_config.region" placeholder="如 ap-chongqing" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="Proxy">
+                  <el-input v-model="form.tencent_cloud_config.proxy" placeholder="CDN/线路前缀,如 jnmy" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </div>
+
+          <div class="cloud-block">
+            <div class="cloud-block-title">华为云配置(tmp_secret_config)</div>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="Secret ID">
+                  <el-input v-model="form.tmp_secret_config.secret_id" type="password" show-password placeholder="secret_id" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="Secret Key">
+                  <el-input v-model="form.tmp_secret_config.secret_key" type="password" show-password placeholder="secret_key" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="Bucket">
+                  <el-input v-model="form.tmp_secret_config.bucket" placeholder="如 fs-131972100" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="App ID">
+                  <el-input v-model="form.tmp_secret_config.app_id" placeholder="app_id" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+            <el-row :gutter="20">
+              <el-col :span="12">
+                <el-form-item label="Region">
+                  <el-input v-model="form.tmp_secret_config.region" placeholder="如 ap-chongqing" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="Proxy">
+                  <el-input v-model="form.tmp_secret_config.proxy" placeholder="CDN/线路前缀,如 fs" />
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </div>
         </el-card>
 
         <!-- 视频线路配置 -->
@@ -191,22 +233,9 @@
 <script>
 import { getConfigByKey, updateConfigByKey } from '@/api/system/config'
 import { listAdminTenantList } from '@/api/admin/sysCompany'
+import { defaultAdminUiForm, normalizeAdminUiConfig } from '@/utils/adminUiConfig'
 
-const defaultForm = () => ({
-  obsAccessKeyId: '',
-  obsSecretAccessKey: '',
-  obsServer: '',
-  obsBucket: '',
-  cosBucket: '',
-  cosRegion: '',
-  videoLinePrimary: '',
-  videoLineSecondary: '',
-  livePath: '',
-  volcanoVideoDomain: '',
-  volcanoVodSpace: '',
-  liveWebSocketUrl: '',
-  courseDefaultType: '1'
-})
+const defaultForm = defaultAdminUiForm
 
 export default {
   name: 'AdminFrontConfig',
@@ -354,7 +383,7 @@ export default {
           if (data.configValue) {
             this.tenantConfigEmpty = false
             try {
-              this.form = { ...defaultForm(), ...JSON.parse(data.configValue) }
+              this.form = normalizeAdminUiConfig(JSON.parse(data.configValue))
             } catch (e) {
               this.form = defaultForm()
               this.tenantConfigEmpty = !!this.selectedTenantId
@@ -381,7 +410,7 @@ export default {
           return
         }
         try {
-          this.form = { ...defaultForm(), ...JSON.parse(response.data.configValue) }
+          this.form = normalizeAdminUiConfig(JSON.parse(response.data.configValue))
           this.msgSuccess('已加载系统默认配置,保存时将写入当前租户')
         } catch (e) {
           this.msgWarning('系统默认配置解析失败')
@@ -453,4 +482,22 @@ export default {
 .load-more:hover {
   color: #66b1ff;
 }
+
+.cloud-block {
+  padding: 4px 0 8px;
+  margin-bottom: 16px;
+  border-bottom: 1px dashed #ebeef5;
+}
+
+.cloud-block:last-child {
+  border-bottom: none;
+  margin-bottom: 0;
+}
+
+.cloud-block-title {
+  font-size: 13px;
+  font-weight: 600;
+  color: #606266;
+  margin-bottom: 12px;
+}
 </style>

+ 294 - 60
src/views/admin/ossConfig/index.vue

@@ -1,9 +1,63 @@
 <template>
   <div class="app-container">
     <el-card v-loading="loading" shadow="never">
-      <div slot="header">
+      <div slot="header" class="page-header">
         <span>OSS云存储配置</span>
+        <el-form :inline="true" size="small" class="tenant-form">
+          <el-form-item label="选择租户">
+            <el-select
+              ref="tenantSelect"
+              v-model="selectedTenantId"
+              placeholder="请输入租户名称搜索"
+              filterable
+              remote
+              clearable
+              :remote-method="handleTenantSearch"
+              :loading="tenantSelectLoading"
+              style="width: 280px"
+              @visible-change="handleTenantDropdownVisible"
+              @clear="handleTenantSelectClear"
+              @change="loadConfig"
+            >
+              <el-option
+                v-for="item in tenantList"
+                :key="item.id"
+                :label="formatTenantLabel(item)"
+                :value="item.id"
+              />
+              <el-option v-if="hasMoreTenants" key="tenant-load-more" disabled class="tenant-load-more-option">
+                <div class="load-more" @click.stop="loadMoreTenants">
+                  <span>加载更多</span>
+                  <i v-if="tenantLoadingMore" class="el-icon-loading" />
+                </div>
+              </el-option>
+            </el-select>
+          </el-form-item>
+        </el-form>
       </div>
+
+      <el-alert
+        v-if="!selectedTenantId"
+        title="当前为系统默认配置(主库),保存后将更新全局配置;选择租户后可单独配置该租户 OSS 参数"
+        type="info"
+        :closable="false"
+        show-icon
+        class="tenant-tip"
+      />
+
+      <el-alert
+        v-if="selectedTenantId && tenantConfigEmpty"
+        title="该租户尚未保存过此配置,可直接填写后保存;如需参考系统默认配置,可点击下方按钮加载(不会自动保存)。"
+        type="warning"
+        :closable="false"
+        show-icon
+        class="tenant-tip"
+      >
+        <el-button type="text" size="small" :loading="fallbackLoading" @click="loadGlobalConfigAsFallback">
+          加载系统默认配置
+        </el-button>
+      </el-alert>
+
       <el-form ref="form" :model="form" label-width="200px">
         <el-form-item label="类型" prop="type">
           <el-radio-group v-model="form.type">
@@ -107,8 +161,8 @@
             </el-form-item>
           </el-card>
         </template>
-
       </el-form>
+
       <div style="text-align: center; margin-top: 20px; padding-bottom: 20px;">
         <el-button type="primary" size="medium" @click="submitForm" :loading="submitLoading">保存配置</el-button>
       </div>
@@ -118,6 +172,39 @@
 
 <script>
 import { getConfigByKey, updateConfigByKey } from '@/api/system/config'
+import { listAdminTenantList } from '@/api/admin/sysCompany'
+
+function defaultOssForm() {
+  return {
+    type: 1,
+    qiniuDomain: '',
+    qiniuPrefix: '',
+    qiniuAccessKey: '',
+    qiniuSecretKey: '',
+    qiniuBucketName: '',
+    aliyunDomain: '',
+    aliyunPrefix: '',
+    aliyunEndPoint: '',
+    aliyunAccessKeyId: '',
+    aliyunAccessKeySecret: '',
+    aliyunBucketName: '',
+    qcloudDomain: '',
+    qcloudPrefix: '',
+    qcloudSecretId: '',
+    qcloudSecretKey: '',
+    qcloudBucketName: '',
+    qcloudRegion: '',
+    huaweiDomain: '',
+    huaweiEndpoint: '',
+    huaweiAK: '',
+    huaweiSK: '',
+    huaweiBucketName: ''
+  }
+}
+
+function mergeOssForm(parsed) {
+  return { ...defaultOssForm(), ...(parsed || {}) }
+}
 
 export default {
   name: 'AdminOssConfig',
@@ -127,81 +214,192 @@ export default {
       submitLoading: false,
       configId: null,
       configKey: 'sys.oss.cloudStorage',
-      form: {
-        type: 1,
-        // 七牛云
-        qiniuDomain: '',
-        qiniuPrefix: '',
-        qiniuAccessKey: '',
-        qiniuSecretKey: '',
-        qiniuBucketName: '',
-        // 阿里云
-        aliyunDomain: '',
-        aliyunPrefix: '',
-        aliyunEndPoint: '',
-        aliyunAccessKeyId: '',
-        aliyunAccessKeySecret: '',
-        aliyunBucketName: '',
-        // 腾讯云
-        qcloudDomain: '',
-        qcloudPrefix: '',
-        qcloudSecretId: '',
-        qcloudSecretKey: '',
-        qcloudBucketName: '',
-        qcloudRegion: '',
-        // 华为云
-        huaweiDomain: '',
-        huaweiEndpoint: '',
-        huaweiAK: '',
-        huaweiSK: '',
-        huaweiBucketName: ''
-      }
+      selectedTenantId: null,
+      tenantConfigEmpty: false,
+      fallbackLoading: false,
+      tenantList: [],
+      tenantTotal: 0,
+      hasMoreTenants: false,
+      tenantSelectLoading: false,
+      tenantLoadingMore: false,
+      tenantQueryParams: {
+        pageNum: 1,
+        pageSize: 20,
+        tenantName: '',
+        status: 1
+      },
+      form: defaultOssForm(),
+      tenantSearchTimer: null,
+      tenantSelectInputEl: null,
+      tenantSelectInputHandler: null
     }
   },
-  created() {
+  mounted() {
     this.loadConfig()
   },
+  beforeDestroy() {
+    this.unbindTenantSelectInputListener()
+  },
   methods: {
-    loadConfig() {
-      this.loading = true
-      getConfigByKey(this.configKey).then(response => {
-        const configValue = response && response.data ? response.data.configValue : null
-        if (response.data) {
-          this.configId = response.data.configId
+    formatTenantLabel(item) {
+      if (item.tenantCode) {
+        return `${item.tenantName}(${item.tenantCode})`
+      }
+      return item.tenantName
+    },
+    handleTenantSearch(query) {
+      if (query === undefined) return
+      this.tenantQueryParams.tenantName = (query || '').trim()
+      this.tenantQueryParams.pageNum = 1
+      this.fetchTenantList()
+    },
+    handleTenantSelectClear() {
+      this.handleTenantSearch('')
+    },
+    getTenantSelectInputEl() {
+      const root = this.$refs.tenantSelect && this.$refs.tenantSelect.$el
+      return root ? root.querySelector('input.el-input__inner') : null
+    },
+    bindTenantSelectInputListener() {
+      this.unbindTenantSelectInputListener()
+      const input = this.getTenantSelectInputEl()
+      if (!input) return
+      this.tenantSelectInputEl = input
+      this.tenantSelectInputHandler = () => {
+        const val = (input.value || '').trim()
+        if (!val && this.tenantQueryParams.tenantName) {
+          if (this.tenantSearchTimer) clearTimeout(this.tenantSearchTimer)
+          this.tenantSearchTimer = setTimeout(() => {
+            this.handleTenantSearch('')
+          }, 150)
+        }
+      }
+      input.addEventListener('input', this.tenantSelectInputHandler)
+    },
+    unbindTenantSelectInputListener() {
+      if (this.tenantSearchTimer) {
+        clearTimeout(this.tenantSearchTimer)
+        this.tenantSearchTimer = null
+      }
+      if (this.tenantSelectInputEl && this.tenantSelectInputHandler) {
+        this.tenantSelectInputEl.removeEventListener('input', this.tenantSelectInputHandler)
+      }
+      this.tenantSelectInputEl = null
+      this.tenantSelectInputHandler = null
+    },
+    syncTenantSearchFromInput() {
+      const input = this.getTenantSelectInputEl()
+      const inputVal = input ? (input.value || '').trim() : ''
+      if (!inputVal && this.tenantQueryParams.tenantName) {
+        this.handleTenantSearch('')
+      }
+    },
+    handleTenantDropdownVisible(visible) {
+      if (visible) {
+        if (this.tenantList.length === 0) {
+          this.handleTenantSearch('')
+        }
+        this.$nextTick(() => {
+          this.syncTenantSearchFromInput()
+          this.bindTenantSelectInputListener()
+        })
+      } else {
+        this.unbindTenantSelectInputListener()
+      }
+    },
+    fetchTenantList(isLoadMore = false) {
+      if (!isLoadMore) {
+        this.tenantSelectLoading = true
+      } else {
+        this.tenantLoadingMore = true
+      }
+      listAdminTenantList(this.tenantQueryParams).then(response => {
+        const rows = response.rows || []
+        if (isLoadMore) {
+          const existIds = new Set(this.tenantList.map(t => t.id))
+          const append = rows.filter(r => !existIds.has(r.id))
+          this.tenantList = this.tenantList.concat(append)
         } else {
-          this.configId = null
+          this.tenantList = rows
         }
-        // 安全解析
-        if (configValue !== null && configValue !== undefined && configValue !== '' && configValue !== 'null') {
-          try {
-            const parsed = JSON.parse(configValue)
-            if (parsed !== null && parsed !== undefined) {
-              this.form = { ...this.form, ...parsed }
+        this.tenantTotal = response.total || 0
+        this.hasMoreTenants = this.tenantList.length < this.tenantTotal
+      }).finally(() => {
+        this.tenantSelectLoading = false
+        this.tenantLoadingMore = false
+      })
+    },
+    loadMoreTenants() {
+      if (this.tenantLoadingMore || !this.hasMoreTenants) return
+      this.tenantQueryParams.pageNum += 1
+      this.fetchTenantList(true)
+    },
+    loadConfig() {
+      this.loading = true
+      this.tenantConfigEmpty = false
+      const tenantId = this.selectedTenantId || undefined
+      getConfigByKey(this.configKey, tenantId).then(response => {
+        const data = response.data
+        if (data) {
+          this.configId = data.configId != null ? data.configId : null
+          const configValue = data.configValue
+          if (configValue !== null && configValue !== undefined && configValue !== '' && configValue !== 'null') {
+            try {
+              this.form = mergeOssForm(JSON.parse(configValue))
+              this.tenantConfigEmpty = false
+            } catch (e) {
+              this.form = defaultOssForm()
+              this.tenantConfigEmpty = !!this.selectedTenantId
             }
-          } catch (e) {
-            // 使用默认值
+          } else {
+            this.form = defaultOssForm()
+            this.tenantConfigEmpty = !!this.selectedTenantId
           }
+        } else {
+          this.configId = null
+          this.form = defaultOssForm()
+          this.tenantConfigEmpty = !!this.selectedTenantId
         }
       }).finally(() => {
         this.loading = false
       })
     },
+    loadGlobalConfigAsFallback() {
+      this.fallbackLoading = true
+      getConfigByKey(this.configKey).then(response => {
+        if (!response.data || !response.data.configValue) {
+          this.msgWarning('系统默认配置为空')
+          return
+        }
+        try {
+          this.form = mergeOssForm(JSON.parse(response.data.configValue))
+          this.msgSuccess('已加载系统默认配置,保存时将写入当前租户')
+        } catch (e) {
+          this.msgWarning('系统默认配置解析失败')
+        }
+      }).finally(() => {
+        this.fallbackLoading = false
+      })
+    },
     submitForm() {
       this.$refs['form'].validate(valid => {
-        if (valid) {
-          this.submitLoading = true
-          const param = {
-            configId: this.configId,
-            configValue: JSON.stringify(this.form)
-          }
-          updateConfigByKey(param).then(response => {
-            if (response.code === 200) {
-              this.msgSuccess('修改成功')
-            }
-          }).finally(() => {
-            this.submitLoading = false
-          })
+        if (!valid) return
+        this.submitLoading = true
+        const param = {
+          configId: (this.configId != null && this.configId !== '') ? Number(this.configId) : null,
+          configName: 'OSS云存储配置',
+          configKey: this.configKey,
+          configValue: JSON.stringify(this.form),
+          tenantId: this.selectedTenantId ? String(this.selectedTenantId) : null
         }
+        updateConfigByKey(param).then(response => {
+          if (response.code === 200) {
+            this.msgSuccess('修改成功')
+            this.loadConfig()
+          }
+        }).finally(() => {
+          this.submitLoading = false
+        })
       })
     }
   }
@@ -213,4 +411,40 @@ export default {
   margin-bottom: 20px;
 }
 
+.page-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  flex-wrap: wrap;
+}
+
+.tenant-form {
+  margin-bottom: 0;
+}
+
+.tenant-form >>> .el-form-item {
+  margin-bottom: 0;
+}
+
+.tenant-tip {
+  margin-bottom: 16px;
+}
+
+.tenant-load-more-option {
+  text-align: center;
+  padding: 8px 0;
+}
+
+.load-more {
+  color: #409eff;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+}
+
+.load-more:hover {
+  color: #66b1ff;
+}
 </style>

+ 2 - 19
src/views/login.vue

@@ -90,6 +90,7 @@ import { encrypt, decrypt } from '@/utils/jsencrypt'
 import WechatLoginDialog from "@/views/WechatLoginDialog.vue";
 import { setToken } from "@/utils/auth";
 import {getConfigByKey} from "@/api/system/config";
+import { buildRuntimeConfigFromAdminUi } from '@/utils/adminUiConfig'
 import Vue from 'vue'
 export default {
   name: "Login",
@@ -154,25 +155,7 @@ export default {
         // 后端配置 JSON
         const form = JSON.parse(configValue)
 
-        // 直接更新全局配置
-        // 安全改造:OBS密钥已移除,前端不再需要AK/SK
-        // 前端OBS上传改为通过后端presigned URL(/common/obs/presignedUploadUrl)
-        const config = {
-          VUE_APP_OBS_SERVER: form.obsServer || '',
-          VUE_APP_OBS_BUCKET: form.obsBucket || '',
-          VUE_APP_VIDEO_LINE_1: form.videoLinePrimary || '',
-          VUE_APP_VIDEO_LINE_2: form.videoLineSecondary || '',
-          VUE_APP_VIDEO_URL: form.volcanoVideoDomain || '',
-          VUE_APP_HSY_SPACE: form.volcanoVodSpace || '',
-          VUE_APP_LIVE_PATH: form.livePath || '/live',
-          VUE_APP_COS_BUCKET: form.cosBucket || '',
-          VUE_APP_LIVE_WS_URL: form.liveWebSocketUrl || '',
-          VUE_APP_COURSE_DEFAULT: form.courseDefaultType || '1',
-          VUE_APP_COS_REGION: form.cosRegion || ''
-        }
-
-        // 更新到全局
-        Vue.prototype.$runtimeConfig = config
+        Vue.prototype.$runtimeConfig = buildRuntimeConfigFromAdminUi(form)
 
       } catch (error) {
         console.error('重新加载运行时配置异常:', error)