lk hai 1 día
pai
achega
472597b8dc
Modificáronse 38 ficheiros con 834 adicións e 1523 borrados
  1. 8 0
      adminui/src/api/admin/consumeRecord.js
  2. 17 0
      adminui/src/api/admin/sysCompany.js
  3. 176 24
      adminui/src/views/admin/dashboard/index.vue
  4. 101 106
      adminui/src/views/admin/sysCompany/index.vue
  5. 61 0
      java/fs-admin/src/main/java/com/fs/admin/controller/AdminConsumeRecordController.java
  6. 56 0
      java/fs-admin/src/main/java/com/fs/admin/controller/CompanyAdminController.java
  7. 1 1
      java/fs-admin/src/main/resources/application-dev.yml
  8. 1 1
      java/fs-agent/src/main/resources/application-dev.yml
  9. 1 1
      java/fs-ai-api/src/main/resources/application-dev.yml
  10. 1 1
      java/fs-ai-call-task/src/main/resources/application-ylrz.yml
  11. 1 1
      java/fs-comm-gateway/src/main/resources/application-ylrz.yml
  12. 1 1
      java/fs-company-app/src/main/resources/application-ylrz.yml
  13. 1 1
      java/fs-qw-api-msg/src/main/resources/application-dev.yml
  14. 1 1
      java/fs-qw-api-msg/src/main/resources/application-ylrz.yml
  15. 1 1
      java/fs-qw-api/src/main/resources/application-ylrz.yml
  16. 1 1
      java/fs-saas-admin/src/main/resources/application-dev.yml
  17. 6 0
      java/fs-service/src/main/java/com/fs/billing/mapper/TenantWalletTxnMapper.java
  18. 4 0
      java/fs-service/src/main/java/com/fs/proxy/mapper/TenantConsumeRecordMapper.java
  19. 61 0
      java/fs-service/src/main/java/com/fs/tenant/domain/TenantInitRecord.java
  20. 33 0
      java/fs-service/src/main/java/com/fs/tenant/mapper/TenantInitRecordMapper.java
  21. 118 18
      java/fs-service/src/main/java/com/fs/tenant/service/TenantAsyncService.java
  22. 2 3
      java/fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java
  23. 1 1
      java/fs-service/src/main/resources/application-dev-saasai.yml
  24. 1 1
      java/fs-service/src/main/resources/application-dev-test.yml
  25. 1 1
      java/fs-service/src/main/resources/application-dev.yml
  26. 1 1
      java/fs-service/src/main/resources/application-ylrz.yml
  27. 35 35
      java/fs-service/src/main/resources/db/tenant-initData.sql
  28. 0 389
      java/fs-service/src/main/resources/db/tenant-initTable-migration-lobster-schema-sync.sql
  29. 16 918
      java/fs-service/src/main/resources/db/tenant-initTable-migration.sql
  30. 4 10
      java/fs-service/src/main/resources/db/tenant-initTable.sql
  31. 10 0
      java/fs-service/src/main/resources/mapper/billing/TenantWalletTxnMapper.xml
  32. 8 1
      java/fs-service/src/main/resources/mapper/proxy/TenantConsumeRecordMapper.xml
  33. 1 1
      java/fs-task/src/main/resources/application-dev.yml
  34. 1 1
      java/fs-user-app/src/main/resources/application-dev.yml
  35. 1 1
      java/fs-user-app/src/main/resources/application-prod.yml
  36. 1 1
      java/fs-worker-app/src/main/resources/application-dev.yml
  37. 1 1
      java/fs-wx-api/src/main/resources/application-ylrz.yml
  38. 99 0
      saasmgnui/src/views/system/config/config.vue

+ 8 - 0
adminui/src/api/admin/consumeRecord.js

@@ -9,6 +9,14 @@ export function listConsumeRecords(query) {
   })
 }
 
