吴树波 3 дней назад
Родитель
Сommit
86e322f050

+ 71 - 0
scripts/aws-sdk-webpack-aliases.js

@@ -0,0 +1,71 @@
+'use strict'
+
+const fs = require('fs')
+const path = require('path')
+
+/**
+ * Webpack 4 ��ʶ�� package.json �� exports ��·����@aws-sdk v3 ����ʹ�� exports��
+ * �轫 @aws-sdk/<pkg>/<subpath> ӳ�䵽ʵ�� dist �ļ������� browser ��������
+ */
+function pickExportTarget(exportConfig) {
+  if (!exportConfig || typeof exportConfig !== 'object') {
+    return null
+  }
+  const browser = exportConfig.browser
+  if (browser) {
+    if (typeof browser === 'string') {
+      return browser
+    }
+    return browser.import || browser.require || browser.module
+  }
+  return exportConfig.module || exportConfig.import || exportConfig.require || exportConfig.node
+}
+
+function buildAwsSdkAliases(projectRoot) {
+  const aliases = {}
+  const awsSdkDir = path.join(projectRoot, 'node_modules', '@aws-sdk')
+  if (!fs.existsSync(awsSdkDir)) {
+    return aliases
+  }
+
+  for (const pkgName of fs.readdirSync(awsSdkDir)) {
+    const pkgRoot = path.join(awsSdkDir, pkgName)
+    const pkgJsonPath = path.join(pkgRoot, 'package.json')
+    if (!fs.existsSync(pkgJsonPath)) {
+      continue
+    }
+
+    let pkgJson
+    try {
+      pkgJson = require(pkgJsonPath)
+    } catch (e) {
+      continue
+    }
+
+    const exportsField = pkgJson.exports
+    if (!exportsField || typeof exportsField !== 'object') {
+      continue
+    }
+
+    for (const [subpath, exportConfig] of Object.entries(exportsField)) {
+      if (subpath === '.' || subpath === './package.json') {
+        continue
+      }
+      if (!subpath.startsWith('./')) {
+        continue
+      }
+
+      const target = pickExportTarget(exportConfig)
+      if (!target || typeof target !== 'string') {
+        continue
+      }
+
+      const aliasKey = `@aws-sdk/${pkgName}/${subpath.slice(2)}`
+      aliases[aliasKey] = path.resolve(pkgRoot, target)
+    }
+  }
+
+  return aliases
+}
+
+module.exports = { buildAwsSdkAliases }

+ 17 - 0
src/api/admin/commGatewayLog.js

