feat(admin): 新增网关开发调试页与多语菜单

补充 DevelopGateway 页面/API、unipay 交易辅助接口,以及 develop 文案与菜单标题多语。
This commit is contained in:
DaxPay Dev
2026-07-17 08:24:28 +08:00
parent d9d6960e04
commit 10f5909b79
24 changed files with 1785 additions and 0 deletions

View File

@@ -0,0 +1,83 @@
import type { Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 网关支付开发调试 API
*
* 仅组参辅助与签名, 真实预下单见 unipay-trade.api (POST /unipay/gateway/pre-pay)
*/
export const DevelopGatewayApi = {
/**
* 网关预下单参数签名
*/
sign(data: DevelopGatewayParam): Promise<Result<DevelopSignResult>> {
return defHttp.post({ url: '/admin/develop/gateway/sign', data });
},
};
/** 开发调试参数包装 */
export interface DevelopGatewayParam {
/** 业务参数 */
param: GatewayPrePayParam;
/** 生成签名使用的私钥(PEM 格式, 不入库) */
privateKey?: string;
}
/** 签名调试结果 */
export interface DevelopSignResult {
/** 待签名原文 */
signStr?: string;
/** 签名值 */
sign?: string;
}
/** 网关预下单参数(与 unipay GatewayPrePayParam 对齐) */
export interface GatewayPrePayParam {
/** 商户号 */
mchNo: string;
/** 应用号 */
appId?: string;
/** 商户订单号 */
bizOrderNo: string;
/** 支付标题 */
title: string;
/** 支付描述 */
description?: string;
/** 支付金额(分) */
amount: number;
/** 网关支付类型: cashier(统一收银台) / aggregate(聚合扫码) */
gatewayPayType: string;
/** 异步通知地址 */
notifyUrl?: string;
/** 同步跳转地址 */
returnUrl?: string;
/** 商户附加参数 */
attach?: string;
/** 支付扩展参数(JSON) */
extraParam?: string;
/** 过期时间(北京时间 yyyy-MM-dd HH:mm:ss) */
expiredTime?: string;
/** 门店号 */
storeNo?: string;
/** 随机串 */
nonceStr?: string;
/** 请求时间(北京时间 yyyy-MM-dd HH:mm:ss) */
reqTime?: string;
/** 签名 */
sign?: string;
}
/** 网关预下单结果 */
export interface GatewayPrePayResult {
/** 平台网关单号 */
orderNo?: string;
/** 商户业务单号 */
bizOrderNo?: string;
/** 业务状态 */
status?: string;
/** 网关落地页 URL */
url?: string;
/** 过期时间 */
expiredTime?: string;
}

View File

@@ -4,6 +4,10 @@
* 调试页通过此 API 模拟真实商户 HTTP 调用, 不走 admin 特权通道。
*/
import type { DaxResult } from './unipay-request';
import type {
GatewayPrePayParam,
GatewayPrePayResult,
} from '#/api/payment/develop/developGateway.api';
import type { PayParam, PayResult } from '#/api/payment/develop/developTrade.api';
import { unipayPost } from './unipay-request';
@@ -17,3 +21,13 @@ import { unipayPost } from './unipay-request';
export function uniPay(data: PayParam): Promise<DaxResult<PayResult>> {
return unipayPost<PayResult>('/unipay/pay', data);
}
/**
* 网关预下单
*
* POST /unipay/gateway/pre-pay
* 请求体须含完整商户签名字段(reqTime/nonceStr/sign 等)
*/
export function uniGatewayPrePay(data: GatewayPrePayParam): Promise<DaxResult<GatewayPrePayResult>> {
return unipayPost<GatewayPrePayResult>('/unipay/gateway/pre-pay', data);
}

View File

@@ -193,6 +193,10 @@ export const PermCodes = {
Auth: {
VIEW: 'develop:auth:view',
},
Gateway: {
VIEW: 'develop:gateway:view',
SIGN: 'develop:gateway:sign',
},
},
Device: {

View File

@@ -219,5 +219,96 @@
"copySuccess": "Copied",
"copyFail": "Copy failed"
}
},
"gateway": {
"page": {
"title": "Gateway Pay Debug",
"tag": "Simulate Merchant API",
"warning": "This page only assembles parameters and signs. On submit it simulates a merchant call to /unipay/gateway/pre-pay to generate a cashier/aggregate landing page (not an admin privileged channel). Please verify parameters."
},
"card": {
"mchApp": "Merchant",
"order": "Order",
"param": "Extra Parameters",
"preview": "Request Preview"
},
"gatewayType": {
"label": "Gateway Type",
"cashier": "Unified Cashier",
"aggregate": "Aggregate QR",
"cashierDesc": "Generate a cashier landing page where users choose a payment method",
"aggregateDesc": "Generate an aggregate QR link, one code for multiple payment methods"
},
"preview": {
"title": "Request JSON",
"signBtn": "Generate Sign Preview",
"empty": "No data"
},
"field": {
"privateKey": "Merchant Private Key",
"mchNo": "Merchant",
"appId": "App",
"bizOrderNo": "Order No.",
"amount": "Amount (cents)",
"title": "Payment Title",
"description": "Description",
"notifyUrl": "Async Notify URL",
"returnUrl": "Sync Return URL",
"expiredTime": "Expire Time",
"storeNo": "Store No."
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "Default 30 min",
"storeNo": "Offline store number",
"title": "Test order",
"description": "Enter payment description"
},
"default": {
"titlePrefix": "Test Gateway Pay"
},
"btn": {
"setPrivateKey": "Set Key",
"genOrderNo": "Generate",
"reset": "Reset",
"submit": "Submit Pre-pay"
},
"privateKey": {
"modalTitle": "Set Merchant Private Key",
"modalTip": "Enter the merchant RSA private key. It is only used for request signing (stored in browser local cache only, not used for admin privileged ordering).",
"placeholder": "Enter merchant private key",
"savedTip": "Private key saved",
"clearedTip": "Private key cleared",
"setTag": "Set",
"setLabel": "Merchant private key set",
"unsetTag": "Not set",
"clearConfirmTitle": "Confirm Clear",
"clearConfirmContent": "Are you sure to clear the merchant private key? You will need to re-enter it."
},
"result": {
"modalTitle": "Pre-pay Result (unipay)",
"url": "Gateway Landing Page URL",
"copyUrl": "Copy URL",
"openUrl": "Open",
"orderNo": "Platform Order No.",
"status": "Status",
"rawResponse": "Full DaxResult Response",
"copyFull": "Copy Full Response"
},
"rule": {
"mchNo": "Please select a merchant",
"bizOrderNo": "Please enter order no.",
"title": "Please enter payment title",
"amount": "Please enter amount",
"gatewayPayType": "Please select gateway type"
},
"msg": {
"signSuccess": "Sign generated",
"prePaySuccess": "Pre-pay request succeeded",
"prePayFail": "Pre-pay request failed",
"inputPrivateKey": "Please set merchant private key first",
"copySuccess": "Copied"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "Disalin",
"copyFail": "Penyalinan gagal"
}
},
"gateway": {
"page": {
"title": "Debug Pembayaran Gateway",
"tag": "Simulasi API Merchant",
"warning": "Halaman ini hanya merakit parameter dan menandatangani. Saat dikirim, akan mensimulasikan panggilan merchant ke /unipay/gateway/pre-pay untuk membuat halaman kasir/QR agregat (bukan kanal privilese admin). Harap verifikasi parameter."
},
"card": {
"mchApp": "Info Merchant",
"order": "Info Pesanan",
"param": "Parameter Tambahan",
"preview": "Pratinjau Permintaan"
},
"gatewayType": {
"label": "Tipe Gateway",
"cashier": "Kasir Terpadu",
"aggregate": "QR Agregat",
"cashierDesc": "Buat halaman kasir, pengguna memilih metode pembayaran",
"aggregateDesc": "Buat tautan QR agregat, satu kode untuk banyak pembayaran"
},
"preview": {
"title": "JSON Permintaan",
"signBtn": "Buat Pratinjau Tanda Tangan",
"empty": "Tidak ada data"
},
"field": {
"privateKey": "Kunci Pribadi Merchant",
"mchNo": "Merchant",
"appId": "Aplikasi",
"bizOrderNo": "No. Pesanan",
"amount": "Jumlah (sen)",
"title": "Judul Pembayaran",
"description": "Deskripsi",
"notifyUrl": "URL Notifikasi Async",
"returnUrl": "URL Return Sync",
"expiredTime": "Waktu Kedaluwarsa",
"storeNo": "No. Toko"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "Default 30 mnt",
"storeNo": "Nomor toko offline",
"title": "Pesanan uji",
"description": "Masukkan deskripsi pembayaran"
},
"default": {
"titlePrefix": "Tes Gateway Pay"
},
"btn": {
"setPrivateKey": "Atur Kunci",
"genOrderNo": "Buat",
"reset": "Reset",
"submit": "Kirim Pre-pay"
},
"privateKey": {
"modalTitle": "Atur Kunci Pribadi Merchant",
"modalTip": "Masukkan kunci pribadi RSA merchant. Hanya digunakan untuk penandatanganan permintaan (disimpan di cache lokal browser, tidak digunakan untuk pesan privilese admin).",
"placeholder": "Masukkan kunci pribadi merchant",
"savedTip": "Kunci pribadi disimpan",
"clearedTip": "Kunci pribadi dihapus",
"setTag": "Diatur",
"setLabel": "Kunci pribadi merchant diatur",
"unsetTag": "Belum diatur",
"clearConfirmTitle": "Konfirmasi Hapus",
"clearConfirmContent": "Yakin ingin menghapus kunci pribadi merchant? Anda perlu memasukkannya kembali."
},
"result": {
"modalTitle": "Hasil Pre-pay (unipay)",
"url": "URL Halaman Gateway",
"copyUrl": "Salin URL",
"openUrl": "Buka",
"orderNo": "No. Pesanan Platform",
"status": "Status",
"rawResponse": "Respons DaxResult Lengkap",
"copyFull": "Salin Respons Lengkap"
},
"rule": {
"mchNo": "Silakan pilih merchant",
"bizOrderNo": "Silakan masukkan no. pesanan",
"title": "Silakan masukkan judul pembayaran",
"amount": "Silakan masukkan jumlah",
"gatewayPayType": "Silakan pilih tipe gateway"
},
"msg": {
"signSuccess": "Tanda tangan dibuat",
"prePaySuccess": "Permintaan pre-pay berhasil",
"prePayFail": "Permintaan pre-pay gagal",
"inputPrivateKey": "Silakan atur kunci pribadi merchant terlebih dahulu",
"copySuccess": "Disalin"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "コピーされました",
"copyFail": "コピーに失敗しました"
}
},
"gateway": {
"page": {
"title": "ゲートウェイ決済デバッグ",
"tag": "マーチャントAPIシミュレート",
"warning": "このページはパラメータの組み立てと署名のみ行います。送信後、/unipay/gateway/pre-pay を呼び出してレジ/集合QRランディングページを生成します管理特権チャネルではありません。パラメータをご確認ください。"
},
"card": {
"mchApp": "マーチャント情報",
"order": "注文情報",
"param": "拡張パラメータ",
"preview": "リクエストプレビュー"
},
"gatewayType": {
"label": "ゲートウェイ種別",
"cashier": "統合レジ",
"aggregate": "集合QR",
"cashierDesc": "レジランディングページを生成し、ユーザーが支払い方法を選択",
"aggregateDesc": "集合QRリンクを生成し、一つのコードで複数支払いを自動識別"
},
"preview": {
"title": "リクエストJSON",
"signBtn": "署名プレビュー生成",
"empty": "データなし"
},
"field": {
"privateKey": "マーチャント秘密鍵",
"mchNo": "マーチャント",
"appId": "アプリ",
"bizOrderNo": "注文番号",
"amount": "支払金額(分)",
"title": "支払タイトル",
"description": "支払説明",
"notifyUrl": "非同期通知URL",
"returnUrl": "同期リダイレクトURL",
"expiredTime": "有効期限",
"storeNo": "店舗番号"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "空欄で30分",
"storeNo": "オフライン店舗番号",
"title": "テスト注文",
"description": "支払説明を入力"
},
"default": {
"titlePrefix": "テストゲートウェイ決済"
},
"btn": {
"setPrivateKey": "秘密鍵設定",
"genOrderNo": "生成",
"reset": "リセット",
"submit": "事前注文送信"
},
"privateKey": {
"modalTitle": "マーチャント秘密鍵設定",
"modalTip": "マーチャントRSA秘密鍵を入力してください。リクエスト署名にのみ使用されますブラウザローカルキャッシュにのみ保存、管理特権注文には使用されません。",
"placeholder": "マーチャント秘密鍵を入力",
"savedTip": "秘密鍵を保存しました",
"clearedTip": "秘密鍵をクリアしました",
"setTag": "設定済み",
"setLabel": "マーチャント秘密鍵設定済み",
"unsetTag": "未設定",
"clearConfirmTitle": "クリア確認",
"clearConfirmContent": "マーチャント秘密鍵をクリアしてよろしいですか?再入力が必要になります。"
},
"result": {
"modalTitle": "事前注文結果 (unipay)",
"url": "ゲートウェイランディングURL",
"copyUrl": "URLコピー",
"openUrl": "開く",
"orderNo": "プラットフォーム注文番号",
"status": "ステータス",
"rawResponse": "完全なDaxResultレスポンス",
"copyFull": "完全なレスポンスをコピー"
},
"rule": {
"mchNo": "マーチャントを選択してください",
"bizOrderNo": "注文番号を入力してください",
"title": "支払タイトルを入力してください",
"amount": "支払金額を入力してください",
"gatewayPayType": "ゲートウェイ種別を選択してください"
},
"msg": {
"signSuccess": "署名を生成しました",
"prePaySuccess": "事前注文リクエスト成功",
"prePayFail": "事前注文リクエスト失敗",
"inputPrivateKey": "先にマーチャント秘密鍵を設定してください",
"copySuccess": "コピーしました"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "복사됨",
"copyFail": "복사 실패"
}
},
"gateway": {
"page": {
"title": "게이트웨이 결제 디버그",
"tag": "가맹점 API 시뮬레이션",
"warning": "이 페이지는 매개변수 조립 및 서명만 수행합니다. 제출 시 /unipay/gateway/pre-pay 를 호출하여 캐시어/통합 QR 랜딩 페이지를 생성합니다 (관리자 권한 채널 아님). 매개변수를 확인해 주세요."
},
"card": {
"mchApp": "가맹점 정보",
"order": "주문 정보",
"param": "확장 매개변수",
"preview": "요청 미리보기"
},
"gatewayType": {
"label": "게이트웨이 유형",
"cashier": "통합 캐시어",
"aggregate": "통합 QR",
"cashierDesc": "캐시어 랜딩 페이지 생성, 사용자가 결제 방법 선택",
"aggregateDesc": "통합 QR 링크 생성, 하나의 코드로 다중 결제 자동 식별"
},
"preview": {
"title": "요청 JSON",
"signBtn": "서명 미리보기 생성",
"empty": "데이터 없음"
},
"field": {
"privateKey": "가맹점 개인키",
"mchNo": "가맹점",
"appId": "앱",
"bizOrderNo": "주문번호",
"amount": "결제 금액(분)",
"title": "결제 제목",
"description": "결제 설명",
"notifyUrl": "비동기 알림 URL",
"returnUrl": "동기 리다이렉트 URL",
"expiredTime": "만료 시간",
"storeNo": "매장 번호"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "비워두면 30분",
"storeNo": "오프라인 매장 번호",
"title": "테스트 주문",
"description": "결제 설명 입력"
},
"default": {
"titlePrefix": "테스트 게이트웨이 결제"
},
"btn": {
"setPrivateKey": "키 설정",
"genOrderNo": "생성",
"reset": "재설정",
"submit": "사전 주문 제출"
},
"privateKey": {
"modalTitle": "가맹점 개인키 설정",
"modalTip": "가맹점 RSA 개인키를 입력하세요. 요청 서명에만 사용됩니다 (브라우저 로컬 캐시에만 저장, 관리자 권한 주문에는 사용되지 않음).",
"placeholder": "가맹점 개인키 입력",
"savedTip": "개인키가 저장되었습니다",
"clearedTip": "개인키가 삭제되었습니다",
"setTag": "설정됨",
"setLabel": "가맹점 개인키 설정됨",
"unsetTag": "미설정",
"clearConfirmTitle": "삭제 확인",
"clearConfirmContent": "가맹점 개인키를 삭제하시겠습니까? 다시 입력해야 합니다."
},
"result": {
"modalTitle": "사전 주문 결과 (unipay)",
"url": "게이트웨이 랜딩 URL",
"copyUrl": "URL 복사",
"openUrl": "열기",
"orderNo": "플랫폼 주문번호",
"status": "상태",
"rawResponse": "전체 DaxResult 응답",
"copyFull": "전체 응답 복사"
},
"rule": {
"mchNo": "가맹점을 선택해 주세요",
"bizOrderNo": "주문번호를 입력해 주세요",
"title": "결제 제목을 입력해 주세요",
"amount": "결제 금액을 입력해 주세요",
"gatewayPayType": "게이트웨이 유형을 선택해 주세요"
},
"msg": {
"signSuccess": "서명이 생성되었습니다",
"prePaySuccess": "사전 주문 요청 성공",
"prePayFail": "사전 주문 요청 실패",
"inputPrivateKey": "먼저 가맹점 개인키를 설정해 주세요",
"copySuccess": "복사되었습니다"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "disalin",
"copyFail": "Salinan gagal"
}
},
"gateway": {
"page": {
"title": "Nyahpepij Bayaran Gateway",
"tag": "Simulasi API Merchant",
"warning": "Halaman ini hanya merakit parameter dan menandatangani. Apabila dihantar, ia akan mensimulasikan panggilan merchant ke /unipay/gateway/pre-pay untuk menjana halaman pendarat juruwang/QR agregat (bukan saluran keistimewaan admin). Sila sahkan parameter."
},
"card": {
"mchApp": "Maklumat Merchant",
"order": "Maklumat Pesanan",
"param": "Parameter Tambahan",
"preview": "Pratonton Permintaan"
},
"gatewayType": {
"label": "Jenis Gateway",
"cashier": "Juruwang Bersatu",
"aggregate": "QR Agregat",
"cashierDesc": "Jana halaman pendarat juruwang, pengguna memilih kaedah pembayaran",
"aggregateDesc": "Jana pautan QR agregat, satu kod untuk pelbagai pembayaran"
},
"preview": {
"title": "JSON Permintaan",
"signBtn": "Jana Pratonton Tandatangan",
"empty": "Tiada data"
},
"field": {
"privateKey": "Kunci Peribadi Merchant",
"mchNo": "Merchant",
"appId": "Aplikasi",
"bizOrderNo": "No. Pesanan",
"amount": "Jumlah (sen)",
"title": "Tajuk Pembayaran",
"description": "Penerangan",
"notifyUrl": "URL Pemberitahuan Async",
"returnUrl": "URL Pulang Sync",
"expiredTime": "Masa Tamat",
"storeNo": "No. Kedai"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "Lalai 30 min",
"storeNo": "Nombor kedai luar talian",
"title": "Pesanan ujian",
"description": "Masukkan penerangan pembayaran"
},
"default": {
"titlePrefix": "Uji Gateway Pay"
},
"btn": {
"setPrivateKey": "Tetapkan Kunci",
"genOrderNo": "Jana",
"reset": "Set Semula",
"submit": "Hantar Pra-bayar"
},
"privateKey": {
"modalTitle": "Tetapkan Kunci Peribadi Merchant",
"modalTip": "Masukkan kunci peribadi RSA merchant. Hanya digunakan untuk menandatangani permintaan (disimpan dalam cache tempatan pelayar, tidak digunakan untuk pesanan keistimewaan admin).",
"placeholder": "Masukkan kunci peribadi merchant",
"savedTip": "Kunci peribadi disimpan",
"clearedTip": "Kunci peribadi dikosongkan",
"setTag": "Ditetapkan",
"setLabel": "Kunci peribadi merchant ditetapkan",
"unsetTag": "Tidak ditetapkan",
"clearConfirmTitle": "Sahkan Kosongkan",
"clearConfirmContent": "Adakah anda pasti mahu mengosongkan kunci peribadi merchant? Anda perlu memasukkannya semula."
},
"result": {
"modalTitle": "Keputusan Pra-bayar (unipay)",
"url": "URL Halaman Pendarat Gateway",
"copyUrl": "Salin URL",
"openUrl": "Buka",
"orderNo": "No. Pesanan Platform",
"status": "Status",
"rawResponse": "Respons DaxResult Penuh",
"copyFull": "Salin Respons Penuh"
},
"rule": {
"mchNo": "Sila pilih merchant",
"bizOrderNo": "Sila masukkan no. pesanan",
"title": "Sila masukkan tajuk pembayaran",
"amount": "Sila masukkan jumlah",
"gatewayPayType": "Sila pilih jenis gateway"
},
"msg": {
"signSuccess": "Tandatangan dijana",
"prePaySuccess": "Permintaan pra-bayar berjaya",
"prePayFail": "Permintaan pra-bayar gagal",
"inputPrivateKey": "Sila tetapkan kunci peribadi merchant terlebih dahulu",
"copySuccess": "Disalin"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "คัดลอกแล้ว",
"copyFail": "การคัดลอกล้มเหลว"
}
},
"gateway": {
"page": {
"title": "ดีบักการชำระเงินเกตเวย์",
"tag": "จำลอง API ร้านค้า",
"warning": "หน้านี้ประกอบพารามิเตอร์และลงนามเท่านั้น เมื่อส่ง จะจำลองการเรียกร้านค้าไปยัง /unipay/gateway/pre-pay เพื่อสร้างหน้าเคาเตอร์/QR รวม (ไม่ใช่ช่องสิทธิพิเศษของผู้ดูแล) กรุณาตรวจสอบพารามิเตอร์"
},
"card": {
"mchApp": "ข้อมูลร้านค้า",
"order": "ข้อมูลคำสั่ง",
"param": "พารามิเตอร์เพิ่มเติม",
"preview": "ดูตัวอย่างคำขอ"
},
"gatewayType": {
"label": "ประเภทเกตเวย์",
"cashier": "เคาเตอร์รวม",
"aggregate": "QR รวม",
"cashierDesc": "สร้างหน้าเคาเตอร์ ผู้ใช้เลือกวิธีการชำระเงิน",
"aggregateDesc": "สร้างลิงก์ QR รวม หนึ่งรหัสสำหรับหลายวิธีการชำระเงิน"
},
"preview": {
"title": "JSON คำขอ",
"signBtn": "สร้างตัวอย่างลายเซ็น",
"empty": "ไม่มีข้อมูล"
},
"field": {
"privateKey": "คีย์ส่วนตัวร้านค้า",
"mchNo": "ร้านค้า",
"appId": "แอป",
"bizOrderNo": "เลขที่คำสั่ง",
"amount": "จำนวนเงิน (สตางค์)",
"title": "ชื่อการชำระเงิน",
"description": "คำอธิบาย",
"notifyUrl": "URL แจ้งเตือน async",
"returnUrl": "URL กลับ sync",
"expiredTime": "เวลาหมดอายุ",
"storeNo": "เลขที่ร้าน"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "ค่าเริ่มต้น 30 นาที",
"storeNo": "เลขที่ร้านออฟไลน์",
"title": "คำสั่งทดสอบ",
"description": "ป้อนคำอธิบายการชำระเงิน"
},
"default": {
"titlePrefix": "ทดสอบเกตเวย์"
},
"btn": {
"setPrivateKey": "ตั้งค่าคีย์",
"genOrderNo": "สร้าง",
"reset": "รีเซ็ต",
"submit": "ส่งคำสั่งซื้อล่วงหน้า"
},
"privateKey": {
"modalTitle": "ตั้งค่าคีย์ส่วนตัวร้านค้า",
"modalTip": "ป้อนคีย์ส่วนตัว RSA ของร้านค้า ใช้สำหรับลงนามคำขอเท่านั้น (เก็บในแคชภายในเบราว์เซอร์ ไม่ใช้สำหรับคำสั่งซื้อสิทธิพิเศษผู้ดูแล)",
"placeholder": "ป้อนคีย์ส่วนตัวร้านค้า",
"savedTip": "บันทึกคีย์ส่วนตัวแล้ว",
"clearedTip": "ลบคีย์ส่วนตัวแล้ว",
"setTag": "ตั้งค่าแล้ว",
"setLabel": "ตั้งค่าคีย์ส่วนตัวร้านค้าแล้ว",
"unsetTag": "ยังไม่ตั้ง",
"clearConfirmTitle": "ยืนยันการลบ",
"clearConfirmContent": "คุณแน่ใจหรือไม่ว่าต้องการลบคีย์ส่วนตัวร้านค้า? คุณจะต้องป้อนใหม่"
},
"result": {
"modalTitle": "ผลคำสั่งซื้อล่วงหน้า (unipay)",
"url": "URL หน้าเกตเวย์",
"copyUrl": "คัดลอก URL",
"openUrl": "เปิด",
"orderNo": "เลขที่คำสั่งแพลตฟอร์ม",
"status": "สถานะ",
"rawResponse": "การตอบสนอง DaxResult แบบเต็ม",
"copyFull": "คัดลอกการตอบสนองแบบเต็ม"
},
"rule": {
"mchNo": "กรุณาเลือกร้านค้า",
"bizOrderNo": "กรุณาป้อนเลขที่คำสั่ง",
"title": "กรุณาป้อนชื่อการชำระเงิน",
"amount": "กรุณาป้อนจำนวนเงิน",
"gatewayPayType": "กรุณาเลือกประเภทเกตเวย์"
},
"msg": {
"signSuccess": "สร้างลายเซ็นแล้ว",
"prePaySuccess": "คำขอสั่งซื้อล่วงหน้าสำเร็จ",
"prePayFail": "คำขอสั่งซื้อล่วงหน้าล้มเหลว",
"inputPrivateKey": "กรุณาตั้งค่าคีย์ส่วนตัวร้านค้าก่อน",
"copySuccess": "คัดลอกแล้ว"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "Đã sao chép",
"copyFail": "Sao chép không thành công"
}
},
"gateway": {
"page": {
"title": "Gỡ lỗi thanh toán cổng",
"tag": "Mô phỏng API merchant",
"warning": "Trang này chỉ lắp ráp tham số và ký. Khi gửi, sẽ mô phỏng lệnh gọi merchant đến /unipay/gateway/pre-pay để tạo trang đích quầy thu ngân/QR tổng hợp (không phải kênh đặc quyền quản trị). Vui lòng xác minh tham số."
},
"card": {
"mchApp": "Thông tin merchant",
"order": "Thông tin đơn hàng",
"param": "Tham số mở rộng",
"preview": "Xem trước yêu cầu"
},
"gatewayType": {
"label": "Loại cổng",
"cashier": "Quầy thu ngân thống nhất",
"aggregate": "QR tổng hợp",
"cashierDesc": "Tạo trang đích quầy thu ngân, người dùng chọn phương thức thanh toán",
"aggregateDesc": "Tạo liên kết QR tổng hợp, một mã cho nhiều phương thức thanh toán"
},
"preview": {
"title": "JSON yêu cầu",
"signBtn": "Tạo xem trước chữ ký",
"empty": "Không có dữ liệu"
},
"field": {
"privateKey": "Khóa riêng merchant",
"mchNo": "Merchant",
"appId": "Ứng dụng",
"bizOrderNo": "Số đơn hàng",
"amount": "Số tiền (xu)",
"title": "Tiêu đề thanh toán",
"description": "Mô tả",
"notifyUrl": "URL thông báo bất đồng bộ",
"returnUrl": "URL trả về đồng bộ",
"expiredTime": "Thời gian hết hạn",
"storeNo": "Số cửa hàng"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "Mặc định 30 phút",
"storeNo": "Số cửa hàng ngoại tuyến",
"title": "Đơn hàng thử nghiệm",
"description": "Nhập mô tả thanh toán"
},
"default": {
"titlePrefix": "Test cổng thanh toán"
},
"btn": {
"setPrivateKey": "Thiết lập khóa",
"genOrderNo": "Tạo",
"reset": "Đặt lại",
"submit": "Gửi đặt trước"
},
"privateKey": {
"modalTitle": "Thiết lập khóa riêng merchant",
"modalTip": "Nhập khóa riêng RSA merchant. Chỉ được sử dụng để ký yêu cầu (chỉ lưu trong bộ nhớ đệm cục bộ trình duyệt, không dùng cho đặt hàng đặc quyền quản trị).",
"placeholder": "Nhập khóa riêng merchant",
"savedTip": "Đã lưu khóa riêng",
"clearedTip": "Đã xóa khóa riêng",
"setTag": "Đã thiết lập",
"setLabel": "Đã thiết lập khóa riêng merchant",
"unsetTag": "Chưa thiết lập",
"clearConfirmTitle": "Xác nhận xóa",
"clearConfirmContent": "Bạn có chắc muốn xóa khóa riêng merchant? Bạn sẽ cần nhập lại."
},
"result": {
"modalTitle": "Kết quả đặt trước (unipay)",
"url": "URL trang đích cổng",
"copyUrl": "Sao chép URL",
"openUrl": "Mở",
"orderNo": "Số đơn hàng nền tảng",
"status": "Trạng thái",
"rawResponse": "Phản hồi DaxResult đầy đủ",
"copyFull": "Sao chép phản hồi đầy đủ"
},
"rule": {
"mchNo": "Vui lòng chọn merchant",
"bizOrderNo": "Vui lòng nhập số đơn hàng",
"title": "Vui lòng nhập tiêu đề thanh toán",
"amount": "Vui lòng nhập số tiền",
"gatewayPayType": "Vui lòng chọn loại cổng"
},
"msg": {
"signSuccess": "Đã tạo chữ ký",
"prePaySuccess": "Yêu cầu đặt trước thành công",
"prePayFail": "Yêu cầu đặt trước thất bại",
"inputPrivateKey": "Vui lòng thiết lập khóa riêng merchant trước",
"copySuccess": "Đã sao chép"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "复制成功",
"copyFail": "复制失败"
}
},
"gateway": {
"page": {
"title": "网关支付调试",
"tag": "模拟商户 API",
"warning": "本页仅组装参数并签名,提交后将模拟商户调用 /unipay/gateway/pre-pay 生成收银台/聚合扫码落地页(非管理端特权通道),请确认参数无误。"
},
"card": {
"mchApp": "商户信息",
"order": "订单信息",
"param": "扩展参数",
"preview": "请求预览"
},
"gatewayType": {
"label": "网关支付类型",
"cashier": "统一收银台",
"aggregate": "聚合扫码",
"cashierDesc": "生成收银台落地页,用户在页面选择支付方式",
"aggregateDesc": "生成聚合扫码链接,一码多付自动识别"
},
"preview": {
"title": "请求参数 JSON",
"signBtn": "生成签名预览",
"empty": "暂无数据"
},
"field": {
"privateKey": "商户私钥",
"mchNo": "商户",
"appId": "应用",
"bizOrderNo": "订单号",
"amount": "支付金额(分)",
"title": "支付标题",
"description": "支付描述",
"notifyUrl": "异步通知地址",
"returnUrl": "同步跳转地址",
"expiredTime": "支付超时时间",
"storeNo": "门店号"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "留空默认 30 分钟",
"storeNo": "线下经营门店号",
"title": "测试订单",
"description": "请输入支付描述"
},
"default": {
"titlePrefix": "测试网关支付"
},
"btn": {
"setPrivateKey": "设置私钥",
"genOrderNo": "生成",
"reset": "重置表单",
"submit": "提交预下单"
},
"privateKey": {
"modalTitle": "设置商户私钥",
"modalTip": "请输入商户 RSA 私钥,仅用于生成请求签名(仅保存在浏览器本地缓存中,不会用于管理端特权下单)。",
"placeholder": "请输入商户私钥",
"savedTip": "私钥保存成功",
"clearedTip": "私钥已清除",
"setTag": "已设置",
"setLabel": "已设置商户私钥",
"unsetTag": "未设置",
"clearConfirmTitle": "确认清除",
"clearConfirmContent": "确定要清除已设置的商户私钥吗?清除后需重新输入。"
},
"result": {
"modalTitle": "预下单结果 (unipay)",
"url": "网关落地页 URL",
"copyUrl": "复制链接",
"openUrl": "打开",
"orderNo": "平台网关单号",
"status": "业务状态",
"rawResponse": "完整 DaxResult 响应",
"copyFull": "复制完整响应"
},
"rule": {
"mchNo": "请选择商户",
"bizOrderNo": "请输入订单号",
"title": "请输入支付标题",
"amount": "请输入支付金额",
"gatewayPayType": "请选择网关支付类型"
},
"msg": {
"signSuccess": "签名生成成功",
"prePaySuccess": "预下单请求成功",
"prePayFail": "预下单请求失败",
"inputPrivateKey": "请先设置商户私钥",
"copySuccess": "复制成功"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "複製成功",
"copyFail": "複製失敗"
}
},
"gateway": {
"page": {
"title": "網關支付調試",
"tag": "模擬商戶 API",
"warning": "本頁僅組裝參數並簽名,提交後將模擬商戶調用 /unipay/gateway/pre-pay 生成收銀台/聚合掃碼落地頁(非管理端特權通道),請確認參數無誤。"
},
"card": {
"mchApp": "商戶資訊",
"order": "訂單資訊",
"param": "擴展參數",
"preview": "請求預覽"
},
"gatewayType": {
"label": "網關支付類型",
"cashier": "統一收銀台",
"aggregate": "聚合掃碼",
"cashierDesc": "生成收銀台落地頁,用戶喺頁面選擇支付方式",
"aggregateDesc": "生成聚合掃碼連結,一碼多付自動識別"
},
"preview": {
"title": "請求參數 JSON",
"signBtn": "生成簽名預覽",
"empty": "暫無數據"
},
"field": {
"privateKey": "商戶私鑰",
"mchNo": "商戶",
"appId": "應用",
"bizOrderNo": "訂單號",
"amount": "支付金額(分)",
"title": "支付標題",
"description": "支付描述",
"notifyUrl": "異步通知地址",
"returnUrl": "同步跳轉地址",
"expiredTime": "支付超時時間",
"storeNo": "門店號"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "留空預設 30 分鐘",
"storeNo": "線下經營門店號",
"title": "測試訂單",
"description": "請輸入支付描述"
},
"default": {
"titlePrefix": "測試網關支付"
},
"btn": {
"setPrivateKey": "設定私鑰",
"genOrderNo": "生成",
"reset": "重置表單",
"submit": "提交預下單"
},
"privateKey": {
"modalTitle": "設定商戶私鑰",
"modalTip": "請輸入商戶 RSA 私鑰,僅用於生成請求簽名(僅保存在瀏覽器本地緩存中,唔會用於管理端特權下單)。",
"placeholder": "請輸入商戶私鑰",
"savedTip": "私鑰保存成功",
"clearedTip": "私鑰已清除",
"setTag": "已設定",
"setLabel": "已設定商戶私鑰",
"unsetTag": "未設定",
"clearConfirmTitle": "確認清除",
"clearConfirmContent": "確定要清除已設定嘅商戶私鑰嗎?清除後需重新輸入。"
},
"result": {
"modalTitle": "預下單結果 (unipay)",
"url": "網關落地頁 URL",
"copyUrl": "複製連結",
"openUrl": "打開",
"orderNo": "平台網關單號",
"status": "業務狀態",
"rawResponse": "完整 DaxResult 響應",
"copyFull": "複製完整響應"
},
"rule": {
"mchNo": "請選擇商戶",
"bizOrderNo": "請輸入訂單號",
"title": "請輸入支付標題",
"amount": "請輸入支付金額",
"gatewayPayType": "請選擇網關支付類型"
},
"msg": {
"signSuccess": "簽名生成成功",
"prePaySuccess": "預下單請求成功",
"prePayFail": "預下單請求失敗",
"inputPrivateKey": "請先設定商戶私鑰",
"copySuccess": "複製成功"
}
}
}

