zyp 1 dzień temu
rodzic
commit
49393b10b1

+ 114 - 0
src/api/saas/billing.js

@@ -0,0 +1,114 @@
+import request from '@/utils/request'
+
+// 计费方案
+export function createFeePlan(data) {
+  return request({
+    url: '/api/fee/plan/create',
+    method: 'post',
+    data: data
+  })
+}
+
+export function saveFeePlanItems(data) {
+  return request({
+    url: '/api/fee/plan/item/save',
+    method: 'post',
+    data: data
+  })
+}
+
+export function saveFlowTiers(data) {
+  return request({
+    url: '/api/fee/plan/flow-tier/save',
+    method: 'post',
+    data: data
+  })
+}
+
+export function publishFeePlan(planCode, version) {
+  return request({
+    url: '/api/fee/plan/publish',
+    method: 'post',
+    params: { planCode, version }
+  })
+}
+
+// 租户计费
+export function bindTenantPlan(data) {
+  return request({
+    url: '/api/fee/tenant/bind-plan',
+    method: 'post',
+    data: data
+  })
+}
+
+export function changeTenantBillingMode(data) {
+  return request({
+    url: '/api/fee/tenant/change-billing-mode',
+    method: 'post',
+    data: data
+  })
+}
+
+export function changeTenantType(data) {
+  return request({
+    url: '/api/fee/tenant/change-type',
+    method: 'post',
+    data: data
+  })
+}
+
+// 钱包
+export function getTenantWallet(tenantId) {
+  return request({
+    url: '/api/fee/wallet/' + tenantId,
+    method: 'get'
+  })
+}
+
+export function rechargeTenantWallet(data) {
+  return request({
+    url: '/api/fee/wallet/recharge',
+    method: 'post',
+    data: data
+  })
+}
+
+// 用量事件
+export function reportUsageEvent(data) {
+  return request({
+    url: '/api/fee/usage/report',
+    method: 'post',
+    data: data
+  })
+}
+
+// 账单与明细
+export function listBillingDetailsAdmin(params) {
+  return request({
+    url: '/api/fee/billing/detail/list',
+    method: 'get',
+    params: params
+  })
+}
+
+export function listBillingDetailsMy() {
+  return request({
+    url: '/api/fee/billing/detail/my',
+    method: 'get'
+  })
+}
+
+// 兼容历史页面调用
+export function listBillingDetails(params) {
+  return listBillingDetailsAdmin(params)
+}
+
+export function generateStatement(params) {
+  return request({
+    url: '/api/fee/statement/generate',
+    method: 'post',
+    params: params
+  })
+}
+

+ 713 - 0
src/views/saas/billing/index.vue