@@ -0,0 +1,17 @@
+import request from '@/utils/request'
+
+/** 通讯网关 API 调用日志(主库 comm_gateway_api_log) */
+export function listCommGatewayLog(query) {
+  return request({
+    url: '/admin/comm-gateway-log/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getCommGatewayLog(logId) {
+  return request({
+    url: '/admin/comm-gateway-log/' + logId,
+    method: 'get'
+  })
+}

+ 16 - 0
src/views/admin/commGatewayCallLog/index.vue

@@ -0,0 +1,16 @@
+<template>
+  <comm-gateway-log-panel
+    api-type="call"
+    page-title="外呼日志"
+    query-permi="platform:commGatewayCallLog:query"
+  />
+</template>
+
+<script>
+import CommGatewayLogPanel from '@/views/admin/components/CommGatewayLogPanel'
+
+export default {
+  name: 'AdminCommGatewayCallLog',
+  components: { CommGatewayLogPanel }
+}
+</script>

+ 16 - 0
src/views/admin/commGatewaySmsLog/index.vue

@@ -0,0 +1,16 @@
+<template>
+  <comm-gateway-log-panel
+    api-type="sms"
+    page-title="短信日志"
+    query-permi="platform:commGatewaySmsLog:query"
+  />
+</template>
+
+<script>
+import CommGatewayLogPanel from '@/views/admin/components/CommGatewayLogPanel'
+
+export default {
+  name: 'AdminCommGatewaySmsLog',
+  components: { CommGatewayLogPanel }
+}
+</script>

+ 308 - 0
src/views/admin/components/CommGatewayLogPanel.vue

@@ -0,0 +1,308 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never" class="mb16 filter-card">
+      <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" label-width="88px">
+        <el-form-item label="租户" prop="companyId">
+          <el-select
+            v-model="queryParams.companyId"
+            filterable
+            clearable
+            placeholder="请选择租户"
+            style="width: 220px"
+          >
+            <el-option
+              v-for="item in companyOptions"
+              :key="item.companyId"
+              :label="item.companyName"
+              :value="item.companyId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item :label="phoneLabel" prop="calleePhone">
+          <el-input
+            v-model="queryParams.calleePhone"
+            :placeholder="'请输入' + phoneLabel"
+            clearable
+            style="width: 180px"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item v-if="apiType === 'call'" label="主叫号码" prop="callerPhone">
+          <el-input
+            v-model="queryParams.callerPhone"
+            placeholder="请输入主叫号码"
+            clearable
+            style="width: 180px"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="调用账号" prop="callerAccount">
+          <el-input
+            v-model="queryParams.callerAccount"
+            placeholder="请输入调用账号"
+            clearable
+            style="width: 160px"
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="是否成功" prop="success">
+          <el-select v-model="queryParams.success" placeholder="全部" clearable style="width: 120px">
+            <el-option label="成功" :value="1" />
+            <el-option label="失败" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="频率限制" prop="limitHit">
+          <el-select v-model="queryParams.limitHit" placeholder="全部" clearable style="width: 120px">
+            <el-option label="是" :value="1" />
+            <el-option label="否" :value="0" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="调用时间">
+          <el-date-picker
+            v-model="dateRange"
+            type="daterange"
+            range-separator="-"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="yyyy-MM-dd"
+            style="width: 240px"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
+    </el-row>
+
+    <el-table v-loading="loading" :data="list" border size="small" height="500">
+      <el-table-column label="日志ID" align="center" prop="logId" width="90" />
+      <el-table-column label="租户" align="center" prop="companyName" min-width="140" show-overflow-tooltip />
+      <el-table-column label="租户ID" align="center" prop="tenantId" width="90" />
+      <el-table-column :label="phoneLabel" align="center" prop="calleePhone" width="130" />
+      <el-table-column v-if="apiType === 'call'" label="主叫号码" align="center" prop="callerPhone" width="130" />
+      <el-table-column v-if="apiType === 'call'" label="线路ID" align="center" prop="gatewayId" width="90" />
+      <el-table-column label="调用账号" align="center" prop="callerAccount" min-width="120" show-overflow-tooltip />
+      <el-table-column label="结果" align="center" prop="success" width="80">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.success === 1" type="success" size="mini">成功</el-tag>
+          <el-tag v-else type="danger" size="mini">失败</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="业务码" align="center" prop="resultCode" width="80" />
+      <el-table-column label="结果说明" align="center" prop="resultMsg" min-width="160" show-overflow-tooltip />
+      <el-table-column label="频率限制" align="center" prop="limitHit" width="90">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.limitHit === 1" type="warning" size="mini">是</el-tag>
+          <span v-else>否</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="计费金额" align="center" prop="billingAmount" width="100">
+        <template slot-scope="scope">
+          <span>{{ formatAmount(scope.row.billingAmount) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="耗时(ms)" align="center" prop="durationMs" width="90" />
+      <el-table-column label="客户端IP" align="center" prop="clientIp" width="130" />
+      <el-table-column label="调用时间" align="center" prop="createTime" width="160" />
+      <el-table-column label="操作" align="center" width="80" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-view"
+            @click="handleDetail(scope.row)"
+            v-hasPermi="[queryPermi]"
+          >详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="detailTitle" :visible.sync="detailOpen" width="780px" append-to-body>
+      <el-descriptions :column="2" border size="small">
+        <el-descriptions-item label="日志ID">{{ detail.logId || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="租户">{{ detail.companyName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="租户ID">{{ detail.tenantId || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="公司ID">{{ detail.companyId || '-' }}</el-descriptions-item>
+        <el-descriptions-item :label="phoneLabel">{{ detail.calleePhone || '-' }}</el-descriptions-item>
+        <el-descriptions-item v-if="apiType === 'call'" label="主叫号码">{{ detail.callerPhone || '-' }}</el-descriptions-item>
+        <el-descriptions-item v-if="apiType === 'call'" label="线路ID">{{ detail.gatewayId || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="调用账号">{{ detail.callerAccount || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="操作人ID">{{ detail.companyUserId || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="鉴权范围">{{ detail.authScope || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="客户端IP">{{ detail.clientIp || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="是否成功">
+          <el-tag v-if="detail.success === 1" type="success" size="mini">成功</el-tag>
+          <el-tag v-else type="danger" size="mini">失败</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="业务码">{{ detail.resultCode != null ? detail.resultCode : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="结果说明" :span="2">{{ detail.resultMsg || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="频率限制">
+          <el-tag v-if="detail.limitHit === 1" type="warning" size="mini">是</el-tag>
+          <span v-else>否</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="限制原因">{{ detail.limitReason || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="计费金额">{{ formatAmount(detail.billingAmount) }}</el-descriptions-item>
+        <el-descriptions-item label="成本价">{{ formatAmount(detail.costPrice) }}</el-descriptions-item>
+        <el-descriptions-item label="售价">{{ formatAmount(detail.calcPrice) }}</el-descriptions-item>
+        <el-descriptions-item label="计费数量">{{ detail.billingQuantity != null ? detail.billingQuantity : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="耗时(ms)">{{ detail.durationMs != null ? detail.durationMs : '-' }}</el-descriptions-item>
+        <el-descriptions-item label="调用时间">{{ detail.createTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="请求路径" :span="2">{{ detail.apiPath || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="请求参数" :span="2">
+          <pre class="json-block">{{ formatJson(detail.requestBody) }}</pre>
+        </el-descriptions-item>
+        <el-descriptions-item label="响应内容" :span="2">
+          <pre class="json-block">{{ formatJson(detail.responseBody) }}</pre>
+        </el-descriptions-item>
+      </el-descriptions>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="detailOpen = false">关 闭</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listCommGatewayLog, getCommGatewayLog } from '@/api/admin/commGatewayLog'
+import { listCompanyOptions } from '@/api/admin/sysCompany'
+
+export default {
+  name: 'CommGatewayLogPanel',
+  props: {
+    apiType: {
+      type: String,
+      required: true
+    },
+    pageTitle: {
+      type: String,
+      default: ''
+    },
+    queryPermi: {
+      type: String,
+      required: true
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      showSearch: true,
+      total: 0,
+      list: [],
+      companyOptions: [],
+      dateRange: [],
+      detailOpen: false,
+      detail: {},
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyId: null,
+        calleePhone: null,
+        callerPhone: null,
+        callerAccount: null,
+        success: null,
+        limitHit: null,
+        apiType: null
+      }
+    }
+  },
+  computed: {
+    phoneLabel() {
+      return this.apiType === 'sms' ? '手机号' : '被叫号码'
+    },
+    detailTitle() {
+      return (this.pageTitle || '通讯日志') + '详情'
+    }
+  },
+  created() {
+    this.queryParams.apiType = this.apiType
+    this.loadCompanyOptions()
+    this.getList()
+  },
+  methods: {
+    loadCompanyOptions() {
+      listCompanyOptions().then(response => {
+        this.companyOptions = response.data || []
+      })
+    },
+    buildQueryParams() {
+      const data = { ...this.queryParams, apiType: this.apiType }
+      if (this.dateRange && this.dateRange.length === 2) {
+        data.params = {
+          beginTime: this.dateRange[0] + ' 00:00:00',
+          endTime: this.dateRange[1] + ' 23:59:59'
+        }
+      }
+      return data
+    },
+    getList() {
+      this.loading = true
+      listCommGatewayLog(this.buildQueryParams()).then(response => {
+        this.list = response.rows || []
+        this.total = response.total || 0
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.dateRange = []
+      this.resetForm('queryForm')
+      this.queryParams.apiType = this.apiType
+      this.handleQuery()
+    },
+    handleDetail(row) {
+      getCommGatewayLog(row.logId).then(response => {
+        this.detail = response.data || {}
+        this.detailOpen = true
+      })
+    },
+    formatAmount(value) {
+      if (value == null || value === '') {
+        return '-'
+      }
+      return Number(value).toFixed(4)
+    },
+    formatJson(text) {
+      if (!text) {
+        return '-'
+      }
+      try {
+        return JSON.stringify(JSON.parse(text), null, 2)
+      } catch (e) {
+        return text
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.filter-card {
+  margin-bottom: 12px;
+}
+.json-block {
+  max-height: 220px;
+  overflow: auto;
+  margin: 0;
+  white-space: pre-wrap;
+  word-break: break-all;
+  font-size: 12px;
+  line-height: 1.5;
+}
+</style>

+ 3 - 1
vue.config.js

@@ -1,6 +1,7 @@
 'use strict'
 const path = require('path')
 const webpack = require('webpack')
+const { buildAwsSdkAliases } = require('./scripts/aws-sdk-webpack-aliases')
 
 function resolve(dir) {
   return path.join(__dirname, dir)
@@ -73,7 +74,8 @@ module.exports = {
     name: name,
     resolve: {
       alias: {
-        '@': resolve('src')
+        '@': resolve('src'),
+        ...buildAwsSdkAliases(__dirname)
       }
     },
     plugins: [