View File

@@ -219,5 +219,96 @@
"copySuccess": "複製成功",
"copyFail": "複製失敗"
}
},
"gateway": {
"page": {
"title": "網關支付調試",
"tag": "模擬商戶 API",
"warning": "本頁僅組裝參數並簽名,提交後將模擬商戶呼叫 /unipay/gateway/pre-pay 產生收銀台/聚合掃碼落地頁(非管理端特權通道),請確認參數無誤。"
},
"card": {
"mchApp": "商戶資訊",
"order": "訂單資訊",
"param": "擴充參數",
"preview": "請求預覽"
},
"gatewayType": {
"label": "網關支付類型",
"cashier": "統一收銀台",
"aggregate": "聚合掃碼",
"cashierDesc": "產生收銀台落地頁,使用者在頁面選擇支付方式",
"aggregateDesc": "產生聚合掃碼連結,一碼多付自動識別"
},
"preview": {
"title": "請求參數 JSON",
"signBtn": "產生簽名預覽",
"empty": "暫無資料"
},
"field": {
"privateKey": "商戶私鑰",
"mchNo": "商戶",
"appId": "應用",
"bizOrderNo": "訂單號",
"amount": "支付金額(分)",
"title": "支付標題",
"description": "支付描述",
"notifyUrl": "非同步通知地址",
"returnUrl": "同步跳轉地址",
"expiredTime": "支付逾時時間",
"storeNo": "門店號"
},
"placeholder": {
"notifyUrl": "https://...",
"returnUrl": "https://...",
"expiredTime": "留空預設 30 分鐘",
"storeNo": "線下經營門店號",
"title": "測試訂單",
"description": "請輸入支付描述"
},
"default": {
"titlePrefix": "測試網關支付"
},
"btn": {
"setPrivateKey": "設定私鑰",
"genOrderNo": "產生",
"reset": "重設表單",
"submit": "提交預下單"
},
"privateKey": {
"modalTitle": "設定商戶私鑰",
"modalTip": "請輸入商戶 RSA 私鑰,僅用於產生請求簽名(僅保存在瀏覽器本地快取中,不會用於管理端特權下單)。",
"placeholder": "請輸入商戶私鑰",
"savedTip": "私鑰儲存成功",
"clearedTip": "私鑰已清除",
"setTag": "已設定",
"setLabel": "已設定商戶私鑰",
"unsetTag": "未設定",
"clearConfirmTitle": "確認清除",
"clearConfirmContent": "確定要清除已設定的商戶私鑰嗎?清除後需重新輸入。"
},
"result": {
"modalTitle": "預下單結果 (unipay)",
"url": "網關落地頁 URL",
"copyUrl": "複製連結",
"openUrl": "開啟",
"orderNo": "平台網關單號",
"status": "業務狀態",
"rawResponse": "完整 DaxResult 回應",
"copyFull": "複製完整回應"
},
"rule": {
"mchNo": "請選擇商戶",
"bizOrderNo": "請輸入訂單號",
"title": "請輸入支付標題",
"amount": "請輸入支付金額",
"gatewayPayType": "請選擇網關支付類型"
},
"msg": {
"signSuccess": "簽名產生成功",
"prePaySuccess": "預下單請求成功",
"prePayFail": "預下單請求失敗",
"inputPrivateKey": "請先設定商戶私鑰",
"copySuccess": "複製成功"
}
}
}

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "QR Code Management",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "WeChat ISV Sub-merchant App",
"menu.develop.auth": "Auth Develop",
"menu.develop.gateway": "Gateway Pay Debug",
"menu.system.notify.wechatConfig": "Wechat Message Notification",
"menu.payment.merchant.aggregateScan": "Aggregate Pay Config",
"menu.payment.merchant.group.manage": "Merchant Management",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "Manajemen Kode QR",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "Aplikasi Sub-pedagang WeChat ISV",
"menu.develop.auth": "Auth Kembangkan",
"menu.develop.gateway": "Debug Pembayaran Gateway",
"menu.system.notify.wechatConfig": "Pemberitahuan Pesan Wechat",
"menu.payment.merchant.aggregateScan": "Konfigurasi Pembayaran Agregat",
"menu.payment.merchant.group.manage": "Manajemen Pedagang",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "QRコード管理",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "WeChat ISV サブマーチャント アプリ",
"menu.develop.auth": "認証開発",
"menu.develop.gateway": "ゲートウェイ決済デバッグ",
"menu.system.notify.wechatConfig": "Wechatメッセージ通知",
"menu.payment.merchant.aggregateScan": "集計給与構成",
"menu.payment.merchant.group.manage": "加盟店管理",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "QR 코드 관리",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "WeChat ISV 하위 판매자 앱",
"menu.develop.auth": "인증 개발",
"menu.develop.gateway": "게이트웨이 결제 디버그",
"menu.system.notify.wechatConfig": "위챗 메시지 알림",
"menu.payment.merchant.aggregateScan": "종합 급여 구성",
"menu.payment.merchant.group.manage": "판매자 관리",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "Pengurusan Kod QR",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "Apl Sub-peniaga WeChat ISV",
"menu.develop.auth": "Membangunkan Pengesahan",
"menu.develop.gateway": "Nyahpepij Bayaran Gateway",
"menu.system.notify.wechatConfig": "Pemberitahuan Mesej Wechat",
"menu.payment.merchant.aggregateScan": "Konfigurasi Gaji Agregat",
"menu.payment.merchant.group.manage": "Pengurusan Peniaga",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "การจัดการรหัส QR",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "แอพผู้ค้าย่อย WeChat ISV",
"menu.develop.auth": "รับรองความถูกต้องพัฒนา",
"menu.develop.gateway": "ดีบักการชำระเงินเกตเวย์",
"menu.system.notify.wechatConfig": "การแจ้งเตือนข้อความ Wechat",
"menu.payment.merchant.aggregateScan": "การกำหนดค่าการชำระเงินรวม",
"menu.payment.merchant.group.manage": "การจัดการผู้ค้า",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "Quản lý mã QR",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "Ứng dụng người bán phụ WeChat ISV",
"menu.develop.auth": "Xác thực phát triển",
"menu.develop.gateway": "Gỡ lỗi thanh toán cổng",
"menu.system.notify.wechatConfig": "Thông báo tin nhắn Wechat",
"menu.payment.merchant.aggregateScan": "Cấu hình trả lương tổng hợp",
"menu.payment.merchant.group.manage": "Quản lý thương gia",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "码牌管理",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "微信服务商子商户应用",
"menu.develop.auth": "认证调试",
"menu.develop.gateway": "网关支付调试",
"menu.system.notify.wechatConfig": "微信消息通知",
"menu.payment.merchant.aggregateScan": "聚合支付配置",
"menu.payment.merchant.codePayConfig": "码牌支付配置",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "碼牌管理",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "微信服務商子商户應用",
"menu.develop.auth": "認證調試",
"menu.develop.gateway": "網關支付調試",
"menu.system.notify.wechatConfig": "微信消息通知",
"menu.payment.merchant.aggregateScan": "聚合支付配置",
"menu.payment.merchant.group.manage": "商户管理",

