feat(callback): 回调记录列表页与交易记录菜单 i18n

运营端新增通道入站回调记录查询页,同步权限码与多语种菜单标题。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
DaxPay Dev
2026-07-21 15:42:09 +08:00
parent 243b293fa0
commit c3bc348f13
23 changed files with 521 additions and 10 deletions

View File

@@ -0,0 +1,43 @@
import type { MchEntity, PageResult, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 通道入站回调记录管理 API
*/
export const PayCallbackRecordApi = {
/** 回调记录分页 */
page(params: PayCallbackRecordQuery & { current?: number; size?: number }): Promise<Result<PageResult<PayCallbackRecordResult>>> {
return defHttp.get({ url: '/admin/callback-record/page', params });
},
/** 回调记录详情 */
getById(id: string): Promise<Result<PayCallbackRecordResult>> {
return defHttp.get({ url: '/admin/callback-record/get-by-id', params: { id } });
},
};
/** 回调记录查询 */
export interface PayCallbackRecordQuery {
mchNo?: string;
appId?: string;
channelMchNo?: string;
tradeNo?: string;
outTradeNo?: string;
channel?: string;
callbackType?: string;
status?: string;
}
/** 回调记录结果 */
export interface PayCallbackRecordResult extends MchEntity {
appId?: string;
channelMchNo?: string;
tradeNo?: string;
outTradeNo?: string;
channel?: string;
callbackType?: string;
notifyInfo?: string;
status?: string;
errorMsg?: string;
}

View File

@@ -189,6 +189,9 @@ export const PermCodes = {
VIEW: 'trade:mch-notice:view',
MANAGE: 'trade:mch-notice:manage',
},
CallbackRecord: {
VIEW: 'trade:callback-record:view',
},
},
Develop: {

View File

@@ -0,0 +1,18 @@
{
"title": "Callback Records",
"tradeNo": "Trade No.",
"outTradeNo": "Channel Trade No.",
"channel": "Channel",
"callbackType": "Callback Type",
"status": "Status",
"errorMsg": "Error",
"notifyInfo": "Notify Payload",
"createTime": "Notify Time",
"placeholderTradeNo": "Enter trade no.",
"placeholderOutTradeNo": "Enter channel trade no.",
"typePay": "Pay",
"typeRefund": "Refund",
"detail": "Callback Detail",
"channelMchNo": "Channel Merchant No.",
"placeholderChannelMchNo": "Enter channel merchant no."
}

View File

@@ -0,0 +1,18 @@
{
"title": "Catatan Callback",
"tradeNo": "No. Transaksi",
"outTradeNo": "No. Transaksi Channel",
"channel": "Channel",
"callbackType": "Jenis Callback",
"status": "Status",
"errorMsg": "Kesalahan",
"notifyInfo": "Payload Notifikasi",
"createTime": "Waktu Notifikasi",
"placeholderTradeNo": "Masukkan no. transaksi",
"placeholderOutTradeNo": "Masukkan no. transaksi channel",
"typePay": "Bayar",
"typeRefund": "Refund",
"detail": "Detail Callback",
"channelMchNo": "No. Pedagang Saluran",
"placeholderChannelMchNo": "Masukkan no. pedagang saluran"
}

View File

@@ -0,0 +1,18 @@
{
"title": "コールバック記録",
"tradeNo": "取引番号",
"outTradeNo": "チャネル取引番号",
"channel": "決済チャネル",
"callbackType": "コールバック種別",
"status": "処理状態",
"errorMsg": "エラー情報",
"notifyInfo": "通知ペイロード",
"createTime": "通知日時",
"placeholderTradeNo": "取引番号を入力",
"placeholderOutTradeNo": "チャネル取引番号を入力",
"typePay": "支払",
"typeRefund": "返金",
"detail": "コールバック詳細",
"channelMchNo": "チャネル加盟店番号",
"placeholderChannelMchNo": "チャネル加盟店番号を入力"
}

View File

@@ -0,0 +1,18 @@
{
"title": "콜백 기록",
"tradeNo": "거래번호",
"outTradeNo": "채널 거래번호",
"channel": "결제 채널",
"callbackType": "콜백 유형",
"status": "처리 상태",
"errorMsg": "오류 정보",
"notifyInfo": "알림 페이로드",
"createTime": "알림 시간",
"placeholderTradeNo": "거래번호 입력",
"placeholderOutTradeNo": "채널 거래번호 입력",
"typePay": "결제",
"typeRefund": "환불",
"detail": "콜백 상세",
"channelMchNo": "채널 가맹점번호",
"placeholderChannelMchNo": "채널 가맹점번호 입력"
}

View File

@@ -0,0 +1,18 @@
{
"title": "Rekod Callback",
"tradeNo": "No. Dagangan",
"outTradeNo": "No. Dagangan Saluran",
"channel": "Saluran",
"callbackType": "Jenis Callback",
"status": "Status",
"errorMsg": "Ralat",
"notifyInfo": "Payload Pemberitahuan",
"createTime": "Masa Pemberitahuan",
"placeholderTradeNo": "Masukkan no. dagangan",
"placeholderOutTradeNo": "Masukkan no. dagangan saluran",
"typePay": "Bayar",
"typeRefund": "Bayaran Balik",
"detail": "Butiran Callback",
"channelMchNo": "No. Pedagang Saluran",
"placeholderChannelMchNo": "Masukkan no. pedagang saluran"
}

View File

@@ -0,0 +1,18 @@
{
"title": "บันทึกการเรียกกลับ",
"tradeNo": "หมายเลขธุรกรรม",
"outTradeNo": "หมายเลขธุรกรรมช่องทาง",
"channel": "ช่องทางชำระเงิน",
"callbackType": "ประเภทการเรียกกลับ",
"status": "สถานะ",
"errorMsg": "ข้อผิดพลาด",
"notifyInfo": "เพย์โหลดการแจ้งเตือน",
"createTime": "เวลาแจ้งเตือน",
"placeholderTradeNo": "กรอกหมายเลขธุรกรรม",
"placeholderOutTradeNo": "กรอกหมายเลขธุรกรรมช่องทาง",
"typePay": "ชำระเงิน",
"typeRefund": "คืนเงิน",
"detail": "รายละเอียดการเรียกกลับ",
"channelMchNo": "รหัสร้านค้าช่องทาง",
"placeholderChannelMchNo": "กรอกรหัสร้านค้าช่องทาง"
}

View File

@@ -0,0 +1,18 @@
{
"title": "Bản ghi callback",
"tradeNo": "Mã giao dịch",
"outTradeNo": "Mã giao dịch kênh",
"channel": "Kênh thanh toán",
"callbackType": "Loại callback",
"status": "Trạng thái",
"errorMsg": "Lỗi",
"notifyInfo": "Payload thông báo",
"createTime": "Thời gian thông báo",
"placeholderTradeNo": "Nhập mã giao dịch",
"placeholderOutTradeNo": "Nhập mã giao dịch kênh",
"typePay": "Thanh toán",
"typeRefund": "Hoàn tiền",
"detail": "Chi tiết callback",
"channelMchNo": "Mã merchant kênh",
"placeholderChannelMchNo": "Nhập mã merchant kênh"
}

View File

@@ -0,0 +1,18 @@
{
"title": "回调记录",
"tradeNo": "交易号",
"outTradeNo": "通道交易号",
"channel": "支付通道",
"callbackType": "回调类型",
"status": "处理状态",
"errorMsg": "错误信息",
"notifyInfo": "通知报文",
"createTime": "通知时间",
"placeholderTradeNo": "请输入交易号",
"placeholderOutTradeNo": "请输入通道交易号",
"typePay": "支付",
"typeRefund": "退款",
"detail": "回调详情",
"channelMchNo": "通道商户号",
"placeholderChannelMchNo": "请输入通道商户号"
}

View File

@@ -0,0 +1,18 @@
{
"title": "回調記錄",
"tradeNo": "交易號",
"outTradeNo": "通道交易號",
"channel": "支付通道",
"callbackType": "回調類型",
"status": "處理狀態",
"errorMsg": "錯誤資訊",
"notifyInfo": "通知報文",
"createTime": "通知時間",
"placeholderTradeNo": "請輸入交易號",
"placeholderOutTradeNo": "請輸入通道交易號",
"typePay": "支付",
"typeRefund": "退款",
"detail": "回調詳情",
"channelMchNo": "通道商戶號",
"placeholderChannelMchNo": "請輸入通道商戶號"
}

View File

@@ -0,0 +1,18 @@
{
"title": "回調記錄",
"tradeNo": "交易號",
"outTradeNo": "通道交易號",
"channel": "支付通道",
"callbackType": "回調類型",
"status": "處理狀態",
"errorMsg": "錯誤資訊",
"notifyInfo": "通知報文",
"createTime": "通知時間",
"placeholderTradeNo": "請輸入交易號",
"placeholderOutTradeNo": "請輸入通道交易號",
"typePay": "支付",
"typeRefund": "退款",
"detail": "回調詳情",
"channelMchNo": "通道商戶號",
"placeholderChannelMchNo": "請輸入通道商戶號"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.system": "System Security",
"menu.system.security.pay": "Pay Security",
"menu.payment.merchant.terminal": "System Terminals",
"menu.device.terminal.channel": "Channel Terminals"
"menu.device.terminal.channel": "Channel Terminals",
"menu.trade.callbackRecord": "Callback Records",
"menu.trade.record": "Trade Records"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.pay": "Keamanan Pembayaran",
"menu.payment.merchant.terminal": "Terminal Sistem",
"menu.device.terminal.channel": "Terminal Saluran",
"menu.trade.mchNotice": "Notifikasi Merchant"
"menu.trade.mchNotice": "Notifikasi Merchant",
"menu.trade.callbackRecord": "Catatan Callback",
"menu.trade.record": "Catatan Transaksi"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.pay": "決済セキュリティ",
"menu.payment.merchant.terminal": "システム端末",
"menu.device.terminal.channel": "チャネル端末",
"menu.trade.mchNotice": "加盟店通知"
"menu.trade.mchNotice": "加盟店通知",
"menu.trade.callbackRecord": "コールバック記録",
"menu.trade.record": "取引記録"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.pay": "결제 보안",
"menu.payment.merchant.terminal": "시스템 단말",
"menu.device.terminal.channel": "채널 단말",
"menu.trade.mchNotice": "가맹점 알림"
"menu.trade.mchNotice": "가맹점 알림",
"menu.trade.callbackRecord": "콜백 기록",
"menu.trade.record": "거래 기록"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.pay": "Keselamatan Pembayaran",
"menu.payment.merchant.terminal": "Terminal Sistem",
"menu.device.terminal.channel": "Terminal Saluran",
"menu.trade.mchNotice": "Notis Pedagang"
"menu.trade.mchNotice": "Notis Pedagang",
"menu.trade.callbackRecord": "Rekod Callback",
"menu.trade.record": "Rekod Dagangan"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.pay": "ความปลอดภัยการชำระเงิน",
"menu.payment.merchant.terminal": "เทอร์มินัลระบบ",
"menu.device.terminal.channel": "เทอร์มินัลช่องทาง",
"menu.trade.mchNotice": "การแจ้งเตือนร้านค้า"
"menu.trade.mchNotice": "การแจ้งเตือนร้านค้า",
"menu.trade.callbackRecord": "บันทึกการเรียกกลับ",
"menu.trade.record": "บันทึกธุรกรรม"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.pay": "Bảo mật thanh toán",
"menu.payment.merchant.terminal": "Thiết bị hệ thống",
"menu.device.terminal.channel": "Thiết bị kênh",
"menu.trade.mchNotice": "Thông báo merchant"
"menu.trade.mchNotice": "Thông báo merchant",
"menu.trade.callbackRecord": "Bản ghi callback",
"menu.trade.record": "Nhật ký giao dịch"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.system": "系统安全配置",
"menu.system.security.pay": "支付安全配置",
"menu.payment.merchant.terminal": "系统终端",
"menu.device.terminal.channel": "通道终端"
"menu.device.terminal.channel": "通道终端",
"menu.trade.callbackRecord": "回调记录",
"menu.trade.record": "交易记录"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.pay": "支付安全配置",
"menu.payment.merchant.terminal": "系統終端",
"menu.device.terminal.channel": "通道終端",
"menu.trade.mchNotice": "商戶通知"
"menu.trade.mchNotice": "商戶通知",
"menu.trade.callbackRecord": "回調記錄",
"menu.trade.record": "交易記錄"
}

View File

@@ -95,5 +95,7 @@
"menu.system.security.pay": "支付安全配置",
"menu.payment.merchant.terminal": "系統終端",
"menu.device.terminal.channel": "通道終端",
"menu.trade.mchNotice": "商戶通知"
"menu.trade.mchNotice": "商戶通知",
"menu.trade.callbackRecord": "回調記錄",
"menu.trade.record": "交易記錄"
}

