feat(admin): 易支付配置页与应用工作台入口

新增 EasyPayConfig 子页(两列分区、V1/V2 显隐、查看/编辑),工作台支付配置入口与权限码;平台公钥展示去 PEM 纯 Base64,补齐多语言菜单与文案。
This commit is contained in:
DaxPay Dev
2026-07-15 12:35:37 +08:00
parent a329653e62
commit cf73c42499
24 changed files with 1172 additions and 16 deletions

View File

@@ -0,0 +1,46 @@
import type { BaseEntity, Result } from '#/types/web';
import { defHttp } from '#/api/request';
/**
* 易支付凭证 API应用级
*/
export const EasyPayCredentialApi = {
/**
* 按应用号查询(不存在则自动创建)
*/
getByAppId(appId: string): Promise<Result<EasyPayCredentialResult>> {
return defHttp.get({ url: '/admin/easypay/credential/get-by-app-id', params: { appId } });
},
/**
* 更新凭证
*/
update(data: EasyPayCredentialParam): Promise<Result<void>> {
return defHttp.post({ url: '/admin/easypay/credential/update', data });
},
};
export interface EasyPayCredentialResult extends BaseEntity {
pid?: number;
appId?: string;
enable?: boolean;
enableV1?: boolean;
enableV2?: boolean;
md5Key?: string;
useSystemKey?: boolean;
publicKey?: string;
platformPublicKey?: string;
easyPayV1ApiUrl?: string;
easyPayV2ApiUrl?: string;
}
export interface EasyPayCredentialParam {
id?: string;
appId: string;
enable?: boolean;
enableV1?: boolean;
enableV2?: boolean;
md5Key?: string;
useSystemKey?: boolean;
publicKey?: string;
}

View File