View File

@@ -64,6 +64,7 @@
"menu.device.qrcode": "碼牌管理",
"menu.payment.merchant.channelMerchant.wechatIsvApp": "微信服務商子商戶應用",
"menu.develop.auth": "認證除錯",
"menu.develop.gateway": "網關支付調試",
"menu.system.notify.wechatConfig": "微信訊息通知",
"menu.payment.merchant.aggregateScan": "聚合支付配置",
"menu.payment.merchant.group.manage": "商戶管理",

View File

@@ -0,0 +1,764 @@
<script setup lang="ts">
import type { FormInstance } from 'antdv-next';
import type {
GatewayPrePayParam,
GatewayPrePayResult,
} from '#/api/payment/develop/developGateway.api';
import type { DaxResult } from '#/api/payment/unipay/unipay-request';
import type { LabelValue } from '#/types/web';
import { computed, onMounted, reactive, ref } from 'vue';
import { JsonViewer } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import { DevelopGatewayApi } from '#/api/payment/develop/developGateway.api';
import { MchAppInfoApi } from '#/api/payment/merchant/mch-app-info.api';
import { MerchantApi } from '#/api/payment/merchant/merchant.api';
import { uniGatewayPrePay } from '#/api/payment/unipay/unipay-trade.api';
import { QrCode } from '#/components/qrcode';
import { useMessage } from '#/hooks/useMessage';
defineOptions({ name: 'DevelopGateway' });
const { message, confirm } = useMessage();
// 表单引用(用于提交前校验)
const formRef = ref<FormInstance>();
// 私钥在 localStorage 中的键名(与交易调试独立, 避免互相覆盖)
const PRIVATE_KEY_STORAGE_KEY = 'daxpay_dev_gateway_private_key';
// 私钥独立存储(标签+弹窗交互, 校验通过 form 自定义规则挂接)
const privateKey = ref('');
const privateKeyVisible = ref(false);
const privateKeyInput = ref('');
const loading = ref(false);
// ===== 表单校验规则 =====
const formRules = computed<Record<string, any[]>>(() => {
return {
mchNo: [{ required: true, message: $t('payment.develop.gateway.rule.mchNo') }],
bizOrderNo: [{ required: true, message: $t('payment.develop.gateway.rule.bizOrderNo') }],
amount: [{ required: true, message: $t('payment.develop.gateway.rule.amount') }],
title: [{ required: true, message: $t('payment.develop.gateway.rule.title') }],
gatewayPayType: [{ required: true, message: $t('payment.develop.gateway.rule.gatewayPayType') }],
// 私钥存独立 ref, 用自定义校验挂到 form 上
privateKey: [
{
validator: async () => {
if (!privateKey.value) {
return Promise.reject(new Error($t('payment.develop.gateway.msg.inputPrivateKey')));
}
},
},
],
};
});
// ===== 表单数据 =====
const form = reactive<GatewayPrePayParam>({
mchNo: '',
appId: '',
bizOrderNo: '',
title: genDefaultTitle(),
amount: 1,
gatewayPayType: 'cashier',
description: '',
});
const signPreviewLoading = ref(false);
// ===== 下拉选项 =====
const mchNoOptions = ref<LabelValue[]>([]);
const mchAppOptions = ref<LabelValue[]>([]);
// ===== 调试结果(完整 unipay DaxResult) =====
const resultVisible = ref(false);
const resultData = ref<DaxResult<GatewayPrePayResult>>({ code: 0 });
// ===== 签名预览(请求预览卡内联展示) =====
const signPreview = reactive({
signStr: '',
sign: '',
});
/**
* 生成商户订单号(时间戳 + 随机串)
*/
function genBizOrderNo() {
const ts = Date.now().toString();
// 随机 4 位
const rand = Math.floor(Math.random() * 10_000)
.toString()
.padStart(4, '0');
form.bizOrderNo = `GW${ts}${rand}`;
}
/**
* 生成默认支付标题(测试网关支付 + yyyyMMddHHmmss)
*/
function genDefaultTitle() {
const d = new Date();
const pad = (n: number) => n.toString().padStart(2, '0');
const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
return `${$t('payment.develop.gateway.default.titlePrefix')}-${ts}`;
}
/** 初始化下拉与默认值 */
async function initData() {
MerchantApi.dropdown().then(({ data }) => {
mchNoOptions.value = data ?? [];
});
if (!form.bizOrderNo) {
genBizOrderNo();
}
}
/** 商户变更: 刷新应用列表 */
function merchantChange() {
form.appId = '';
mchAppOptions.value = [];
if (!form.mchNo) return;
// 应用列表
MchAppInfoApi.page({ mchNo: form.mchNo, size: 100 }).then(({ data }) => {
mchAppOptions.value =
data?.records?.map((item) => ({
label: item.appName ? `${item.appName} (${item.appId})` : (item.appId ?? ''),
value: item.appId ?? '',
})) ?? [];
});
}
/** 下拉搜索过滤 */
function filterOption(input: string, option: any) {
return option?.label?.toString().toLowerCase().includes(input.toLowerCase());
}
// ===== 私钥相关 =====
/** 打开私钥设置弹窗 */
function showPrivateKeyModal() {
privateKeyInput.value = privateKey.value;
privateKeyVisible.value = true;
}
/** 保存私钥(同步到 localStorage) */
function savePrivateKey() {
privateKey.value = privateKeyInput.value;
if (privateKey.value) {
localStorage.setItem(PRIVATE_KEY_STORAGE_KEY, privateKey.value);
} else {
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY);
}
privateKeyVisible.value = false;
// 清除私钥字段校验态
formRef.value?.clearValidate?.(['privateKey']);
message.success($t('payment.develop.gateway.privateKey.savedTip'));
}
/** 清除私钥 */
function clearPrivateKey() {
confirm({
title: $t('payment.develop.gateway.privateKey.clearConfirmTitle'),
content: $t('payment.develop.gateway.privateKey.clearConfirmContent'),
onOk() {
privateKey.value = '';
localStorage.removeItem(PRIVATE_KEY_STORAGE_KEY);
formRef.value?.clearValidate?.(['privateKey']);
message.success($t('payment.develop.gateway.privateKey.clearedTip'));
},
});
}
// ===== 实时请求预览 =====
/**
* 生成东八区请求时间字面量 yyyy-MM-dd HH:mm:ss
* (与 PaymentCommonParam.reqTime 文档一致, 禁止用 toISOString 的 UTC)
*/
function formatReqTimeCst(): string {
return new Date().toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai' });
}
/** 生成随机 nonce(16 位字母数字) */
function genNonceStr(): string {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let s = '';
for (let i = 0; i < 16; i++) {
s += chars[Math.floor(Math.random() * chars.length)];
}
return s;
}
/** 剔除空串/null/undefined, 避免参与签名或污染请求体 */
function cleanPayload(raw: GatewayPrePayParam): GatewayPrePayParam {
const cleaned: Record<string, any> = {};
for (const [k, v] of Object.entries(raw)) {
if (v !== '' && v != null) {
cleaned[k] = v;
}
}
return cleaned as GatewayPrePayParam;
}
/**
* 组装提交参数(含公共字段 reqTime/nonceStr)
*/
function buildPayload(): GatewayPrePayParam {
const payload: GatewayPrePayParam = {
...form,
// 每次组参刷新公共字段, 贴近真实商户 SDK
reqTime: formatReqTimeCst(),
nonceStr: genNonceStr(),
// 签名由后续步骤写入, 预览阶段不带旧 sign
sign: undefined,
};
return cleanPayload(payload);
}
/** 当前请求 JSON 预览(剔除空值) */
const requestPreview = computed(() => {
return JSON.stringify(buildPayload(), null, 2);
});
/** 生成签名预览(内联展示, 不弹结果) */
async function handleSignPreview() {
try {
await formRef.value?.validateFields(['privateKey']);
} catch {
// 校验失败: 表单已显示错误提示
return;
}
signPreviewLoading.value = true;
try {
const { data } = await DevelopGatewayApi.sign({
param: buildPayload(),
privateKey: privateKey.value,
});
signPreview.signStr = data?.signStr ?? '';
signPreview.sign = data?.sign ?? '';
message.success($t('payment.develop.gateway.msg.signSuccess'));
} finally {
signPreviewLoading.value = false;
}
}
/** 复制文本到剪贴板 */
async function copyText(text: string, successKey = 'payment.develop.gateway.msg.copySuccess') {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
message.success($t(successKey));
} catch {
// 忽略
}
}
// ===== 提交 =====
/**
* 模拟商户调用 unipay 发起网关预下单
* 1. admin 仅签名 2. 浏览器 POST /unipay/gateway/pre-pay
*/
async function handlePrePay() {
// 表单字段校验(含私钥自定义规则)
try {
await formRef.value?.validate();
} catch {
// 校验未通过,字段错误已由表单自动展示
return;
}
loading.value = true;
try {
// 组参(含 reqTime/nonceStr)
const payload = buildPayload();
// 管理端签名(与 Java PaySignUtil 一致; 失败由 defHttp toast 并 throw)
const { data: signRes } = await DevelopGatewayApi.sign({
param: payload,
privateKey: privateKey.value,
});
payload.sign = signRes?.sign;
signPreview.signStr = signRes?.signStr ?? '';
signPreview.sign = signRes?.sign ?? '';
// 直调网关预下单(无 Accesstoken, 完整商户契约)
try {
const dax = await uniGatewayPrePay(payload);
resultData.value = dax ?? { code: -1 };
resultVisible.value = true;
if (dax?.code === 0) {
message.success($t('payment.develop.gateway.msg.prePaySuccess'));
} else {
// 业务失败仍展示完整 DaxResult, 便于联调
message.warning(dax?.msg || $t('payment.develop.gateway.msg.prePayFail'));
}
} catch {
// unipay 网络/非业务异常
message.error($t('payment.develop.gateway.msg.prePayFail'));
}
} finally {
loading.value = false;
}
}
/** 重置表单 */
function handleReset() {
Object.assign(form, {
mchNo: '',
appId: '',
bizOrderNo: '',
title: genDefaultTitle(),
amount: 1,
gatewayPayType: 'cashier',
description: '',
notifyUrl: undefined,
returnUrl: undefined,
expiredTime: undefined,
storeNo: undefined,
});
mchAppOptions.value = [];
signPreview.signStr = '';
signPreview.sign = '';
genBizOrderNo();
}
// ===== 结果展示 =====
/** unipay 业务 data */
const prePayResult = computed(() => resultData.value.data);
/** 结果中的落地页 URL */
const gatewayUrl = computed(() => prePayResult.value?.url ?? '');
/** 在新窗口打开落地页 */
function openGatewayUrl() {
if (gatewayUrl.value) {
window.open(gatewayUrl.value, '_blank');
}
}
/** 复制完整 DaxResult JSON */
function copyFullResult() {
copyText(JSON.stringify(resultData.value, null, 2));
}
onMounted(() => {
const saved = localStorage.getItem(PRIVATE_KEY_STORAGE_KEY);
if (saved) {
privateKey.value = saved;
}
initData();
});
</script>
<template>
<div class="develop-gateway p-4 pb-28">
<a-spin :spinning="loading">
<a-form ref="formRef" layout="vertical" :model="form" :rules="formRules">
<a-row :gutter="16">
<!-- ============ 左列 ============ -->
<a-col :lg="12" :xs="24">
<div class="flex flex-col gap-4">
<!-- 卡1: 商户与网关类型 -->
<a-card class="rounded-xl shadow-sm">
<template #title>
<div class="flex items-center gap-2">
<IconifyIcon icon="ant-design:shop-outlined" class="text-blue-500" />
<span class="font-semibold">{{ $t('payment.develop.gateway.card.mchApp') }}</span>
</div>
</template>
<!-- 私钥状态行 -->
<a-form-item :label="$t('payment.develop.gateway.field.privateKey')" name="privateKey">
<div class="flex items-center gap-2">
<a-tag v-if="privateKey" color="success">
<IconifyIcon icon="ant-design:check-circle-outlined" class="mr-0.5" />
{{ $t('payment.develop.gateway.privateKey.setTag') }}
</a-tag>
<a-tag v-else color="default">{{ $t('payment.develop.gateway.privateKey.unsetTag') }}</a-tag>
<span v-if="privateKey" class="text-xs text-muted-foreground">{{
$t('payment.develop.gateway.privateKey.setLabel')
}}</span>
<div class="flex shrink-0 gap-1">
<a-button size="small" type="primary" @click="showPrivateKeyModal">
<template #icon><IconifyIcon icon="ant-design:key-outlined" /></template>
{{ $t('payment.develop.gateway.btn.setPrivateKey') }}
</a-button>
<a-button v-if="privateKey" danger size="small" @click="clearPrivateKey">
<template #icon><IconifyIcon icon="ant-design:delete-outlined" /></template>
</a-button>
</div>
</div>
</a-form-item>
<!-- 网关支付类型 -->
<a-form-item :label="$t('payment.develop.gateway.gatewayType.label')" name="gatewayPayType">
<a-radio-group v-model:value="form.gatewayPayType" button-style="solid">
<a-radio-button value="cashier">{{ $t('payment.develop.gateway.gatewayType.cashier') }}</a-radio-button>
<a-radio-button value="aggregate">{{ $t('payment.develop.gateway.gatewayType.aggregate') }}</a-radio-button>
</a-radio-group>
<div class="mt-1 text-xs text-muted-foreground">
{{
form.gatewayPayType === 'cashier'
? $t('payment.develop.gateway.gatewayType.cashierDesc')
: $t('payment.develop.gateway.gatewayType.aggregateDesc')
}}
</div>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="$t('payment.develop.gateway.field.mchNo')" name="mchNo">
<a-select
v-model:value="form.mchNo"
show-search
:options="mchNoOptions"
:placeholder="$t('payment.develop.gateway.field.mchNo')"
:filter-option="filterOption"
allow-clear
@change="merchantChange"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="$t('payment.develop.gateway.field.appId')" name="appId">
<a-select
v-model:value="form.appId"
show-search
:options="mchAppOptions"
:placeholder="$t('payment.develop.gateway.field.appId')"
:filter-option="filterOption"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 卡2: 扩展参数 -->
<a-card class="rounded-xl shadow-sm">
<template #title>
<div class="flex items-center gap-2">
<IconifyIcon icon="ant-design:setting-outlined" class="text-purple-500" />
<span class="font-semibold">{{ $t('payment.develop.gateway.card.param') }}</span>
</div>
</template>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item :label="$t('payment.develop.gateway.field.notifyUrl')" name="notifyUrl">
<a-input
v-model:value="form.notifyUrl"
:placeholder="$t('payment.develop.gateway.placeholder.notifyUrl')"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label="$t('payment.develop.gateway.field.returnUrl')" name="returnUrl">
<a-input
v-model:value="form.returnUrl"
:placeholder="$t('payment.develop.gateway.placeholder.returnUrl')"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="$t('payment.develop.gateway.field.expiredTime')" name="expiredTime">
<a-date-picker
v-model:value="form.expiredTime"
show-time
value-format="YYYY-MM-DD HH:mm:ss"
style="width: 100%"
:placeholder="$t('payment.develop.gateway.placeholder.expiredTime')"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="$t('payment.develop.gateway.field.storeNo')" name="storeNo">
<a-input
v-model:value="form.storeNo"
:placeholder="$t('payment.develop.gateway.placeholder.storeNo')"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
</div>
</a-col>
<!-- ============ 右列 ============ -->
<a-col :lg="12" :xs="24">
<div class="flex flex-col gap-4">
<!-- 卡3: 订单信息 -->
<a-card class="rounded-xl shadow-sm">
<template #title>
<div class="flex items-center gap-2">
<IconifyIcon icon="ant-design:file-text-outlined" class="text-cyan-500" />
<span class="font-semibold">{{ $t('payment.develop.gateway.card.order') }}</span>
</div>
</template>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item :label="$t('payment.develop.gateway.field.bizOrderNo')" name="bizOrderNo">
<a-input v-model:value="form.bizOrderNo">
<template #suffix>
<a-button size="small" type="link" @click="genBizOrderNo">
<template #icon><IconifyIcon icon="ant-design:reload-outlined" /></template>
{{ $t('payment.develop.gateway.btn.genOrderNo') }}
</a-button>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="$t('payment.develop.gateway.field.amount')" name="amount">
<a-input-number
v-model:value="form.amount"
:min="1"
:precision="0"
:step="1"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="$t('payment.develop.gateway.field.title')" name="title">
<a-input v-model:value="form.title" :placeholder="$t('payment.develop.gateway.placeholder.title')">
<template #suffix>
<a-button size="small" type="link" @click="form.title = genDefaultTitle()">
<template #icon><IconifyIcon icon="ant-design:reload-outlined" /></template>
{{ $t('payment.develop.gateway.btn.genOrderNo') }}
</a-button>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label="$t('payment.develop.gateway.field.description')" name="description">
<a-textarea
v-model:value="form.description"
:rows="3"
:placeholder="$t('payment.develop.gateway.placeholder.description')"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
<!-- 卡4: 请求预览(实时 JSON + 签名预览) -->
<a-card class="rounded-xl shadow-sm">
<template #title>
<div class="flex items-center gap-2">
<IconifyIcon icon="ant-design:code-outlined" class="text-amber-500" />
<span class="font-semibold">{{ $t('payment.develop.gateway.card.preview') }}</span>
</div>
</template>
<template #extra>
<a-button size="small" type="primary" :loading="signPreviewLoading" @click="handleSignPreview">
<template #icon><IconifyIcon icon="ant-design:safety-certificate-outlined" /></template>
{{ $t('payment.develop.gateway.preview.signBtn') }}
</a-button>
</template>
<!-- 实时请求 JSON -->
<div class="mb-3 text-xs font-medium text-muted-foreground">
{{ $t('payment.develop.gateway.preview.title') }}
</div>
<pre class="code-box rounded-lg border border-border bg-muted/40 p-3 text-xs">{{ requestPreview }}</pre>
<!-- 签名预览结果 -->
<template v-if="signPreview.signStr || signPreview.sign">
<a-divider class="!my-3" />
<div class="preview-item mb-3">
<div class="mb-1 flex items-center justify-between">
<span class="text-xs font-medium text-foreground">
{{ $t('payment.develop.sign.field.signStr') }}
</span>
<a-button size="small" type="link" @click="copyText(signPreview.signStr)">
<IconifyIcon icon="ant-design:copy-outlined" />
</a-button>
</div>
<div class="code-box break-all rounded-lg border border-border bg-muted/40 p-3 text-xs">
{{ signPreview.signStr || $t('payment.develop.gateway.preview.empty') }}
</div>
</div>
<div class="preview-item">
<div class="mb-1 flex items-center justify-between">
<span class="text-xs font-medium text-foreground">
{{ $t('payment.develop.sign.field.signValue') }}
</span>
<a-button size="small" type="link" @click="copyText(signPreview.sign)">
<IconifyIcon icon="ant-design:copy-outlined" />
</a-button>
</div>
<div class="code-box break-all rounded-lg border border-border bg-muted/40 p-3 text-xs">
{{ signPreview.sign || $t('payment.develop.gateway.preview.empty') }}
</div>
</div>
</template>
</a-card>
</div>
</a-col>
</a-row>
<!-- ============ 底部悬浮操作栏 ============ -->
<div class="action-bar">
<div class="flex justify-center gap-4">
<a-button class="action-btn" size="large" @click="handleReset">
<template #icon><IconifyIcon icon="ant-design:undo-outlined" /></template>
{{ $t('payment.develop.gateway.btn.reset') }}
</a-button>
<a-button class="action-btn" danger size="large" type="primary" :loading="loading" @click="handlePrePay">
<template #icon><IconifyIcon icon="ant-design:rocket-outlined" /></template>
{{ $t('payment.develop.gateway.btn.submit') }}
</a-button>
</div>
</div>
</a-form>
</a-spin>
<!-- 设置私钥弹窗 -->
<a-modal
v-model:open="privateKeyVisible"
:title="$t('payment.develop.gateway.privateKey.modalTitle')"
:mask-closable="false"
destroy-on-hidden
@ok="savePrivateKey"
>
<div class="mb-4 text-sm text-muted-foreground">
{{ $t('payment.develop.gateway.privateKey.modalTip') }}
</div>
<a-textarea
v-model:value="privateKeyInput"
:rows="10"
allow-clear
class="code-textarea"
:placeholder="$t('payment.develop.gateway.privateKey.placeholder')"
/>
</a-modal>
<!-- 调试结果弹窗(完整 unipay DaxResult) -->
<a-modal
v-model:open="resultVisible"
:title="$t('payment.develop.gateway.result.modalTitle')"
:footer="null"
:width="640"
destroy-on-hidden
>
<!-- 响应摘要 -->
<div class="mb-4 flex flex-wrap items-center gap-2">
<a-tag :color="resultData.code === 0 ? 'success' : 'error'">
code: {{ resultData.code }}
</a-tag>
<span class="text-sm text-muted-foreground">{{ resultData.msg }}</span>
<span v-if="resultData.traceId" class="text-xs text-muted-foreground">
traceId: {{ resultData.traceId }}
</span>
</div>
<template v-if="gatewayUrl">
<div class="flex flex-col items-center">
<!-- 落地页 URL 二维码(扫码进入收银台/聚合页测试) -->
<div class="mb-3 rounded-lg border border-border bg-card p-1 shadow-sm">
<QrCode :value="gatewayUrl" :width="240" />
</div>
<div class="w-full">
<div class="mb-1 flex items-center justify-between">
<span class="text-xs font-medium text-muted-foreground">
{{ $t('payment.develop.gateway.result.url') }}
</span>
<div class="flex gap-1">
<a-button size="small" type="link" @click="copyText(gatewayUrl)">
<IconifyIcon icon="ant-design:copy-outlined" />
{{ $t('payment.develop.gateway.result.copyUrl') }}
</a-button>
<a-button size="small" type="link" @click="openGatewayUrl">
<IconifyIcon icon="ant-design:export-outlined" />
{{ $t('payment.develop.gateway.result.openUrl') }}
</a-button>
</div>
</div>
<div class="code-box mb-3 break-all rounded-lg border border-border bg-muted/40 p-3 text-xs">
{{ gatewayUrl }}
</div>
</div>
<!-- 订单号与状态 -->
<div v-if="prePayResult" class="w-full">
<a-descriptions size="small" :column="2" bordered>
<a-descriptions-item :label="$t('payment.develop.gateway.result.orderNo')">
{{ prePayResult.orderNo }}
</a-descriptions-item>
<a-descriptions-item :label="$t('payment.develop.gateway.result.status')">
{{ prePayResult.status }}
</a-descriptions-item>
</a-descriptions>
</div>
</div>
</template>
<!-- 完整 DaxResult(联调对照文档) -->
<div class="mb-1 mt-2 text-xs font-medium text-muted-foreground">
{{ $t('payment.develop.gateway.result.rawResponse') }}
</div>
<JsonViewer class="json-viewer-box mb-3" :value="resultData" :expand-depth="2" boxed copyable />
<div class="flex gap-2">
<a-button block @click="copyFullResult">
<template #icon><IconifyIcon icon="ant-design:copy-outlined" /></template>
{{ $t('payment.develop.gateway.result.copyFull') }}
</a-button>
</div>
</a-modal>
</div>
</template>
<style scoped lang="less">
.develop-gateway {
.code-textarea {
width: 100%;
padding: 8px 11px;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 13px;
line-height: 1.6;
background-color: hsl(var(--muted) / 0.4);
border-radius: 6px;
transition: all 0.3s;
&:focus {
background-color: hsl(var(--background));
}
}
.code-box {
max-height: 240px;
overflow-y: auto;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 12.5px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-all;
color: hsl(var(--foreground));
}
.monospace {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
}
// 底部悬浮居中胶囊栏: 固定在视口底部, 始终可点
.action-bar {
position: fixed;
bottom: 16px;
left: 50%;
z-index: 30;
padding: 10px 24px;
background-color: hsl(var(--card) / 0.9);
border: 1px solid hsl(var(--border));
border-radius: 9999px;
backdrop-filter: blur(8px);
box-shadow: 0 8px 24px hsl(var(--foreground) / 0.12%);
transform: translateX(-50%);
.action-btn {
min-width: 140px;
box-shadow: 0 4px 12px hsl(var(--primary) / 0.15);
}
}
}
</style>