View File

@@ -0,0 +1,265 @@
<script lang="ts" setup>
import type { VxeTableInstance, VxeToolbarInstance } from 'vxe-table';
import { computed, onMounted, ref } from 'vue';
import { $t } from '@vben/locales';
import {
PayCallbackRecordApi,
type PayCallbackRecordQuery,
type PayCallbackRecordResult,
} from '#/api/payment/record/callback-record.api';
import { BQuery, type QueryField } from '#/components/query';
import { useDict } from '#/hooks/useDict';
defineOptions({ name: 'PayCallbackRecordList' });
const { dictConvert, dictItems: channelOptions } = useDict('channel');
const { dictItems: statusOptions } = useDict('callback_status');
const loading = ref(false);
const xTable = ref<VxeTableInstance>();
const xToolbar = ref<VxeToolbarInstance>();
const queryForm = ref<PayCallbackRecordQuery>({});
const pageConfig = ref({ currentPage: 1, pageSize: 10, total: 0 });
const tableData = ref<PayCallbackRecordResult[]>([]);
const drawerVisible = ref(false);
const detail = ref<PayCallbackRecordResult>({});
const callbackTypeOptions = computed(() => [
// 支付
{ label: $t('payment.record.callbackRecord.typePay'), value: 'pay' },
// 退款
{ label: $t('payment.record.callbackRecord.typeRefund'), value: 'refund' },
]);
const queryFields = computed<QueryField[]>(() => [
{
type: 'string',
field: 'channelMchNo',
// 通道商户号
name: $t('payment.record.callbackRecord.channelMchNo'),
placeholder: $t('payment.record.callbackRecord.placeholderChannelMchNo'),
},
{
type: 'string',
field: 'tradeNo',
// 交易号
name: $t('payment.record.callbackRecord.tradeNo'),
placeholder: $t('payment.record.callbackRecord.placeholderTradeNo'),
},
{
type: 'string',
field: 'outTradeNo',
// 通道交易号
name: $t('payment.record.callbackRecord.outTradeNo'),
placeholder: $t('payment.record.callbackRecord.placeholderOutTradeNo'),
},
{
type: 'select',
field: 'channel',
// 支付通道
name: $t('payment.record.callbackRecord.channel'),
options: channelOptions.value,
},
{
type: 'select',
field: 'callbackType',
// 回调类型
name: $t('payment.record.callbackRecord.callbackType'),
options: callbackTypeOptions.value,
},
{
type: 'select',
field: 'status',
// 处理状态
name: $t('payment.record.callbackRecord.status'),
options: statusOptions.value,
},
{
type: 'string',
field: 'mchNo',
name: $t('payment.order.field.merchant'),
},
]);
function callbackTypeLabel(type?: string) {
if (type === 'pay') return $t('payment.record.callbackRecord.typePay');
if (type === 'refund') return $t('payment.record.callbackRecord.typeRefund');
return type || '-';
}
function formatNotifyInfo(raw?: string) {
if (!raw) return '-';
try {
return JSON.stringify(JSON.parse(raw), null, 2);
} catch {
return raw;
}
}
async function queryPage() {
loading.value = true;
try {
const { data } = await PayCallbackRecordApi.page({
...queryForm.value,
current: pageConfig.value.currentPage,
size: pageConfig.value.pageSize,
});
tableData.value = data?.records || [];
pageConfig.value.total = Number(data?.total || 0);
} finally {
loading.value = false;
}
}
function resetQuery() {
queryForm.value = {};
pageConfig.value.currentPage = 1;
queryPage();
}
async function handleView(row: PayCallbackRecordResult) {
drawerVisible.value = true;
detail.value = row;
if (row.id) {
const { data } = await PayCallbackRecordApi.getById(row.id);
if (data) {
detail.value = data;
}
}
}
onMounted(() => {
xTable.value?.connectToolbar(xToolbar.value as VxeToolbarInstance);
queryPage();
});
</script>
<template>
<div class="m-3 p-3 bg-background rounded-lg list-page-compact">
<a-card>
<BQuery :fields="queryFields" :query-params="queryForm" @query="queryPage" @reset="resetQuery" />
</a-card>
<div class="mt-4">
<a-card>
<vxe-toolbar ref="xToolbar" custom refresh :refresh-options="{ queryMethod: queryPage }" />
<vxe-table ref="xTable" :row-config="{ keyField: 'id' }" :data="tableData" :loading="loading">
<vxe-column type="seq" :title="$t('common.seq')" width="60" align="center" />
<vxe-column field="mchName" :title="$t('payment.order.field.merchant')" :min-width="140">
<template #default="{ row }">
<div class="flex flex-col">
<span>{{ row.mchName || row.mchNo || '-' }}</span>
<span v-if="row.mchNo" class="text-xs text-muted-foreground">{{ row.mchNo }}</span>
</div>
</template>
</vxe-column>
<vxe-column
field="channelMchNo"
:title="$t('payment.record.callbackRecord.channelMchNo')"
:min-width="160"
show-overflow
/>
<vxe-column
field="tradeNo"
:title="$t('payment.record.callbackRecord.tradeNo')"
:min-width="200"
show-overflow
/>
<vxe-column
field="outTradeNo"
:title="$t('payment.record.callbackRecord.outTradeNo')"
:min-width="200"
show-overflow
/>
<vxe-column field="channel" :title="$t('payment.record.callbackRecord.channel')" :min-width="120">
<template #default="{ row }">
{{ dictConvert('channel', row.channel) || row.channel || '-' }}
</template>
</vxe-column>
<vxe-column
field="callbackType"
:title="$t('payment.record.callbackRecord.callbackType')"
:min-width="100"
>
<template #default="{ row }">{{ callbackTypeLabel(row.callbackType) }}</template>
</vxe-column>
<vxe-column field="status" :title="$t('payment.record.callbackRecord.status')" :min-width="110">
<template #default="{ row }">
{{ dictConvert('callback_status', row.status) || row.status || '-' }}
</template>
</vxe-column>
<vxe-column
field="errorMsg"
:title="$t('payment.record.callbackRecord.errorMsg')"
:min-width="180"
show-overflow
/>
<vxe-column
field="createTime"
:title="$t('payment.record.callbackRecord.createTime')"
:min-width="170"
formatter="formatDateTime"
/>
<vxe-column fixed="right" :width="100" :show-overflow="false" :title="$t('common.operation')">
<template #default="{ row }">
<a-button type="link" size="small" @click="handleView(row)">
{{ $t('common.view') }}
</a-button>
</template>
</vxe-column>
</vxe-table>
<vxe-pager
size="medium"
:loading="loading"
:current-page="pageConfig.currentPage"
:page-size="pageConfig.pageSize"
:total="pageConfig.total"
@page-change="
({ currentPage, pageSize }) => {
pageConfig.currentPage = currentPage;
pageConfig.pageSize = pageSize;
queryPage();
}
"
/>
</a-card>
</div>
<a-drawer
v-model:open="drawerVisible"
:title="$t('payment.record.callbackRecord.detail')"
width="720"
destroy-on-close
>
<div class="mb-4 space-y-2 text-sm">
<div>{{ $t('payment.record.callbackRecord.tradeNo') }}: {{ detail.tradeNo || '-' }}</div>
<div>{{ $t('payment.record.callbackRecord.outTradeNo') }}: {{ detail.outTradeNo || '-' }}</div>
<div>
{{ $t('payment.record.callbackRecord.channelMchNo') }}: {{ detail.channelMchNo || '-' }}
</div>
<div>
{{ $t('payment.record.callbackRecord.channel') }}:
{{ dictConvert('channel', detail.channel) || detail.channel || '-' }}
</div>
<div>
{{ $t('payment.record.callbackRecord.callbackType') }}: {{ callbackTypeLabel(detail.callbackType) }}
</div>
<div>
{{ $t('payment.record.callbackRecord.status') }}:
{{ dictConvert('callback_status', detail.status) || detail.status || '-' }}
</div>
<div v-if="detail.errorMsg">
{{ $t('payment.record.callbackRecord.errorMsg') }}: {{ detail.errorMsg }}
</div>
</div>
<div class="mb-2 text-sm font-medium">{{ $t('payment.record.callbackRecord.notifyInfo') }}</div>
<pre class="max-h-[60vh] overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap break-all">{{
formatNotifyInfo(detail.notifyInfo)
}}</pre>
</a-drawer>
</div>
</template>