@@ -71,6 +71,11 @@ export const PermCodes = {
VIEW: 'merchant:gateway-cashier:view',
MANAGE: 'merchant:gateway-cashier:manage',
},
/** 易支付协议配置 menuCode=merchant:easypay */
EasyPay: {
VIEW: 'merchant:easypay:view',
MANAGE: 'merchant:easypay:manage',
},
Store: {
VIEW: 'merchant:store:view',
MANAGE: 'merchant:store:manage',

View File

@@ -47,5 +47,38 @@
"deleteDesc": "This cannot be undone. Proceed with caution",
"noActions": "No actions available",
"openActionAria": "Open app configuration"
},
"easypay": {
"v1Hint": "V1 is weakly secured; enable only for legacy clients",
"useSystemKey": "Use system key pair",
"v2Url": "V2 API base URL",
"desc": "Expose EasyPay V1/V2 protocol APIs",
"title": "EasyPay Config",
"notifyHint": "Async notify is not sent in this release; poll query API",
"platformPublicKey": "Platform RSA public key",
"enableV1": "Enable V1",
"confirmSave": "Save EasyPay configuration?",
"md5Key": "V1 MD5 key",
"pid": "EasyPay merchant id (pid)",
"enableV2": "Enable V2",
"md5KeyPh": "Enter MD5 key",
"publicKey": "Merchant RSA public key",
"v1Url": "V1 API base URL",
"enable": "Enable",
"section": {
"endpoint": "Basics",
"v1": "V1 protocol",
"v2": "V2 protocol"
},
"pidDesc": "App-assigned EasyPay merchant id used as pid by integrators",
"v2UrlDesc": "V2 protocol API base URL to share with integrators",
"v1UrlDesc": "V1 protocol API base URL for legacy clients only",
"enableDesc": "When off, this app rejects EasyPay protocol requests",
"enableV2Desc": "Recommended; use V2 for new integrations",
"enableV1Desc": "V1 is weakly secured; enable only for legacy clients",
"md5KeyDesc": "MD5 key used for V1 request signing",
"useSystemKeyDesc": "Use the platform key pair so merchant public key is not required",
"publicKeyDesc": "Merchant RSA public key when using a custom key pair",
"platformPublicKeyDesc": "Platform public key for integrator signature verification"
}
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "Hal ini tidak dapat dibatalkan. Lanjutkan dengan hati-hati",
"noActions": "Tidak ada tindakan yang tersedia",
"openActionAria": "Buka konfigurasi aplikasi"
},
"easypay": {
"v1Hint": "V1 memiliki keamanan lemah; aktifkan hanya untuk klien lama",
"useSystemKey": "Gunakan pasangan kunci sistem",
"v2Url": "URL dasar API V2",
"desc": "Menyediakan API protokol EasyPay V1/V2",
"title": "Konfigurasi EasyPay",
"notifyHint": "Notifikasi async tidak dikirim pada rilis ini; lakukan polling API query",
"platformPublicKey": "Kunci publik RSA platform",
"enableV1": "Aktifkan V1",
"confirmSave": "Simpan konfigurasi EasyPay?",
"md5Key": "Kunci MD5 V1",
"pid": "ID merchant EasyPay (pid)",
"enableV2": "Aktifkan V2",
"md5KeyPh": "Masukkan kunci MD5",
"publicKey": "Kunci publik RSA merchant",
"v1Url": "URL dasar API V1",
"enable": "Aktifkan",
"section": {
"endpoint": "Dasar",
"v1": "Protokol V1",
"v2": "Protokol V2"
},
"pidDesc": "ID merchant EasyPay yang ditetapkan per aplikasi, digunakan sebagai pid oleh integrator",
"v2UrlDesc": "URL dasar API protokol V2 untuk dibagikan ke integrator",
"v1UrlDesc": "URL dasar API protokol V1, hanya untuk klien lama",
"enableDesc": "Saat nonaktif, aplikasi ini menolak permintaan protokol EasyPay",
"enableV2Desc": "Direkomendasikan; gunakan V2 untuk integrasi baru",
"enableV1Desc": "V1 memiliki keamanan lemah; aktifkan hanya untuk klien lama",
"md5KeyDesc": "Kunci MD5 untuk penandatanganan permintaan V1",
"useSystemKeyDesc": "Gunakan pasangan kunci platform sehingga kunci publik merchant tidak diperlukan",
"publicKeyDesc": "Kunci publik RSA merchant saat menggunakan pasangan kunci kustom",
"platformPublicKeyDesc": "Kunci publik platform untuk verifikasi tanda tangan integrator"
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "これを元に戻すことはできません。慎重に進めてください",
"noActions": "利用可能なアクションはありません",
"openActionAria": "アプリ設定を開く"
},
"easypay": {
"v1Hint": "V1 は安全性が低いため、旧システム互換時のみ有効にしてください",
"useSystemKey": "システム鍵ペアを使用",
"v2Url": "V2 接続URL",
"desc": "EasyPay V1/V2 プロトコルによる外部連携",
"title": "EasyPay設定",
"notifyHint": "本バージョンでは非同期通知を送信しません。query で注文状態をポーリングしてください",
"platformPublicKey": "プラットフォーム RSA 公開鍵",
"enableV1": "V1 を有効化",
"confirmSave": "EasyPay 設定を保存しますか?",
"md5Key": "V1 MD5 鍵",
"pid": "EasyPay 加盟店番号 (pid)",
"enableV2": "V2 を有効化",
"md5KeyPh": "MD5 鍵を入力",
"publicKey": "加盟店 RSA 公開鍵",
"v1Url": "V1 接続URL",
"enable": "有効",
"section": {
"endpoint": "基本接続",
"v1": "V1 プロトコル設定",
"v2": "V2 プロトコル設定"
},
"pidDesc": "アプリ単位で割り当てられる EasyPay 加盟店番号。連携側は pid として使用",
"v2UrlDesc": "V2 プロトコル API のルートURL。連携側へ共有",
"v1UrlDesc": "V1 プロトコル API のルートURL。旧システム互換時のみ",
"enableDesc": "無効にすると当該アプリは EasyPay リクエストを拒否します",
"enableV2Desc": "推奨。新規連携は V2 を利用してください",
"enableV1Desc": "V1 は安全性が低いため、旧システム互換時のみ有効にしてください",
"md5KeyDesc": "V1 署名用の MD5 鍵",
"useSystemKeyDesc": "プラットフォーム鍵ペアを使う場合、加盟店公開鍵の入力は不要です",
"publicKeyDesc": "独自鍵ペア利用時の加盟店 RSA 公開鍵",
"platformPublicKeyDesc": "連携側の署名検証用プラットフォーム公開鍵"
}
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "이 작업은 취소할 수 없습니다. 주의해서 진행하세요",
"noActions": "사용 가능한 작업이 없습니다.",
"openActionAria": "앱 구성 열기"
},
"easypay": {
"v1Hint": "V1은 보안이 약하므로 레거시 호환 시에만 활성화하세요",
"useSystemKey": "시스템 키 쌍 사용",
"v2Url": "V2 연동 주소",
"desc": "EasyPay V1/V2 프로토콜 외부 연동",
"title": "EasyPay 설정",
"notifyHint": "이번 버전에서는 비동기 알림을 보내지 않습니다. query로 주문 상태를 폴링하세요",
"platformPublicKey": "플랫폼 RSA 공개키",
"enableV1": "V1 사용",
"confirmSave": "EasyPay 설정을 저장하시겠습니까?",
"md5Key": "V1 MD5 키",
"pid": "EasyPay 가맹점 번호(pid)",
"enableV2": "V2 사용",
"md5KeyPh": "MD5 키 입력",
"publicKey": "가맹점 RSA 공개키",
"v1Url": "V1 연동 주소",
"enable": "사용",
"section": {
"endpoint": "기본 연동",
"v1": "V1 프로토콜 설정",
"v2": "V2 프로토콜 설정"
},
"pidDesc": "앱별로 할당된 EasyPay 가맹점 번호. 연동측에서 pid로 사용",
"v2UrlDesc": "V2 프로토콜 API 루트 URL. 연동측에 공유",
"v1UrlDesc": "V1 프로토콜 API 루트 URL. 레거시 호환 전용",
"enableDesc": "끄면 해당 앱은 EasyPay 요청을 거부합니다",
"enableV2Desc": "권장. 신규 연동은 V2를 사용하세요",
"enableV1Desc": "V1은 보안이 약하므로 레거시 호환 시에만 활성화하세요",
"md5KeyDesc": "V1 서명용 MD5 키",
"useSystemKeyDesc": "플랫폼 키 쌍을 사용하면 가맹점 공개키를 입력할 필요가 없습니다",
"publicKeyDesc": "자체 키 쌍 사용 시 가맹점 RSA 공개키",
"platformPublicKeyDesc": "연동측 서명 검증용 플랫폼 공개키"
}
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "Ini tidak boleh dibuat asal. Teruskan dengan berhati-hati",
"noActions": "Tiada tindakan tersedia",
"openActionAria": "Buka konfigurasi apl"
},
"easypay": {
"v1Hint": "V1 mempunyai keselamatan yang lemah; aktifkan hanya untuk klien lama",
"useSystemKey": "Gunakan pasangan kunci sistem",
"v2Url": "URL asas API V2",
"desc": "Mendedahkan API protokol EasyPay V1/V2",
"title": "Konfigurasi EasyPay",
"notifyHint": "Pemberitahuan async tidak dihantar dalam keluaran ini; tinjau API query",
"platformPublicKey": "Kunci awam RSA platform",
"enableV1": "Aktifkan V1",
"confirmSave": "Simpan konfigurasi EasyPay?",
"md5Key": "Kunci MD5 V1",
"pid": "ID pedagang EasyPay (pid)",
"enableV2": "Aktifkan V2",
"md5KeyPh": "Masukkan kunci MD5",
"publicKey": "Kunci awam RSA pedagang",
"v1Url": "URL asas API V1",
"enable": "Aktifkan",
"section": {
"endpoint": "Asas",
"v1": "Protokol V1",
"v2": "Protokol V2"
},
"pidDesc": "ID pedagang EasyPay yang ditetapkan setiap aplikasi, digunakan sebagai pid oleh pengintegrasi",
"v2UrlDesc": "URL asas API protokol V2 untuk dikongsi dengan pengintegrasi",
"v1UrlDesc": "URL asas API protokol V1, hanya untuk klien lama",
"enableDesc": "Apabila dimatikan, aplikasi ini menolak permintaan protokol EasyPay",
"enableV2Desc": "Disyorkan; gunakan V2 untuk integrasi baharu",
"enableV1Desc": "V1 mempunyai keselamatan yang lemah; aktifkan hanya untuk klien lama",
"md5KeyDesc": "Kunci MD5 untuk penandatanganan permintaan V1",
"useSystemKeyDesc": "Gunakan pasangan kunci platform supaya kunci awam pedagang tidak diperlukan",
"publicKeyDesc": "Kunci awam RSA pedagang apabila menggunakan pasangan kunci tersuai",
"platformPublicKeyDesc": "Kunci awam platform untuk pengesahan tandatangan pengintegrasi"
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "สิ่งนี้ไม่สามารถยกเลิกได้ ดำเนินการด้วยความระมัดระวัง",
"noActions": "ไม่มีการดำเนินการใดๆ",
"openActionAria": "เปิดการกำหนดค่าแอป"
},
"easypay": {
"v1Hint": "V1 มีความปลอดภัยต่ำ; เปิดใช้งานเฉพาะไคลเอนต์เก่า",
"useSystemKey": "ใช้คู่คีย์ของระบบ",
"v2Url": "URL ฐาน API V2",
"desc": "เปิดเผย API โปรโตคอล EasyPay V1/V2",
"title": "การตั้งค่า EasyPay",
"notifyHint": "รุ่นนี้ยังไม่ส่งการแจ้งเตือนแบบ async; ให้สอบถาม query API",
"platformPublicKey": "รหัสสาธารณะ RSA ของแพลตฟอร์ม",
"enableV1": "เปิดใช้งาน V1",
"confirmSave": "บันทึกการตั้งค่า EasyPay?",
"md5Key": "คีย์ MD5 V1",
"pid": "รหัสผู้ค้า EasyPay (pid)",
"enableV2": "เปิดใช้งาน V2",
"md5KeyPh": "กรอกคีย์ MD5",
"publicKey": "รหัสสาธารณะ RSA ของผู้ค้า",
"v1Url": "URL ฐาน API V1",
"enable": "เปิดใช้งาน",
"section": {
"endpoint": "พื้นฐาน",
"v1": "โปรโตคอล V1",
"v2": "โปรโตคอล V2"
},
"pidDesc": "รหัสผู้ค้า EasyPay ที่กำหนดให้แต่ละแอป ใช้เป็น pid โดยผู้เชื่อมต่อ",
"v2UrlDesc": "URL ฐาน API โปรโตคอล V2 สำหรับแชร์ให้ผู้เชื่อมต่อ",
"v1UrlDesc": "URL ฐาน API โปรโตคอล V1 สำหรับไคลเอนต์เก่าเท่านั้น",
"enableDesc": "เมื่อปิด แอปนี้จะปฏิเสธคำขอโปรโตคอล EasyPay",
"enableV2Desc": "แนะนำ; ใช้ V2 สำหรับการเชื่อมต่อใหม่",
"enableV1Desc": "V1 มีความปลอดภัยต่ำ; เปิดใช้งานเฉพาะไคลเอนต์เก่า",
"md5KeyDesc": "คีย์ MD5 สำหรับการเซ็นคำขอ V1",
"useSystemKeyDesc": "ใช้คู่คีย์ของแพลตฟอร์มจึงไม่ต้องกรอกรหัสสาธารณะผู้ค้า",
"publicKeyDesc": "รหัสสาธารณะ RSA ของผู้ค้า เมื่อใช้คู่คีย์ที่กำหนดเอง",
"platformPublicKeyDesc": "รหัสสาธารณะของแพลตฟอร์มสำหรับการตรวจสอบลายเซ็นผู้เชื่อมต่อ"
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "Điều này không thể hoàn tác được. Tiến hành thận trọng",
"noActions": "Không có hành động nào",
"openActionAria": "Mở cấu hình ứng dụng"
},
"easypay": {
"v1Hint": "V1 có bảo mật yếu; chỉ bật cho khách hàng cũ",
"useSystemKey": "Dùng cặp khóa hệ thống",
"v2Url": "URL cơ sở API V2",
"desc": "Cung cấp API giao thức EasyPay V1/V2",
"title": "Cấu hình EasyPay",
"notifyHint": "Bản này chưa gửi thông báo async; hãy polling query API",
"platformPublicKey": "Khóa công khai RSA nền tảng",
"enableV1": "Bật V1",
"confirmSave": "Lưu cấu hình EasyPay?",
"md5Key": "Khóa MD5 V1",
"pid": "ID merchant EasyPay (pid)",
"enableV2": "Bật V2",
"md5KeyPh": "Nhập khóa MD5",
"publicKey": "Khóa công khai RSA merchant",
"v1Url": "URL cơ sở API V1",
"enable": "Bật",
"section": {
"endpoint": "Cơ bản",
"v1": "Giao thức V1",
"v2": "Giao thức V2"
},
"pidDesc": "ID merchant EasyPay cấp theo ứng dụng, dùng làm pid bởi bên tích hợp",
"v2UrlDesc": "URL cơ sở API giao thức V2 để chia sẻ với bên tích hợp",
"v1UrlDesc": "URL cơ sở API giao thức V1, chỉ cho khách hàng cũ",
"enableDesc": "Khi tắt, ứng dụng này từ chối yêu cầu giao thức EasyPay",
"enableV2Desc": "Đề nghị; dùng V2 cho tích hợp mới",
"enableV1Desc": "V1 có bảo mật yếu; chỉ bật cho khách hàng cũ",
"md5KeyDesc": "Khóa MD5 dùng để ký yêu cầu V1",
"useSystemKeyDesc": "Dùng cặp khóa nền tảng nên không cần khóa công khai merchant",
"publicKeyDesc": "Khóa công khai RSA merchant khi dùng cặp khóa tùy chỉnh",
"platformPublicKeyDesc": "Khóa công khai nền tảng để bên tích hợp xác minh chữ ký"
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "删除后不可恢复,请谨慎操作",
"noActions": "暂无可用操作",
"openActionAria": "进入应用配置"
},
"easypay": {
"v1Hint": "V1 安全性较弱,仅兼容旧系统时开启",
"useSystemKey": "使用系统密钥",
"v2Url": "V2 对接地址",
"desc": "对外兼容易支付 V1/V2 协议接入",
"title": "易支付配置",
"notifyHint": "本期暂不发送异步通知,请使用 query 轮询订单状态",
"platformPublicKey": "平台 RSA 公钥",
"enableV1": "开启 V1",
"confirmSave": "确定保存易支付配置吗?",
"md5Key": "V1 MD5 密钥",
"pid": "易支付商户号(pid)",
"enableV2": "开启 V2",
"md5KeyPh": "请输入 MD5 密钥",
"publicKey": "商户 RSA 公钥",
"v1Url": "V1 对接地址",
"enable": "启用",
"section": {
"endpoint": "基础对接",
"v1": "V1 协议配置",
"v2": "V2 协议配置"
},
"pidDesc": "系统按应用分配的易支付商户号,对接方作为 pid 使用",
"v2UrlDesc": "V2 协议 API 根地址,复制给对接方",
"v1UrlDesc": "V1 协议 API 根地址,仅兼容旧系统时使用",
"enableDesc": "关闭后该应用将拒绝易支付协议请求",
"enableV2Desc": "推荐开启;新对接请走 V2",
"enableV1Desc": "V1 安全性较弱,仅兼容旧系统时开启",
"md5KeyDesc": "V1 签名用 MD5 密钥",
"useSystemKeyDesc": "使用平台密钥对时无需填写商户公钥",
"publicKeyDesc": "商户侧 RSA 公钥(自签时填写)",
"platformPublicKeyDesc": "平台公钥,对接方验签使用"
}
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "刪除後不可恢復,請謹慎操作",
"noActions": "暫無可用操作",
"openActionAria": "進入應用配置"
},
"easypay": {
"v1Hint": "V1 安全性較弱,僅兼容舊系統時開啟",
"useSystemKey": "使用系統密鑰",
"v2Url": "V2 對接地址",
"desc": "對外兼容易支付 V1/V2 協議接入",
"title": "易支付配置",
"notifyHint": "本期暫不發送異步通知,請使用 query 輪詢訂單狀態",
"platformPublicKey": "平台 RSA 公鑰",
"enableV1": "開啟 V1",
"confirmSave": "確定保存易支付配置嗎?",
"md5Key": "V1 MD5 密鑰",
"pid": "易支付商戶號(pid)",
"enableV2": "開啟 V2",
"md5KeyPh": "請輸入 MD5 密鑰",
"publicKey": "商戶 RSA 公鑰",
"v1Url": "V1 對接地址",
"enable": "啟用",
"section": {
"endpoint": "基礎對接",
"v1": "V1 協議配置",
"v2": "V2 協議配置"
},
"pidDesc": "系統按應用分配的易支付商戶號,對接方作為 pid 使用",
"v2UrlDesc": "V2 協議 API 根位址,複製給對接方",
"v1UrlDesc": "V1 協議 API 根位址,僅兼容舊系統時使用",
"enableDesc": "關閉後該應用將拒絕易支付協議請求",
"enableV2Desc": "建議開啟;新對接請走 V2",
"enableV1Desc": "V1 安全性較弱,僅兼容舊系統時開啟",
"md5KeyDesc": "V1 簽名用 MD5 密鑰",
"useSystemKeyDesc": "使用平台密鑰對時無需填寫商戶公鑰",
"publicKeyDesc": "商戶側 RSA 公鑰(自簽時填寫)",
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用"
}
}
}

View File

@@ -47,5 +47,38 @@
"deleteDesc": "刪除後不可恢復,請謹慎操作",
"noActions": "暫無可用操作",
"openActionAria": "進入應用配置"
},
"easypay": {
"v1Hint": "V1 安全性較弱,僅兼容舊系統時開啟",
"useSystemKey": "使用系統密鑰",
"v2Url": "V2 對接地址",
"desc": "對外兼容易支付 V1/V2 協議接入",
"title": "易支付配置",
"notifyHint": "本期暫不發送異步通知,請使用 query 輪詢訂單狀態",
"platformPublicKey": "平台 RSA 公鑰",
"enableV1": "開啟 V1",
"confirmSave": "確定保存易支付配置嗎?",
"md5Key": "V1 MD5 密鑰",
"pid": "易支付商戶號(pid)",
"enableV2": "開啟 V2",
"md5KeyPh": "請輸入 MD5 密鑰",
"publicKey": "商戶 RSA 公鑰",
"v1Url": "V1 對接地址",
"enable": "啟用",
"section": {
"endpoint": "基礎對接",
"v1": "V1 協議配置",
"v2": "V2 協議配置"
},
"pidDesc": "系統按應用分配的易支付商戶號,對接方作為 pid 使用",
"v2UrlDesc": "V2 協議 API 根位址,複製給對接方",
"v1UrlDesc": "V1 協議 API 根位址,僅兼容舊系統時使用",
"enableDesc": "關閉後該應用將拒絕易支付協議請求",
"enableV2Desc": "建議開啟;新對接請走 V2",
"enableV1Desc": "V1 安全性較弱,僅兼容舊系統時開啟",
"md5KeyDesc": "V1 簽名用 MD5 密鑰",
"useSystemKeyDesc": "使用平台密鑰對時無需填寫商戶公鑰",
"publicKeyDesc": "商戶側 RSA 公鑰(自簽時填寫)",
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用"
}
}
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "App Configuration",
"menu.payment.lakala.config": "Lakala ISV Configuration",
"menu.payment.merchant.group.channelApp": "Channel App Config",
"menu.payment.config.group.channelIsv": "Channel ISV Config"
"menu.payment.config.group.channelIsv": "Channel ISV Config",
"menu.payment.merchant.easypay": "EasyPay Config"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "Konfigurasi Aplikasi",
"menu.payment.lakala.config": "Konfigurasi ISV Lakala",
"menu.payment.merchant.group.channelApp": "Konfigurasi Aplikasi Saluran",
"menu.payment.config.group.channelIsv": "Konfigurasi ISV Saluran"
"menu.payment.config.group.channelIsv": "Konfigurasi ISV Saluran",
"menu.payment.merchant.easypay": "EasyPay Config"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "アプリの構成",
"menu.payment.lakala.config": "Lakala ISV 構成",
"menu.payment.merchant.group.channelApp": "チャネルアプリ構成",
"menu.payment.config.group.channelIsv": "チャネル ISV 構成"
"menu.payment.config.group.channelIsv": "チャネル ISV 構成",
"menu.payment.merchant.easypay": "EasyPay設定"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "앱 구성",
"menu.payment.lakala.config": "Lakala ISV 구성",
"menu.payment.merchant.group.channelApp": "채널 앱 구성",
"menu.payment.config.group.channelIsv": "채널 ISV 구성"
"menu.payment.config.group.channelIsv": "채널 ISV 구성",
"menu.payment.merchant.easypay": "EasyPay 설정"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "Konfigurasi Apl",
"menu.payment.lakala.config": "Konfigurasi ISV Lakala",
"menu.payment.merchant.group.channelApp": "Konfigurasi Apl Saluran",
"menu.payment.config.group.channelIsv": "Saluran ISV Config"
"menu.payment.config.group.channelIsv": "Saluran ISV Config",
"menu.payment.merchant.easypay": "EasyPay Config"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "การกำหนดค่าแอป",
"menu.payment.lakala.config": "การกำหนดค่า Lakala ISV",
"menu.payment.merchant.group.channelApp": "การกำหนดค่าแอปช่อง",
"menu.payment.config.group.channelIsv": "การกำหนดค่า ISV ของช่องสัญญาณ"
"menu.payment.config.group.channelIsv": "การกำหนดค่า ISV ของช่องสัญญาณ",
"menu.payment.merchant.easypay": "EasyPay Config"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "Cấu hình ứng dụng",
"menu.payment.lakala.config": "Cấu hình Lakala ISV",
"menu.payment.merchant.group.channelApp": "Cấu hình ứng dụng kênh",
"menu.payment.config.group.channelIsv": "Cấu hình ISV kênh"
"menu.payment.config.group.channelIsv": "Cấu hình ISV kênh",
"menu.payment.merchant.easypay": "EasyPay Config"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "应用配置",
"menu.payment.lakala.config": "拉卡拉服务商配置",
"menu.payment.merchant.group.channelApp": "通道应用配置",
"menu.payment.config.group.channelIsv": "通道服务商配置"
"menu.payment.config.group.channelIsv": "通道服务商配置",
"menu.payment.merchant.easypay": "易支付配置"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "應用配置",
"menu.payment.lakala.config": "拉卡拉服務商配置",
"menu.payment.merchant.group.channelApp": "通道應用配置",
"menu.payment.config.group.channelIsv": "通道服務商配置"
"menu.payment.config.group.channelIsv": "通道服務商配置",
"menu.payment.merchant.easypay": "易支付配置"
}

View File

@@ -78,5 +78,6 @@
"menu.payment.merchant.appWorkbench": "應用配置",
"menu.payment.lakala.config": "拉卡拉服務商配置",
"menu.payment.merchant.group.channelApp": "通道應用配置",
"menu.payment.config.group.channelIsv": "通道服務商配置"
"menu.payment.config.group.channelIsv": "通道服務商配置",
"menu.payment.merchant.easypay": "易支付配置"
}

View File

@@ -106,6 +106,17 @@
action: 'notify',
});
}
// 易支付协议配置subpage /payment/merchant/easypay?mchNo&appId
if (hasPermission(PermCodes.Merchant.EasyPay.VIEW)) {
paymentCards.push({
key: 'easypay',
// 卡片标题与菜单 i18n_key 一致
title: $t('menu.payment.merchant.easypay'),
icon: 'ant-design:api-outlined',
description: $t('payment.merchant.app.easypay.desc'),
route: '/payment/merchant/easypay',
});
}
if (paymentCards.length > 0) {
groups.push({
group: $t('payment.merchant.app.app.groupConfig'),

View File

@@ -0,0 +1,754 @@
<script lang="ts" setup>
import { computed, onMounted, reactive, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { $t } from '@vben/locales';
import { IconifyIcon } from '@vben-core/icons';
import { useClipboard } from '@vueuse/core';
import {
EasyPayCredentialApi,
type EasyPayCredentialParam,
type EasyPayCredentialResult,
} from '#/api/payment/merchant/easypay-credential.api';
import { MchAppInfoApi, type MchAppInfoResult } from '#/api/payment/merchant/mch-app-info.api';
import RouteQueryMissingState from '#/components/route/RouteQueryMissingState.vue';
import { PermCodes } from '#/constants/perm-codes';
import { useFormEdit } from '#/hooks/useFormEdit';
import { useMessage } from '#/hooks/useMessage';
import { usePermission } from '#/hooks/usePermission';
import { normalizeRouteQueryValue, useRequiredRouteQuery } from '#/hooks/useRequiredRouteQuery';
defineOptions({ name: 'EasyPayConfig' });
const route = useRoute();
const router = useRouter();
const { copy } = useClipboard();
const { confirm, message } = useMessage();
const { hasPermission } = usePermission();
const { diffForm } = useFormEdit();
const routeContext = useRequiredRouteQuery({
keys: ['mchNo', 'appId'],
messageKey: computed(() =>
normalizeRouteQueryValue(route.query.mchNo)
? 'payment.common.route.missingAppContext'
: 'payment.common.route.missingMchNo',
),
fallbackPath: computed(() => {
const no = normalizeRouteQueryValue(route.query.mchNo);
const app = normalizeRouteQueryValue(route.query.appId);
if (no && app) {
return { path: '/payment/merchant/app/manage', query: { mchNo: no, appId: app } };
}
return no ? { path: '/payment/merchant/app', query: { mchNo: no } } : '/payment/merchant';
}),
});
const mchNo = computed(() => routeContext.query.value.mchNo);
const appId = computed(() => routeContext.query.value.appId);
const canManage = computed(() => hasPermission(PermCodes.Merchant.EasyPay.MANAGE));
const loading = ref(false);
const saving = ref(false);
// 编辑模式
const isEditing = ref(false);
const appInfo = ref<MchAppInfoResult>({});
const form = reactive<EasyPayCredentialResult>({
enable: false,
enableV1: false,
enableV2: true,
useSystemKey: true,
});
// 原始脱敏数据,用于 diffForm 比对
const originalForm = ref<EasyPayCredentialResult>({});
/**
* 平台公钥规范化:去掉 PEM 头尾与空白,得到可直接复制的纯 Base64
*/
function normalizePublicKey(pem?: string) {
if (!pem) {
return pem;
}
return pem
.replace(/-----BEGIN PUBLIC KEY-----/g, '')
.replace(/-----END PUBLIC KEY-----/g, '')
.replace(/\s+/g, '');
}
/**
* 重置表单默认值后合并服务端数据
*/
function applyCredential(data?: EasyPayCredentialResult | null) {
Object.assign(
form,
{
id: undefined,
pid: undefined,
appId: appId.value,
enable: false,
enableV1: false,
enableV2: true,
useSystemKey: true,
md5Key: undefined,
publicKey: undefined,
platformPublicKey: undefined,
easyPayV1ApiUrl: undefined,
easyPayV2ApiUrl: undefined,
},
data || {},
);
// 展示/复制用纯 Base64与商业版、后端 Result 一致;前端再兜底一次)
form.platformPublicKey = normalizePublicKey(form.platformPublicKey);
originalForm.value = { ...form };
}
/**
* 加载应用信息(页头展示名)
*/
async function loadAppInfo() {
if (!mchNo.value || !appId.value) {
return;
}
const { data } = await MchAppInfoApi.page({
mchNo: mchNo.value,
current: 1,
size: 200,
});
const app = data?.records?.find((a) => a.appId === appId.value);
appInfo.value = app || {};
}
/**
* 加载易支付凭证
*/
async function loadCredential() {
if (!appId.value) {
return;
}
loading.value = true;
try {
const { data } = await EasyPayCredentialApi.getByAppId(appId.value);
applyCredential(data);
} finally {
loading.value = false;
}
}
/**
* 返回应用工作台
*/
function handleBack() {
router.push({
path: '/payment/merchant/app/manage',
query: { mchNo: mchNo.value, appId: appId.value },
});
}
/**
* 进入编辑模式
*/
function handleEdit() {
isEditing.value = true;
}
/**
* 取消编辑
*/
function handleCancel() {
confirm({
title: $t('common.confirm'),
content: $t('common.confirmCancelContent'),
okText: $t('common.okText'),
cancelText: $t('common.cancelText'),
onOk: async () => {
await loadCredential();
isEditing.value = false;
},
});
}
/**
* 复制文本
*/
function handleCopy(text?: string | number | null) {
if (text === undefined || text === null || text === '') {
return;
}
copy(String(text));
message.success($t('common.operationSuccess'));
}
/**
* 保存配置
*/
function handleSave() {
if (!form.appId || !canManage.value) {
return;
}
confirm({
title: $t('common.confirm'),
content: $t('payment.merchant.app.easypay.confirmSave'),
okText: $t('common.okText'),
cancelText: $t('common.cancelText'),
onOk: async () => {
saving.value = true;
try {
// 敏感字段未修改时不提交
const sensitiveData = diffForm(originalForm, form, 'md5Key', 'publicKey');
const submitData: EasyPayCredentialParam = {
id: form.id,
appId: form.appId!,
enable: form.enable,
enableV1: form.enableV1,
enableV2: form.enableV2,
useSystemKey: form.useSystemKey,
...sensitiveData,
};
await EasyPayCredentialApi.update(submitData);
message.success($t('common.operationSuccess'));
isEditing.value = false;
await loadCredential();
} finally {
saving.value = false;
}
},
});
}
async function bootstrap() {
if (!routeContext.isValid.value) {
return;
}
isEditing.value = false;
await loadAppInfo();
await loadCredential();
}
watch(
() => [routeContext.query.value.mchNo, routeContext.query.value.appId],
() => {
bootstrap();
},
);
onMounted(() => {
bootstrap();
});
</script>
<template>
<RouteQueryMissingState
v-if="!routeContext.isValid"
:description="$t('payment.common.route.missingAppContext')"
:back-text="$t('payment.merchant.app.app.backToAppList')"
@back="routeContext.goFallback"
/>
<div v-else class="m-4">
<a-card variant="borderless" class="rounded-xl shadow-sm">
<template #title>
<div class="flex items-center gap-2">
<a-button
type="text"
class="flex items-center justify-center rounded-full hover:bg-accent"
@click="handleBack"
>
<template #icon>
<IconifyIcon icon="ant-design:arrow-left-outlined" class="text-lg" />
</template>
</a-button>
<!-- 页头与菜单 i18n_key 一致 -->
<span class="text-lg font-bold text-foreground">
{{ $t('menu.payment.merchant.easypay') }}
</span>
<span v-if="appInfo.appName" class="text-sm text-muted-foreground">
({{ appInfo.appName }})
</span>
</div>
</template>
<template #extra>
<template v-if="canManage">
<template v-if="!isEditing">
<a-button type="primary" @click="handleEdit">{{ $t('common.edit') }}</a-button>
</template>
<template v-else>
<a-space>
<a-button @click="handleCancel">{{ $t('common.cancelText') }}</a-button>
<a-button type="primary" :loading="saving" @click="handleSave">
{{ $t('common.save') }}
</a-button>
</a-space>
</template>
</template>
</template>
<a-spin :spinning="loading">
<div class="easypay-form-container max-w-6xl px-4 py-2">
<!-- 信息提示 -->
<div class="info-banner">
<IconifyIcon icon="ant-design:info-circle-filled" />
<span>{{ $t('payment.merchant.app.easypay.desc') }}</span>
</div>
<div class="info-banner warning">
<IconifyIcon icon="ant-design:warning-filled" />
<span>{{ $t('payment.merchant.app.easypay.notifyHint') }}</span>
</div>
<a-form layout="vertical" class="module-form">
<!-- 基础对接 -->
<div class="config-section">
<div class="config-section__title">
{{ $t('payment.merchant.app.easypay.section.endpoint') }}
</div>
<div class="config-grid">
<!-- 总开关半列 -->
<div class="config-item config-item--block">
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.enable') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.enableDesc') }}
</div>
</div>
<a-radio-group
v-model:value="form.enable"
button-style="solid"
:disabled="!isEditing"
>
<a-radio-button :value="true">{{ $t('common.enable') }}</a-radio-button>
<a-radio-button :value="false">{{ $t('common.disable') }}</a-radio-button>
</a-radio-group>
</div>
<!-- pid半列 -->
<div class="config-item config-item--block">
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.pid') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.pidDesc') }}
</div>
</div>
<div class="readonly-field">
<a-input :value="form.pid" disabled class="font-mono text-sm" />
<a-button type="link" size="small" @click="handleCopy(form.pid)">
<template #icon>
<IconifyIcon icon="ant-design:copy-outlined" />
</template>
{{ $t('common.copy') }}
</a-button>
</div>
</div>
<!-- V2 URL整行 -->
<div class="config-item config-item--block config-item--full">
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.v2Url') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.v2UrlDesc') }}
</div>
</div>
<div class="readonly-field">
<a-input :value="form.easyPayV2ApiUrl" disabled class="font-mono text-sm" />
<a-button type="link" size="small" @click="handleCopy(form.easyPayV2ApiUrl)">
<template #icon>
<IconifyIcon icon="ant-design:copy-outlined" />
</template>
{{ $t('common.copy') }}
</a-button>
</div>
</div>
<!-- V1 URL整行 -->
<div class="config-item config-item--block config-item--full">
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.v1Url') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.v1UrlDesc') }}
</div>
</div>
<div class="readonly-field">
<a-input :value="form.easyPayV1ApiUrl" disabled class="font-mono text-sm" />
<a-button type="link" size="small" @click="handleCopy(form.easyPayV1ApiUrl)">
<template #icon>
<IconifyIcon icon="ant-design:copy-outlined" />
</template>
{{ $t('common.copy') }}
</a-button>
</div>
</div>
</div>
</div>
<!-- V1 协议配置 -->
<div class="config-section">
<div class="config-section__title">
{{ $t('payment.merchant.app.easypay.section.v1') }}
</div>
<div class="config-grid">
<!-- 开启 V1半列 -->
<div class="config-item config-item--block">
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.enableV1') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.enableV1Desc') }}
</div>
</div>
<a-radio-group
v-model:value="form.enableV1"
button-style="solid"
:disabled="!isEditing"
>
<a-radio-button :value="true">{{ $t('common.enable') }}</a-radio-button>
<a-radio-button :value="false">{{ $t('common.disable') }}</a-radio-button>
</a-radio-group>
</div>
<!-- MD5半列 V1 开启时显示 -->
<div v-if="form.enableV1" class="config-item config-item--block">
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.md5Key') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.md5KeyDesc') }}
</div>
</div>
<a-input
v-model:value="form.md5Key"
:disabled="!isEditing"
:placeholder="$t('payment.merchant.app.easypay.md5KeyPh')"
allow-clear
/>
</div>
</div>
</div>
<!-- V2 协议配置 -->
<div class="config-section">
<div class="config-section__title">
{{ $t('payment.merchant.app.easypay.section.v2') }}
</div>
<div class="config-grid">
<!-- 开启 V2半列 -->
<div class="config-item config-item--block">
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.enableV2') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.enableV2Desc') }}
</div>
</div>
<a-radio-group
v-model:value="form.enableV2"
button-style="solid"
:disabled="!isEditing"
>
<a-radio-button :value="true">{{ $t('common.enable') }}</a-radio-button>
<a-radio-button :value="false">{{ $t('common.disable') }}</a-radio-button>
</a-radio-group>
</div>
<!-- 使用系统密钥半列 V2 开启时显示 -->
<div v-if="form.enableV2" class="config-item config-item--block">
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.useSystemKey') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.useSystemKeyDesc') }}
</div>
</div>
<a-radio-group
v-model:value="form.useSystemKey"
button-style="solid"
:disabled="!isEditing"
>
<a-radio-button :value="true">{{ $t('common.yes') }}</a-radio-button>
<a-radio-button :value="false">{{ $t('common.no') }}</a-radio-button>
</a-radio-group>
</div>
<!-- 平台公钥整行 V2 开启 -->
<div
v-if="form.enableV2"
class="config-item config-item--block config-item--full"
>
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.platformPublicKey') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.platformPublicKeyDesc') }}
</div>
</div>
<div class="textarea-wrapper">
<a-textarea
:value="form.platformPublicKey"
:rows="4"
readonly
class="readonly-textarea font-mono text-sm"
/>
<div class="textarea-actions">
<a-button
type="link"
size="small"
@click="handleCopy(form.platformPublicKey)"
>
<template #icon>
<IconifyIcon icon="ant-design:copy-outlined" />
</template>
{{ $t('common.copy') }}
</a-button>
</div>
</div>
</div>
<!-- 商户公钥整行V2 开启且不使用系统密钥 -->
<div
v-if="form.enableV2 && !form.useSystemKey"
class="config-item config-item--block config-item--full"
>
<div class="config-item__main">
<div class="config-item__label">
{{ $t('payment.merchant.app.easypay.publicKey') }}
</div>
<div class="config-item__desc">
{{ $t('payment.merchant.app.easypay.publicKeyDesc') }}
</div>
</div>
<div class="textarea-wrapper">
<a-textarea
v-model:value="form.publicKey"
:rows="4"
:disabled="!isEditing"
allow-clear
class="font-mono text-sm"
/>
</div>
</div>
</div>
</div>
</a-form>
</div>
</a-spin>
</a-card>
</div>
</template>
<style scoped lang="less">
.easypay-form-container {
margin: 0 auto;
}
.module-form {
display: flex;
flex-direction: column;
gap: 28px;
}
.info-banner {
display: flex;
gap: 12px;
align-items: flex-start;
padding: 12px 16px;
margin-bottom: 12px;
font-size: 13px;
line-height: 1.6;
color: hsl(var(--foreground));
background-color: hsl(var(--primary) / 5%);
border: 1px solid hsl(var(--primary) / 20%);
border-radius: 8px;
:deep(.iconify) {
flex-shrink: 0;
margin-top: 2px;
font-size: 18px;
color: hsl(var(--primary));
}
&.warning {
margin-bottom: 24px;
background-color: hsl(var(--warning) / 5%);
border-color: hsl(var(--warning) / 20%);
:deep(.iconify) {
color: hsl(var(--warning));
}
}
}
.config-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.config-section__title {
font-size: 15px;
font-weight: 600;
color: hsl(var(--foreground));
}
/* 两列栅格:默认半列,整行用 --full */
.config-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.config-item {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 14px 16px;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 12px;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
&:hover {
border-color: hsl(var(--primary) / 30%);
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
}
}
.config-item--block {
flex-direction: column;
align-items: stretch;
}
.config-item--full {
grid-column: 1 / -1;
}
.config-item__main {
flex: 1;
min-width: 0;
}
.config-item__label {
font-size: 14px;
font-weight: 500;
color: hsl(var(--foreground));
}
.config-item__desc {
margin-top: 4px;
font-size: 12px;
line-height: 1.6;
color: hsl(var(--muted-foreground));
}
.readonly-field {
display: flex;
gap: 4px;
align-items: center;
width: 100%;
margin-top: 10px;
:deep(.ant-input) {
flex: 1;
}
}
.config-item--block > .ant-radio-group {
margin-top: 10px;
}
.config-item--block > .ant-input,
.config-item--block > .ant-input-affix-wrapper {
margin-top: 10px;
}
.textarea-wrapper {
position: relative;
width: 100%;
margin-top: 10px;
overflow: hidden;
background: hsl(var(--card));
border: 1px solid hsl(var(--border));
border-radius: 6px;
transition: all 0.3s;
&:hover {
border-color: hsl(var(--primary) / 50%);
}
&:focus-within {
border-color: hsl(var(--primary));
box-shadow: 0 0 0 2px hsl(var(--primary) / 20%);
}
:deep(.ant-input) {
padding: 12px;
font-size: 13px;
color: hsl(var(--foreground));
resize: none;
background: transparent;
border: none;
box-shadow: none;
&:focus {
box-shadow: none;
}
&::placeholder {
color: hsl(var(--muted-foreground));
}
}
.readonly-textarea {
cursor: default;
background-color: hsl(var(--muted) / 50%);
:deep(.ant-input) {
cursor: default;
}
}
.textarea-actions {
display: flex;
justify-content: flex-end;
padding: 6px 12px;
background: hsl(var(--muted) / 30%);
border-top: 1px solid hsl(var(--border));
}
}
:deep(.ant-input-disabled),
:deep(.ant-input[disabled]) {
color: hsl(var(--muted-foreground));
cursor: not-allowed;
background-color: hsl(var(--muted) / 30%);
&:hover {
border-color: hsl(var(--border));
}
}
@media (max-width: 768px) {
.config-grid {
grid-template-columns: 1fr;
}
.config-item--full {
grid-column: auto;
}
}
</style>