浏览代码

feat: 首页统计admin企业细分+商户联动, 转账记录租户/企业过滤, 消费记录页签, 退出登录JWT过期静默处理, 筛选框布局整理

- 首页: admin角色展示企业选择器, 商户与企业下拉联动过滤, 默认查所有
- 转账记录: 后端 transferList 增加 tenant_id/enterprise_id 过滤, 前端加筛选下拉
- 消费记录: 新建 ConsumeVO + GET /consume-list 端点, 前端新增"消费记录"tab
- 企业API: /enterprise/all 支持 tenant_id 参数, EnterpriseService 三路分发(admin/指定租户/普通用户), 修复非admin未按tenantId过滤的bug
- 退出登录: JWT过期时从 ExpiredJwtException.getClaims() 提取session信息继续清理Redis
- 筛选框: 商户/企业/收款方类型合并到同一行
alphah 13 小时之前
父节点
当前提交
e8117d5f99

+ 39 - 0
frontend/src/api/module_payment/account.ts

@@ -119,6 +119,8 @@ export const AccountAPI = {
     page_size?: number;
     out_biz_no?: string;
     status?: string;
+    tenant_id?: number;
+    enterprise_id?: string;
   }) {
     return request<ApiResponse<TransferListResp>>({
       url: `${API_PATH}/transfer`,
@@ -127,6 +129,24 @@ export const AccountAPI = {
     });
   },
 
+  consumeList(params: {
+    page_no?: number;
+    page_size?: number;
+    tenant_id?: number;
+    enterprise_id?: string;
+    employee_id?: string;
+    consume_type?: string;
+    status?: string;
+    start_time?: string;
+    end_time?: string;
+  }) {
+    return request<ApiResponse<PageResult<ConsumeVO[]>>>({
+      url: `${API_PATH}/consume-list`,
+      method: "get",
+      params,
+    });
+  },
+
   consumeDetail(params: {
     pay_no: string;
     enterprise_id?: string;
@@ -219,6 +239,25 @@ export interface TransferListResp {
   items: TransferOutSchema[];
 }
 
+export interface ConsumeVO {
+  id: number;
+  tenant_id: number;
+  enterprise_id: string;
+  employee_id: string;
+  pay_no: string;
+  account_id: string;
+  consume_type: string;
+  consume_amount: string;
+  gmt_biz_create: string;
+  gmt_recieve_pay: string;
+  peer_pay_amount: string;
+  status: string;
+  expense_scene_code: string;
+  expense_type: string;
+  created_time: string;
+  updated_time: string;
+}
+
 export interface ConsumeDetailSchema {
   pay_no?: string;
   enterprise_id?: string;

+ 2 - 1
frontend/src/api/module_payment/enterprise.ts

@@ -80,10 +80,11 @@ const enterpriseApi = {
   /**
    * 获取当前登录用户下的所有企业
    */
-  all: () => {
+  all: (tenantId?: number) => {
     return request({
       url: "/payment/enterprise/all",
       method: "get",
+      params: tenantId != null ? { tenant_id: tenantId } : {},
     });
   },
 

+ 8 - 14
frontend/src/store/modules/enterprise.store.ts

@@ -63,9 +63,9 @@ export const useEnterpriseStore = defineStore("enterprise", {
   },
 
   actions: {
-    async fetchEnterpriseList() {
+    async fetchEnterpriseList(tenantId?: number) {
       try {
-        const res = await EnterpriseAPI.all();
+        const res = await EnterpriseAPI.all(tenantId);
         const list = res.data.data || [];
         return list.map((item: any) => ({
           enterprise_id: item.enterprise_id,
@@ -73,24 +73,18 @@ export const useEnterpriseStore = defineStore("enterprise", {
           name: item.name,
           short_name: item.short_name,
           status: item.status,
-          // ...item,
         }));
       } catch (error) {
         console.error("获取企业列表失败:", error);
       }
     },
 
-    async loadEnterpriseList() {
-      // 先从sessionStorage获取企业列表,避免重复请求
-      // const enterpriseListJson = sessionStorage.getItem(ENTERPRISE_LIST_KEY);
-      // if (enterpriseListJson) {
-      //   this.enterpriseList = JSON.parse(enterpriseListJson);
-      // } else {
-      //   this.enterpriseList = (await this.fetchEnterpriseList()) || [];
-      //   if (this.enterpriseList.length > 0) {
-      //     sessionStorage.setItem(ENTERPRISE_LIST_KEY, JSON.stringify(this.enterpriseList));
-      //   }
-      // }
+    async loadEnterpriseList(tenantId?: number) {
+      // 传了 tenantId 则强制重载(跳过缓存)
+      if (tenantId != null) {
+        this.enterpriseList = (await this.fetchEnterpriseList(tenantId)) || [];
+        return;
+      }
       if (!this.enterpriseList || this.enterpriseList.length == 0) {
         this.enterpriseList = (await this.fetchEnterpriseList()) || [];
         console.log("this.enterpriseList:", this.enterpriseList);

+ 32 - 25
frontend/src/views/dashboard/tenant.vue

@@ -29,23 +29,26 @@
         <h2>转账数据统计</h2>
         <div class="section-header-right">
           <el-select
-            v-model="selectedPayeeType"
-            placeholder="收款方类型"
-            clearable
-            class="payee-select"
-            @change="handlePayeeTypeChange"
+            v-if="is_platform_user"
+            v-model="currentTenantId"
+            placeholder="选择商户"
+            style="width: 180px"
           >
-            <el-option label="支付宝" value="ALIPAY_ACCOUNT" />
-            <el-option label="银行卡" value="BANK_CARD" />
+            <el-option
+              v-for="tenant in allTenantData"
+              :key="tenant.id"
+              :label="tenant.name"
+              :value="tenant.id"
+            />
           </el-select>
           <el-select
-            v-if="!is_platform_user"
             v-model="selectedEnterpriseId"
             placeholder="选择企业"
             filterable
             class="enterprise-select"
             @change="handleEnterpriseChange"
           >
+            <el-option label="全部" :value="undefined" />
             <el-option
               v-for="item in enterpriseStore.getEnterpriseList"
               :key="item.enterprise_id"
@@ -53,20 +56,19 @@
               :value="item.enterprise_id"
             />
           </el-select>
+          <el-select
+            v-model="selectedPayeeType"
+            placeholder="收款方类型"
+            clearable
+            class="payee-select"
+            @change="handlePayeeTypeChange"
+          >
+            <el-option label="支付宝" value="ALIPAY_ACCOUNT" />
+            <el-option label="银行卡" value="BANK_CARD" />
+          </el-select>
         </div>
       </div>
 
-      <div v-if="is_platform_user" style="margin-bottom: 16px; width: 300px;">
-        <el-select v-model="currentTenantId" placeholder="选择商户">
-          <el-option
-            v-for="tenant in allTenantData"
-            :key="tenant.id"
-            :label="tenant.name"
-            :value="tenant.id"
-          />
-        </el-select>
-      </div>
-
       <div class="data-cards">
         <div class="data-card">
           <div class="card-title">今日转账金额(元)</div>
@@ -220,18 +222,23 @@ async function fetchAllTenantData() {
 onMounted(async () => {
   if (is_platform_user.value) {
     await fetchAllTenantData();
-  } else {
-    await enterpriseStore.loadEnterpriseList();
-    if (!selectedEnterpriseId.value) {
-      selectedEnterpriseId.value = enterpriseStore.getCurrentEnterprise?.enterprise_id;
-    }
+  }
+  await enterpriseStore.loadEnterpriseList();
+  if (!is_platform_user.value && !selectedEnterpriseId.value) {
+    selectedEnterpriseId.value = enterpriseStore.getCurrentEnterprise?.enterprise_id;
   }
   fetchStatAmount();
   fetchConsumeAmount();
   fetchSummaryAmount();
 });
 
-watch(currentTenantId, () => {
+watch(currentTenantId, async (newTenantId) => {
+  // 绕过 store 缓存,按商户直接重载企业列表
+  enterpriseStore.enterpriseList = (await enterpriseStore.fetchEnterpriseList(
+    newTenantId || undefined
+  )) || [];
+  // 重置到"全部",由用户手动选择企业
+  selectedEnterpriseId.value = undefined;
   fetchStatAmount();
   fetchConsumeAmount();
   fetchSummaryAmount();

+ 208 - 2
frontend/src/views/module_payment/account/index.vue

@@ -289,6 +289,16 @@
                     <el-option label="退票" value="REFUND" />
                   </el-select>
                 </el-form-item>
+                <el-form-item v-if="is_platform_user" label="租户">
+                  <el-select v-model="transferSearchForm.tenant_id" placeholder="选择租户" clearable filterable>
+                    <el-option v-for="t in allTenantData" :key="t.id" :label="t.name" :value="t.id" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="企业">
+                  <el-select v-model="transferSearchForm.enterprise_id" placeholder="选择企业" clearable filterable>
+                    <el-option v-for="e in enterpriseStore.getEnterpriseList" :key="e.enterprise_id" :label="e.name" :value="e.enterprise_id" />
+                  </el-select>
+                </el-form-item>
                 <el-form-item>
                   <el-button type="primary" @click="handleTransferSearch">查询</el-button>
                   <el-button @click="handleTransferSearchReset">重置</el-button>
@@ -350,6 +360,95 @@
         </div>
       </el-tab-pane>
 
+      <el-tab-pane label="消费记录" name="consume-record">
+        <div class="tab-content">
+          <el-card>
+            <template #header>
+              <div class="card-header">
+                <span>消费记录列表</span>
+              </div>
+            </template>
+            <div class="mb-4">
+              <el-form :inline="true" :model="consumeSearchForm">
+                <el-form-item v-if="is_platform_user" label="租户">
+                  <el-select v-model="consumeSearchForm.tenant_id" placeholder="选择租户" clearable filterable>
+                    <el-option v-for="t in allTenantData" :key="t.id" :label="t.name" :value="t.id" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="企业">
+                  <el-select v-model="consumeSearchForm.enterprise_id" placeholder="选择企业" clearable filterable>
+                    <el-option v-for="e in enterpriseStore.getEnterpriseList" :key="e.enterprise_id" :label="e.name" :value="e.enterprise_id" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="消费类型">
+                  <el-select v-model="consumeSearchForm.consume_type" placeholder="全部" clearable>
+                    <el-option label="消费" value="CONSUME" />
+                    <el-option label="退款" value="REFUND" />
+                    <el-option label="转账" value="TRANSFER" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="状态">
+                  <el-select v-model="consumeSearchForm.status" placeholder="全部" clearable>
+                    <el-option label="未处理" value="NEW" />
+                    <el-option label="已处理" value="PROCESSED" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="时间范围">
+                  <el-date-picker
+                    v-model="consumeSearchForm.dateRange"
+                    type="daterange"
+                    range-separator="至"
+                    start-placeholder="开始日期"
+                    end-placeholder="结束日期"
+                    value-format="YYYY-MM-DD"
+                    @change="handleConsumeDateChange"
+                  />
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" @click="handleConsumeSearch">查询</el-button>
+                  <el-button @click="handleConsumeSearchReset">重置</el-button>
+                </el-form-item>
+              </el-form>
+            </div>
+
+            <el-table :data="consumeRecordList" border stripe v-loading="consumeListLoading">
+              <template #empty>
+                <el-empty description="暂无数据" />
+              </template>
+              <el-table-column prop="pay_no" label="账单号" min-width="180" />
+              <el-table-column prop="enterprise_id" label="企业ID" min-width="120" />
+              <el-table-column prop="employee_id" label="员工ID" min-width="120" />
+              <el-table-column prop="consume_amount" label="消费金额" min-width="120">
+                <template #default="{ row }">
+                  ¥{{ Number(row.consume_amount).toFixed(2) }}
+                </template>
+              </el-table-column>
+              <el-table-column prop="consume_type" label="消费类型" min-width="100">
+                <template #default="{ row }">
+                  <el-tag :type="getConsumeTypeTag(row.consume_type)">
+                    {{ getConsumeTypeLabel(row.consume_type) }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="status" label="状态" min-width="100">
+                <template #default="{ row }">
+                  <el-tag :type="getConsumeStatusType(row.status)">
+                    {{ getConsumeStatusLabel(row.status) }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column prop="gmt_biz_create" label="业务时间" min-width="160" />
+            </el-table>
+            <div class="mt-4 flex justify-end">
+              <el-pagination v-model:current-page="consumePage.page_no" v-model:page-size="consumePage.page_size"
+                :total="consumePage.total" :page-sizes="[10, 20, 50, 100]"
+                layout="total, sizes, prev, pager, next, jumper" @size-change="handleConsumeListChange"
+                @current-change="handleConsumeListChange" />
+            </div>
+          </el-card>
+        </div>
+      </el-tab-pane>
+
       <!-- <el-tab-pane label="账单查询" name="consume">
         <div class="tab-content">
           <el-card>
@@ -599,11 +698,12 @@ defineOptions({
   inheritAttrs: false,
 });
 
-import { useEnterpriseStore } from "@/store/modules/enterprise.store";
+import { useEnterpriseStore, useUserStore } from "@/store";
 import AccountAPI from "@/api/module_payment/account";
 import AccountOverview from "./components/AccountOverview.vue";
 import TransferDetail from "./components/TransferDetail.vue";
 import ConsumeDetail from "./components/ConsumeDetail.vue";
+import TenantAPI, { TenantTable } from "@/api/module_system/tenant";
 import { ref, reactive, computed, onMounted, watch } from "vue";
 import { Refresh, Loading } from "@element-plus/icons-vue";
 import { ElMessage, ElMessageBox } from "element-plus";
@@ -611,6 +711,9 @@ import type { FormInstance, FormRules } from "element-plus";
 import * as ExcelJS from "exceljs";
 
 const enterpriseStore = useEnterpriseStore();
+const userStore = useUserStore();
+
+const is_platform_user = computed(() => userStore.is_platform_user);
 
 const activeTab = ref("overview");
 const pageLoading = ref(false);
@@ -746,8 +849,12 @@ const transferPage = reactive({
 const transferSearchForm = reactive({
   out_biz_no: "",
   status: "",
+  tenant_id: undefined as number | undefined,
+  enterprise_id: undefined as string | undefined,
 });
 
+const allTenantData = ref<TenantTable[]>([]);
+
 const transferDetailVisible = ref(false);
 const currentTransferOutBizNo = ref("");
 
@@ -1320,6 +1427,8 @@ async function handleTransferSearch() {
 function handleTransferSearchReset() {
   transferSearchForm.out_biz_no = "";
   transferSearchForm.status = "";
+  transferSearchForm.tenant_id = undefined;
+  transferSearchForm.enterprise_id = undefined;
   handleTransferSearch();
 }
 
@@ -1331,6 +1440,8 @@ async function fetchTransferList() {
       page_size: transferPage.page_size,
       out_biz_no: transferSearchForm.out_biz_no || undefined,
       status: transferSearchForm.status || undefined,
+      tenant_id: transferSearchForm.tenant_id || undefined,
+      enterprise_id: transferSearchForm.enterprise_id || undefined,
     });
     transferList.value = res.data.data.items || [];
     transferPage.total = res.data.data.total || 0;
@@ -1343,6 +1454,90 @@ function handleTransferListChange() {
   fetchTransferList();
 }
 
+async function fetchAllTenantData() {
+  try {
+    const res = await TenantAPI.listTenant({ page_no: 1, page_size: 100 });
+    allTenantData.value = [{ id: undefined as any, name: "全部" }, ...(res.data.data?.items || [])];
+  } catch (error) {
+    console.error("获取所有租户数据失败:", error);
+  }
+}
+
+// ========== 消费记录 ==========
+
+const consumeRecordList = ref<any[]>([]);
+const consumeListLoading = ref(false);
+const consumePage = reactive({ page_no: 1, page_size: 10, total: 0 });
+const consumeSearchForm = reactive({
+  tenant_id: undefined as number | undefined,
+  enterprise_id: undefined as string | undefined,
+  consume_type: undefined as string | undefined,
+  status: undefined as string | undefined,
+  start_time: undefined as string | undefined,
+  end_time: undefined as string | undefined,
+  dateRange: null as Date[] | null,
+});
+
+function handleConsumeDateChange() {
+  if (consumeSearchForm.dateRange && consumeSearchForm.dateRange.length === 2) {
+    consumeSearchForm.start_time = (consumeSearchForm.dateRange as any)[0];
+    consumeSearchForm.end_time = (consumeSearchForm.dateRange as any)[1];
+  } else {
+    consumeSearchForm.start_time = undefined;
+    consumeSearchForm.end_time = undefined;
+  }
+}
+
+async function fetchConsumeList() {
+  consumeListLoading.value = true;
+  try {
+    const res = await AccountAPI.consumeList({
+      page_no: consumePage.page_no,
+      page_size: consumePage.page_size,
+      tenant_id: consumeSearchForm.tenant_id || undefined,
+      enterprise_id: consumeSearchForm.enterprise_id || undefined,
+      consume_type: consumeSearchForm.consume_type || undefined,
+      status: consumeSearchForm.status || undefined,
+      start_time: consumeSearchForm.start_time || undefined,
+      end_time: consumeSearchForm.end_time || undefined,
+    });
+    consumeRecordList.value = res.data.data.items || [];
+    consumePage.total = res.data.data.total || 0;
+  } finally {
+    consumeListLoading.value = false;
+  }
+}
+
+function handleConsumeSearch() { consumePage.page_no = 1; fetchConsumeList(); }
+function handleConsumeSearchReset() {
+  consumeSearchForm.tenant_id = undefined;
+  consumeSearchForm.enterprise_id = undefined;
+  consumeSearchForm.consume_type = undefined;
+  consumeSearchForm.status = undefined;
+  consumeSearchForm.start_time = undefined;
+  consumeSearchForm.end_time = undefined;
+  consumeSearchForm.dateRange = null;
+  handleConsumeSearch();
+}
+function handleConsumeListChange() { fetchConsumeList(); }
+
+function getConsumeTypeTag(type: string) {
+  const map: Record<string, any> = { CONSUME: '', REFUND: 'warning', TRANSFER: 'info' };
+  return map[type] || 'info';
+}
+function getConsumeTypeLabel(type: string) {
+  const map: Record<string, string> = { CONSUME: '消费', REFUND: '退款', TRANSFER: '转账' };
+  return map[type] || type;
+}
+function getConsumeStatusType(status: string) {
+  const map: Record<string, any> = { NEW: 'info', PROCESSED: 'success' };
+  return map[status] || 'info';
+}
+function getConsumeStatusLabel(status: string) {
+  const map: Record<string, string> = { NEW: '未处理', PROCESSED: '已处理' };
+  return map[status] || status;
+}
+
 function handleViewTransferDetail(outBizNo: string) {
   currentTransferOutBizNo.value = outBizNo;
   transferDetailVisible.value = true;
@@ -1472,7 +1667,15 @@ async function handleGoTab(tab: string) {
   }
 }
 
-onMounted(() => {
+onMounted(async () => {
+  if (is_platform_user.value) {
+    await fetchAllTenantData();
+  }
+  await enterpriseStore.loadEnterpriseList();
+  if (!is_platform_user.value) {
+    transferSearchForm.enterprise_id = currentEnterpriseId.value;
+    consumeSearchForm.enterprise_id = currentEnterpriseId.value;
+  }
   if (currentEnterpriseId.value) {
     authorizeForm.enterprise_id = currentEnterpriseId.value;
     createForm.enterprise_id = currentEnterpriseId.value;
@@ -1486,6 +1689,9 @@ watch(activeTab, async (newValue) => {
   if (newValue === "transfer") {
     await handleTransferSearch();
   }
+  if (newValue === "consume-record") {
+    await handleConsumeSearch();
+  }
 })
 
 // watch(batchTransferVisible, (newValue) => {

+ 21 - 2
java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java

@@ -3,6 +3,7 @@ package com.payment.platform.module.payment.account.controller;
 import com.payment.platform.common.response.PageResult;
 import com.payment.platform.common.response.Result;
 import com.payment.platform.module.payment.account.dto.AccountVO;
+import com.payment.platform.module.payment.account.dto.ConsumeVO;
 import com.payment.platform.module.payment.account.dto.TransferCreateDTO;
 import com.payment.platform.module.payment.account.dto.TransferVO;
 import com.payment.platform.module.payment.account.service.AccountService;
@@ -64,8 +65,26 @@ public class AccountController {
     @GetMapping("/transfer")
     public Result<PageResult<TransferVO>> transferList(
             @RequestParam(name = "page_no", defaultValue = "1") int pageNo, @RequestParam(name = "page_size", defaultValue = "20") int pageSize,
-            @RequestParam(name = "out_biz_no", required = false) String outBizNo, @RequestParam(required = false) String status) {
-        return Result.ok(accountService.transferList(pageNo, pageSize, outBizNo, status));
+            @RequestParam(name = "out_biz_no", required = false) String outBizNo, @RequestParam(required = false) String status,
+            @RequestParam(name = "tenant_id", required = false) Long tenantId,
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId) {
+        return Result.ok(accountService.transferList(pageNo, pageSize, outBizNo, status, tenantId, enterpriseId));
+    }
+
+    @PreAuthorize("@perm.hasAny('module_payment:account:transfer:list')")
+    @GetMapping("/consume-list")
+    public Result<PageResult<ConsumeVO>> consumeList(
+            @RequestParam(name = "page_no", defaultValue = "1") int pageNo,
+            @RequestParam(name = "page_size", defaultValue = "20") int pageSize,
+            @RequestParam(name = "tenant_id", required = false) Long tenantId,
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId,
+            @RequestParam(name = "employee_id", required = false) String employeeId,
+            @RequestParam(name = "consume_type", required = false) String consumeType,
+            @RequestParam(name = "status", required = false) String status,
+            @RequestParam(name = "start_time", required = false) String startTime,
+            @RequestParam(name = "end_time", required = false) String endTime) {
+        return Result.ok(accountService.consumeList(pageNo, pageSize, tenantId, enterpriseId,
+                employeeId, consumeType, status, startTime, endTime));
     }
 
     @PreAuthorize("@perm.hasAny('module_payment:account:transfer:detail')")

+ 59 - 0
java/src/main/java/com/payment/platform/module/payment/account/dto/ConsumeVO.java

@@ -0,0 +1,59 @@
+package com.payment.platform.module.payment.account.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.OffsetDateTime;
+
+@Data
+public class ConsumeVO {
+
+    @Schema(description = "ID")
+    private Long id;
+
+    @Schema(description = "租户ID")
+    private Long tenantId;
+
+    @Schema(description = "企业ID")
+    private String enterpriseId;
+
+    @Schema(description = "员工ID")
+    private String employeeId;
+
+    @Schema(description = "账单号")
+    private String payNo;
+
+    @Schema(description = "账户ID")
+    private String accountId;
+
+    @Schema(description = "消费类型: CONSUME/REFUND/TRANSFER")
+    private String consumeType;
+
+    @Schema(description = "消费金额")
+    private BigDecimal consumeAmount;
+
+    @Schema(description = "业务创建时间")
+    private OffsetDateTime gmtBizCreate;
+
+    @Schema(description = "收款时间")
+    private OffsetDateTime gmtRecievePay;
+
+    @Schema(description = "实际支付金额")
+    private BigDecimal peerPayAmount;
+
+    @Schema(description = "状态: NEW/PROCESSED")
+    private String status;
+
+    @Schema(description = "消费场景代码")
+    private String expenseSceneCode;
+
+    @Schema(description = "消费类型代码")
+    private String expenseType;
+
+    @Schema(description = "创建时间")
+    private OffsetDateTime createdTime;
+
+    @Schema(description = "更新时间")
+    private OffsetDateTime updatedTime;
+}

+ 44 - 1
java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.payment.platform.common.response.PageResult;
 import com.payment.platform.common.utils.ExcelUtil;
 import com.payment.platform.module.payment.account.dto.AccountVO;
+import com.payment.platform.module.payment.account.dto.ConsumeVO;
 import com.payment.platform.module.payment.account.dto.TransferCreateDTO;
 import com.payment.platform.module.payment.account.dto.TransferVO;
 import com.payment.platform.module.payment.account.entity.AccountEntity;
@@ -15,6 +16,7 @@ import com.payment.platform.module.payment.account.mapper.AccountMapper;
 import com.payment.platform.module.payment.account.mapper.DepositMapper;
 import com.payment.platform.module.payment.account.mapper.TransferMapper;
 import com.payment.platform.module.payment.account.mapper.WithdrawMapper;
+import com.payment.platform.module.payment.notification.entity.PayBillEntity;
 import com.payment.platform.module.payment.notification.mapper.PayBillMapper;
 import com.payment.platform.common.exception.BusinessException;
 import com.payment.platform.core.alipay.AlipayClientFactory;
@@ -123,16 +125,36 @@ public class AccountService {
                 "amount_of_all",  result.getOrDefault("amount_of_all", BigDecimal.ZERO).toPlainString());
     }
 
-    public PageResult<TransferVO> transferList(int pageNo, int pageSize, String outBizNo, String status) {
+    public PageResult<TransferVO> transferList(int pageNo, int pageSize, String outBizNo, String status,
+            Long tenantId, String enterpriseId) {
         var w = new LambdaQueryWrapper<TransferEntity>().orderByDesc(TransferEntity::getId);
         if (outBizNo != null && !outBizNo.isBlank()) w.eq(TransferEntity::getOutBizNo, outBizNo);
         if (status != null && !status.isBlank()) w.eq(TransferEntity::getStatus, status);
+        if (tenantId != null) w.eq(TransferEntity::getTenantId, tenantId);
+        if (enterpriseId != null && !enterpriseId.isBlank()) w.eq(TransferEntity::getEnterpriseId, enterpriseId);
         var p = new Page<TransferEntity>(pageNo, pageSize);
         var r = transferMapper.selectPage(p, w);
         List<TransferVO> items = r.getRecords().stream().map(this::transferVO).toList();
         return PageResult.of(pageNo, pageSize, r.getTotal(), items);
     }
 
+    public PageResult<ConsumeVO> consumeList(int pageNo, int pageSize,
+            Long tenantId, String enterpriseId, String employeeId,
+            String consumeType, String status, String startTime, String endTime) {
+        var w = new LambdaQueryWrapper<PayBillEntity>().orderByDesc(PayBillEntity::getId);
+        if (tenantId != null) w.eq(PayBillEntity::getTenantId, tenantId);
+        if (enterpriseId != null && !enterpriseId.isBlank()) w.eq(PayBillEntity::getEnterpriseId, enterpriseId);
+        if (employeeId != null && !employeeId.isBlank()) w.eq(PayBillEntity::getEmployeeId, employeeId);
+        if (consumeType != null && !consumeType.isBlank()) w.eq(PayBillEntity::getConsumeType, consumeType);
+        if (status != null && !status.isBlank()) w.eq(PayBillEntity::getStatus, status);
+        if (startTime != null && !startTime.isBlank()) w.ge(PayBillEntity::getGmtBizCreate, parseDateTime(startTime));
+        if (endTime != null && !endTime.isBlank()) w.le(PayBillEntity::getGmtBizCreate, parseDateTime(endTime));
+        var p = new Page<PayBillEntity>(pageNo, pageSize);
+        var r = payBillMapper.selectPage(p, w);
+        List<ConsumeVO> items = r.getRecords().stream().map(this::consumeVO).toList();
+        return PageResult.of(pageNo, pageSize, r.getTotal(), items);
+    }
+
     @SuppressWarnings("unused")
     public TransferVO transferDetail(String outBizNo) {
         TransferEntity t = transferMapper.selectOne(
@@ -441,6 +463,27 @@ public class AccountService {
         return vo;
     }
 
+    private ConsumeVO consumeVO(PayBillEntity e) {
+        ConsumeVO vo = new ConsumeVO();
+        vo.setId(e.getId());
+        vo.setTenantId(e.getTenantId());
+        vo.setEnterpriseId(e.getEnterpriseId());
+        vo.setEmployeeId(e.getEmployeeId());
+        vo.setPayNo(e.getPayNo());
+        vo.setAccountId(e.getAccountId());
+        vo.setConsumeType(e.getConsumeType());
+        vo.setConsumeAmount(e.getConsumeAmount());
+        vo.setGmtBizCreate(e.getGmtBizCreate());
+        vo.setGmtRecievePay(e.getGmtRecievePay());
+        vo.setPeerPayAmount(e.getPeerPayAmount());
+        vo.setStatus(e.getStatus());
+        vo.setExpenseSceneCode(e.getExpenseSceneCode());
+        vo.setExpenseType(e.getExpenseType());
+        vo.setCreatedTime(e.getCreatedTime());
+        vo.setUpdatedTime(e.getUpdatedTime());
+        return vo;
+    }
+
     // ==================== 通知回调状态更新 ====================
 
     /**

+ 4 - 3
java/src/main/java/com/payment/platform/module/payment/enterprise/controller/EnterpriseController.java

@@ -33,11 +33,12 @@ public class EnterpriseController {
     }
 
     @GetMapping("/all")
-    public Result<List<EnterpriseVO>> all() {
+    public Result<List<EnterpriseVO>> all(
+            @RequestParam(name = "tenant_id", required = false) Long requestTenantId) {
         LoginUser u = currentUser();
         boolean isAdmin = u != null && u.getIsSuperuser() != null && u.getIsSuperuser();
-        Long tid = u != null ? u.getTenantId() : null;
-        return Result.ok(enterpriseService.all(tid, isAdmin));
+        Long jwtTenantId = u != null ? u.getTenantId() : null;
+        return Result.ok(enterpriseService.all(jwtTenantId, isAdmin, requestTenantId));
     }
 
     @PostMapping

+ 14 - 6
java/src/main/java/com/payment/platform/module/payment/enterprise/service/EnterpriseService.java

@@ -25,16 +25,24 @@ public class EnterpriseService {
     private final EnterpriseMapper enterpriseMapper;
     private final AlipayEnterpriseService alipayEnterpriseService;
 
-    /** 查询企业列表 — admin看全部,普通用户看自己租户 */
-    public List<EnterpriseVO> all(Long tenantId, boolean isAdmin) {
+    /** 查询企业列表 — 指定 tenantId 按租户过滤,admin 默认看全部,普通用户看自己租户 */
+    public List<EnterpriseVO> all(Long jwtTenantId, boolean isAdmin, Long requestTenantId) {
+        // 显式传入 tenant_id(admin 下钻):按指定租户过滤
+        if (requestTenantId != null) {
+            List<EnterpriseEntity> models = enterpriseMapper.selectList(
+                    new LambdaQueryWrapper<EnterpriseEntity>().eq(EnterpriseEntity::getTenantId, requestTenantId));
+            return models == null ? List.of() : models.stream().map(this::toListVO).collect(Collectors.toList());
+        }
+        // admin 默认看全部
         if (isAdmin) {
             List<EnterpriseEntity> models = enterpriseMapper.selectList(null);
             return models == null ? List.of() : models.stream().map(this::toListVO).collect(Collectors.toList());
         }
-        if (tenantId == null || tenantId == 1) return List.of();
-        List<EnterpriseEntity> models = enterpriseMapper.selectList(null);
-        if (models == null || models.isEmpty()) return List.of();
-        return models.stream().map(this::toListVO).collect(Collectors.toList());
+        // 普通用户按 JWT tenantId 过滤
+        if (jwtTenantId == null || jwtTenantId == 1) return List.of();
+        List<EnterpriseEntity> models = enterpriseMapper.selectList(
+                new LambdaQueryWrapper<EnterpriseEntity>().eq(EnterpriseEntity::getTenantId, jwtTenantId));
+        return models == null ? List.of() : models.stream().map(this::toListVO).collect(Collectors.toList());
     }
 
     /** 分页查询 */

+ 4 - 0
java/src/main/java/com/payment/platform/module/system/auth/service/AuthService.java

@@ -3,6 +3,7 @@ package com.payment.platform.module.system.auth.service;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.useragent.UserAgent;
 import cn.hutool.http.useragent.UserAgentUtil;
+import io.jsonwebtoken.ExpiredJwtException;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.payment.platform.common.enums.RedisInitKeyConfig;
 import com.payment.platform.common.exception.BusinessException;
@@ -229,6 +230,9 @@ public class AuthService {
         String sessionInfoJson;
         try {
             sessionInfoJson = jwtUtils.getSub(tokenStr);
+        } catch (ExpiredJwtException e) {
+            sessionInfoJson = e.getClaims().getSubject();
+            log.info("退出登录-token已过期,从过期token中提取session信息用于清理");
         } catch (Exception e) {
             log.warn("退出登录-解析token失败: {}", e.getMessage());
             return false;