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。
docs/superpowers/specs/2026-08-11-transfer-export-design.mdparseDateTime(结束日期解析为当天 00:00:00,与消费记录一致)transferList 逐条一致(eq 精确匹配)module_payment:account:transfer:list(按钮 v-hasPerm 同权限码)yyyy-MM-dd HH:mm:ss(后端 atZoneSameInstant(Asia/Shanghai),前端 dayjs)java/src/test 不存在)、无前端单测(无 vitest/jest);每任务验证 = 编译/类型检查 + 逻辑对照 + 最终 Playwright 端到端../java(相对工作目录 frontend/),Maven 编译:cd ../java && mvn -q compile -DskipTestsFiles:
../java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java:190-201../java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java:76-84Interfaces:
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 签名与查询条件改为:
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);
}
AccountController.transferList 改为:
@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));
}
Run: cd ../java && mvn -q compile -DskipTests
Expected: BUILD SUCCESS(无编译错误)
[ ] Step 4: Commit
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: 转账列表支持时间范围筛选"
Files:
../java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java:237-249../java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java:295-369Interfaces:
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 整体替换为:
@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);
}
AccountService.transferExport 方法体替换为(注意保留类中已存在的 ObjectMapper om、ExcelUtil.exportToExcel 用法):
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());
}
}
AccountService.java 顶部 java.time 包 import 区若无 java.time.ZoneId,在 import java.time.OffsetDateTime; 后添加:
import java.time.ZoneId;
(若已存在则跳过此步;DateTimeFormatter 若缺失同样补 import java.time.format.DateTimeFormatter;)
Run: cd ../java && mvn -q compile -DskipTests
Expected: BUILD SUCCESS
[ ] Step 5: Commit
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,类型翻译补齐"
Files:
src/api/module_payment/account.ts:184-197(transferList)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 类型加时间参数
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 参数类型替换
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
git add src/api/module_payment/account.ts && git commit -m "feat: 转账导出API参数对齐表单筛选(6条件全可选),transferList支持时间筛选"
Files:
src/views/module_payment/account/index.vue:1158-1163(transferSearchForm 定义)src/views/module_payment/account/index.vue:299-326(筛选表单模板)src/views/module_payment/account/index.vue:1762-1786(重置 + fetchTransferList)src/views/module_payment/account/index.vue(script 顶部 import 区,加 dayjs)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 改为:
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,
});
index.vue:299-326 的筛选表单:订单号 input 加 style="width: 220px",状态/租户/企业 select 加 style="width: 180px",并在企业 select 的 </el-form-item> 后、按钮 </el-form-item> 前插入:
<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>
在 handleTransferSearchReset(约 1762 行)前添加:
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;
}
}
handleTransferSearchReset(index.vue:1762-1768)加三行:
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();
}
index.vue:1773-1780 的请求参数加两行:
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,
});
<script setup lang="ts"> 的 import 区(文件顶部)添加:
import dayjs from "dayjs";
(若已存在则跳过)
index.vue:353 的创建时间列替换为:
<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>
Run: pnpm ts:check 2>&1 | grep -c "module_payment/account/index.vue"
Expected: 8(既有错误基线,无新增——对比基线:修改前该文件恰有 8 条既有错误,全部在改动行之外)
[ ] Step 9: Commit
git add src/views/module_payment/account/index.vue && git commit -m "feat: 转账记录筛选表单增加时间范围选择器,控件加宽,创建时间列格式化为东8"
Files:
src/views/module_payment/account/index.vue:288-295(注释按钮块 → 启用)src/views/module_payment/account/index.vue:730-761(下载转账报表 dialog 模板)src/views/module_payment/account/index.vue:802-806(downloadLoading/showDownloadMenu/selectedQuickOption/customDateRange 状态)src/views/module_payment/account/index.vue:2049-2145(handleDownload/calculateTimeRange/formatDateTime/handleCustomDateChange)src/views/module_payment/account/index.vue(script 新增 handleExportTransfer + transferExportLoading)Interfaces:
AccountAPI.exportTransferReport(6 可选参数)、Task 4 的 transferSearchForm(含时间字段)Produces: transferExportLoading: Ref<boolean>;handleExportTransfer() — 按当前表单筛选导出 转账记录.xlsx
[ ] Step 1: 启用下载按钮(替换注释块)
index.vue:288-295 的注释块替换为:
<el-button
type="primary"
icon="Download"
class="ml-auto"
:loading="transferExportLoading"
v-hasPerm="['module_payment:account:transfer:list']"
@click="handleExportTransfer"
>
下载报表
</el-button>
删除 index.vue:730-761 整个 <el-dialog title="下载转账报表" ...> 块(含 </el-dialog>),其内部引用 showDownloadMenu/selectedQuickOption/customDateRange/handleDownload/downloadLoading/handleCustomDateChange 的状态与函数在 Step 3-5 同步删除。
删除 index.vue:802-806:
const downloadLoading = ref(false);
const showDownloadMenu = ref(false);
const selectedQuickOption = ref("today");
const customDateRange = ref<Date[] | null>(null);
删除 index.vue:2049-2145(handleDownload、calculateTimeRange、formatDateTime、handleCustomDateChange 整个函数块),在原位置(</script> 前)添加:
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;
}
}
Run: grep -nE "showDownloadMenu|customDateRange|selectedQuickOption|downloadLoading|handleDownload|calculateTimeRange|formatDateTime|handleCustomDateChange" src/views/module_payment/account/index.vue
Expected: 无输出(全部清理干净)
Run: pnpm ts:check 2>&1 | grep -c "module_payment/account/index.vue"
Expected: 8(既有基线,无新增)
[ ] Step 7: Commit
git add src/views/module_payment/account/index.vue && git commit -m "feat: 转账记录下载报表按钮接入导出接口(跟随表单筛选),清理旧导出死代码"
Files: 无(验证任务,不产生代码改动)
Run: cd ../java && mvn spring-boot:run(后台);pnpm dev(后台,默认端口 5173,具体以 .env/vite 配置为准)
用 Playwright MCP 登录后进入"资金账户 → 转账记录" tab:
点击"下载报表"按钮,捕获下载事件:
转账记录.xlsxYYYY-MM-DD HH:mm:ss 格式且为东8墙钟时间;"收款方类型"为中文(支付宝账户/银行卡);数据条数与当前筛选下的列表一致快速检查同页其他功能(单笔转账、批量转账模板下载)不受影响;pnpm ts:check 全量错误数不高于改动前基线