@@ -0,0 +1,713 @@
+<template>
+  <div class="app-container billing-page">
+    <el-alert
+      title="使用说明:先选租户,再配置方案与收费项;英文编码为系统识别码,已配中文释义。"
+      type="info"
+      :closable="false"
+      show-icon
+      class="mb16"
+    />
+    <el-card shadow="never" class="mb16">
+      <div slot="header">
+        <span>租户选择</span>
+      </div>
+      <el-form :inline="true" label-width="90px">
+        <el-form-item label="租户">
+          <el-select v-model="currentTenantId" placeholder="请选择租户" filterable style="width: 360px" @change="handleTenantChange">
+            <el-option v-for="item in tenantOptions" :key="item.id" :label="item.tenantName + '(' + item.tenantCode + ')'" :value="item.id" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-tabs v-model="activeTab" type="border-card">
+      <el-tab-pane label="计费方案" name="plan">
+        <el-row :gutter="16">
+          <el-col :span="8">
+            <el-card shadow="never">
+              <div slot="header"><span>创建/发布方案</span></div>
+              <el-alert
+                title="方案编码建议固定(如 STANDARD),版本号递增;发布后租户绑定该版本生效。"
+                type="warning"
+                :closable="false"
+                show-icon
+                class="mb12"
+              />
+              <el-form ref="planForm" :model="planForm" :rules="planRules" label-width="100px">
+                <el-form-item label="方案编码" prop="planCode"><el-input v-model="planForm.planCode" placeholder="例如:STANDARD" /></el-form-item>
+                <el-form-item label="方案名称" prop="planName"><el-input v-model="planForm.planName" placeholder="例如:标准方案" /></el-form-item>
+                <el-form-item label="版本号" prop="version"><el-input-number v-model="planForm.version" :min="1" /></el-form-item>
+                <el-form-item label="备注"><el-input v-model="planForm.remark" type="textarea" :rows="2" placeholder="可填写适用范围、价格说明" /></el-form-item>
+                <el-form-item>
+                  <el-button type="primary" @click="submitCreatePlan">创建草稿</el-button>
+                  <el-button type="success" plain @click="submitPublishPlan">发布</el-button>
+                </el-form-item>
+              </el-form>
+            </el-card>
+          </el-col>
+          <el-col :span="16">
+            <el-card shadow="never" class="mb16">
+              <div slot="header" class="card-header">
+                <span>收费项配置</span>
+                <el-button size="mini" type="primary" plain @click="addPlanItemRow">新增收费项</el-button>
+              </div>
+              <el-alert
+                title="说明:单价按“每计费单位多少元”;Token单位表示“多少Token折算1个计费单位”。"
+                type="info"
+                :closable="false"
+                show-icon
+                class="mb12"
+              />
+              <el-table :data="planItemRows" border class="charge-item-table">
+                <el-table-column label="收费项" min-width="210" show-overflow-tooltip>
+                  <template slot-scope="scope">
+                    <el-select
+                      v-model="scope.row.itemCode"
+                      filterable
+                      placeholder="请选择收费项"
+                      style="width: 100%"
+                      @change="handleChargeItemChange(scope.row)"
+                    >
+                      <el-option-group
+                        v-for="group in chargeItemGroupOptions"
+                        :key="group.label"
+                        :label="group.label"
+                      >
+                        <el-option
+                          v-for="item in group.options"
+                          :key="item.value"
+                          :label="item.label"
+                          :value="item.value"
+                        />
+                      </el-option-group>
+                    </el-select>
+                  </template>
+                </el-table-column>
+                <el-table-column label="计费单位" width="110">
+                  <template slot-scope="scope">
+                    <el-input v-model="scope.row.unit" disabled />
+                  </template>
+                </el-table-column>
+                <el-table-column label="单价(元)" width="140">
+                  <template slot-scope="scope">
+                    <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="4" :step="0.01" :controls="false" />
+                  </template>
+                </el-table-column>
+                <el-table-column label="Token换算单位" width="140" show-overflow-tooltip>
+                  <template slot-scope="scope">
+                    <el-input :value="displayTokenUnit(scope.row.tokenUnit)" disabled />
+                  </template>
+                </el-table-column>
+                <el-table-column label="最小计费单位" width="130">
+                  <template slot-scope="scope">
+                    <el-input :value="displayMinCharge(scope.row.minChargeUnit)" disabled />
+                  </template>
+                </el-table-column>
+                <el-table-column label="启用" width="70">
+                  <template slot-scope="scope"><el-switch v-model="scope.row.enabledFlag" /></template>
+                </el-table-column>
+                <el-table-column label="操作" width="70" align="center">
+                  <template slot-scope="scope"><el-button type="text" @click="removePlanItemRow(scope.$index)">删除</el-button></template>
+                </el-table-column>
+              </el-table>
+              <div class="mt12">
+                <el-button type="primary" @click="submitSaveItems">保存收费项</el-button>
+              </div>
+            </el-card>
+
+            <el-card shadow="never">
+              <div slot="header" class="card-header">
+                <span>流量阶梯配置</span>
+                <el-button size="mini" type="primary" plain @click="addFlowTierRow">新增阶梯</el-button>
+              </div>
+              <el-alert
+                title="说明:预存金额命中区间后,流量按对应单价计费;上限为空表示无上限。"
+                type="info"
+                :closable="false"
+                show-icon
+                class="mb12"
+              />
+              <el-table :data="flowTierRows" border class="flow-tier-table">
+                <el-table-column label="预存下限(元)" width="165">
+                  <template slot-scope="scope"><el-input-number v-model="scope.row.minPrepayAmount" :min="0" :precision="2" :controls="false" /></template>
+                </el-table-column>
+                <el-table-column label="预存上限(元)" width="165">
+                  <template slot-scope="scope"><el-input-number v-model="scope.row.maxPrepayAmount" :min="0" :precision="2" :controls="false" /></template>
+                </el-table-column>
+                <el-table-column label="流量单价(元)" width="150">
+                  <template slot-scope="scope"><el-input-number v-model="scope.row.unitPrice" :min="0" :precision="4" :step="0.01" :controls="false" /></template>
+                </el-table-column>
+                <el-table-column label="排序号" width="100">
+                  <template slot-scope="scope"><el-input-number v-model="scope.row.sortNo" :min="0" :controls="false" /></template>
+                </el-table-column>
+                <el-table-column label="操作" width="80" align="center">
+                  <template slot-scope="scope"><el-button type="text" @click="removeFlowTierRow(scope.$index)">删除</el-button></template>
+                </el-table-column>
+              </el-table>
+              <div class="mt12">
+                <el-button type="primary" @click="submitSaveFlowTiers">保存流量阶梯</el-button>
+              </div>
+            </el-card>
+          </el-col>
+        </el-row>
+      </el-tab-pane>
+
+      <el-tab-pane label="租户绑定与钱包" name="tenantWallet">
+        <el-row :gutter="16">
+          <el-col :span="12">
+            <el-card shadow="never">
+              <div slot="header"><span>租户计费绑定</span></div>
+              <el-alert
+                title="说明:先绑定方案,再决定“预付费/后付费”和“AI/非AI”类型。"
+                type="info"
+                :closable="false"
+                show-icon
+                class="mb12"
+              />
+              <el-form ref="bindFormRef" :model="bindForm" :rules="bindRules" label-width="110px">
+                <el-form-item label="租户ID"><el-input v-model="bindForm.tenantId" disabled /></el-form-item>
+                <el-form-item label="计费模式" prop="billingMode">
+                  <el-select v-model="bindForm.billingMode">
+                    <el-option label="预付费 PREPAID" value="PREPAID" />
+                    <el-option label="后付费 POSTPAID" value="POSTPAID" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="租户类型" prop="tenantType">
+                  <el-select v-model="bindForm.tenantType">
+                    <el-option label="非AI NON_AI" value="NON_AI" />
+                    <el-option label="AI" value="AI" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="方案编码" prop="planCode"><el-input v-model="bindForm.planCode" placeholder="例如:STANDARD" /></el-form-item>
+                <el-form-item label="方案版本" prop="planVersion"><el-input-number v-model="bindForm.planVersion" :min="1" /></el-form-item>
+                <el-form-item>
+                  <el-button type="primary" @click="submitBindPlan">绑定方案</el-button>
+                  <el-button plain @click="submitChangeBillingMode">仅改计费模式</el-button>
+                  <el-button plain @click="submitChangeTenantType">仅改租户类型</el-button>
+                </el-form-item>
+              </el-form>
+            </el-card>
+          </el-col>
+          <el-col :span="12">
+            <el-card shadow="never" class="mb16">
+              <div slot="header" class="card-header">
+                <span>钱包信息</span>
+                <el-button size="mini" type="primary" plain @click="loadWallet">刷新</el-button>
+              </div>
+              <el-descriptions :column="1" border>
+                <el-descriptions-item label="租户ID">{{ walletInfo.tenantId || '-' }}</el-descriptions-item>
+                <el-descriptions-item label="可用余额(元)">{{ walletInfo.balanceAmount || 0 }}</el-descriptions-item>
+                <el-descriptions-item label="累计充值">{{ walletInfo.totalRecharge || 0 }}</el-descriptions-item>
+                <el-descriptions-item label="累计消费">{{ walletInfo.totalCost || 0 }}</el-descriptions-item>
+              </el-descriptions>
+            </el-card>
+            <el-card shadow="never">
+              <div slot="header"><span>钱包充值</span></div>
+              <el-form ref="rechargeFormRef" :model="rechargeForm" :rules="rechargeRules" label-width="100px">
+                <el-form-item label="租户ID"><el-input v-model="rechargeForm.tenantId" disabled /></el-form-item>
+                <el-form-item label="充值金额" prop="amount"><el-input-number v-model="rechargeForm.amount" :min="0.01" :precision="2" :step="100" /></el-form-item>
+                <el-form-item label="业务单号"><el-input v-model="rechargeForm.bizNo" placeholder="例如:线下打款单号" /></el-form-item>
+                <el-form-item label="备注"><el-input v-model="rechargeForm.remark" placeholder="例如:3月续费充值" /></el-form-item>
+                <el-form-item><el-button type="primary" @click="submitRecharge">提交充值</el-button></el-form-item>
+              </el-form>
+            </el-card>
+          </el-col>
+        </el-row>
+      </el-tab-pane>
+
+      <el-tab-pane label="事件上报与账单" name="billing">
+        <el-row :gutter="16">
+          <el-col :span="10">
+            <el-card shadow="never" class="mb16">
+              <div slot="header"><span>用量事件上报</span></div>
+              <el-alert
+                title="说明:此功能主要用于联调/演示,生产场景通常由业务系统自动上报。"
+                type="warning"
+                :closable="false"
+                show-icon
+                class="mb12"
+              />
+              <el-form ref="eventFormRef" :model="eventForm" :rules="eventRules" label-width="120px">
+                <el-form-item label="事件ID" prop="eventId"><el-input v-model="eventForm.eventId" /></el-form-item>
+                <el-form-item label="租户ID"><el-input v-model="eventForm.tenantId" disabled /></el-form-item>
+                <el-form-item label="事件类型" prop="eventType">
+                  <el-select v-model="eventForm.eventType">
+                    <el-option label="流量 (FLOW)" value="FLOW" />
+                    <el-option label="通话 (CALL)" value="CALL" />
+                    <el-option label="SOP Token (TOKEN_SOP)" value="TOKEN_SOP" />
+                    <el-option label="AI回复Token (TOKEN_AI_REPLY)" value="TOKEN_AI_REPLY" />
+                    <el-option label="加微 (ADD_WECHAT)" value="ADD_WECHAT" />
+                    <el-option label="开户 (OPEN_ACCOUNT)" value="OPEN_ACCOUNT" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="子类型"><el-input v-model="eventForm.subType" placeholder="例如:外呼 CALL_OUT、呼入 CALL_IN" /></el-form-item>
+                <el-form-item label="业务ID"><el-input v-model="eventForm.bizId" /></el-form-item>
+                <el-form-item label="用量值" prop="usageValue"><el-input-number v-model="eventForm.usageValue" :min="0" :precision="6" /></el-form-item>
+                <el-form-item label="用量单位"><el-input v-model="eventForm.usageUnit" placeholder="例如:KB / SECOND / TOKEN / 次" /></el-form-item>
+                <el-form-item label="发生时间"><el-date-picker v-model="eventForm.occurredAt" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" /></el-form-item>
+                <el-form-item label="是否AI外呼">
+                  <el-switch v-model="eventForm.isAiCallFlag" />
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" @click="submitReportEvent">上报并计费</el-button>
+                </el-form-item>
+              </el-form>
+            </el-card>
+
+            <el-card shadow="never">
+              <div slot="header"><span>生成账单</span></div>
+              <el-alert
+                title="说明:账单会汇总“未入账明细”,建议先确认时间范围再生成。"
+                type="info"
+                :closable="false"
+                show-icon
+                class="mb12"
+              />
+              <el-form ref="statementFormRef" :model="statementForm" :rules="statementRules" label-width="110px">
+                <el-form-item label="租户ID"><el-input v-model="statementForm.tenantId" disabled /></el-form-item>
+                <el-form-item label="账期类型" prop="periodType">
+                  <el-select v-model="statementForm.periodType">
+                    <el-option label="按月 (MONTH)" value="MONTH" />
+                    <el-option label="按周 (WEEK)" value="WEEK" />
+                    <el-option label="自定义 (CUSTOM)" value="CUSTOM" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="开始时间" prop="periodStart">
+                  <el-date-picker v-model="statementForm.periodStart" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" />
+                </el-form-item>
+                <el-form-item label="结束时间" prop="periodEnd">
+                  <el-date-picker v-model="statementForm.periodEnd" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" />
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" @click="submitGenerateStatement">生成账单</el-button>
+                </el-form-item>
+              </el-form>
+            </el-card>
+          </el-col>
+          <el-col :span="14">
+            <el-card shadow="never">
+              <div slot="header" class="card-header">
+                <span>计费明细</span>
+                <el-button size="mini" type="primary" plain @click="loadBillingDetails">刷新</el-button>
+              </div>
+              <el-table :data="detailList" border height="650">
+                <el-table-column label="事件ID" prop="eventId" min-width="140" />
+                <el-table-column label="类型" prop="eventType" width="120" />
+                <el-table-column label="子类型" prop="subType" width="120" />
+                <el-table-column label="用量" prop="usageValue" width="110" />
+                <el-table-column label="金额" prop="amount" width="110" />
+                <el-table-column label="发生时间" prop="occurredAt" min-width="160" />
+              </el-table>
+            </el-card>
+          </el-col>
+        </el-row>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script>
+import {
+  createFeePlan,
+  saveFeePlanItems,
+  saveFlowTiers,
+  publishFeePlan,
+  bindTenantPlan,
+  changeTenantBillingMode,
+  changeTenantType,
+  getTenantWallet,
+  rechargeTenantWallet,
+  reportUsageEvent,
+  listBillingDetails,
+  generateStatement
+} from '@/api/saas/billing'
+import { tenantList } from '@/api/tenant/tenant'
+
+export default {
+  name: 'SaasBilling',
+  data() {
+    return {
+      activeTab: 'plan',
+      tenantOptions: [],
+      currentTenantId: null,
+      walletInfo: {},
+      detailList: [],
+      chargeItemGroupOptions: [
+        {
+          label: '流量类',
+          options: [
+            { value: 'FLOW_POSTPAID', label: '流量后付费' }
+          ]
+        },
+        {
+          label: '通话类',
+          options: [
+            { value: 'CALL_OUT', label: '外呼通话' },
+            { value: 'CALL_IN', label: '呼入通话' },
+            { value: 'AI_CALL', label: 'AI外呼附加费' }
+          ]
+        },
+        {
+          label: 'Token类',
+          options: [
+            { value: 'SOP_TOKEN', label: 'SOP Token' },
+            { value: 'AI_REPLY_TOKEN', label: 'AI回复Token' }
+          ]
+        },
+        {
+          label: '开户类',
+          options: [
+            { value: 'OPEN_ACCOUNT_NON_AI', label: '开户费-非AI' },
+            { value: 'OPEN_ACCOUNT_AI', label: '开户费-AI' }
+          ]
+        },
+        {
+          label: '其他',
+          options: [
+            { value: 'ADD_WECHAT', label: '加微数量' }
+          ]
+        }
+      ],
+      chargeItemMetaMap: {
+        FLOW_POSTPAID: { unit: 'GB', tokenUnit: null, minChargeUnit: 1, suggestedUnitPrice: 0.2 },
+        CALL_OUT: { unit: 'MIN', tokenUnit: null, minChargeUnit: 1, suggestedUnitPrice: 0.3 },
+        CALL_IN: { unit: 'MIN', tokenUnit: null, minChargeUnit: 1, suggestedUnitPrice: 0.2 },
+        AI_CALL: { unit: 'MIN', tokenUnit: null, minChargeUnit: 1, suggestedUnitPrice: 0.15 },
+        SOP_TOKEN: { unit: 'TOKEN', tokenUnit: 100000, minChargeUnit: 1, suggestedUnitPrice: 1 },
+        AI_REPLY_TOKEN: { unit: 'TOKEN', tokenUnit: 100000, minChargeUnit: 1, suggestedUnitPrice: 1 },
+        ADD_WECHAT: { unit: 'COUNT', tokenUnit: null, minChargeUnit: 1, suggestedUnitPrice: 0.5 },
+        OPEN_ACCOUNT_NON_AI: { unit: 'TIME', tokenUnit: null, minChargeUnit: 1, suggestedUnitPrice: 1000 },
+        OPEN_ACCOUNT_AI: { unit: 'TIME', tokenUnit: null, minChargeUnit: 1, suggestedUnitPrice: 3000 }
+      },
+      planForm: {
+        planCode: 'STANDARD',
+        planName: '标准方案',
+        version: 1,
+        remark: ''
+      },
+      planRules: {
+        planCode: [{ required: true, message: '请输入方案编码', trigger: 'blur' }],
+        planName: [{ required: true, message: '请输入方案名称', trigger: 'blur' }],
+        version: [{ required: true, message: '请输入版本号', trigger: 'change' }]
+      },
+      planItemRows: [
+        { itemCode: 'FLOW_POSTPAID', unit: 'KB', unitPrice: 0.2, tokenUnit: 100000, minChargeUnit: 1, enabledFlag: true },
+        { itemCode: 'CALL_OUT', unit: 'MIN', unitPrice: 0.3, tokenUnit: 100000, minChargeUnit: 1, enabledFlag: true },
+        { itemCode: 'CALL_IN', unit: 'MIN', unitPrice: 0.2, tokenUnit: 100000, minChargeUnit: 1, enabledFlag: true }
+      ],
+      flowTierRows: [
+        { minPrepayAmount: 0, maxPrepayAmount: 100000, unitPrice: 0.1, sortNo: 1 },
+        { minPrepayAmount: 100000, maxPrepayAmount: 200000, unitPrice: 0.08, sortNo: 2 }
+      ],
+      bindForm: {
+        tenantId: null,
+        tenantType: 'NON_AI',
+        billingMode: 'PREPAID',
+        planCode: 'STANDARD',
+        planVersion: 1
+      },
+      originalBindForm: {
+        tenantType: 'NON_AI',
+        billingMode: 'PREPAID'
+      },
+      bindRules: {
+        billingMode: [{ required: true, message: '请选择计费模式', trigger: 'change' }],
+        tenantType: [{ required: true, message: '请选择租户类型', trigger: 'change' }],
+        planCode: [{ required: true, message: '请输入方案编码', trigger: 'blur' }],
+        planVersion: [{ required: true, message: '请输入方案版本', trigger: 'change' }]
+      },
+      rechargeForm: {
+        tenantId: null,
+        amount: 1000,
+        bizNo: '',
+        remark: ''
+      },
+      rechargeRules: {
+        amount: [{ required: true, message: '请输入充值金额', trigger: 'change' }]
+      },
+      eventForm: {
+        eventId: '',
+        tenantId: null,
+        eventType: 'FLOW',
+        subType: '',
+        bizId: '',
+        usageValue: 1,
+        usageUnit: 'KB',
+        occurredAt: '',
+        isAiCallFlag: false
+      },
+      eventRules: {
+        eventId: [{ required: true, message: '请输入事件ID', trigger: 'blur' }],
+        eventType: [{ required: true, message: '请选择事件类型', trigger: 'change' }],
+        usageValue: [{ required: true, message: '请输入用量值', trigger: 'change' }]
+      },
+      statementForm: {
+        tenantId: null,
+        periodType: 'MONTH',
+        periodStart: '',
+        periodEnd: ''
+      },
+      statementRules: {
+        periodType: [{ required: true, message: '请选择账期类型', trigger: 'change' }],
+        periodStart: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
+        periodEnd: [{ required: true, message: '请选择结束时间', trigger: 'change' }]
+      }
+    }
+  },
+  created() {
+    this.loadTenantOptions()
+    this.eventForm.eventId = this.buildEventId()
+  },
+  methods: {
+    buildEventId() {
+      return 'EVT_' + new Date().getTime()
+    },
+    loadTenantOptions() {
+      tenantList({}).then(res => {
+        this.tenantOptions = res.rows || res.data || []
+        if (this.tenantOptions.length > 0) {
+          this.currentTenantId = this.tenantOptions[0].id
+          this.syncTenantIdToForms()
+          this.originalBindForm = {
+            tenantType: this.bindForm.tenantType,
+            billingMode: this.bindForm.billingMode
+          }
+          this.loadWallet()
+          this.loadBillingDetails()
+        }
+      })
+    },
+    syncTenantIdToForms() {
+      this.bindForm.tenantId = this.currentTenantId
+      this.rechargeForm.tenantId = this.currentTenantId
+      this.eventForm.tenantId = this.currentTenantId
+      this.statementForm.tenantId = this.currentTenantId
+    },
+    handleTenantChange() {
+      this.syncTenantIdToForms()
+      this.originalBindForm = {
+        tenantType: this.bindForm.tenantType,
+        billingMode: this.bindForm.billingMode
+      }
+      this.loadWallet()
+      this.loadBillingDetails()
+    },
+    addPlanItemRow() {
+      this.planItemRows.push({ itemCode: '', unit: '', unitPrice: 0, tokenUnit: 100000, minChargeUnit: 1, enabledFlag: true })
+    },
+    handleChargeItemChange(row) {
+      const meta = this.chargeItemMetaMap[row.itemCode]
+      if (!meta) return
+      row.unit = meta.unit
+      row.tokenUnit = meta.tokenUnit
+      row.minChargeUnit = meta.minChargeUnit
+      row.unitPrice = meta.suggestedUnitPrice
+    },
+    displayTokenUnit(tokenUnit) {
+      return tokenUnit == null ? '-' : tokenUnit
+    },
+    displayMinCharge(minChargeUnit) {
+      return minChargeUnit == null ? '-' : minChargeUnit
+    },
+    removePlanItemRow(index) {
+      this.planItemRows.splice(index, 1)
+    },
+    addFlowTierRow() {
+      this.flowTierRows.push({ minPrepayAmount: 0, maxPrepayAmount: null, unitPrice: 0, sortNo: this.flowTierRows.length + 1 })
+    },
+    removeFlowTierRow(index) {
+      this.flowTierRows.splice(index, 1)
+    },
+    submitCreatePlan() {
+      this.$refs.planForm.validate(valid => {
+        if (!valid) return
+        createFeePlan(this.planForm).then(() => {
+          this.msgSuccess('方案创建成功')
+        })
+      })
+    },
+    submitSaveItems() {
+      if (!this.planForm.planCode || !this.planForm.version) {
+        this.msgError('请先填写方案编码和版本')
+        return
+      }
+      const payload = {
+        planCode: this.planForm.planCode,
+        version: this.planForm.version,
+        items: this.planItemRows.map(item => ({
+          itemCode: item.itemCode,
+          unit: item.unit,
+          unitPrice: item.unitPrice,
+          tokenUnit: item.tokenUnit,
+          minChargeUnit: item.minChargeUnit,
+          enabled: item.enabledFlag ? 1 : 0
+        }))
+      }
+      saveFeePlanItems(payload).then(() => {
+        this.msgSuccess('收费项保存成功')
+      })
+    },
+    submitSaveFlowTiers() {
+      if (!this.planForm.planCode || !this.planForm.version) {
+        this.msgError('请先填写方案编码和版本')
+        return
+      }
+      const payload = {
+        planCode: this.planForm.planCode,
+        version: this.planForm.version,
+        tiers: this.flowTierRows
+      }
+      saveFlowTiers(payload).then(() => {
+        this.msgSuccess('流量阶梯保存成功')
+      })
+    },
+    submitPublishPlan() {
+      publishFeePlan(this.planForm.planCode, this.planForm.version).then(() => {
+        this.msgSuccess('方案发布成功')
+      })
+    },
+    submitBindPlan() {
+      if (!this.currentTenantId) {
+        this.msgError('请先选择租户')
+        return
+      }
+      this.$refs.bindFormRef.validate(valid => {
+        if (!valid) return
+        bindTenantPlan(this.bindForm).then(() => {
+          this.msgSuccess('绑定方案成功')
+          this.originalBindForm.tenantType = this.bindForm.tenantType
+          this.originalBindForm.billingMode = this.bindForm.billingMode
+        })
+      })
+    },
+    submitChangeBillingMode() {
+      if (this.bindForm.billingMode === this.originalBindForm.billingMode) {
+        this.$message.warning('计费模式未变更,无需提交')
+        return
+      }
+      changeTenantBillingMode({
+        tenantId: this.bindForm.tenantId,
+        billingMode: this.bindForm.billingMode
+      }).then(() => {
+        this.msgSuccess('计费模式修改成功')
+        this.originalBindForm.billingMode = this.bindForm.billingMode
+      })
+    },
+    submitChangeTenantType() {
+      if (this.bindForm.tenantType === this.originalBindForm.tenantType) {
+        this.$message.warning('租户类型未变更,无需提交')
+        return
+      }
+      changeTenantType({
+        tenantId: this.bindForm.tenantId,
+        tenantType: this.bindForm.tenantType
+      }).then(() => {
+        this.msgSuccess('租户类型修改成功')
+        this.originalBindForm.tenantType = this.bindForm.tenantType
+      })
+    },
+    loadWallet() {
+      if (!this.currentTenantId) return
+      getTenantWallet(this.currentTenantId).then(res => {
+        this.walletInfo = res.data || {}
+      })
+    },
+    submitRecharge() {
+      if (!this.currentTenantId) {
+        this.msgError('请先选择租户')
+        return
+      }
+      this.$refs.rechargeFormRef.validate(valid => {
+        if (!valid) return
+        rechargeTenantWallet(this.rechargeForm).then(() => {
+          this.msgSuccess('充值成功')
+          this.loadWallet()
+        })
+      })
+    },
+    submitReportEvent() {
+      if (!this.currentTenantId) {
+        this.msgError('请先选择租户')
+        return
+      }
+      this.$refs.eventFormRef.validate(valid => {
+        if (!valid) return
+        const payload = {
+          eventId: this.eventForm.eventId,
+          tenantId: this.eventForm.tenantId,
+          eventType: this.eventForm.eventType,
+          subType: this.eventForm.subType,
+          bizId: this.eventForm.bizId,
+          usageValue: this.eventForm.usageValue,
+          usageUnit: this.eventForm.usageUnit,
+          occurredAt: this.eventForm.occurredAt,
+          extJson: {
+            isAiCall: this.eventForm.isAiCallFlag
+          }
+        }
+        reportUsageEvent(payload).then(res => {
+          const result = res.data || {}
+          this.msgSuccess('上报成功,计费金额:' + (result.amount || 0))
+          this.eventForm.eventId = this.buildEventId()
+          this.loadWallet()
+          this.loadBillingDetails()
+        })
+      })
+    },
+    loadBillingDetails() {
+      if (!this.currentTenantId) return
+      listBillingDetails({ tenantId: this.currentTenantId }).then(res => {
+        this.detailList = res.rows || []
+      })
+    },
+    submitGenerateStatement() {
+      if (!this.currentTenantId) {
+        this.msgError('请先选择租户')
+        return
+      }
+      this.$refs.statementFormRef.validate(valid => {
+        if (!valid) return
+        generateStatement(this.statementForm).then(res => {
+          const data = res.data || {}
+          this.msgSuccess('账单生成成功:' + (data.statementNo || '-'))
+          this.loadBillingDetails()
+        })
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.billing-page .mb16 {
+  margin-bottom: 16px;
+}
+.billing-page .mb12 {
+  margin-bottom: 12px;
+}
+.billing-page .mt12 {
+  margin-top: 12px;
+}
+.billing-page .card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+.billing-page .charge-item-table ::v-deep .el-input-number {
+  width: 100%;
+}
+.billing-page .charge-item-table ::v-deep .el-input-number .el-input__inner {
+  text-align: left;
+  padding-left: 10px;
+  padding-right: 10px;
+}
+.billing-page .flow-tier-table ::v-deep .el-input-number {
+  width: 100%;
+}
+.billing-page .flow-tier-table ::v-deep .el-input-number .el-input__inner {
+  text-align: left;
+  padding-left: 10px;
+  padding-right: 10px;
+}
+</style>
+

+ 92 - 0
src/views/saas/billingAdmin/index.vue

@@ -0,0 +1,92 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never" class="mb16">
+      <div slot="header">
+        <span>总账号-租户费用明细</span>
+      </div>
+      <el-form :inline="true" label-width="90px">
+        <el-form-item label="租户筛选">
+          <el-select v-model="queryTenantId" clearable filterable placeholder="不选=全部租户" style="width: 360px">
+            <el-option
+              v-for="item in tenantOptions"
+              :key="item.id"
+              :label="item.tenantName + '(' + item.tenantCode + ')'"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="loadDetails">查询</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card shadow="never">
+      <div slot="header" class="header-row">
+        <span>费用明细列表</span>
+        <el-button size="mini" type="primary" plain @click="loadDetails">刷新</el-button>
+      </div>
+      <el-table :data="detailList" v-loading="loading" border>
+        <el-table-column label="租户ID" prop="tenantId" width="100" />
+        <el-table-column label="事件ID" prop="eventId" min-width="180" />
+        <el-table-column label="事件类型" prop="eventType" width="120" />
+        <el-table-column label="子类型" prop="subType" width="120" />
+        <el-table-column label="计费用量" prop="usageValue" width="120" />
+        <el-table-column label="金额" prop="amount" width="120" />
+        <el-table-column label="发生时间" prop="occurredAt" min-width="180" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { tenantList } from '@/api/tenant/tenant'
+import { listBillingDetailsAdmin } from '@/api/saas/billing'
+
+export default {
+  name: 'SaasBillingAdmin',
+  data() {
+    return {
+      loading: false,
+      queryTenantId: null,
+      tenantOptions: [],
+      detailList: []
+    }
+  },
+  created() {
+    this.loadTenantOptions()
+    this.loadDetails()
+  },
+  methods: {
+    loadTenantOptions() {
+      tenantList({}).then(res => {
+        this.tenantOptions = res.rows || res.data || []
+      })
+    },
+    loadDetails() {
+      this.loading = true
+      const params = {}
+      if (this.queryTenantId) {
+        params.tenantId = this.queryTenantId
+      }
+      listBillingDetailsAdmin(params).then(res => {
+        this.detailList = res.rows || []
+      }).finally(() => {
+        this.loading = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.mb16 {
+  margin-bottom: 16px;
+}
+.header-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>
+

+ 54 - 0
src/views/saas/billingTenant/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <div slot="header" class="header-row">
+        <span>租户费用明细(我的)</span>
+        <el-button size="mini" type="primary" plain @click="loadMyDetails">刷新</el-button>
+      </div>
+      <el-table :data="detailList" v-loading="loading" border>
+        <el-table-column label="事件ID" prop="eventId" min-width="180" />
+        <el-table-column label="事件类型" prop="eventType" width="120" />
+        <el-table-column label="子类型" prop="subType" width="120" />
+        <el-table-column label="计费用量" prop="usageValue" width="120" />
+        <el-table-column label="金额" prop="amount" width="120" />
+        <el-table-column label="发生时间" prop="occurredAt" min-width="180" />
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { listBillingDetailsMy } from '@/api/saas/billing'
+
+export default {
+  name: 'SaasBillingTenant',
+  data() {
+    return {
+      loading: false,
+      detailList: []
+    }
+  },
+  created() {
+    this.loadMyDetails()
+  },
+  methods: {
+    loadMyDetails() {
+      this.loading = true
+      listBillingDetailsMy().then(res => {
+        this.detailList = res.rows || []
+      }).finally(() => {
+        this.loading = false
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.header-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+</style>
+