feat(payment): i18n payment error codes and label localization

Pairs with the backend structured payment errors (reason + metadata). The
frontend now maps reason codes to localized messages with metadata as
interpolation variables, and automatically localizes raw config-field names
(e.g. "certSerial" → "证书序列号") using the existing UI-label i18n
namespace.

- frontend/src/utils/apiError.ts
  - extractApiErrorCode now prefers the string `reason` over the numeric HTTP
    `code`; reason is granular enough to drive i18n lookup, HTTP code is not.
  - New extractApiErrorMetadata to pull interpolation params off the error.
  - New extractI18nErrorMessage(err, t, namespace, fallback): looks up
    `<namespace>.<REASON>` in i18n and substitutes metadata. Before
    substitution, `metadata.key` and `metadata.keys` (slash-joined) are
    re-translated through `admin.settings.payment.field_<key>` so users see
    "缺少必填项:证书序列号" instead of "缺少必填项:certSerial".

- frontend/src/i18n/locales/{zh,en}.ts
  - Add payment.errors entries for every structured reason code returned by
    the backend (PAYMENT_DISABLED, INVALID_AMOUNT, TOO_MANY_PENDING,
    DAILY_LIMIT_EXCEEDED, NO_AVAILABLE_INSTANCE, PAYMENT_PROVIDER_MISCONFIGURED,
    WXPAY_CONFIG_MISSING_KEY / INVALID_KEY_LENGTH / INVALID_KEY, NOT_FOUND,
    FORBIDDEN, CONFLICT, INVALID_ORDER_TYPE, INVALID_STATUS,
    BALANCE_NOT_ENOUGH, REFUND_AMOUNT_EXCEEDED, REFUND_FAILED, and more),
    with placeholders for template variables.

- 13 payment-related Vue files
  - Migrate catch-block error reporting from extractApiErrorMessage to
    extractI18nErrorMessage(err, t, 'payment.errors', fallback).
  - Remove the ad-hoc paymentErrorMap computed in SettingsView.vue, which the
    new helper supersedes (it reads i18n directly via t).

- frontend/src/components/payment/providerConfig.ts
  - wxpay: publicKey and publicKeyId are now required (was optional), matching
    the pubkey-only verifier direction; certSerial is already required.

This PR is drop-in safe: reason-preferring extractApiErrorCode is backward
compatible with callers that pass their own i18nMap, and error codes missing
from i18n fall back to the existing message-based path.
This commit is contained in:
erio
2026-04-20 20:06:53 +08:00
parent 79192cf65b
commit 40d4e167cd
16 changed files with 177 additions and 47 deletions

View File

@@ -5620,7 +5620,33 @@ export default {
errors: {
tooManyPending: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单',
cancelRateLimited: '取消订单过于频繁,请稍后再试',
// Structured error codes (reason strings from backend ApplicationError)
PAYMENT_DISABLED: '支付系统已关闭',
USER_INACTIVE: '账号已被禁用',
BALANCE_PAYMENT_DISABLED: '余额充值功能已关闭',
INVALID_AMOUNT: '金额无效',
INVALID_INPUT: '参数有误',
PLAN_NOT_AVAILABLE: '套餐不存在或已下架',
GROUP_NOT_FOUND: '订阅分组不可用',
GROUP_TYPE_MISMATCH: '分组类型不是订阅类型',
TOO_MANY_PENDING: '待支付订单过多(最多 {max} 个),请先完成或取消现有订单',
DAILY_LIMIT_EXCEEDED: '今日充值已达上限,剩余额度 {remaining}',
PAYMENT_GATEWAY_ERROR: '支付方式不可用',
NO_AVAILABLE_INSTANCE: '暂无可用的支付通道',
PAYMENT_PROVIDER_MISCONFIGURED: '支付通道配置错误,请联系管理员',
WXPAY_CONFIG_MISSING_KEY: '微信支付配置缺少必填项:{key}',
WXPAY_CONFIG_INVALID_KEY_LENGTH: '微信支付 {key} 长度错误,应为 {expected} 字节(实际 {actual}',
WXPAY_CONFIG_INVALID_KEY: '微信支付 {key} 格式错误,请确认复制了完整的 PEM 内容',
PENDING_ORDERS: '该服务商有未完成的订单,请等待订单完成后再操作',
CANCEL_RATE_LIMITED: '取消订单过于频繁,请稍后再试',
NOT_FOUND: '订单不存在',
FORBIDDEN: '无权限操作此订单',
CONFLICT: '订单状态已变更,请刷新',
INVALID_ORDER_TYPE: '仅余额订单可申请退款',
INVALID_STATUS: '当前订单状态不允许此操作',
BALANCE_NOT_ENOUGH: '退款金额超过余额',
REFUND_AMOUNT_EXCEEDED: '退款金额超过充值金额',
REFUND_FAILED: '退款失败',
},
stripePay: '立即支付',
stripeSuccessProcessing: '支付成功,正在处理订单...',