Sfoglia il codice sorgente

docs: 转账记录导出Excel实现计划

alphaH 2 settimane fa
parent
commit
1c48a7015c
1 ha cambiato i file con 551 aggiunte e 0 eliminazioni
  1. 551 0
      .claude/plan/2026-08-11-transfer-export.md

+ 551 - 0
.claude/plan/2026-08-11-transfer-export.md

@@ -0,0 +1,551 @@
+# 转账记录导出 Excel + 时间筛选 + 导出修复 — 实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 转账管理页面(`module_payment/account/index.vue` "转账记录" tab)导出 Excel 跟随表单筛选条件,新增时间范围筛选,并修复导出时间格式(东8)与收款方类型翻译。
+
+**Architecture:** 改造后端现有 `GET /transfer/export`(参数改为与 `transferList` 一致的 6 个可选筛选条件、导出列 11 列、创建时间格式化东8、类型 map 补齐),`transferList` 同步支持时间筛选;前端恢复"下载报表"按钮(blob 下载)、筛选表单加时间选择器并加宽控件、清理旧导出死代码。
+
+**Tech Stack:** Java 17 / Spring Boot / MyBatis-Plus / Lombok;Vue3 + Vite + TypeScript + Element Plus + dayjs + ExcelJS。
+
+## Global Constraints
+
+- 设计文档(已批准):`docs/superpowers/specs/2026-08-11-transfer-export-design.md`
+- 时间解析沿用 `parseDateTime`(结束日期解析为当天 00:00:00,与消费记录一致)
+- 条件语义与 `transferList` 逐条一致(`eq` 精确匹配)
+- 权限保持 `module_payment:account:transfer:list`(按钮 `v-hasPerm` 同权限码)
+- 导出列 11 列:序号、订单号、商户订单号、金额(元)、收款方姓名、收款方类型、状态、转账标题、创建时间、备注、失败原因
+- 创建时间统一东8 `yyyy-MM-dd HH:mm:ss`(后端 `atZoneSameInstant(Asia/Shanghai)`,前端 dayjs)
+- 空数据导出空 sheet,不特判
+- 项目无后端测试框架(`java/src/test` 不存在)、无前端单测(无 vitest/jest);每任务验证 = 编译/类型检查 + 逻辑对照 + 最终 Playwright 端到端
+- 后端项目路径 `../java`(相对工作目录 frontend/),Maven 编译:`cd ../java && mvn -q compile -DskipTests`
+
+---
+
+### Task 1: 后端 transferList 支持时间范围筛选
+
+**Files:**
+- Modify: `../java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java:190-201`
+- Modify: `../java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java:76-84`
+
+**Interfaces:**
+- Produces: `transferList(int pageNo, int pageSize, String outBizNo, String status, Long tenantId, String enterpriseId, String startTime, String endTime)` — 新参数 `startTime`/`endTime`(`yyyy-MM-dd` 或 `yyyy-MM-dd HH:mm:ss`,可空)
+
+- [ ] **Step 1: service 加时间过滤参数**
+
+`AccountService.transferList` 签名与查询条件改为:
+
+```java
+public PageResult<TransferVO> transferList(int pageNo, int pageSize, String outBizNo, String status,
+        Long tenantId, String enterpriseId, String startTime, String endTime) {
+    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);
+    if (startTime != null && !startTime.isBlank()) w.ge(TransferEntity::getCreatedTime, parseDateTime(startTime));
+    if (endTime != null && !endTime.isBlank()) w.le(TransferEntity::getCreatedTime, parseDateTime(endTime));
+    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);
+}
+```
+
+- [ ] **Step 2: controller 透传新参数**
+
+`AccountController.transferList` 改为:
+
+```java
+@PreAuthorize("@perm.hasAny('module_payment:account:transfer:list')")
+@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,
+        @RequestParam(name = "tenant_id", required = false) Long tenantId,
+        @RequestParam(name = "enterprise_id", required = false) String enterpriseId,
+        @RequestParam(name = "start_time", required = false) String startTime,
+        @RequestParam(name = "end_time", required = false) String endTime) {
+    return Result.ok(accountService.transferList(pageNo, pageSize, outBizNo, status, tenantId, enterpriseId, startTime, endTime));
+}
+```
+
+- [ ] **Step 3: 编译验证**
+
+Run: `cd ../java && mvn -q compile -DskipTests`
+Expected: BUILD SUCCESS(无编译错误)
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd ../java && git add src/main/java/com/payment/platform/module/payment/account/service/AccountService.java src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java && git commit -m "feat: 转账列表支持时间范围筛选"
+```
+
+---
+
+### Task 2: 后端 transferExport 改造(6 条件 + 11 列 + 东8时间 + 类型翻译)
+
+**Files:**
+- Modify: `../java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java:237-249`
+- Modify: `../java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java:295-369`
+
+**Interfaces:**
+- Consumes: Task 1 的 `parseDateTime`(已有)与筛选条件语义
+- Produces: `transferExport(String outBizNo, String status, Long tenantId, String enterpriseId, String startTime, String endTime)` 返回 xlsx 字节数组;下载文件名 `transfer_report.xlsx`
+
+- [ ] **Step 1: controller 参数改为 6 条件**
+
+`AccountController.transferExport` 整体替换为:
+
+```java
+@PreAuthorize("@perm.hasAny('module_payment:account:transfer:list')")
+@GetMapping("/transfer/export")
+public void transferExport(
+        @RequestParam(name = "out_biz_no", required = false) String outBizNo,
+        @RequestParam(name = "status", required = false) String status,
+        @RequestParam(name = "tenant_id", required = false) Long tenantId,
+        @RequestParam(name = "enterprise_id", required = false) String enterpriseId,
+        @RequestParam(name = "start_time", required = false) String startTime,
+        @RequestParam(name = "end_time", required = false) String endTime,
+        HttpServletResponse response) throws IOException {
+    byte[] bytes = accountService.transferExport(outBizNo, status, tenantId, enterpriseId, startTime, endTime);
+    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+    response.setHeader("Content-Disposition",
+            "attachment; filename=transfer_report.xlsx");
+    response.getOutputStream().write(bytes);
+}
+```
+
+- [ ] **Step 2: service 方法整体改写**
+
+`AccountService.transferExport` 方法体替换为(注意保留类中已存在的 `ObjectMapper om`、`ExcelUtil.exportToExcel` 用法):
+
+```java
+public byte[] transferExport(String outBizNo, String status, Long tenantId, String enterpriseId,
+        String startTime, String endTime) {
+    try {
+        LambdaQueryWrapper<TransferEntity> w = new LambdaQueryWrapper<>();
+        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);
+        if (startTime != null && !startTime.isBlank()) w.ge(TransferEntity::getCreatedTime, parseDateTime(startTime));
+        if (endTime != null && !endTime.isBlank()) w.le(TransferEntity::getCreatedTime, parseDateTime(endTime));
+        w.orderByDesc(TransferEntity::getId);
+
+        List<TransferEntity> records = transferMapper.selectList(w);
+
+        Map<String, String> statusMap = Map.of(
+                "DEALING", "处理中",
+                "SUCCESS", "成功",
+                "FAIL", "失败",
+                "REFUND", "退票");
+
+        Map<String, String> payeeTypeMap = Map.of(
+                "ALIPAY_USER_ID", "支付宝账户",
+                "ALIPAY_LOGON_ID", "支付宝账户",
+                "ALIPAY_ACCOUNT", "支付宝账户",
+                "BANKCARD_ACCOUNT", "银行卡",
+                "BANK_CARD", "银行卡");
+
+        ObjectMapper om = new ObjectMapper();
+        List<Map<String, Object>> listData = new ArrayList<>();
+        DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+        int seq = 1;
+        for (TransferEntity r : records) {
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("序号", seq++);
+            row.put("订单号", r.getOutBizNo() != null ? r.getOutBizNo() : "");
+            row.put("商户订单号", r.getOrderNo() != null ? r.getOrderNo() : "");
+            row.put("金额(元)", r.getAmount() != null ? r.getAmount().toPlainString() : "0");
+            row.put("状态", statusMap.getOrDefault(r.getStatus(), r.getStatus() != null ? r.getStatus() : ""));
+            row.put("转账标题", r.getOrderTitle() != null ? r.getOrderTitle() : "");
+            row.put("创建时间", r.getCreatedTime() != null
+                    ? r.getCreatedTime().atZoneSameInstant(ZoneId.of("Asia/Shanghai")).format(timeFmt) : "");
+
+            // 解析 payeeInfo JSON
+            String name = "";
+            String identityType = "";
+            if (r.getPayeeInfo() != null && !r.getPayeeInfo().isBlank()) {
+                try {
+                    @SuppressWarnings("unchecked")
+                    Map<String, Object> payeeInfo = om.readValue(r.getPayeeInfo(), Map.class);
+                    name = String.valueOf(payeeInfo.getOrDefault("name", ""));
+                    identityType = String.valueOf(payeeInfo.getOrDefault("identity_type", ""));
+                } catch (Exception ignored) {
+                    // ignore invalid JSON
+                }
+            }
+            row.put("收款方姓名", name);
+            row.put("收款方类型", payeeTypeMap.getOrDefault(identityType, identityType));
+            row.put("备注", r.getRemark() != null ? r.getRemark() : "");
+            row.put("失败原因", r.getErrorMsg() != null ? r.getErrorMsg() : "");
+            listData.add(row);
+        }
+
+        Map<String, String> mappingDict = new LinkedHashMap<>();
+        mappingDict.put("序号", "序号");
+        mappingDict.put("订单号", "订单号");
+        mappingDict.put("商户订单号", "商户订单号");
+        mappingDict.put("金额(元)", "金额(元)");
+        mappingDict.put("收款方姓名", "收款方姓名");
+        mappingDict.put("收款方类型", "收款方类型");
+        mappingDict.put("状态", "状态");
+        mappingDict.put("转账标题", "转账标题");
+        mappingDict.put("创建时间", "创建时间");
+        mappingDict.put("备注", "备注");
+        mappingDict.put("失败原因", "失败原因");
+
+        log.info("导出转账记录报表: {} 条", listData.size());
+        return ExcelUtil.exportToExcel(listData, mappingDict);
+    } catch (Exception e) {
+        log.error("导出转账记录失败", e);
+        throw new RuntimeException("导出转账记录失败: " + e.getMessage());
+    }
+}
+```
+
+- [ ] **Step 3: 补充缺失 import**
+
+`AccountService.java` 顶部 java.time 包 import 区若无 `java.time.ZoneId`,在 `import java.time.OffsetDateTime;` 后添加:
+
+```java
+import java.time.ZoneId;
+```
+
+(若已存在则跳过此步;`DateTimeFormatter` 若缺失同样补 `import java.time.format.DateTimeFormatter;`)
+
+- [ ] **Step 4: 编译验证**
+
+Run: `cd ../java && mvn -q compile -DskipTests`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd ../java && git add src/main/java/com/payment/platform/module/payment/account/service/AccountService.java src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java && git commit -m "feat: 转账导出接口对齐表单筛选条件,导出列加备注/失败原因,时间格式东8,类型翻译补齐"
+```
+
+---
+
+### Task 3: 前端 API 参数更新(transferList + exportTransferReport)
+
+**Files:**
+- Modify: `src/api/module_payment/account.ts:184-197`(transferList)
+- Modify: `src/api/module_payment/account.ts:230-241`(exportTransferReport)
+
+**Interfaces:**
+- Produces: `AccountAPI.transferList(params)` 新增可选 `start_time`/`end_time`;`AccountAPI.exportTransferReport(params)` 参数改为 6 个可选字段(无必填)
+
+- [ ] **Step 1: transferList 类型加时间参数**
+
+```ts
+transferList(params: {
+  page_no?: number;
+  page_size?: number;
+  out_biz_no?: string;
+  status?: string;
+  tenant_id?: number;
+  enterprise_id?: string;
+  start_time?: string;
+  end_time?: string;
+}) {
+  return request<ApiResponse<TransferListResp>>({
+    url: `${API_PATH}/transfer`,
+    method: "get",
+    params,
+  });
+},
+```
+
+- [ ] **Step 2: exportTransferReport 参数类型替换**
+
+```ts
+exportTransferReport(params: {
+  out_biz_no?: string;
+  status?: string;
+  tenant_id?: number;
+  enterprise_id?: string;
+  start_time?: string;
+  end_time?: string;
+}) {
+  return request({
+    url: `${API_PATH}/transfer/export`,
+    method: "get",
+    params,
+    responseType: "blob",
+  });
+},
+```
+
+- [ ] **Step 3: 类型检查**
+
+Run: `pnpm ts:check 2>&1 | grep -c "src/api/module_payment/account.ts"`
+Expected: 0(该文件无类型错误;项目其他文件存在既有错误基线,忽略)
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add src/api/module_payment/account.ts && git commit -m "feat: 转账导出API参数对齐表单筛选(6条件全可选),transferList支持时间筛选"
+```
+
+---
+
+### Task 4: 前端筛选表单(时间选择器 + 控件加宽 + 查询/重置适配)
+
+**Files:**
+- Modify: `src/views/module_payment/account/index.vue:1158-1163`(transferSearchForm 定义)
+- Modify: `src/views/module_payment/account/index.vue:299-326`(筛选表单模板)
+- Modify: `src/views/module_payment/account/index.vue:1762-1786`(重置 + fetchTransferList)
+- Modify: `src/views/module_payment/account/index.vue`(script 顶部 import 区,加 dayjs)
+- Modify: `src/views/module_payment/account/index.vue:353`(创建时间列 formatter)
+
+**Interfaces:**
+- Produces: `transferSearchForm` 新增 `dateRange: string[] | null`、`start_time: string | undefined`、`end_time: string | undefined`;`handleTransferDateChange()`;`fetchTransferList` 传时间参数
+
+- [ ] **Step 1: transferSearchForm 增加时间字段**
+
+`index.vue:1158-1163` 改为:
+
+```ts
+const transferSearchForm = reactive({
+  out_biz_no: "",
+  status: "",
+  tenant_id: undefined as number | undefined,
+  enterprise_id: undefined as string | undefined,
+  dateRange: null as string[] | null,
+  start_time: undefined as string | undefined,
+  end_time: undefined as string | undefined,
+});
+```
+
+- [ ] **Step 2: 表单模板加时间选择器 + 控件加宽**
+
+`index.vue:299-326` 的筛选表单:订单号 input 加 `style="width: 220px"`,状态/租户/企业 select 加 `style="width: 180px"`,并在企业 select 的 `</el-form-item>` 后、按钮 `</el-form-item>` 前插入:
+
+```html
+<el-form-item label="时间范围">
+  <el-date-picker
+    v-model="transferSearchForm.dateRange"
+    type="daterange"
+    range-separator="至"
+    start-placeholder="开始日期"
+    end-placeholder="结束日期"
+    value-format="YYYY-MM-DD"
+    style="width: 280px"
+    @change="handleTransferDateChange"
+  />
+</el-form-item>
+```
+
+- [ ] **Step 3: 新增 handleTransferDateChange(仿 handleConsumeDateChange)**
+
+在 `handleTransferSearchReset`(约 1762 行)前添加:
+
+```ts
+function handleTransferDateChange() {
+  if (transferSearchForm.dateRange && transferSearchForm.dateRange.length === 2) {
+    transferSearchForm.start_time = (transferSearchForm.dateRange as any)[0];
+    transferSearchForm.end_time = (transferSearchForm.dateRange as any)[1];
+  } else {
+    transferSearchForm.start_time = undefined;
+    transferSearchForm.end_time = undefined;
+  }
+}
+```
+
+- [ ] **Step 4: 重置清空时间条件**
+
+`handleTransferSearchReset`(`index.vue:1762-1768`)加三行:
+
+```ts
+function handleTransferSearchReset() {
+  transferSearchForm.out_biz_no = "";
+  transferSearchForm.status = "";
+  transferSearchForm.tenant_id = undefined;
+  transferSearchForm.enterprise_id = undefined;
+  transferSearchForm.dateRange = null;
+  transferSearchForm.start_time = undefined;
+  transferSearchForm.end_time = undefined;
+  handleTransferSearch();
+}
+```
+
+- [ ] **Step 5: fetchTransferList 传时间参数**
+
+`index.vue:1773-1780` 的请求参数加两行:
+
+```ts
+    const res = await AccountAPI.transferList({
+      page_no: transferPage.page_no,
+      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,
+      start_time: transferSearchForm.start_time || undefined,
+      end_time: transferSearchForm.end_time || undefined,
+    });
+```
+
+- [ ] **Step 6: script 顶部引入 dayjs**
+
+`<script setup lang="ts">` 的 import 区(文件顶部)添加:
+
+```ts
+import dayjs from "dayjs";
+```
+
+(若已存在则跳过)
+
+- [ ] **Step 7: 创建时间列 formatter**
+
+`index.vue:353` 的创建时间列替换为:
+
+```html
+<el-table-column prop="created_time" label="创建时间" min-width="160">
+  <template #default="{ row }">
+    {{ row.created_time ? dayjs(row.created_time).format("YYYY-MM-DD HH:mm:ss") : "" }}
+  </template>
+</el-table-column>
+```
+
+- [ ] **Step 8: 类型检查**
+
+Run: `pnpm ts:check 2>&1 | grep -c "module_payment/account/index.vue"`
+Expected: 8(既有错误基线,无新增——对比基线:修改前该文件恰有 8 条既有错误,全部在改动行之外)
+
+- [ ] **Step 9: Commit**
+
+```bash
+git add src/views/module_payment/account/index.vue && git commit -m "feat: 转账记录筛选表单增加时间范围选择器,控件加宽,创建时间列格式化为东8"
+```
+
+---
+
+### Task 5: 前端下载报表按钮(改写旧导出 + 清理死代码)
+
+**Files:**
+- Modify: `src/views/module_payment/account/index.vue:288-295`(注释按钮块 → 启用)
+- Delete: `src/views/module_payment/account/index.vue:730-761`(下载转账报表 dialog 模板)
+- Delete: `src/views/module_payment/account/index.vue:802-806`(downloadLoading/showDownloadMenu/selectedQuickOption/customDateRange 状态)
+- Delete: `src/views/module_payment/account/index.vue:2049-2145`(handleDownload/calculateTimeRange/formatDateTime/handleCustomDateChange)
+- Modify: `src/views/module_payment/account/index.vue`(script 新增 handleExportTransfer + transferExportLoading)
+
+**Interfaces:**
+- Consumes: Task 3 的 `AccountAPI.exportTransferReport`(6 可选参数)、Task 4 的 `transferSearchForm`(含时间字段)
+- Produces: `transferExportLoading: Ref<boolean>`;`handleExportTransfer()` — 按当前表单筛选导出 `转账记录.xlsx`
+
+- [ ] **Step 1: 启用下载按钮(替换注释块)**
+
+`index.vue:288-295` 的注释块替换为:
+
+```html
+                <el-button
+                  type="primary"
+                  icon="Download"
+                  class="ml-auto"
+                  :loading="transferExportLoading"
+                  v-hasPerm="['module_payment:account:transfer:list']"
+                  @click="handleExportTransfer"
+                >
+                  下载报表
+                </el-button>
+```
+
+- [ ] **Step 2: 删除旧导出 dialog 模板**
+
+删除 `index.vue:730-761` 整个 `<el-dialog title="下载转账报表" ...>` 块(含 `</el-dialog>`),其内部引用 `showDownloadMenu`/`selectedQuickOption`/`customDateRange`/`handleDownload`/`downloadLoading`/`handleCustomDateChange` 的状态与函数在 Step 3-5 同步删除。
+
+- [ ] **Step 3: 删除旧状态**
+
+删除 `index.vue:802-806`:
+
+```ts
+const downloadLoading = ref(false);
+const showDownloadMenu = ref(false);
+const selectedQuickOption = ref("today");
+const customDateRange = ref<Date[] | null>(null);
+```
+
+- [ ] **Step 4: 新增 handleExportTransfer(替换 handleDownload 等死代码)**
+
+删除 `index.vue:2049-2145`(`handleDownload`、`calculateTimeRange`、`formatDateTime`、`handleCustomDateChange` 整个函数块),在原位置(`</script>` 前)添加:
+
+```ts
+const transferExportLoading = ref(false);
+
+async function handleExportTransfer() {
+  transferExportLoading.value = true;
+  try {
+    const res = await AccountAPI.exportTransferReport({
+      out_biz_no: transferSearchForm.out_biz_no || undefined,
+      status: transferSearchForm.status || undefined,
+      tenant_id: transferSearchForm.tenant_id || undefined,
+      enterprise_id: transferSearchForm.enterprise_id || undefined,
+      start_time: transferSearchForm.start_time || undefined,
+      end_time: transferSearchForm.end_time || undefined,
+    });
+    const blob = new Blob([res.data], {
+      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+    });
+    const url = window.URL.createObjectURL(blob);
+    const a = document.createElement("a");
+    a.href = url;
+    a.download = "转账记录.xlsx";
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+    window.URL.revokeObjectURL(url);
+    ElMessage.success("导出成功");
+  } catch (error) {
+    ElMessage.error("导出失败,请稍后重试");
+  } finally {
+    transferExportLoading.value = false;
+  }
+}
+```
+
+- [ ] **Step 5: 确认无残留引用**
+
+Run: `grep -nE "showDownloadMenu|customDateRange|selectedQuickOption|downloadLoading|handleDownload|calculateTimeRange|formatDateTime|handleCustomDateChange" src/views/module_payment/account/index.vue`
+Expected: 无输出(全部清理干净)
+
+- [ ] **Step 6: 类型检查**
+
+Run: `pnpm ts:check 2>&1 | grep -c "module_payment/account/index.vue"`
+Expected: 8(既有基线,无新增)
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add src/views/module_payment/account/index.vue && git commit -m "feat: 转账记录下载报表按钮接入导出接口(跟随表单筛选),清理旧导出死代码"
+```
+
+---
+
+### Task 6: 端到端验证(Playwright)
+
+**Files:** 无(验证任务,不产生代码改动)
+
+- [ ] **Step 1: 启动后端与前端**
+
+Run: `cd ../java && mvn spring-boot:run`(后台);`pnpm dev`(后台,默认端口 5173,具体以 `.env`/vite 配置为准)
+
+- [ ] **Step 2: Playwright 验证筛选联动**
+
+用 Playwright MCP 登录后进入"资金账户 → 转账记录" tab:
+1. 选择时间范围(如近 7 天)→ 点查询 → 断言列表只显示范围内数据
+2. 组合筛选(状态=成功 + 时间范围)→ 查询 → 列表与筛选一致
+3. 点重置 → 断言时间范围被清空、列表恢复全量
+
+- [ ] **Step 3: Playwright 验证导出**
+
+点击"下载报表"按钮,捕获下载事件:
+1. 断言下载文件名为 `转账记录.xlsx`
+2. 用脚本读取 xlsx(exceljs)验证:表头恰 11 列(含备注、失败原因);"创建时间"值为 `YYYY-MM-DD HH:mm:ss` 格式且为东8墙钟时间;"收款方类型"为中文(支付宝账户/银行卡);数据条数与当前筛选下的列表一致
+
+- [ ] **Step 4: 回归检查**
+
+快速检查同页其他功能(单笔转账、批量转账模板下载)不受影响;`pnpm ts:check` 全量错误数不高于改动前基线