+// 消费记录汇总统计(消费总额/消费笔数/涉及租户数/代理分佣)
+export function getConsumeRecordSummary() {
+  return request({
+    url: '/admin/consume-record/summary',
+    method: 'get'
+  })
+}
+
 // 获取消费记录详情
 export function getConsumeRecord(id) {
   return request({

+ 17 - 0
adminui/src/api/admin/sysCompany.js

@@ -69,6 +69,23 @@ export function addCompany(data) {
   })
 }
 
+// 重试租户初始化
+export function retryInit(tenantCode) {
+  return request({
+    url: '/admin/company/retryInit',
+    method: 'post',
+    params: { tenantCode }
+  })
+}
+
+// 查询租户初始化步骤详情
+export function getInitRecords(tenantCode) {
+  return request({
+    url: '/admin/company/initRecord/' + tenantCode,
+    method: 'get'
+  })
+}
+
 // 更新租户信息
 export function updateTenant(data) {
   return request({

+ 176 - 24
adminui/src/views/admin/dashboard/index.vue

@@ -45,33 +45,50 @@
         </el-col>
       </el-row>
     </el-card>
-    <!-- 外呼日志统计(按租户) -->
-    <el-card shadow="hover" class="quick-card" style="margin-top: 16px; width: 850px;">
-      <div slot="header" class="card-header">
-        <div class="card-header-left">
-          <div class="card-header-icon" style="background: linear-gradient(135deg, #f59e0b, #fbbf24);">
-            <i class="el-icon-phone-outline" style="color:#fff;font-size:16px"></i>
+    <!-- 统计区:外呼日志统计 + 消费扣款概览 -->
+    <div class="stats-row">
+      <!-- 外呼日志统计(按租户) -->
+      <el-card shadow="hover" class="quick-card stats-call-card">
+        <div slot="header" class="card-header">
+          <div class="card-header-left">
+            <div class="card-header-icon" style="background: linear-gradient(135deg, #f59e0b, #fbbf24);">
+              <i class="el-icon-phone-outline" style="color:#fff;font-size:16px"></i>
+            </div>
+            <span class="card-header-title">外呼日志统计(按租户)</span>
           </div>
-          <span class="card-header-title">外呼日志统计(按租户)</span>
         </div>
-      </div>
-      <el-table v-loading="callStatsLoading" :data="callStats" border size="small">
-        <el-table-column label="租户名称" prop="tenantName" min-width="80" show-overflow-tooltip />
-        <el-table-column label="租户编码" prop="tenantCode" min-width="80" show-overflow-tooltip />
-        <el-table-column label="调用次数" prop="callCount" width="140" align="center" />
-        <el-table-column label="计费金额" prop="totalAmount" width="180" align="right">
-          <template slot-scope="scope">
-            <span>{{ formatMoney(scope.row.totalAmount) }}</span>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-card>
+        <div ref="callChart" v-loading="callStatsLoading" class="call-chart"></div>
+      </el-card>
+
+      <!-- 消费扣款概览 -->
+      <el-card shadow="hover" class="quick-card stats-kpi-card">
+        <div slot="header" class="card-header">
+          <div class="card-header-left">
+            <div class="card-header-icon" style="background: linear-gradient(135deg, #10b981, #34d399);">
+              <i class="el-icon-coin" style="color:#fff;font-size:16px"></i>
+            </div>
+            <span class="card-header-title">消费扣款概览</span>
+          </div>
+        </div>
+        <div class="kpi-grid">
+          <div class="kpi-item" v-for="card in consumeKpiCards" :key="card.label">
+            <div class="kpi-icon" :style="{ background: card.bg, color: card.color }"><i :class="card.icon"></i></div>
+            <div class="kpi-info">
+              <span class="kpi-value">{{ card.value }}</span>
+              <span class="kpi-label">{{ card.label }}</span>
+            </div>
+          </div>
+        </div>
+      </el-card>
+    </div>
+
   </div>
 </template>
 
 <script>
 import { getDashboardData } from '@/api/admin/dashboard'
 import { getCommGatewayLogStats } from '@/api/admin/commGatewayLog'
+import { getConsumeRecordSummary } from '@/api/admin/consumeRecord'
 
 export default {
   name: 'AdminDashboard',
@@ -94,17 +111,31 @@ export default {
         { title: '消费记录', path: '/admin/finance/consumeRecord', icon: 'el-icon-coin', color: '#f59e0b' }
       ],
       callStats: [],
-      callStatsLoading: false
+      callStatsLoading: false,
+      callChart: null,
+      consumeKpiCards: [
+        { label: '消费总额', value: '¥0.00', icon: 'el-icon-wallet', color: '#1890ff', bg: '#e6f7ff' },
+        { label: '消费笔数', value: 0, icon: 'el-icon-document', color: '#fa8c16', bg: '#fff7e6' },
+        { label: '涉及租户数', value: 0, icon: 'el-icon-office-building', color: '#52c41a', bg: '#f6ffed' },
+        { label: '代理分佣', value: '¥0.00', icon: 'el-icon-user-solid', color: '#722ed1', bg: '#f9f0ff' }
+      ]
     }
   },
   mounted() {
     this.updateTime()
     this.timeTimer = setInterval(this.updateTime, 1000)
+    window.addEventListener('resize', this.handleResize)
     this.fetchDashboardData()
     this.fetchCallStats()
+    this.fetchConsumeSummary()
   },
   beforeDestroy() {
     clearInterval(this.timeTimer)
+    window.removeEventListener('resize', this.handleResize)
+    if (this.callChart) {
+      this.callChart.dispose()
+      this.callChart = null
+    }
   },
   methods: {
     async fetchDashboardData() {
@@ -134,17 +165,73 @@ export default {
       try {
         const res = await getCommGatewayLogStats({ apiType: 'call' })
         this.callStats = res.data || []
+        this.$nextTick(() => this.renderCallChart())
       } catch (e) {
         console.error('加载外呼日志统计失败', e)
       } finally {
         this.callStatsLoading = false
       }
     },
-    formatMoney(value) {
-      if (value == null || value === '') {
-        return '¥0.00'
+    renderCallChart() {
+      const el = this.$refs.callChart
+      if (!el || !this.echarts) return
+      if (!this.callChart) {
+        this.callChart = this.echarts.init(el)
+      }
+      const data = this.callStats || []
+      const names = data.map(item => item.tenantName || item.tenantCode || '-')
+      const callCounts = data.map(item => item.callCount || 0)
+      const amounts = data.map(item => Number(item.totalAmount) || 0)
+
+      this.callChart.setOption({
+        tooltip: {
+          trigger: 'axis',
+          axisPointer: { type: 'shadow' },
+          formatter: (params) => {
+            const head = params && params.length ? params[0].name : ''
+            const lines = (params || []).map(p => {
+              if (p.seriesName === '计费金额') {
+                return `${p.marker}${p.seriesName}:¥${Number(p.value).toFixed(2)}`
+              }
+              return `${p.marker}${p.seriesName}:${p.value}`
+            })
+            return `${head}<br/>${lines.join('<br/>')}`
+          }
+        },
+        legend: { data: ['调用次数', '计费金额'] },
+        grid: { left: 8, right: 8, top: 40, bottom: 8, containLabel: true },
+        xAxis: {
+          type: 'category',
+          data: names,
+          axisLabel: { interval: 0, rotate: names.length > 4 ? 30 : 0 }
+        },
+        yAxis: [
+          { type: 'value', name: '调用次数', minInterval: 1 },
+          { type: 'value', name: '计费金额', axisLabel: { formatter: '¥{value}' } }
+        ],
+        series: [
+          { name: '调用次数', type: 'bar', data: callCounts, barMaxWidth: 28, itemStyle: { color: '#3b82f6' } },
+          { name: '计费金额', type: 'bar', yAxisIndex: 1, data: amounts, barMaxWidth: 28, itemStyle: { color: '#10b981' } }
+        ]
+      })
+    },
+    handleResize() {
+      if (this.callChart) {
+        this.callChart.resize()
+      }
+    },
+    async fetchConsumeSummary() {
+      try {
+        const res = await getConsumeRecordSummary()
+        const data = res.data
+        if (!data) return
+        this.consumeKpiCards[0].value = data.totalAmount != null ? '¥' + Number(data.totalAmount).toFixed(2) : '¥0.00'
+        this.consumeKpiCards[1].value = data.totalCount != null ? data.totalCount : 0
+        this.consumeKpiCards[2].value = data.tenantCount != null ? data.tenantCount : 0
+        this.consumeKpiCards[3].value = data.totalCommission != null ? '¥' + Number(data.totalCommission).toFixed(2) : '¥0.00'
+      } catch (e) {
+        console.error('加载消费扣款概览失败', e)
       }
-      return '¥' + Number(value).toFixed(2)
     },
     updateTime() {
       const now = new Date()
@@ -273,4 +360,69 @@ export default {
   font-weight: 600;
   color: #1e293b;
 }
+.stats-row {
+  display: flex;
+  align-items: stretch;
+  flex-wrap: wrap;
+  gap: 16px;
+  margin-top: 16px;
+}
+.stats-call-card {
+  width: 840px;
+  flex-shrink: 0;
+}
+.call-chart {
+  width: 100%;
+  height: 320px;
+}
+.stats-kpi-card {
+  flex: 1;
+  min-width: 320px;
+  display: flex;
+  flex-direction: column;
+}
+.stats-kpi-card ::v-deep .el-card__body {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+.kpi-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-auto-rows: 1fr;
+  gap: 12px;
+  flex: 1;
+}
+.kpi-item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 16px 12px;
+  border-radius: 12px;
+  background: #f8fafc;
+}
+.kpi-icon {
+  width: 60px;
+  height: 60px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 28px;
+  flex-shrink: 0;
+}
+.kpi-info {
+  min-width: 0;
+}
+.kpi-value {
+  display: block;
+  font-size: 26px;
+  font-weight: 700;
+  color: #1e293b;
+  line-height: 1.2;
+}
+.kpi-label {
+  font-size: 15px;
+  color: #94a3b8;
+}
 </style>

+ 101 - 106
adminui/src/views/admin/sysCompany/index.vue

@@ -52,36 +52,52 @@
       <el-table-column label="企微用户数" align="center" prop="qwUserCount" min-width="90" />
       <el-table-column label="过期时间" align="center" prop="expireTime" min-width="100" />
       <el-table-column label="归属代理" align="center" prop="proxyName" min-width="90" />
-      <el-table-column label="状态" align="center" min-width="70">
+      <el-table-column label="状态" align="center" min-width="90">
         <template slot-scope="scope">
           <el-tag v-if="scope.row.status == 1" type="success" size="mini">正常</el-tag>
-          <el-tag v-else type="danger" size="mini">禁用</el-tag>
+          <el-tag v-else-if="scope.row.status == 2" type="warning" size="mini">初始化中</el-tag>
+          <el-tag v-else-if="scope.row.status == 3" type="danger" size="mini">初始化失败</el-tag>
+          <el-tag v-else type="info" size="mini">禁用</el-tag>
         </template>
       </el-table-column>
       <el-table-column label="操作" align="center" width="520" fixed="right" class-name="small-padding fixed-width">
         <template slot-scope="scope">
-          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleView(scope.row)">编辑</el-button>
-          <el-button size="mini" type="text" icon="el-icon-key" style="color:#722ed1" @click="handleResetPwd(scope.row)">重置密码</el-button>
-          <el-button size="mini" type="text" style="color:#52c41a" icon="el-icon-coin" @click="handleRecharge(scope.row)">充值/扣款</el-button>
-          <el-button size="mini" type="text" style="color:#1890ff" icon="el-icon-connection" @click="handleChannelManage(scope.row)" v-hasPermi="['admin:platform:edit']">渠道管理</el-button>
-          <el-button size="mini" type="text" style="color:#1890ff" icon="el-icon-s-operation" @click="handleExecutionConfig(scope.row)" v-hasPermi="['admin:platform:edit']">执行策略</el-button>
-          <el-button size="mini" type="text" style="color:#722ed1" icon="el-icon-menu" @click="handleEditMenu(scope.row, 'sys')">管理端菜单</el-button>
-          <el-button size="mini" type="text" style="color:#e6a23c" icon="el-icon-price-tag" @click="handleModulePricing(scope.row)">模块定价</el-button>
-          <el-button size="mini" type="text" style="color:#13c2c2" icon="el-icon-sell" @click="handleEditMenu(scope.row, 'com')">销售菜单</el-button>
+          <template v-if="scope.row.status != 2 && scope.row.status != 3">
+            <el-button size="mini" type="text" icon="el-icon-edit" @click="handleView(scope.row)">编辑</el-button>
+            <el-button size="mini" type="text" icon="el-icon-key" style="color:#722ed1" @click="handleResetPwd(scope.row)">重置密码</el-button>
+            <el-button size="mini" type="text" style="color:#52c41a" icon="el-icon-coin" @click="handleRecharge(scope.row)">充值/扣款</el-button>
+            <el-button size="mini" type="text" style="color:#1890ff" icon="el-icon-connection" @click="handleChannelManage(scope.row)" v-hasPermi="['admin:platform:edit']">渠道管理</el-button>
+            <el-button size="mini" type="text" style="color:#1890ff" icon="el-icon-s-operation" @click="handleExecutionConfig(scope.row)" v-hasPermi="['admin:platform:edit']">执行策略</el-button>
+            <el-button size="mini" type="text" style="color:#722ed1" icon="el-icon-menu" @click="handleEditMenu(scope.row, 'sys')">管理端菜单</el-button>
+            <el-button size="mini" type="text" style="color:#e6a23c" icon="el-icon-price-tag" @click="handleModulePricing(scope.row)">模块定价</el-button>
+            <el-button size="mini" type="text" style="color:#13c2c2" icon="el-icon-sell" @click="handleEditMenu(scope.row, 'com')">销售菜单</el-button>
+            <el-button
+              v-if="scope.row.status == 1"
+              size="mini" type="text" style="color:#fa8c16"
+              icon="el-icon-lock"
+              @click="handleDisable(scope.row)"
+              v-hasPermi="['admin:platform:edit']"
+            >禁用</el-button>
+            <el-button
+              v-else
+              size="mini" type="text" style="color:#52c41a"
+              icon="el-icon-unlock"
+              @click="handleEnable(scope.row)"
+              v-hasPermi="['admin:platform:edit']"
+            >启用</el-button>
+          </template>
           <el-button
-            v-if="scope.row.status == 1"
-            size="mini" type="text" style="color:#fa8c16"
-            icon="el-icon-lock"
-            @click="handleDisable(scope.row)"
-            v-hasPermi="['admin:platform:edit']"
-          >禁用</el-button>
+            v-if="scope.row.status == 3"
+            size="mini" type="text" style="color:#e6a23c"
+            icon="el-icon-refresh"
+            @click="handleRetry(scope.row)"
+          >重试初始化</el-button>
           <el-button
-            v-else
-            size="mini" type="text" style="color:#52c41a"
-            icon="el-icon-unlock"
-            @click="handleEnable(scope.row)"
-            v-hasPermi="['admin:platform:edit']"
-          >启用</el-button>
+            v-if="scope.row.status == 2 || scope.row.status == 3"
+            size="mini" type="text" style="color:#409EFF"
+            icon="el-icon-document"
+            @click="handleInitDetail(scope.row)"
+          >执行详情</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -464,11 +480,36 @@
         <el-button type="primary" :loading="pricingDialog.submitting" size="medium" icon="el-icon-check" @click="submitModulePricing">保 存</el-button>
       </div>
     </el-dialog>
+
+    <!-- ===== 执行详情弹窗 ===== -->
+    <el-dialog title="初始化执行详情" :visible.sync="initDetailDialog.visible" width="780px" append-to-body destroy-on-close>
+      <el-table :data="initDetailDialog.records" border size="small" v-loading="initDetailDialog.loading">
+        <el-table-column label="步骤" prop="stepOrder" align="center" width="60" />
+        <el-table-column label="步骤名称" prop="stepName" min-width="120" />
+        <el-table-column label="状态" align="center" width="100">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.stepStatus === 0" type="info" size="mini">待执行</el-tag>
+            <el-tag v-else-if="scope.row.stepStatus === 1" type="warning" size="mini">执行中</el-tag>
+            <el-tag v-else-if="scope.row.stepStatus === 2" type="success" size="mini">成功</el-tag>
+            <el-tag v-else-if="scope.row.stepStatus === 3" type="danger" size="mini">失败</el-tag>
+            <el-tag v-else type="info" size="mini">未知</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="失败原因" min-width="240" show-overflow-tooltip>
+          <template slot-scope="scope">
+            <span v-if="scope.row.failReason">{{ scope.row.failReason }}</span>
+            <span v-else style="color:#c0c4cc">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="开始时间" prop="startTime" min-width="140" />
+        <el-table-column label="结束时间" prop="endTime" min-width="140" />
+      </el-table>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-import { listAllCompanies, getCompanyInfo, addCompany, updateTenant, disableCompany, enableCompany, rechargeCompany, exportCompany, getTenantMenuTree, editTenantMenu, resetTenantPwd, listLobsterIndustryCatalog } from '@/api/admin/sysCompany'
+import { listAllCompanies, getCompanyInfo, addCompany, retryInit, getInitRecords, updateTenant, disableCompany, enableCompany, rechargeCompany, exportCompany, getTenantMenuTree, editTenantMenu, resetTenantPwd, listLobsterIndustryCatalog } from '@/api/admin/sysCompany'
 import ChannelManage from '@/views/saas/tenant/ChannelManage'
 import ExecutionConfigManage from '@/views/saas/tenant/ExecutionConfigManage'
 import { listTrafficPricing, addTrafficPricing, updateTrafficPricing } from '@/api/admin/trafficPricing'
@@ -487,9 +528,18 @@ export default {
       total: 0,
       companyList: [],
       industryCatalogOptions: [],
+      // 执行详情弹窗
+      initDetailDialog: {
+        visible: false,
+        tenantName: '',
+        loading: false,
+        records: []
+      },
       statusOptions: [
         { value: 1, label: '正常' },
-        { value: 3, label: '禁用' }
+        { value: 2, label: '初始化中' },
+        { value: 3, label: '初始化失败' },
+        { value: 0, label: '禁用' }
       ],
       queryParams: {
         pageNum: 1,
@@ -792,6 +842,33 @@ export default {
         })
       })
     },
+    /** 重试初始化 */
+    handleRetry(row) {
+      this.$confirm('确定重试租户 [' + row.tenantName + '] 的初始化流程吗?', '重试初始化', {
+        confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
+      }).then(() => {
+        retryInit(row.tenantCode).then(() => {
+          this.$message.success('重试初始化已触发,请稍后刷新查看结果')
+          this.getList()
+        }).catch(() => {
+          this.$message.error('重试失败,请稍后再试')
+        })
+      }).catch(() => {})
+    },
+    /** 查看执行详情 */
+    handleInitDetail(row) {
+      this.initDetailDialog.visible = true
+      this.initDetailDialog.tenantName = row.tenantName
+      this.initDetailDialog.loading = true
+      this.initDetailDialog.records = []
+      getInitRecords(row.tenantCode).then(res => {
+        this.initDetailDialog.records = res.rows || []
+      }).catch(() => {
+        this.$message.error('获取执行详情失败')
+      }).finally(() => {
+        this.initDetailDialog.loading = false
+      })
+    },
     handleExport() {
       this.exportLoading = true
       exportCompany(this.queryParams).then(response => {
@@ -902,18 +979,6 @@ export default {
         if (node) node.expanded = false
       })
     },
-    checkAllMenuNodes() {
-      const tree = this.$refs.menuTree
-      if (!tree) return
-      const allKeys = []
-      this.walkMenuTreeNodes(this.menuDialog.treeData, n => allKeys.push(n.menuId))
-      tree.setCheckedKeys(allKeys)
-    },
-    uncheckAllMenuNodes() {
-      const tree = this.$refs.menuTree
-      if (!tree) return
-      tree.setCheckedKeys([])
-    },
     onMenuDialogOpened() {
       if (this.menuDialog.companyId) {
         this.loadTenantMenuTree()
@@ -1010,76 +1075,6 @@ export default {
       })
     },
     /** 提交模块定价 */
-    submitModulePricing() {
-      this.pricingDialog.submitting = true
-      const tenantId = this.pricingDialog.tenantId
-      const promises = this.pricingDialog.modules.map(m => {
-        const data = {
-          tenantId: tenantId,
-          serviceType: m.serviceType,
-          price: m.price,
-          costPrice: m.costPrice,
-          status: m.status
-        }
-        if (m.id) {
-          data.id = m.id
-          return updateTrafficPricing(data)
-        } else {
-          return addTrafficPricing(data)
-        }
-      })
-      Promise.all(promises).then(() => {
-        this.$message.success('模块定价保存成功')
-        this.pricingDialog.visible = false
-      }).catch(() => {
-        this.$message.error('部分定价保存失败,请重试')
-      }).finally(() => {
-        this.pricingDialog.submitting = false
-      })
-    },
-    /** 模块定价 */
-    handleModulePricing(row) {
-      this.pricingDialog = {
-        visible: true,
-        tenantId: row.id,
-        tenantName: row.tenantName,
-        loading: true,
-        submitting: false,
-        modules: []
-      }
-      // 从收费配置API获取启用的模块列表(排除手拨外呼serviceType=6)
-      Promise.all([
-        listServiceCost(),
-        listTrafficPricing({ tenantId: row.id, pageSize: 100 })
-      ]).then(([costRes, pricingRes]) => {
-        const costList = (costRes.data || []).filter(c => c.enabled === 1 && c.serviceType !== 6)
-        const tenantPricings = pricingRes.rows || []
-        const pricingMap = {}
-        tenantPricings.forEach(p => { pricingMap[p.serviceType] = p })
-        this.pricingDialog.modules = costList.map(c => {
-          const existing = pricingMap[c.serviceType]
-          // 如果租户定价为空或为0,则使用全局定价作为默认值
-          const tenantPrice = existing ? existing.price : null
-          const tenantCostPrice = existing ? existing.costPrice : null
-          return {
-            serviceType: c.serviceType,
-            moduleName: c.configName,
-            unit: c.feeUnit,
-            globalPrice: c.feeStandard,
-            globalCost: c.platformCost,
-            price: tenantPrice || c.feeStandard,
-            costPrice: tenantCostPrice || c.platformCost,
-            status: existing ? existing.status : 1,
-            id: existing ? existing.id : null
-          }
-        })
-        this.pricingDialog.loading = false
-      }).catch(() => {
-        this.pricingDialog.loading = false
-        this.$message.error('加载定价数据失败')
-      })
-    },
-    /** 提交模块定价 */
     submitModulePricing() {
       this.pricingDialog.submitting = true
       const tenantId = this.pricingDialog.tenantId

+ 61 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/AdminConsumeRecordController.java

@@ -1,6 +1,8 @@
 package com.fs.admin.controller;
 
+import java.math.BigDecimal;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -15,11 +17,13 @@ import com.fs.common.annotation.Log;
 import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.redis.RedisCache;
 import com.fs.common.enums.BusinessType;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.company.domain.Company;
 import com.fs.company.mapper.CompanyMapper;
+import com.fs.proxy.mapper.TenantConsumeRecordMapper;
 import com.fs.tenant.domain.TenantInfo;
 import com.fs.tenant.mapper.TenantInfoMapper;
 import com.github.pagehelper.PageInfo;
@@ -50,6 +54,63 @@ public class AdminConsumeRecordController extends BaseController {
     @Autowired
     private TenantInfoMapper tenantInfoMapper;
 
+    @Autowired
+    private TenantConsumeRecordMapper tenantConsumeRecordMapper;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /** 消费记录汇总统计缓存 key(自动加系统租户前缀) */
+    private static final String CONSUME_SUMMARY_CACHE_KEY = "admin:consume-record:summary";
+
+    /**
+     * 消费记录汇总统计(总后台首页看板消费KPI概览),结果缓存 1 小时。
+     * 返回:totalAmount 消费总额、totalCount 消费笔数、tenantCount 涉及租户数、totalCommission 代理分佣
+     */
+    @PreAuthorize("@ss.hasPermi('admin:consumeRecord:list')")
+    @GetMapping("/summary")
+    public AjaxResult summary() {
+        Map<String, Object> cached = redisCache.getCacheObject(CONSUME_SUMMARY_CACHE_KEY);
+        if (cached != null) {
+            return AjaxResult.success(cached);
+        }
+
+        Map<String, Object> result = buildConsumeSummary();
+        redisCache.setCacheObject(CONSUME_SUMMARY_CACHE_KEY, result, 1, TimeUnit.HOURS);
+        return AjaxResult.success(result);
+    }
+
+    private Map<String, Object> buildConsumeSummary() {
+        Map<String, Object> walletSummary = tenantWalletTxnMapper.summaryConsumeRecords();
+
+        BigDecimal totalAmount = BigDecimal.ZERO;
+        long totalCount = 0L;
+        long tenantCount = 0L;
+        if (walletSummary != null) {
+            if (walletSummary.get("total_amount") != null) {
+                totalAmount = new BigDecimal(String.valueOf(walletSummary.get("total_amount")));
+            }
+            if (walletSummary.get("total_count") != null) {
+                totalCount = ((Number) walletSummary.get("total_count")).longValue();
+            }
+            if (walletSummary.get("tenant_count") != null) {
+                tenantCount = ((Number) walletSummary.get("tenant_count")).longValue();
+            }
+        }
+
+        BigDecimal totalCommission = tenantConsumeRecordMapper.sumCommissionAll();
+        if (totalCommission == null) {
+            totalCommission = BigDecimal.ZERO;
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalAmount", totalAmount);
+        result.put("totalCount", totalCount);
+        result.put("tenantCount", tenantCount);
+        result.put("totalCommission", totalCommission);
+        return result;
+    }
+
     @PreAuthorize("@ss.hasPermi('admin:consumeRecord:list')")
     @GetMapping("/list")
     public TableDataInfo list(@RequestParam(required = false) Long tenantId,

+ 56 - 0
java/fs-admin/src/main/java/com/fs/admin/controller/CompanyAdminController.java

@@ -1,5 +1,6 @@
 package com.fs.admin.controller;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.fs.admin.annotation.TenantDS;
 import com.fs.common.annotation.Log;
 import com.fs.common.annotation.RepeatSubmit;
@@ -20,7 +21,10 @@ import com.fs.proxy.domain.ProxyTenantRel;
 import com.fs.proxy.service.BalanceService;
 import com.fs.proxy.service.ProxyTenantRelService;
 import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.domain.TenantInitRecord;
 import com.fs.tenant.mapper.TenantInfoMapper;
+import com.fs.tenant.mapper.TenantInitRecordMapper;
+import com.fs.tenant.service.TenantAsyncService;
 import com.fs.tenant.service.TenantInfoService;
 import com.fs.company.service.workflow.config.LobsterIndustryCatalog;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -52,6 +56,12 @@ public class CompanyAdminController extends BaseController {
     @Autowired
     private TenantInfoMapper tenantInfoMapper;
 
+    @Autowired
+    private TenantAsyncService tenantAsyncService;
+
+    @Autowired
+    private TenantInitRecordMapper tenantInitRecordMapper;
+
     @Autowired
     private TenantDataSourceManager tenantDataSourceManager;
 
@@ -115,6 +125,52 @@ public class CompanyAdminController extends BaseController {
         return i > 0 ? R.ok("租户数据库初始化中,请稍后") : R.error("租户创建失败");
     }
 
+    /**
+     * 重试失败的租户初始化
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:edit')")
+    @Log(title = "租户管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/retryInit")
+    public R retryInit(@RequestParam String tenantCode) {
+        TenantInfo tenantInfo = tenantInfoMapper.selectTenantInfoByCode(tenantCode);
+        if (tenantInfo == null) {
+            return R.error("租户不存在,tenantCode=" + tenantCode);
+        }
+        if (Objects.equals(tenantInfo.getStatus(), 1)) {
+            return R.error("租户已是启用状态,无需重试");
+        }
+        if (Objects.equals(tenantInfo.getStatus(), 2)) {
+            return R.error("租户初始化正在执行中,请稍后重试");
+        }
+        if (!Objects.equals(tenantInfo.getStatus(), 3)) {
+            return R.error("只有初始化失败的租户才能重试,当前状态=" + tenantInfo.getStatus());
+        }
+
+        // 重置为“初始化中”,异步重跑失败的步骤
+        TenantInfo resetInfo = new TenantInfo();
+        resetInfo.setStatus(2);
+        tenantInfoMapper.update(resetInfo, new LambdaQueryWrapper<TenantInfo>()
+                .eq(TenantInfo::getTenantCode, tenantCode));
+
+        List<SysMenu> sysMenus = tenantInfoMapper.selectMenuList(new SysMenu());
+        List<TenantCompanyMenu> companyMenus = tenantInfoMapper.selectCompanyMenuList(new TenantCompanyMenu());
+        tenantAsyncService.asyncInit(tenantInfo, sysMenus, companyMenus);
+        return R.ok("重试初始化已触发,请稍后查看结果");
+    }
+
+    /**
+     * 查询租户初始化步骤执行详情
+     */
+    @PreAuthorize("@ss.hasPermi('admin:company:query')")
+    @GetMapping("/initRecord/{tenantCode}")
+    public R initRecord(@PathVariable String tenantCode) {
+        List<TenantInitRecord> records = tenantInitRecordMapper.selectList(
+                new LambdaQueryWrapper<TenantInitRecord>()
+                        .eq(TenantInitRecord::getTenantCode, tenantCode)
+                        .orderByAsc(TenantInitRecord::getStepOrder));
+        return R.ok().put("rows", records);
+    }
+
     /**
      * 修改租户
      */

+ 1 - 1
java/fs-admin/src/main/resources/application-dev.yml

@@ -91,7 +91,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-agent/src/main/resources/application-dev.yml

@@ -94,7 +94,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-ai-api/src/main/resources/application-dev.yml

@@ -91,7 +91,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-ai-call-task/src/main/resources/application-ylrz.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-comm-gateway/src/main/resources/application-ylrz.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-company-app/src/main/resources/application-ylrz.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-qw-api-msg/src/main/resources/application-dev.yml

@@ -91,7 +91,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-qw-api-msg/src/main/resources/application-ylrz.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-qw-api/src/main/resources/application-ylrz.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-saas-admin/src/main/resources/application-dev.yml

@@ -94,7 +94,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 6 - 0
java/fs-service/src/main/java/com/fs/billing/mapper/TenantWalletTxnMapper.java

@@ -46,5 +46,11 @@ public interface TenantWalletTxnMapper extends BaseMapper<TenantWalletTxn> {
      * 汇总所有租户的充值总额(仅 ADMIN_RECHARGE,amount 为正)。
      */
     BigDecimal sumTotalRechargeAll();
+
+    /**
+     * 消费记录汇总统计(txn_type='CONSUME',排除管理员手动扣款 ADMIN_DEDUCT)。
+     * 返回字段:total_amount 消费总额、total_count 消费笔数、tenant_count 涉及租户数。
+     */
+    Map<String, Object> summaryConsumeRecords();
 }
 

+ 4 - 0
java/fs-service/src/main/java/com/fs/proxy/mapper/TenantConsumeRecordMapper.java

@@ -1,5 +1,6 @@
 package com.fs.proxy.mapper;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@@ -53,4 +54,7 @@ public interface TenantConsumeRecordMapper extends BaseMapper<TenantConsumeRecor
 
     /** 按租户+流水号查询(幂等扣费) */
     TenantConsumeRecord selectByTenantAndOrderNo(@Param("tenantId") Long tenantId, @Param("orderNo") String orderNo);
+
+    /** 汇总所有已完成消费记录的代理分佣总额(proxy_profit) */
+    BigDecimal sumCommissionAll();
 }

+ 61 - 0
java/fs-service/src/main/java/com/fs/tenant/domain/TenantInitRecord.java

@@ -0,0 +1,61 @@
+package com.fs.tenant.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 租户初始化流程步骤执行记录对象 tenant_init_record
+ *
+ * @author fs
+ * @date 2026-08-14
+ */
+@Data
+@TableName(value = "tenant_init_record")
+public class TenantInitRecord {
+
+    /** 主键ID */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 租户ID,关联tenant_info.id */
+    private Long tenantId;
+
+    /** 租户编码 */
+    private String tenantCode;
+
+    /** 步骤序号 */
+    private Integer stepOrder;
+
+    /** 步骤名称 */
+    private String stepName;
+
+    /** 步骤编码 */
+    private String stepCode;
+
+    /** 步骤状态:0-待执行,1-执行中,2-成功,3-失败 */
+    private Integer stepStatus;
+
+    /** 失败原因 */
+    private String failReason;
+
+    /** 开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    /** 结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date endTime;
+
+    /** 创建时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 更新时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updateTime;
+}

+ 33 - 0
java/fs-service/src/main/java/com/fs/tenant/mapper/TenantInitRecordMapper.java

@@ -0,0 +1,33 @@
+package com.fs.tenant.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.tenant.domain.TenantInitRecord;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
+
+/**
+ * 租户初始化流程步骤执行记录Mapper
+ *
+ * @author fs
+ * @date 2026-08-14
+ */
+public interface TenantInitRecordMapper extends BaseMapper<TenantInitRecord> {
+
+    /**
+     * 更新步骤状态为执行中(支持待执行0→1 和 失败重试3→1)
+     */
+    @Update("update tenant_init_record set step_status = 1, fail_reason = null, start_time = now(), end_time = null, update_time = now() where id = #{id} and step_status in (0, 3)")
+    int startOrRetryStep(@Param("id") Long id);
+
+    /**
+     * 标记步骤成功
+     */
+    @Update("update tenant_init_record set step_status = 2, end_time = now(), update_time = now() where id = #{id}")
+    int markStepSuccess(@Param("id") Long id);
+
+    /**
+     * 标记步骤失败
+     */
+    @Update("update tenant_init_record set step_status = 3, fail_reason = #{failReason}, end_time = now(), update_time = now() where id = #{id}")
+    int markStepFail(@Param("id") Long id, @Param("failReason") String failReason);
+}

+ 118 - 18
java/fs-service/src/main/java/com/fs/tenant/service/TenantAsyncService.java

@@ -3,13 +3,16 @@ package com.fs.tenant.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fs.common.exception.CustomException;
 import com.fs.common.core.domain.entity.SysMenu;
 
 import com.fs.common.core.domain.entity.TenantCompanyMenu;
 import com.fs.tenant.domain.TenantExecuteTask;
 import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.domain.TenantInitRecord;
 import com.fs.tenant.domain.TenantSqlExecuteRecord;
 import com.fs.tenant.mapper.TenantInfoMapper;
+import com.fs.tenant.mapper.TenantInitRecordMapper;
 import com.fs.utils.TenantUtils;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -22,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
 
 import java.nio.charset.StandardCharsets;
 import java.sql.Connection;
+import java.util.Date;
 import java.util.List;
 
 @Service
@@ -31,34 +35,130 @@ public class TenantAsyncService {
     @Autowired
     private TenantInfoMapper tenantInfoMapper;
     @Autowired
+    private TenantInitRecordMapper tenantInitRecordMapper;
+    @Autowired
     private ITenantSqlExecuteRecordService tenantSqlExecuteRecordService;
 
     @Async
-    public void asyncInit(TenantInfo tenantInfo, List<SysMenu> menus,List<TenantCompanyMenu> companyMenus) {
+    public void asyncInit(TenantInfo tenantInfo, List<SysMenu> menus, List<TenantCompanyMenu> companyMenus) {
+        String tenantCode = tenantInfo.getTenantCode();
         try {
-            TenantUtils.initTables(tenantInfo.getDbIp(), tenantInfo.getDbPort(),tenantInfo.getDbName(), tenantInfo.getDbAccount(), tenantInfo.getDbPwd());
-            TenantUtils.initMenus(tenantInfo.getDbIp(), tenantInfo.getDbPort(),tenantInfo.getDbName(), tenantInfo.getDbAccount(), tenantInfo.getDbPwd(),menus);
-            TenantUtils.initCompanyMenus(tenantInfo.getDbIp(), tenantInfo.getDbPort(),tenantInfo.getDbName(), tenantInfo.getDbAccount(), tenantInfo.getDbPwd(),companyMenus);
-            if (tenantInfo.getAdminUserName() != null && !tenantInfo.getAdminUserName().isEmpty()
-                    && tenantInfo.getAdminPassword() != null && !tenantInfo.getAdminPassword().isEmpty()) {
-                TenantUtils.createTenantAdminUser(tenantInfo.getDbIp(), tenantInfo.getDbPort(),
-                        tenantInfo.getDbName(), tenantInfo.getDbAccount(), tenantInfo.getDbPwd(),
-                        tenantInfo.getAdminUserName(), tenantInfo.getAdminPassword());
+            // 首次初始化时创建步骤记录,重试时复用已有记录(只重跑失败/待执行的步骤)
+            ensureInitRecords(tenantInfo);
+
+            boolean allSuccess = true;
+            List<TenantInitRecord> records = tenantInitRecordMapper.selectList(
+                    new LambdaQueryWrapper<TenantInitRecord>()
+                            .eq(TenantInitRecord::getTenantCode, tenantCode)
+                            .orderByAsc(TenantInitRecord::getStepOrder));
+
+            for (TenantInitRecord record : records) {
+                // 已成功的步骤跳过
+                if (Integer.valueOf(2).equals(record.getStepStatus())) {
+                    continue;
+                }
+
+                try {
+                    tenantInitRecordMapper.startOrRetryStep(record.getId());
+                    executeStepByCode(record.getStepCode(), tenantInfo, menus, companyMenus);
+                    tenantInitRecordMapper.markStepSuccess(record.getId());
+                    log.info("租户初始化步骤[{}]执行成功,tenantCode={}", record.getStepName(), tenantCode);
+                } catch (Exception e) {
+                    String rawReason = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
+                    String failReason = rawReason.length() > 2000 ? rawReason.substring(0, 2000) + "..." : rawReason;
+                    tenantInitRecordMapper.markStepFail(record.getId(), failReason);
+                    allSuccess = false;
+                    log.error("租户初始化步骤[{}]执行失败,tenantCode={}", record.getStepName(), tenantCode, e);
+                    break;
+                }
             }
-            tenantInfo.setStatus(1);
-            LambdaQueryWrapper<TenantInfo> queryWrapper = new LambdaQueryWrapper<>();
-            queryWrapper.eq(TenantInfo::getTenantCode, tenantInfo.getTenantCode());
-            tenantInfoMapper.update(tenantInfo, queryWrapper);
+
+            updateTenantStatus(tenantCode, allSuccess ? 1 : 3);
         } catch (Exception e) {
-            tenantInfo.setStatus(3);
-            LambdaQueryWrapper<TenantInfo> queryWrapper = new LambdaQueryWrapper<>();
-            queryWrapper.eq(TenantInfo::getTenantCode, tenantInfo.getTenantCode());
-            tenantInfoMapper.update(tenantInfo, queryWrapper);
-            log.error("初始化失败:{}", e.getMessage());
+            log.error("租户初始化全局异常,tenantCode={}", tenantCode, e);
+            try {
+                updateTenantStatus(tenantCode, 3);
+            } catch (Exception ignore) {
+                log.error("更新租户状态为初始化失败时异常,tenantCode={}", tenantCode, ignore);
+            }
             throw new RuntimeException(e);
         }
     }
 
+    /**
+     * 确保步骤记录存在(首次初始化时创建,重试时复用)
+     */
+    private void ensureInitRecords(TenantInfo tenantInfo) {
+        Integer count = tenantInitRecordMapper.selectCount(new LambdaQueryWrapper<TenantInitRecord>()
+                .eq(TenantInitRecord::getTenantCode, tenantInfo.getTenantCode()));
+        if (count != null && count > 0) {
+            return;
+        }
+
+        Date now = new Date();
+        String[][] steps = {
+                {"1", "初始化表结构与数据", "INIT_TABLES"},
+                {"2", "初始化管理端菜单", "INIT_MENUS"},
+                {"3", "初始化销售端菜单", "INIT_COMPANY_MENUS"},
+                {"4", "创建管理员账号", "CREATE_ADMIN_USER"}
+        };
+        for (String[] step : steps) {
+            TenantInitRecord record = new TenantInitRecord();
+            record.setTenantId(tenantInfo.getId());
+            record.setTenantCode(tenantInfo.getTenantCode());
+            record.setStepOrder(Integer.valueOf(step[0]));
+            record.setStepName(step[1]);
+            record.setStepCode(step[2]);
+            record.setStepStatus(0);
+            record.setCreateTime(now);
+            record.setUpdateTime(now);
+            tenantInitRecordMapper.insert(record);
+        }
+    }
+
+    /**
+     * 根据步骤编码执行对应的初始化逻辑
+     */
+    private void executeStepByCode(String stepCode, TenantInfo tenantInfo,
+                                   List<SysMenu> menus, List<TenantCompanyMenu> companyMenus) throws Exception {
+        String ip = tenantInfo.getDbIp();
+        String port = tenantInfo.getDbPort();
+        String dbName = tenantInfo.getDbName();
+        String account = tenantInfo.getDbAccount();
+        String pwd = tenantInfo.getDbPwd();
+
+        switch (stepCode) {
+            case "INIT_TABLES":
+                TenantUtils.initTables(ip, port, dbName, account, pwd);
+                break;
+            case "INIT_MENUS":
+                TenantUtils.initMenus(ip, port, dbName, account, pwd, menus);
+                break;
+            case "INIT_COMPANY_MENUS":
+                TenantUtils.initCompanyMenus(ip, port, dbName, account, pwd, companyMenus);
+                break;
+            case "CREATE_ADMIN_USER":
+                if (tenantInfo.getAdminUserName() != null && !tenantInfo.getAdminUserName().isEmpty()
+                        && tenantInfo.getAdminPassword() != null && !tenantInfo.getAdminPassword().isEmpty()) {
+                    TenantUtils.createTenantAdminUser(ip, port, dbName, account, pwd,
+                            tenantInfo.getAdminUserName(), tenantInfo.getAdminPassword());
+                }
+                break;
+            default:
+                throw new CustomException("未知初始化步骤编码:" + stepCode);
+        }
+    }
+
+    /**
+     * 更新租户初始化状态
+     */
+    private void updateTenantStatus(String tenantCode, Integer status) {
+        TenantInfo updateInfo = new TenantInfo();
+        updateInfo.setStatus(status);
+        tenantInfoMapper.update(updateInfo, new LambdaQueryWrapper<TenantInfo>()
+                .eq(TenantInfo::getTenantCode, tenantCode));
+    }
+
     // 异步执行
     @Async
     public void executeSqlScriptAsync(MultipartFile file, List<TenantExecuteTask> tasks) {

+ 2 - 3
java/fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java

@@ -166,9 +166,8 @@ public class TenantInfoServiceImpl extends ServiceImpl<TenantInfoMapper, TenantI
 
             tenantInfo.setDbUrl(TenantUtils.buildJdbcUrl(tenantInfo.getDbIp(),tenantInfo.getDbPort(), tenantInfo.getDbName()));
             tenantInfo.setCreateTime(DateUtils.getNowDate());
-            if (tenantInfo.getStatus() == null) {
-                tenantInfo.setStatus(1);
-            }
+            // 新增租户统一进入“初始化中”状态,异步初始化完成后再置为 1(成功)或 3(失败)
+            tenantInfo.setStatus(2);
             if (tenantInfo.getBalance() == null) {
                 tenantInfo.setBalance(BigDecimal.ZERO);
             }

+ 1 - 1
java/fs-service/src/main/resources/application-dev-saasai.yml

@@ -85,7 +85,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-service/src/main/resources/application-dev-test.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-service/src/main/resources/application-dev.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-service/src/main/resources/application-ylrz.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 35 - 35
java/fs-service/src/main/resources/db/tenant-initData.sql

@@ -5737,62 +5737,62 @@ VALUES (1, '开始节点', 'start', '自动生成触发条件和变量初始化'
        (100, '外部API', 'external_api', '调用外部API接口', 'external', 100);
 -- . 插入系统菜单(代理管理 / admin:proxy:* 权限体系)
 --    父菜单
-INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES
-('代理管理', 0, 2, 'proxy', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'user', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), '代理管理目录');
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES
+('代理管理', 0, 2, 'proxy', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'user', 'admin', now(), '', now(), '代理管理目录');
 
 --    子菜单 - 代理列表
-INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
-SELECT '代理列表', m.menu_id, 1, 'list', 'admin/proxy/index', NULL, 1, 0, 'C', '0', '0', 'admin:proxy:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), '代理列表菜单'
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理列表', m.menu_id, 1, 'list', 'admin/proxy/index', NULL, 1, 0, 'C', '0', '0', 'admin:proxy:list', '#', 'admin', now(), '', now(), '代理列表菜单'
 FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
 
 --    按钮 - 新增
-INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
-SELECT '代理新增', m.menu_id, 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理新增', m.menu_id, 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:add', '#', 'admin', now(), '', now(), ''
 FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
 
 --    按钮 - 编辑
-INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
-SELECT '代理编辑', m.menu_id, 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理编辑', m.menu_id, 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:edit', '#', 'admin', now(), '', now(), ''
 FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
 
 --    按钮 - 删除
-INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
-SELECT '代理删除', m.menu_id, 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理删除', m.menu_id, 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:remove', '#', 'admin', now(), '', now(), ''
 FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
 
 --    按钮 - 查询(用于详情)
-INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
-SELECT '代理查询', m.menu_id, 5, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:query', '#', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), ''
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`)
+SELECT '代理查询', m.menu_id, 5, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:proxy:query', '#', 'admin', now(), '', now(), ''
 FROM `sys_menu` m WHERE m.menu_name = '代理管理' AND m.parent_id = 0 AND m.perms IS NULL AND m.menu_type = 'M';
 -- 新增菜单
-INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `is_show`, `link_type`, `link_url`, `sort`, `app_id`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES
+INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES
 -- 供应商管理
-('供应商管理', 0, 5, 'supplier', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'tree', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), '供应商管理目录'),
-('供应商列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/supplier/index', NULL, 1, 0, 'C', '0', '0', 'admin:supplier:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), ''),
-('供应商新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''),
-('供应商编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''),
-('供应商删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''),
-('供应商查询', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 5, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:query', '#', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), ''),
+('供应商管理', 0, 5, 'supplier', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'tree', 'admin', now(), '', now(), '供应商管理目录'),
+('供应商列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/supplier/index', NULL, 1, 0, 'C', '0', '0', 'admin:supplier:list', '#', 'admin', now(), '', now(), ''),
+('供应商新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:add', '#', 'admin', now(), '', now(), ''),
+('供应商编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:edit', '#', 'admin', now(), '', now(), ''),
+('供应商删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:remove', '#', 'admin', now(), '', now(), ''),
+('供应商查询', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='供应商管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 5, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:supplier:query', '#', 'admin', now(), '', now(), ''),
 -- 设备类型管理
-('设备类型管理', 0, 6, 'deviceType', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'component', b'1', b'0', NULL, 6, NULL, 'admin', now(), '', now(), '设备类型管理目录'),
-('设备类型列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/deviceType/index', NULL, 1, 0, 'C', '0', '0', 'admin:deviceType:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), ''),
-('设备类型新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''),
-('设备类型编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''),
-('设备类型删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''),
+('设备类型管理', 0, 6, 'deviceType', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'component', 'admin', now(), '', now(), '设备类型管理目录'),
+('设备类型列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/deviceType/index', NULL, 1, 0, 'C', '0', '0', 'admin:deviceType:list', '#', 'admin', now(), '', now(), ''),
+('设备类型新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:add', '#', 'admin', now(), '', now(), ''),
+('设备类型编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:edit', '#', 'admin', now(), '', now(), ''),
+('设备类型删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceType:remove', '#', 'admin', now(), '', now(), ''),
 -- 设备厂商管理
-('设备厂商管理', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 5, 'manufacturer', 'admin/deviceManufacturer/index', NULL, 1, 0, 'C', '0', '0', 'admin:deviceManufacturer:list', '#', b'1', b'0', NULL, 5, NULL, 'admin', now(), '', now(), ''),
-('设备厂商新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 6, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:add', '#', b'1', b'0', NULL, 6, NULL, 'admin', now(), '', now(), ''),
-('设备厂商编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 7, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:edit', '#', b'1', b'0', NULL, 7, NULL, 'admin', now(), '', now(), ''),
-('设备厂商删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 8, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:remove', '#', b'1', b'0', NULL, 8, NULL, 'admin', now(), '', now(), ''),
+('设备厂商管理', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 5, 'manufacturer', 'admin/deviceManufacturer/index', NULL, 1, 0, 'C', '0', '0', 'admin:deviceManufacturer:list', '#', 'admin', now(), '', now(), ''),
+('设备厂商新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 6, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:add', '#', 'admin', now(), '', now(), ''),
+('设备厂商编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 7, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:edit', '#', 'admin', now(), '', now(), ''),
+('设备厂商删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='设备类型管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 8, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:deviceManufacturer:remove', '#', 'admin', now(), '', now(), ''),
 -- 租户套餐管理
-('套餐管理', 0, 3, 'tenantPackage', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'peoples', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), '套餐管理目录'),
-('套餐列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/tenantPackage/index', NULL, 1, 0, 'C', '0', '0', 'admin:tenantPackage:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), ''),
-('套餐新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:add', '#', b'1', b'0', NULL, 2, NULL, 'admin', now(), '', now(), ''),
-('套餐编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:edit', '#', b'1', b'0', NULL, 3, NULL, 'admin', now(), '', now(), ''),
-('套餐删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:remove', '#', b'1', b'0', NULL, 4, NULL, 'admin', now(), '', now(), ''),
+('套餐管理', 0, 3, 'tenantPackage', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'peoples', 'admin', now(), '', now(), '套餐管理目录'),
+('套餐列表', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'list', 'admin/tenantPackage/index', NULL, 1, 0, 'C', '0', '0', 'admin:tenantPackage:list', '#', 'admin', now(), '', now(), ''),
+('套餐新增', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 2, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:add', '#', 'admin', now(), '', now(), ''),
+('套餐编辑', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 3, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:edit', '#', 'admin', now(), '', now(), ''),
+('套餐删除', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='套餐管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 4, NULL, NULL, NULL, 1, 0, 'F', '0', '0', 'admin:tenantPackage:remove', '#', 'admin', now(), '', now(), ''),
 -- 财务总账
-('财务管理', 0, 8, 'finance', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'money', b'1', b'0', NULL, 8, NULL, 'admin', now(), '', now(), '财务管理目录'),
-('财务总账', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='财务管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'ledger', 'admin/finance/ledger', NULL, 1, 0, 'C', '0', '0', 'admin:finance:list', '#', b'1', b'0', NULL, 1, NULL, 'admin', now(), '', now(), '');
+('财务管理', 0, 8, 'finance', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'money', 'admin', now(), '', now(), '财务管理目录'),
+('财务总账', (SELECT menu_id FROM (SELECT menu_id FROM sys_menu WHERE menu_name='财务管理' AND parent_id=0 AND menu_type='M' LIMIT 1) AS t), 1, 'ledger', 'admin/finance/ledger', NULL, 1, 0, 'C', '0', '0', 'admin:finance:list', '#', 'admin', now(), '', now(), '');
 -- 进货分成菜单
 INSERT INTO `sys_menu` (`menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `remark`)
 VALUES ('进货分成', 0, 9, 'purchaseSplit', NULL, NULL, 1, 0, 'M', '0', '0', NULL, 'el-icon-s-cooperation', 'admin', NOW(), '代理端进货分成查询');

+ 0 - 389
java/fs-service/src/main/resources/db/tenant-initTable-migration-lobster-schema-sync.sql

@@ -1,11 +1,7 @@
 -- =============================================
 -- Lobster schema sync for existing tenant DBs
--- Mirrors fs-agent db/migration/tenant V202606* DDL changes
--- Safe to re-run (column/index existence checks)
 -- =============================================
 
-SET @dbname = DATABASE();
-
 -- lobster_execution_config
 CREATE TABLE IF NOT EXISTS `lobster_execution_config` (
     `id` bigint NOT NULL AUTO_INCREMENT,
@@ -62,294 +58,6 @@ CREATE TABLE IF NOT EXISTS `lobster_user_preference` (
     UNIQUE KEY `uk_company_channel_user` (`company_id`, `channel_type`, `external_user_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='龙虾用户偏好';
 
--- helper macro: add column if missing
--- company_workflow_lobster.*
-SET @tablename = 'company_workflow_lobster';
-SET @columnname = 'enable_content_personalization';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster ADD COLUMN enable_content_personalization tinyint DEFAULT NULL AFTER execution_mode'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'enable_flow_personalization';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster ADD COLUMN enable_flow_personalization tinyint DEFAULT NULL AFTER enable_content_personalization'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'strict_fixed_workflow';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster ADD COLUMN strict_fixed_workflow tinyint DEFAULT NULL AFTER enable_flow_personalization'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'auto_start_instance_on_tag';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster ADD COLUMN auto_start_instance_on_tag tinyint DEFAULT NULL AFTER strict_fixed_workflow'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'template_kind';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster ADD COLUMN template_kind varchar(20) NOT NULL DEFAULT ''MASTER'' AFTER auto_start_instance_on_tag'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'source_template_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster ADD COLUMN source_template_id bigint DEFAULT NULL AFTER template_kind'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='company_workflow_lobster' AND INDEX_NAME='idx_lobster_template_kind')>0,'SELECT 1','CREATE INDEX idx_lobster_template_kind ON company_workflow_lobster (company_id, template_kind, del_flag)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- company_tag_template_binding.*
-SET @tablename = 'company_tag_template_binding';
-SET @columnname = 'exclude_tags';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_tag_template_binding ADD COLUMN exclude_tags varchar(2000) DEFAULT NULL AFTER match_condition'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'target_channels';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_tag_template_binding ADD COLUMN target_channels varchar(500) DEFAULT NULL AFTER exclude_tags'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'source_template_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_tag_template_binding ADD COLUMN source_template_id bigint DEFAULT NULL AFTER template_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'task_template_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_tag_template_binding ADD COLUMN task_template_id bigint DEFAULT NULL AFTER source_template_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='company_tag_template_binding' AND INDEX_NAME='idx_tag_binding_task_template')>0,'SELECT 1','CREATE INDEX idx_tag_binding_task_template ON company_tag_template_binding (company_id, task_template_id, del_flag)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- tag user rel / task channel columns
-SET @tablename = 'company_lobster_tag_user_rel';
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_tag_user_rel ADD COLUMN channel_type varchar(30) NOT NULL DEFAULT ''QW'' AFTER template_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'channel_contact_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_tag_user_rel ADD COLUMN channel_contact_id bigint DEFAULT NULL AFTER channel_type'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'channel_user_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_tag_user_rel ADD COLUMN channel_user_id varchar(128) DEFAULT NULL AFTER channel_contact_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'channel_context_json';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_tag_user_rel ADD COLUMN channel_context_json text AFTER channel_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='company_lobster_tag_user_rel' AND INDEX_NAME='idx_lobster_rel_channel_contact')>0,'SELECT 1','CREATE INDEX idx_lobster_rel_channel_contact ON company_lobster_tag_user_rel (company_id, channel_type, channel_contact_id, del_flag)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'company_workflow_lobster_task';
-SET @columnname = 'external_contact_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster_task ADD COLUMN external_contact_id bigint DEFAULT NULL AFTER binding_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster_task ADD COLUMN channel_type varchar(30) NOT NULL DEFAULT ''QW'' AFTER external_contact_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'channel_contact_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster_task ADD COLUMN channel_contact_id bigint DEFAULT NULL AFTER channel_type'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'channel_context_json';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_workflow_lobster_task ADD COLUMN channel_context_json text AFTER channel_contact_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- memory layer channel_type (profile P2 + multi-channel)
-SET @tablename = 'customer_fact';
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE customer_fact ADD COLUMN channel_type varchar(20) NOT NULL DEFAULT ''QW'' AFTER external_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'lobster_user_profile';
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_user_profile ADD COLUMN channel_type varchar(20) NOT NULL DEFAULT ''QW'' AFTER external_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'lobster_user_preference';
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename)=0 OR (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_user_preference ADD COLUMN channel_type varchar(20) NOT NULL DEFAULT ''QW'' AFTER external_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'customer_habit';
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE customer_habit ADD COLUMN channel_type varchar(20) NOT NULL DEFAULT ''QW'' AFTER external_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'lobster_conversation_summary';
-SET @columnname = 'external_user_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN external_user_id varchar(128) DEFAULT NULL AFTER contact_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN channel_type varchar(20) NOT NULL DEFAULT ''QW'' AFTER external_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'lobster_dialogue_state';
-SET @columnname = 'external_user_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_dialogue_state ADD COLUMN external_user_id varchar(128) NOT NULL DEFAULT '''' AFTER instance_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_dialogue_state ADD COLUMN channel_type varchar(20) NOT NULL DEFAULT ''QW'' AFTER external_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'detected_intent';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_dialogue_state ADD COLUMN detected_intent varchar(50) DEFAULT NULL AFTER node_code'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'sentiment';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_dialogue_state ADD COLUMN sentiment varchar(20) DEFAULT NULL AFTER detected_intent'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'last_reply';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_dialogue_state ADD COLUMN last_reply varchar(500) DEFAULT NULL AFTER sentiment'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'create_time';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_dialogue_state ADD COLUMN create_time datetime DEFAULT CURRENT_TIMESTAMP'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- profile config field_type
-SET @tablename = 'company_lobster_profile_config';
-SET @columnname = 'field_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_profile_config ADD COLUMN field_type varchar(32) DEFAULT NULL AFTER field_label'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- evolution suggestion extras
-SET @tablename = 'lobster_evolution_suggestion';
-SET @columnname = 'channel_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_evolution_suggestion ADD COLUMN channel_type varchar(20) DEFAULT ''QW'' AFTER node_code'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'previous_content';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_evolution_suggestion ADD COLUMN previous_content text AFTER suggested_content'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'sample_size';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_evolution_suggestion ADD COLUMN sample_size int DEFAULT NULL AFTER confidence'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_chat_record' AND INDEX_NAME='idx_company_channel_ext_time')>0,'SELECT 1','ALTER TABLE lobster_chat_record ADD KEY idx_company_channel_ext_time (company_id, channel_type, external_user_id, message_time)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_conversation_summary' AND INDEX_NAME='idx_company_channel_ext')>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD KEY idx_company_channel_ext (company_id, channel_type, external_user_id)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- lobster_conversation_summary: auto-summary + 11-dim extended fields
-SET @tablename = 'lobster_conversation_summary';
-SET @columnname = 'summary_text';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN summary_text text DEFAULT NULL COMMENT ''自动摘要原始文本'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'chat_msg_count';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN chat_msg_count int DEFAULT NULL COMMENT ''聊天消息计数'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'key_intents';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN key_intents text DEFAULT NULL COMMENT ''关键意图JSON'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'key_variables';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN key_variables text DEFAULT NULL COMMENT ''关键变量JSON'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'customer_concerns';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN customer_concerns text DEFAULT NULL COMMENT ''客户关心点 JSON数组'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'customer_attitude';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN customer_attitude varchar(32) DEFAULT NULL COMMENT ''客户态度'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'customer_intention_level';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN customer_intention_level varchar(16) DEFAULT NULL COMMENT ''客户意向度 none/weak/medium/strong'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'customer_questions';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN customer_questions text DEFAULT NULL COMMENT ''客户疑问 JSON数组'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'extended_summary';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN extended_summary text DEFAULT NULL COMMENT ''扩展摘要 11维度JSON'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'stage';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN stage varchar(64) DEFAULT NULL COMMENT ''当前阶段'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'next_action_hint';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_conversation_summary ADD COLUMN next_action_hint text DEFAULT NULL COMMENT ''下一步行动提示'''));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_evolution_suggestion' AND INDEX_NAME='idx_company_channel_node')>0,'SELECT 1','ALTER TABLE lobster_evolution_suggestion ADD KEY idx_company_channel_node (company_id, channel_type, node_code)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'lobster_workflow_instance';
-SET @columnname = 'binding_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_workflow_instance ADD COLUMN binding_id bigint DEFAULT NULL AFTER workflow_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_workflow_instance' AND INDEX_NAME='idx_lobster_instance_binding')>0,'SELECT 1','CREATE INDEX idx_lobster_instance_binding ON lobster_workflow_instance (company_id, binding_id, del_flag)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'dept_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_workflow_instance' AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_workflow_instance ADD COLUMN dept_id bigint DEFAULT NULL COMMENT ''分公司/部门ID(创建时快照)'' AFTER channel_type'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'company_user_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_workflow_instance' AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_workflow_instance ADD COLUMN company_user_id bigint DEFAULT NULL COMMENT ''销售 company_user.user_id'' AFTER dept_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'qw_user_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_workflow_instance' AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_workflow_instance ADD COLUMN qw_user_id bigint DEFAULT NULL COMMENT ''企微号 qw_user.id'' AFTER company_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'wx_account_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_workflow_instance' AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_workflow_instance ADD COLUMN wx_account_id bigint DEFAULT NULL COMMENT ''个微号 company_wx_account.id'' AFTER qw_user_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_workflow_instance' AND INDEX_NAME='idx_lobster_instance_scope')>0,'SELECT 1','CREATE INDEX idx_lobster_instance_scope ON lobster_workflow_instance (company_id, channel_type, company_user_id, del_flag)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_workflow_instance' AND INDEX_NAME='idx_lobster_instance_qw')>0,'SELECT 1','CREATE INDEX idx_lobster_instance_qw ON lobster_workflow_instance (company_id, qw_user_id, del_flag)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_workflow_instance' AND INDEX_NAME='idx_lobster_instance_wx')>0,'SELECT 1','CREATE INDEX idx_lobster_instance_wx ON lobster_workflow_instance (company_id, wx_account_id, del_flag)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'lobster_learning_replay_buffer';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND INDEX_NAME='idx_company_quality_time')>0,'SELECT 1','ALTER TABLE lobster_learning_replay_buffer ADD KEY idx_company_quality_time (company_id, quality_score, create_time)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- lobster_execution_config: drop task-level policy columns (moved to task_copy)
-SET @tablename = 'lobster_execution_config';
-SET @columnname = 'enable_content_personalization';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'ALTER TABLE lobster_execution_config DROP COLUMN enable_content_personalization','SELECT 1'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'enable_flow_personalization';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'ALTER TABLE lobster_execution_config DROP COLUMN enable_flow_personalization','SELECT 1'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'strict_fixed_workflow';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'ALTER TABLE lobster_execution_config DROP COLUMN strict_fixed_workflow','SELECT 1'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- lobster_execution_config: message dedup columns (V20260626)
-SET @tablename = 'lobster_execution_config';
-SET @columnname = 'message_dedup_enabled';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_execution_config ADD COLUMN message_dedup_enabled tinyint NOT NULL DEFAULT 1 COMMENT ''1=启用出站消息去重'' AFTER quality_multiturn_min_avg_score_percent'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'message_dedup_exact_window_size';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_execution_config ADD COLUMN message_dedup_exact_window_size int NOT NULL DEFAULT 5 COMMENT ''精确去重窗口条数'' AFTER message_dedup_enabled'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'message_dedup_semantic_threshold';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_execution_config ADD COLUMN message_dedup_semantic_threshold decimal(4,2) NOT NULL DEFAULT 0.85 COMMENT ''语义相似阈值'' AFTER message_dedup_exact_window_size'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'message_dedup_window_duration_seconds';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_execution_config ADD COLUMN message_dedup_window_duration_seconds int NOT NULL DEFAULT 300 COMMENT ''去重窗口时长(秒)'' AFTER message_dedup_semantic_threshold'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
 -- lobster_tenant_keywords (V20260627)
 CREATE TABLE IF NOT EXISTS `lobster_tenant_keywords` (
     `id` bigint NOT NULL AUTO_INCREMENT,
@@ -389,74 +97,14 @@ CREATE TABLE IF NOT EXISTS `lobster_sales_corpus_task` (
     KEY `idx_batch_no` (`batch_no`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销冠语料导入任务';
 
-SET @tablename = 'lobster_sales_corpus';
-SET @columnname = 'customer_question';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN customer_question text NULL COMMENT ''客户问题'' AFTER dialog_text'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'sales_answer';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN sales_answer text NULL COMMENT ''销冠回答'' AFTER customer_question'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'task_id';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN task_id bigint NULL COMMENT ''导入任务ID'' AFTER company_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'salesperson_name';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN salesperson_name varchar(100) NULL COMMENT ''销冠姓名'' AFTER task_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'status';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN status varchar(32) NULL COMMENT ''条目状态'' AFTER tags'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'quality_status';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN quality_status varchar(32) NULL COMMENT ''质量状态'' AFTER status'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'optimized_answer';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN optimized_answer text NULL COMMENT ''优化后回答'' AFTER quality_status'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'ai_feedback';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN ai_feedback text NULL COMMENT ''AI分析反馈'' AFTER optimized_answer'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'kb_written';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN kb_written tinyint DEFAULT 0 COMMENT ''是否已写入知识库'' AFTER ai_feedback'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'corpus_kind';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN corpus_kind varchar(32) DEFAULT ''sales_champion'' COMMENT ''sales_champion|industry_matrix'' AFTER company_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'industry_type';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE lobster_sales_corpus ADD COLUMN industry_type varchar(64) DEFAULT NULL COMMENT ''行业分类'' AFTER corpus_kind'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
 UPDATE lobster_sales_corpus SET corpus_kind = 'sales_champion'
 WHERE corpus_kind IS NULL OR corpus_kind = '';
 
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_sales_corpus' AND INDEX_NAME='idx_corpus_kind_industry')>0,'SELECT 1','CREATE INDEX idx_corpus_kind_industry ON lobster_sales_corpus (company_id, corpus_kind, industry_type, scenario)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='lobster_sales_corpus' AND INDEX_NAME='idx_task_id')>0,'SELECT 1','CREATE INDEX idx_task_id ON lobster_sales_corpus (task_id)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
--- company_tag_template_binding.task_name (UI display)
-SET @tablename = 'company_tag_template_binding';
-SET @columnname = 'task_name';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_tag_template_binding ADD COLUMN task_name varchar(128) DEFAULT NULL COMMENT ''执行任务名称'' AFTER company_id'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
 -- backfill empty workflow variable codes (AI preview save var_code NOT NULL)
 UPDATE company_workflow_lobster_variable
 SET var_code = CONCAT('var_', id)
 WHERE del_flag = 0 AND (var_code IS NULL OR TRIM(var_code) = '');
 
--- Note: customer_profile_identity + 32 projection + 32 event shards are also created by
--- ProfileSchemaSqlHelper during TenantUtils.initTables and LobsterProfileSchemaBootstrapService on tenant switch.
-
 -- =============================================
 -- Customer profile auxiliary tables (missing on legacy tenant DBs)
 -- =============================================
@@ -590,43 +238,6 @@ CREATE TABLE IF NOT EXISTS `customer_profile_identity` (
     KEY `idx_shard` (`company_id`, `shard_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
--- customer_fact / profile_config 扩展列(老租户补齐)
-SET @tablename = 'customer_fact';
-SET @columnname = 'fact_source';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE customer_fact ADD COLUMN fact_source varchar(20) NOT NULL DEFAULT ''manual'' COMMENT ''事实来源 manual/regex/ai_inferred'' AFTER fact_type'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'extract_confidence';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE customer_fact ADD COLUMN extract_confidence double DEFAULT NULL COMMENT ''AI抽取置信度'' AFTER fact_source'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='customer_fact' AND INDEX_NAME='idx_company_user_source')>0,'SELECT 1','ALTER TABLE customer_fact ADD INDEX idx_company_user_source (company_id, external_user_id, fact_source)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @tablename = 'company_lobster_profile_config';
-SET @columnname = 'field_source';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_profile_config ADD COLUMN field_source varchar(20) NOT NULL DEFAULT ''manual'' COMMENT ''字段来源'' AFTER field_type'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'importance_score';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_profile_config ADD COLUMN importance_score int NOT NULL DEFAULT 0 COMMENT ''重要性评分'' AFTER field_source'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'ai_confidence_threshold';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_profile_config ADD COLUMN ai_confidence_threshold double NOT NULL DEFAULT 0.6 COMMENT ''AI抽取最低置信度'' AFTER importance_score'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'last_ai_extract_time';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_profile_config ADD COLUMN last_ai_extract_time datetime DEFAULT NULL COMMENT ''最近AI抽取时间'' AFTER ai_confidence_threshold'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @columnname = 'hit_count';
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME=@tablename AND COLUMN_NAME=@columnname)>0,'SELECT 1','ALTER TABLE company_lobster_profile_config ADD COLUMN hit_count int NOT NULL DEFAULT 0 COMMENT ''被命中次数'' AFTER last_ai_extract_time'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
-SET @preparedStatement = (SELECT IF((SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA=@dbname AND TABLE_NAME='company_lobster_profile_config' AND INDEX_NAME='idx_company_source')>0,'SELECT 1','ALTER TABLE company_lobster_profile_config ADD INDEX idx_company_source (company_id, field_source)'));
-PREPARE s FROM @preparedStatement; EXECUTE s; DEALLOCATE PREPARE s;
-
 -- lobster_vector_store (semantic pattern matching cache)
 CREATE TABLE IF NOT EXISTS `lobster_vector_store` (
     `id` bigint NOT NULL AUTO_INCREMENT,

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 16 - 918
java/fs-service/src/main/resources/db/tenant-initTable-migration.sql


+ 4 - 10
java/fs-service/src/main/resources/db/tenant-initTable.sql

@@ -1124,6 +1124,7 @@ CREATE TABLE `chat_session`
     `unread_count`     int NULL DEFAULT 0 COMMENT '未读数',
     `instance_id`      bigint NULL DEFAULT NULL COMMENT '工作流实例ID',
     `external_user_id` varchar(128) NULL DEFAULT NULL COMMENT '外部用户ID',
+    `qw_external_user_id` bigint NULL DEFAULT NULL COMMENT '企微外部用户ID',
     PRIMARY KEY (`session_id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '会话表' ROW_FORMAT = DYNAMIC;
 
@@ -2284,11 +2285,13 @@ CREATE TABLE `company_user`
     `im_nick_name`             varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '健康管家昵称,用于App IM显示',
     `invitation_code`          varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邀请码',
     `cid_server_id`            bigint NULL DEFAULT NULL COMMENT 'cid服务id',
+    `analyse_data`             text NULL COMMENT '分析数据',
     `ai_sip_call_user_id`      bigint NULL DEFAULT NULL COMMENT 'sip外呼绑定用户id',
     `expire_time`              datetime DEFAULT NULL COMMENT '激活码到期时间',
     `app_use_voice_id` int NULL DEFAULT NULL COMMENT 'type0存储克隆音色表id,其他存储company_app_voice_info表id',
     `app_use_voice_type` int NULL DEFAULT NULL COMMENT '0、克隆音色,其他对应app_voice_type字典',
     `app_call_interval` int NULL DEFAULT NULL COMMENT '设备自动外呼间隔,单位秒',
+    `mp_subscribed` int(1) NULL DEFAULT 0 COMMENT '是否已订阅公众号通知 (0未订阅 1已订阅)',
     PRIMARY KEY (`user_id`) USING BTREE,
     UNIQUE INDEX `username`(`user_name`) USING BTREE,
     INDEX                      `idcard`(`id_card`) USING BTREE
@@ -14557,6 +14560,7 @@ CREATE TABLE `qw_external_contact_info`
     `product_talk`        varchar(255)   NULL DEFAULT NULL COMMENT '产品交流',
     `disease_talk`        varchar(255)   NULL DEFAULT NULL COMMENT '疾病交流',
     `phone`               varchar(255)   NULL DEFAULT NULL COMMENT '手机号',
+    `user_info`           text NULL DEFAULT NULL COMMENT '用户信息(沟通摘要等)',
     PRIMARY KEY (`id`) USING BTREE,
     UNIQUE INDEX `external_contact_id`(`external_contact_id` ASC) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 COMMENT = '外部联系人信息表' ROW_FORMAT = DYNAMIC;
@@ -18850,8 +18854,6 @@ CREATE TABLE `crm_customer_call_app_log`
     INDEX              `idx_call_device_card`(`call_device_id`, `call_card_id`) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'APP客户直接外呼通话记录' ROW_FORMAT = DYNAMIC;
 
-ALTER TABLE company_user ADD COLUMN mp_subscribed INT(1) DEFAULT 0 COMMENT '是否已订阅公众号通知 (0未订阅 1已订阅)';
-
 CREATE TABLE IF NOT EXISTS lobster_workflow_node_type (
     id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID',
     node_type INT NOT NULL COMMENT '节点类型编码',
@@ -19096,10 +19098,6 @@ CREATE TABLE `company_lobster_profile_config` (
     create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
     update_by VARCHAR(64),
     update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    importance_score INT NOT NULL DEFAULT 0 COMMENT '重要性评分',
-    ai_confidence_threshold DOUBLE NOT NULL DEFAULT 0.6 COMMENT 'AI抽取最低置信度',
-    last_ai_extract_time DATETIME NULL DEFAULT NULL COMMENT '最近AI抽取时间',
-    hit_count INT NOT NULL DEFAULT 0 COMMENT '被命中次数',
     KEY idx_company (company_id),
     KEY idx_company_field (company_id, field_key),
     KEY idx_company_source (company_id, field_source)
@@ -21157,10 +21155,6 @@ CREATE TABLE `qw_user_license`
     INDEX                `idx_status`(`status` ASC) USING BTREE
 ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '企微用户许可激活码表' ROW_FORMAT = DYNAMIC;
 
-ALTER TABLE qw_external_contact_info ADD COLUMN `user_info` text DEFAULT NULL COMMENT '用户信息(沟通摘要等)';
-ALTER TABLE chat_session ADD COLUMN qw_external_user_id BIGINT DEFAULT null COMMENT '企微外部用户ID';
-
-
 CREATE TABLE `company_user_license` (
     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
     `order_id` varchar(255) DEFAULT NULL COMMENT '订单号',

+ 10 - 0
java/fs-service/src/main/resources/mapper/billing/TenantWalletTxnMapper.xml

@@ -60,4 +60,14 @@
         WHERE biz_type = 'ADMIN_RECHARGE'
     </select>
 
+    <select id="summaryConsumeRecords" resultType="java.util.Map">
+        SELECT
+            IFNULL(SUM(ABS(amount)), 0) AS total_amount,
+            COUNT(1) AS total_count,
+            COUNT(DISTINCT tenant_id) AS tenant_count
+        FROM tenant_wallet_txn
+        WHERE txn_type = 'CONSUME'
+          AND biz_type != 'ADMIN_DEDUCT'
+    </select>
+
 </mapper>

+ 8 - 1
java/fs-service/src/main/resources/mapper/proxy/TenantConsumeRecordMapper.xml

@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.proxy.mapper.TenantConsumeRecordMapper">
 
@@ -233,4 +233,11 @@
         group by consume_type
     </select>
 
+    <select id="sumCommissionAll" resultType="java.math.BigDecimal">
+        select ifnull(sum(r.proxy_profit), 0)
+        from tenant_consume_record r
+        inner join proxy_tenant_rel ptr on r.tenant_id = ptr.tenant_id and ptr.status = 1
+        where r.status = 1
+    </select>
+
 </mapper>

+ 1 - 1
java/fs-task/src/main/resources/application-dev.yml

@@ -80,7 +80,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 initialSize: 5
                 minIdle: 10
                 maxActive: 20

+ 1 - 1
java/fs-user-app/src/main/resources/application-dev.yml

@@ -85,7 +85,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-user-app/src/main/resources/application-prod.yml

@@ -81,7 +81,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-worker-app/src/main/resources/application-dev.yml

@@ -93,7 +93,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 1 - 1
java/fs-wx-api/src/main/resources/application-ylrz.yml

@@ -79,7 +79,7 @@ spring:
                 master:
                     url: jdbc:mysql://129.28.164.235:3306/easycallcenter365?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                     username: root
-                    password: easycallcenter365
+                    password: easycallYl@123..
                 # 初始连接数
                 initialSize: 5
                 # 最小连接池数量

+ 99 - 0
saasmgnui/src/views/system/config/config.vue

@@ -3,6 +3,88 @@
     <div class="config__tabs-card">
       <el-tabs v-model="activeName" @tab-click="handleClick">
 
+      <el-tab-pane label="OSS配置" name="sys.oss.cloudStorage">
+          <el-form ref="form5" :model="form5" :rules="rules5" label-width="160px">
+            <el-form-item label="类型" prop="type">
+               <el-radio-group v-model="form5.type">
+                <el-radio :label="1">七牛云</el-radio>
+                <el-radio :label="2">阿里云</el-radio>
+                <el-radio :label="3">腾讯云</el-radio>
+                <el-radio :label="4">华为云</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item v-if="form5.type==1" label="七牛绑定的域名" prop="qiniuDomain">
+                <el-input  v-model="form5.qiniuDomain"  label="请输入七牛绑定的域名"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==1" label="七牛路径前缀" prop="qiniuPrefix">
+                <el-input  v-model="form5.qiniuPrefix" label="请输入七牛路径前缀"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==1" label="七牛ACCESS_KEY" prop="qiniuAccessKey">
+                <el-input  v-model="form5.qiniuAccessKey" label="请输入七牛ACCESS_KEY"></el-input>
+            </el-form-item>
+            <el-form-item v-if="form5.type==1" label="七牛SecretKey" prop="qiniuSecretKey">
+                <el-input  v-model="form5.qiniuSecretKey" label="七牛SecretKey不能为空"></el-input>
+            </el-form-item>
+            <el-form-item v-if="form5.type==1" label="七牛空间名" prop="qiniuBucketName">
+                <el-input  v-model="form5.qiniuBucketName" label="七牛空间名不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==2" label="阿里云绑定的域名" prop="aliyunDomain">
+                <el-input  v-model="form5.aliyunDomain" label="阿里云绑定的域名不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==2" label="阿里云路径前缀" prop="aliyunPrefix">
+                <el-input  v-model="form5.aliyunPrefix" label="阿里云路径前缀不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==2" label="阿里云EndPoint" prop="aliyunEndPoint">
+                <el-input  v-model="form5.aliyunEndPoint" label="阿里云EndPoint不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==2" label="阿里云AccessKeyId" prop="aliyunAccessKeyId">
+                <el-input  v-model="form5.aliyunAccessKeyId" label="阿里云AccessKeyId不能为空"></el-input>
+            </el-form-item>
+            <el-form-item v-if="form5.type==2" label="阿里云AccessKeySecret" prop="aliyunAccessKeySecret">
+                <el-input  v-model="form5.aliyunAccessKeySecret" label="阿里云AccessKeySecret不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==2" label="阿里云BucketName" prop="aliyunBucketName">
+                <el-input  v-model="form5.aliyunBucketName" label="阿里云BucketName不能为空"></el-input>
+            </el-form-item>
+              <el-form-item v-if="form5.type==3" label="腾讯云绑定的域名" prop="qcloudDomain">
+                <el-input  v-model="form5.qcloudDomain" label="腾讯云绑定的域名格式不正确"></el-input>
+            </el-form-item>
+              <el-form-item v-if="form5.type==3" label="腾讯云前缀" prop="qcloudPrefix">
+                <el-input  v-model="form5.qcloudPrefix" label="腾讯云前缀不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==3" label="腾讯云SecretId" prop="qcloudSecretId">
+                <el-input  v-model="form5.qcloudSecretId" label="腾讯云SecretId不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==3" label="腾讯云SecretKey" prop="qcloudSecretKey">
+                <el-input  v-model="form5.qcloudSecretKey" label="腾讯云SecretKey不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==3" label="腾讯云BucketName" prop="qcloudBucketName">
+                <el-input  v-model="form5.qcloudBucketName" label="腾讯云BucketName不能为空"></el-input>
+            </el-form-item>
+             <el-form-item v-if="form5.type==3" label="所属地区" prop="qcloudRegion">
+                <el-input  v-model="form5.qcloudRegion" label="所属地区不能为空"></el-input>
+            </el-form-item>
+            <el-form-item v-if="form5.type==4" label="华为云绑定的域名" prop="huaweiDomain">
+              <el-input  v-model="form5.huaweiDomain" label="华为云绑定的域名格式不正确"></el-input>
+            </el-form-item>
+            <el-form-item v-if="form5.type==4" label="华为云Endpoint" prop="huaweiEndpoint">
+              <el-input  v-model="form5.huaweiEndpoint" label="华为云Endpoint不能为空"></el-input>
+            </el-form-item>
+            <el-form-item v-if="form5.type==4" label="华为云AK" prop="huaweiAK">
+              <el-input  v-model="form5.huaweiAK" label="华为云AK不能为空"></el-input>
+            </el-form-item>
+            <el-form-item v-if="form5.type==4" label="华为云SK" prop="huaweiSK">
+              <el-input  v-model="form5.huaweiSK" label="华为云SK不能为空"></el-input>
+            </el-form-item>
+            <el-form-item v-if="form5.type==4" label="华为云BucketName" prop="huaweiBucketName">
+              <el-input  v-model="form5.huaweiBucketName" label="华为云BucketName不能为空"></el-input>
+            </el-form-item>
+            <div   class="footer">
+              <el-button type="primary" @click="submitForm5">提  交</el-button>
+            </div>
+          </el-form>
+      </el-tab-pane>
+
       <el-tab-pane label="CID配置" name="cId.config">
         <el-form :model="form40" label-width="200px">
           <el-form-item label="是否开启手机号配置" prop="enablePhoneConfig">
@@ -1619,6 +1701,7 @@ export default {
       form2: {},
       form3: {},
       form4: {},
+      form5: {},
       form6: {},
       form7: {},
       form8: {},
@@ -1756,6 +1839,7 @@ export default {
       // 表单校验
       rules1: {},
       rules3: {},
+      rules5: {},
       rules4: {
         doctorRegister: [
           { required: true, message: '请输入注册协议', trigger: 'blur' }
@@ -1970,6 +2054,9 @@ export default {
             this.loadOutboundGatewayList()
           }
         }
+        if (key == 'sys.oss.cloudStorage') {
+          this.form5 = this.safeParseConfig(configValue, {})
+        }
         if (key == 'his.agreementConfig') {
           this.form3 = this.safeParseConfig(configValue, { ...this.form3 })
         }
@@ -2124,6 +2211,18 @@ export default {
         }
       })
     },
+    submitForm5() {
+      this.$refs['form5'].validate(valid => {
+        if (valid) {
+          var param = { configId: this.configId, configValue: JSON.stringify(this.form5) }
+          updateConfigByKey(param).then(response => {
+            if (response.code === 200) {
+              this.msgSuccess('修改成功')
+            }
+          })
+        }
+      })
+    },
     submitForm7() {
       if (this.form7.isIdVerification  == null){
         this.form7.isIdVerification = 0;

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio