feat(easypay): 商户RSA密钥对在线生成与公私钥纯Base64规范化

- 两端 EasyPayConfig 商户公钥区新增"生成密钥对"按钮与私钥弹窗
- 生成/保存时去除PEM头尾与换行,公私钥均以纯Base64存储(易支付要求)
- easypay 节点新增5个i18n key,两端10语种同步
This commit is contained in:
DaxPay Dev
2026-07-24 20:10:01 +08:00
parent cc843c3421
commit 5748e5c2b6
22 changed files with 325 additions and 58 deletions

View File

@@ -82,6 +82,11 @@
"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"
"platformPublicKeyDesc": "Platform public key for integrator signature verification",
"genRsaKeyPair": "Generate Key Pair",
"confirmGenPublicKey": "The public key already exists. Regenerating will overwrite it. Continue?",
"genKeyPairTitle": "RSA Key Pair",
"privateKey": "Private Key",
"privateKeyWarning": "The private key is shown only once. Keep it safe — it cannot be recovered if lost."
}
}

View File

@@ -82,6 +82,11 @@
"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"
"platformPublicKeyDesc": "Kunci publik platform untuk verifikasi tanda tangan integrator",
"genRsaKeyPair": "Buat Pasangan Kunci",
"confirmGenPublicKey": "Kunci publik sudah ada. Membuat ulang akan menimpa kunci yang ada. Lanjutkan?",
"genKeyPairTitle": "Pasangan Kunci RSA",
"privateKey": "Kunci Privat",
"privateKeyWarning": "Kunci privat hanya ditampilkan sekali di sini. Simpan dengan aman — tidak dapat dipulihkan jika hilang."
}
}

View File

@@ -82,6 +82,11 @@
"md5KeyDesc": "V1 署名用の MD5 鍵",
"useSystemKeyDesc": "プラットフォーム鍵ペアを使う場合、加盟店公開鍵の入力は不要です",
"publicKeyDesc": "独自鍵ペア利用時の加盟店 RSA 公開鍵",
"platformPublicKeyDesc": "連携側の署名検証用プラットフォーム公開鍵"
"platformPublicKeyDesc": "連携側の署名検証用プラットフォーム公開鍵",
"genRsaKeyPair": "鍵ペアを生成",
"confirmGenPublicKey": "公開鍵が既に存在します。再生成すると元の公開鍵が上書きされます。再生成しますか?",
"genKeyPairTitle": "RSA鍵ペア",
"privateKey": "秘密鍵",
"privateKeyWarning": "秘密鍵はここに一度だけ表示されます。確実に保管してください。紛失すると復元できません。"
}
}

View File

@@ -82,6 +82,11 @@
"md5KeyDesc": "V1 서명용 MD5 키",
"useSystemKeyDesc": "플랫폼 키 쌍을 사용하면 가맹점 공개키를 입력할 필요가 없습니다",
"publicKeyDesc": "자체 키 쌍 사용 시 가맹점 RSA 공개키",
"platformPublicKeyDesc": "연동측 서명 검증용 플랫폼 공개키"
"platformPublicKeyDesc": "연동측 서명 검증용 플랫폼 공개키",
"genRsaKeyPair": "키 쌍 생성",
"confirmGenPublicKey": "공개 키가 이미 존재합니다. 다시 생성하면 기존 공개 키를 덮어씁니다. 다시 생성하시겠습니까?",
"genKeyPairTitle": "RSA 키 쌍",
"privateKey": "개인 키",
"privateKeyWarning": "개인 키는 여기에 한 번만 표시됩니다. 안전하게 보관하세요. 분실 시 복구할 수 없습니다."
}
}

View File

@@ -82,6 +82,11 @@
"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"
"platformPublicKeyDesc": "Kunci awam platform untuk pengesahan tandatangan pengintegrasi",
"genRsaKeyPair": "Jana Pasangan Kunci",
"confirmGenPublicKey": "Kunci awam telah wujud. Menjana semula akan menulis ganti kunci sedia ada. Teruskan?",
"genKeyPairTitle": "Pasangan Kunci RSA",
"privateKey": "Kunci Peribadi",
"privateKeyWarning": "Kunci peribadi hanya dipaparkan sekali di sini. Simpan dengan selamat — tidak boleh dipulihkan jika hilang."
}
}

View File

@@ -82,6 +82,11 @@
"md5KeyDesc": "คีย์ MD5 สำหรับการเซ็นคำขอ V1",
"useSystemKeyDesc": "ใช้คู่คีย์ของแพลตฟอร์มจึงไม่ต้องกรอกรหัสสาธารณะผู้ค้า",
"publicKeyDesc": "รหัสสาธารณะ RSA ของผู้ค้า เมื่อใช้คู่คีย์ที่กำหนดเอง",
"platformPublicKeyDesc": "รหัสสาธารณะของแพลตฟอร์มสำหรับการตรวจสอบลายเซ็นผู้เชื่อมต่อ"
"platformPublicKeyDesc": "รหัสสาธารณะของแพลตฟอร์มสำหรับการตรวจสอบลายเซ็นผู้เชื่อมต่อ",
"genRsaKeyPair": "สร้างคู่คีย์",
"confirmGenPublicKey": "มีคีย์สาธารณะอยู่แล้ว การสร้างใหม่จะเขียนทับคีย์เดิม ต้องการสร้างใหม่หรือไม่?",
"genKeyPairTitle": "คู่คีย์ RSA",
"privateKey": "คีย์ส่วนตัว",
"privateKeyWarning": "คีย์ส่วนตัวจะแสดงที่นี่เพียงครั้งเดียว โปรดเก็บรักษาให้ดี หากสูญหายจะกู้คืนไม่ได้"
}
}

View File

@@ -82,6 +82,11 @@
"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ý"
"platformPublicKeyDesc": "Khóa công khai nền tảng để bên tích hợp xác minh chữ ký",
"genRsaKeyPair": "Tạo cặp khóa",
"confirmGenPublicKey": "Khóa công khai đã tồn tại. Tạo lại sẽ ghi đè khóa hiện có. Tiếp tục?",
"genKeyPairTitle": "Cặp khóa RSA",
"privateKey": "Khóa riêng",
"privateKeyWarning": "Khóa riêng chỉ hiển thị một lần tại đây. Vui lòng lưu giữ an toàn — không thể khôi phục nếu mất."
}
}

View File

@@ -82,6 +82,11 @@
"md5KeyDesc": "V1 签名用 MD5 密钥",
"useSystemKeyDesc": "使用平台密钥对时无需填写商户公钥",
"publicKeyDesc": "商户侧 RSA 公钥(自签时填写)",
"platformPublicKeyDesc": "平台公钥,对接方验签使用"
"platformPublicKeyDesc": "平台公钥,对接方验签使用",
"genRsaKeyPair": "生成密钥对",
"confirmGenPublicKey": "公钥已存在,重新生成将覆盖原有公钥,确定要重新生成吗?",
"genKeyPairTitle": "RSA密钥对",
"privateKey": "私钥",
"privateKeyWarning": "私钥仅在此处展示一次,请务必妥善保管,丢失无法找回"
}
}

View File

@@ -82,6 +82,11 @@
"md5KeyDesc": "V1 簽名用 MD5 密鑰",
"useSystemKeyDesc": "使用平台密鑰對時無需填寫商戶公鑰",
"publicKeyDesc": "商戶側 RSA 公鑰(自簽時填寫)",
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用"
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用",
"genRsaKeyPair": "產生金鑰對",
"confirmGenPublicKey": "公鑰已存在,重新產生將覆蓋原有公鑰,確定要重新產生嗎?",
"genKeyPairTitle": "RSA金鑰對",
"privateKey": "私鑰",
"privateKeyWarning": "私鑰僅在此處顯示一次,請務必妥善保管,遺失無法找回"
}
}

View File

@@ -82,6 +82,11 @@
"md5KeyDesc": "V1 簽名用 MD5 密鑰",
"useSystemKeyDesc": "使用平台密鑰對時無需填寫商戶公鑰",
"publicKeyDesc": "商戶側 RSA 公鑰(自簽時填寫)",
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用"
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用",
"genRsaKeyPair": "產生金鑰對",
"confirmGenPublicKey": "公鑰已存在,重新產生將覆蓋原有公鑰,確定要重新產生嗎?",
"genKeyPairTitle": "RSA金鑰對",
"privateKey": "私鑰",
"privateKeyWarning": "私鑰僅在此處顯示一次,請務必妥善保管,遺失無法找回"
}
}

View File

@@ -13,6 +13,7 @@
type EasyPayCredentialParam,
type EasyPayCredentialResult,
} from '#/api/payment/merchant/easypay-credential.api';
import { KeyGenApi } from '#/api/payment/merchant/key-gen.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';
@@ -64,18 +65,36 @@
});
// 原始脱敏数据,用于 diffForm 比对
const originalForm = ref<EasyPayCredentialResult>({});
// RSA 密钥对弹窗
const keyPairVisible = ref(false);
const privateKeyContent = ref('');
/**
* 平台公钥规范化:去掉 PEM 头尾与空白,得到可直接复制的纯 Base64
* 公钥规范化:去掉 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, '');
.replaceAll('-----BEGIN PUBLIC KEY-----', '')
.replaceAll('-----END PUBLIC KEY-----', '')
.replaceAll(/\s+/g, '');
}
/**
* 私钥规范化:去掉 PEM 头尾与空白,得到可直接复制的纯 Base64易支付要求
*/
function normalizePrivateKey(pem?: string) {
if (!pem) {
return pem;
}
return pem
.replaceAll('-----BEGIN PRIVATE KEY-----', '')
.replaceAll('-----END PRIVATE KEY-----', '')
.replaceAll('-----BEGIN RSA PRIVATE KEY-----', '')
.replaceAll('-----END RSA PRIVATE KEY-----', '')
.replaceAll(/\s+/g, '');
}
/**
@@ -173,7 +192,7 @@
/**
* 复制文本
*/
function handleCopy(text?: string | number | null) {
function handleCopy(text?: null | number | string) {
if (text === undefined || text === null || text === '') {
return;
}
@@ -181,6 +200,39 @@
message.success($t('common.operationSuccess'));
}
/**
* 生成 RSA 密钥对(公钥已存在时二次确认)
*/
async function handleGenRsaKeyPair() {
if (form.publicKey) {
confirm({
title: $t('common.confirm'),
content: $t('payment.merchant.app.easypay.confirmGenPublicKey'),
okText: $t('common.okText'),
cancelText: $t('common.cancelText'),
onOk: async () => {
await doGenRsaKeyPair();
},
});
} else {
await doGenRsaKeyPair();
}
}
/**
* 执行生成 RSA 密钥对:公钥去 PEM 头尾后填入表单,私钥弹窗展示
*/
async function doGenRsaKeyPair() {
const { data } = await KeyGenApi.genRsaKeyPair();
if (data) {
// 公钥去注释与换行(纯 Base64 存储),前端处理
form.publicKey = normalizePublicKey(data.publicKey);
// 私钥同样去 PEM 头尾与换行(易支付要求纯 Base64弹窗展示
privateKeyContent.value = normalizePrivateKey(data.privateKey) ?? '';
keyPairVisible.value = true;
}
}
/**
* 保存配置
*/
@@ -196,6 +248,10 @@
onOk: async () => {
saving.value = true;
try {
// 商户公钥去 PEM 头尾与换行(纯 Base64 存储),保存时兜底处理
if (form.publicKey) {
form.publicKey = normalizePublicKey(form.publicKey);
}
// 敏感字段未修改时不提交
const sensitiveData = diffForm(originalForm, form, 'md5Key', 'publicKey');
const submitData: EasyPayCredentialParam = {
@@ -263,9 +319,7 @@
<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>
<span v-if="appInfo.appName" class="text-sm text-muted-foreground"> ({{ appInfo.appName }}) </span>
</div>
</template>
<template #extra>
@@ -313,11 +367,7 @@
{{ $t('payment.merchant.app.easypay.enableDesc') }}
</div>
</div>
<a-radio-group
v-model:value="form.enable"
button-style="solid"
:disabled="!isEditing"
>
<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>
@@ -404,11 +454,7 @@
{{ $t('payment.merchant.app.easypay.enableV1Desc') }}
</div>
</div>
<a-radio-group
v-model:value="form.enableV1"
button-style="solid"
:disabled="!isEditing"
>
<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>
@@ -450,11 +496,7 @@
{{ $t('payment.merchant.app.easypay.enableV2Desc') }}
</div>
</div>
<a-radio-group
v-model:value="form.enableV2"
button-style="solid"
:disabled="!isEditing"
>
<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>
@@ -470,21 +512,14 @@
{{ $t('payment.merchant.app.easypay.useSystemKeyDesc') }}
</div>
</div>
<a-radio-group
v-model:value="form.useSystemKey"
button-style="solid"
:disabled="!isEditing"
>
<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 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') }}
@@ -501,11 +536,7 @@
class="readonly-textarea font-mono text-sm"
/>
<div class="textarea-actions">
<a-button
type="link"
size="small"
@click="handleCopy(form.platformPublicKey)"
>
<a-button type="link" size="small" @click="handleCopy(form.platformPublicKey)">
<template #icon>
<IconifyIcon icon="ant-design:copy-outlined" />
</template>
@@ -536,6 +567,14 @@
allow-clear
class="font-mono text-sm"
/>
<div class="textarea-actions">
<a-button type="link" size="small" :disabled="!isEditing" @click="handleGenRsaKeyPair">
<template #icon>
<IconifyIcon icon="ant-design:key-outlined" />
</template>
{{ $t('payment.merchant.app.easypay.genRsaKeyPair') }}
</a-button>
</div>
</div>
</div>
</div>
@@ -544,6 +583,38 @@
</div>
</a-spin>
</a-card>
<!-- RSA 密钥对弹窗 -->
<a-modal
v-model:open="keyPairVisible"
:title="$t('payment.merchant.app.easypay.genKeyPairTitle')"
:footer="null"
width="640px"
:mask-closable="false"
>
<div class="space-y-4">
<div class="info-banner warning">
<IconifyIcon icon="ant-design:warning-filled" />
<span>{{ $t('payment.merchant.app.easypay.privateKeyWarning') }}</span>
</div>
<div>
<div class="mb-2 flex items-center justify-between">
<span class="font-medium text-foreground">
{{ $t('payment.merchant.app.easypay.privateKey') }}
</span>
<a-button size="small" type="primary" ghost @click="handleCopy(privateKeyContent)">
<template #icon>
<IconifyIcon icon="ant-design:copy-outlined" />
</template>
{{ $t('common.copy') }}
</a-button>
</div>
<div class="textarea-wrapper">
<a-textarea :value="privateKeyContent" :rows="10" readonly class="readonly-textarea font-mono text-sm" />
</div>
</div>
</div>
</a-modal>
</div>
</template>

View File

@@ -86,6 +86,11 @@
"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"
"platformPublicKeyDesc": "Platform public key for integrator signature verification",
"genRsaKeyPair": "Generate Key Pair",
"confirmGenPublicKey": "The public key already exists. Regenerating will overwrite it. Continue?",
"genKeyPairTitle": "RSA Key Pair",
"privateKey": "Private Key",
"privateKeyWarning": "The private key is shown only once. Keep it safe — it cannot be recovered if lost."
}
}

View File

@@ -86,6 +86,11 @@
"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"
"platformPublicKeyDesc": "Kunci publik platform untuk verifikasi tanda tangan integrator",
"genRsaKeyPair": "Buat Pasangan Kunci",
"confirmGenPublicKey": "Kunci publik sudah ada. Membuat ulang akan menimpa kunci yang ada. Lanjutkan?",
"genKeyPairTitle": "Pasangan Kunci RSA",
"privateKey": "Kunci Privat",
"privateKeyWarning": "Kunci privat hanya ditampilkan sekali di sini. Simpan dengan aman — tidak dapat dipulihkan jika hilang."
}
}

View File

@@ -86,6 +86,11 @@
"md5KeyDesc": "V1 署名用の MD5 鍵",
"useSystemKeyDesc": "プラットフォーム鍵ペアを使う場合、加盟店公開鍵の入力は不要です",
"publicKeyDesc": "独自鍵ペア利用時の加盟店 RSA 公開鍵",
"platformPublicKeyDesc": "連携側の署名検証用プラットフォーム公開鍵"
"platformPublicKeyDesc": "連携側の署名検証用プラットフォーム公開鍵",
"genRsaKeyPair": "鍵ペアを生成",
"confirmGenPublicKey": "公開鍵が既に存在します。再生成すると元の公開鍵が上書きされます。再生成しますか?",
"genKeyPairTitle": "RSA鍵ペア",
"privateKey": "秘密鍵",
"privateKeyWarning": "秘密鍵はここに一度だけ表示されます。確実に保管してください。紛失すると復元できません。"
}
}

View File

@@ -86,6 +86,11 @@
"md5KeyDesc": "V1 서명용 MD5 키",
"useSystemKeyDesc": "플랫폼 키 쌍을 사용하면 가맹점 공개키를 입력할 필요가 없습니다",
"publicKeyDesc": "자체 키 쌍 사용 시 가맹점 RSA 공개키",
"platformPublicKeyDesc": "연동측 서명 검증용 플랫폼 공개키"
"platformPublicKeyDesc": "연동측 서명 검증용 플랫폼 공개키",
"genRsaKeyPair": "키 쌍 생성",
"confirmGenPublicKey": "공개 키가 이미 존재합니다. 다시 생성하면 기존 공개 키를 덮어씁니다. 다시 생성하시겠습니까?",
"genKeyPairTitle": "RSA 키 쌍",
"privateKey": "개인 키",
"privateKeyWarning": "개인 키는 여기에 한 번만 표시됩니다. 안전하게 보관하세요. 분실 시 복구할 수 없습니다."
}
}

View File

@@ -86,6 +86,11 @@
"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"
"platformPublicKeyDesc": "Kunci awam platform untuk pengesahan tandatangan pengintegrasi",
"genRsaKeyPair": "Jana Pasangan Kunci",
"confirmGenPublicKey": "Kunci awam telah wujud. Menjana semula akan menulis ganti kunci sedia ada. Teruskan?",
"genKeyPairTitle": "Pasangan Kunci RSA",
"privateKey": "Kunci Peribadi",
"privateKeyWarning": "Kunci peribadi hanya dipaparkan sekali di sini. Simpan dengan selamat — tidak boleh dipulihkan jika hilang."
}
}

View File

@@ -86,6 +86,11 @@
"md5KeyDesc": "คีย์ MD5 สำหรับการเซ็นคำขอ V1",
"useSystemKeyDesc": "ใช้คู่คีย์ของแพลตฟอร์มจึงไม่ต้องกรอกรหัสสาธารณะผู้ค้า",
"publicKeyDesc": "รหัสสาธารณะ RSA ของผู้ค้า เมื่อใช้คู่คีย์ที่กำหนดเอง",
"platformPublicKeyDesc": "รหัสสาธารณะของแพลตฟอร์มสำหรับการตรวจสอบลายเซ็นผู้เชื่อมต่อ"
"platformPublicKeyDesc": "รหัสสาธารณะของแพลตฟอร์มสำหรับการตรวจสอบลายเซ็นผู้เชื่อมต่อ",
"genRsaKeyPair": "สร้างคู่คีย์",
"confirmGenPublicKey": "มีคีย์สาธารณะอยู่แล้ว การสร้างใหม่จะเขียนทับคีย์เดิม ต้องการสร้างใหม่หรือไม่?",
"genKeyPairTitle": "คู่คีย์ RSA",
"privateKey": "คีย์ส่วนตัว",
"privateKeyWarning": "คีย์ส่วนตัวจะแสดงที่นี่เพียงครั้งเดียว โปรดเก็บรักษาให้ดี หากสูญหายจะกู้คืนไม่ได้"
}
}

View File

@@ -86,6 +86,11 @@
"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ý"
"platformPublicKeyDesc": "Khóa công khai nền tảng để bên tích hợp xác minh chữ ký",
"genRsaKeyPair": "Tạo cặp khóa",
"confirmGenPublicKey": "Khóa công khai đã tồn tại. Tạo lại sẽ ghi đè khóa hiện có. Tiếp tục?",
"genKeyPairTitle": "Cặp khóa RSA",
"privateKey": "Khóa riêng",
"privateKeyWarning": "Khóa riêng chỉ hiển thị một lần tại đây. Vui lòng lưu giữ an toàn — không thể khôi phục nếu mất."
}
}

View File

@@ -86,6 +86,11 @@
"md5KeyDesc": "V1 签名用 MD5 密钥",
"useSystemKeyDesc": "使用平台密钥对时无需填写商户公钥",
"publicKeyDesc": "商户侧 RSA 公钥(自签时填写)",
"platformPublicKeyDesc": "平台公钥,对接方验签使用"
"platformPublicKeyDesc": "平台公钥,对接方验签使用",
"genRsaKeyPair": "生成密钥对",
"confirmGenPublicKey": "公钥已存在,重新生成将覆盖原有公钥,确定要重新生成吗?",
"genKeyPairTitle": "RSA密钥对",
"privateKey": "私钥",
"privateKeyWarning": "私钥仅在此处展示一次,请务必妥善保管,丢失无法找回"
}
}

View File

@@ -86,6 +86,11 @@
"md5KeyDesc": "V1 簽名用 MD5 密鑰",
"useSystemKeyDesc": "使用平台密鑰對時無需填寫商戶公鑰",
"publicKeyDesc": "商戶側 RSA 公鑰(自簽時填寫)",
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用"
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用",
"genRsaKeyPair": "產生金鑰對",
"confirmGenPublicKey": "公鑰已存在,重新產生將覆蓋原有公鑰,確定要重新產生嗎?",
"genKeyPairTitle": "RSA金鑰對",
"privateKey": "私鑰",
"privateKeyWarning": "私鑰僅在此處顯示一次,請務必妥善保管,遺失無法找回"
}
}

View File

@@ -86,6 +86,11 @@
"md5KeyDesc": "V1 簽名用 MD5 密鑰",
"useSystemKeyDesc": "使用平台密鑰對時無需填寫商戶公鑰",
"publicKeyDesc": "商戶側 RSA 公鑰(自簽時填寫)",
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用"
"platformPublicKeyDesc": "平台公鑰,對接方驗簽使用",
"genRsaKeyPair": "產生金鑰對",
"confirmGenPublicKey": "公鑰已存在,重新產生將覆蓋原有公鑰,確定要重新產生嗎?",
"genKeyPairTitle": "RSA金鑰對",
"privateKey": "私鑰",
"privateKeyWarning": "私鑰僅在此處顯示一次,請務必妥善保管,遺失無法找回"
}
}

View File

@@ -13,6 +13,7 @@
type EasyPayCredentialParam,
type EasyPayCredentialResult,
} from '#/api/payment/merchant/easypay-credential.api';
import { KeyGenApi } from '#/api/payment/merchant/key-gen.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';
@@ -54,9 +55,12 @@
});
// 原始脱敏数据,用于 diffForm 比对
const originalForm = ref<EasyPayCredentialResult>({});
// RSA 密钥对弹窗
const keyPairVisible = ref(false);
const privateKeyContent = ref('');
/**
* 平台公钥规范化:去掉 PEM 头尾与空白,得到可直接复制的纯 Base64
* 公钥规范化:去掉 PEM 头尾与空白,得到可直接复制的纯 Base64
*/
function normalizePublicKey(pem?: string) {
if (!pem) {
@@ -68,6 +72,21 @@
.replaceAll(/\s+/g, '');
}
/**
* 私钥规范化:去掉 PEM 头尾与空白,得到可直接复制的纯 Base64易支付要求
*/
function normalizePrivateKey(pem?: string) {
if (!pem) {
return pem;
}
return pem
.replaceAll('-----BEGIN PRIVATE KEY-----', '')
.replaceAll('-----END PRIVATE KEY-----', '')
.replaceAll('-----BEGIN RSA PRIVATE KEY-----', '')
.replaceAll('-----END RSA PRIVATE KEY-----', '')
.replaceAll(/\s+/g, '');
}
/**
* 重置表单默认值后合并服务端数据
*/
@@ -163,6 +182,39 @@
message.success($t('common.operationSuccess'));
}
/**
* 生成 RSA 密钥对(公钥已存在时二次确认)
*/
async function handleGenRsaKeyPair() {
if (form.publicKey) {
confirm({
title: $t('common.confirm'),
content: $t('payment.merchant.app.easypay.confirmGenPublicKey'),
okText: $t('common.okText'),
cancelText: $t('common.cancelText'),
onOk: async () => {
await doGenRsaKeyPair();
},
});
} else {
await doGenRsaKeyPair();
}
}
/**
* 执行生成 RSA 密钥对:公钥去 PEM 头尾后填入表单,私钥弹窗展示
*/
async function doGenRsaKeyPair() {
const { data } = await KeyGenApi.genRsaKeyPair();
if (data) {
// 公钥去注释与换行(纯 Base64 存储),前端处理
form.publicKey = normalizePublicKey(data.publicKey);
// 私钥同样去 PEM 头尾与换行(易支付要求纯 Base64弹窗展示
privateKeyContent.value = normalizePrivateKey(data.privateKey) ?? '';
keyPairVisible.value = true;
}
}
/**
* 保存配置
*/
@@ -178,6 +230,10 @@
onOk: async () => {
saving.value = true;
try {
// 商户公钥去 PEM 头尾与换行(纯 Base64 存储),保存时兜底处理
if (form.publicKey) {
form.publicKey = normalizePublicKey(form.publicKey);
}
// 敏感字段未修改时不提交
const sensitiveData = diffForm(originalForm.value, form, 'md5Key', 'publicKey');
const submitData: EasyPayCredentialParam = {
@@ -481,6 +537,14 @@
allow-clear
class="font-mono text-sm"
/>
<div class="textarea-actions">
<a-button type="link" size="small" :disabled="!isEditing" @click="handleGenRsaKeyPair">
<template #icon>
<IconifyIcon icon="ant-design:key-outlined" />
</template>
{{ $t('payment.merchant.app.easypay.genRsaKeyPair') }}
</a-button>
</div>
</div>
</div>
</div>
@@ -489,6 +553,38 @@
</div>
</a-spin>
</a-card>
<!-- RSA 密钥对弹窗 -->
<a-modal
v-model:open="keyPairVisible"
:title="$t('payment.merchant.app.easypay.genKeyPairTitle')"
:footer="null"
width="640px"
:mask-closable="false"
>
<div class="space-y-4">
<div class="info-banner warning">
<IconifyIcon icon="ant-design:warning-filled" />
<span>{{ $t('payment.merchant.app.easypay.privateKeyWarning') }}</span>
</div>
<div>
<div class="mb-2 flex items-center justify-between">
<span class="font-medium text-foreground">
{{ $t('payment.merchant.app.easypay.privateKey') }}
</span>
<a-button size="small" type="primary" ghost @click="handleCopy(privateKeyContent)">
<template #icon>
<IconifyIcon icon="ant-design:copy-outlined" />
</template>
{{ $t('common.copy') }}
</a-button>
</div>
<div class="textarea-wrapper">
<a-textarea :value="privateKeyContent" :rows="10" readonly class="readonly-textarea font-mono text-sm" />
</div>
</div>
</div>
</a-modal>
</div>
</template>