Merge pull request 'feat(payment): tighten gateways and implement Epay' (#306) from worktree-tighten-payment-gateways into main

Reviewed-on: CYL/wordpress-theme-aksrt#306
This commit is contained in:
2026-07-26 13:13:25 +08:00
11 changed files with 470 additions and 264 deletions
@@ -2,10 +2,11 @@
<PaymentSectionPage
section="gateway"
title="收款接口"
desc="管理支付网关、查看连接状态、配置回调口令。"
note="当前支持人工确认与测试联调;回调校验口令在接入真实网关时直接复用。"
desc="管理余额、支付宝、微信、PayPal、Stripe、易支付与卡密兑换通道。"
note="以渠道开关与凭据是否齐全为准;异步回调由各网关验签。卡密为兑换入口,不进入收银台。"
:links="[
{ label: '余额充值', path: '/payment/recharge' },
{ label: '充值卡', path: '/payment/card-pass' },
{ label: '付费内容', path: '/payment/paid' },
]"
>
@@ -16,7 +17,7 @@
<n-grid-item v-for="gw in gateways" :key="gw.id">
<div class="aksrt-gateway__card">
<div class="aksrt-gateway__icon" :class="gw.configured ? 'aksrt-gateway__icon--active' : ''">
<i :class="gw.id === 'balance' ? 'fas fa-wallet' : 'fas fa-credit-card'"></i>
<i :class="iconFor(gw.id)"></i>
</div>
<div>
<strong>{{ gw.name }}</strong>
@@ -31,39 +32,8 @@
<n-empty v-if="!loading && gateways.length === 0" description="暂无已注册的支付网关" />
</n-card>
<n-card class="aksrt-section-card" title="运行摘要">
<n-grid :cols="4" :x-gap="12" :y-gap="12">
<n-grid-item>
<div class="aksrt-gateway__stat">
<strong>{{ modeLabel }}</strong>
<p>当前模式</p>
</div>
</n-grid-item>
<n-grid-item>
<div class="aksrt-gateway__stat">
<strong :class="overview.gateway?.test_enabled ? 'aksrt-text-warning' : ''">
{{ overview.gateway?.test_enabled ? '启用' : '关闭' }}
</strong>
<p>测试网关</p>
</div>
</n-grid-item>
<n-grid-item>
<div class="aksrt-gateway__stat">
<strong :class="overview.gateway?.manual_review ? 'aksrt-text-info' : ''">
{{ overview.gateway?.manual_review ? '启用' : '关闭' }}
</strong>
<p>人工复核</p>
</div>
</n-grid-item>
<n-grid-item>
<div class="aksrt-gateway__stat">
<n-tag :type="overview.gateway?.callback_secret ? 'success' : 'warning'" size="small" round>
{{ overview.gateway?.callback_secret ? '已设置' : '未设置' }}
</n-tag>
<p>回调口令</p>
</div>
</n-grid-item>
</n-grid>
<n-card v-if="overview.help_text" class="aksrt-section-card" title="运行说明">
<p class="aksrt-gateway__help">{{ overview.help_text }}</p>
</n-card>
</n-spin>
</template>
@@ -71,7 +41,7 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { useMessage } from 'naive-ui'
import PaymentSectionPage from '@/pages/payment/PaymentSectionPage.vue'
import api from '@/api'
@@ -81,12 +51,14 @@ const gateways = ref([])
const overview = ref({})
const loading = ref(false)
const modeLabel = computed(() => {
const mode = overview.value.gateway?.mode
if (mode === 'test_gateway') return '测试网关'
if (mode === 'adapter_ready') return '适配器就绪'
return '人工确认'
})
function iconFor(id) {
if (id === 'balance') return 'fas fa-wallet'
if (id === 'alipay' || id === 'epay_alipay') return 'fab fa-alipay'
if (id === 'wechatpay' || id === 'epay_wxpay') return 'fab fa-weixin'
if (id === 'paypal') return 'fab fa-paypal'
if (id === 'stripe') return 'fab fa-stripe-s'
return 'fas fa-credit-card'
}
onMounted(async () => {
loading.value = true
@@ -98,7 +70,9 @@ onMounted(async () => {
if (gwRes?.data?.gateways) {
gateways.value = gwRes.data.gateways
}
if (ovRes?.data) {
if (ovRes?.data?.gateway) {
overview.value = ovRes.data.gateway
} else if (ovRes?.data) {
overview.value = ovRes.data
}
} catch {
@@ -149,31 +123,10 @@ onMounted(async () => {
color: var(--color-ink-4, #8a909c);
}
.aksrt-gateway__stat {
text-align: center;
padding: 16px;
border-radius: 8px;
background: var(--surface-2, #fafafa);
}
.aksrt-gateway__stat strong {
display: block;
font-size: 18px;
font-weight: 600;
color: var(--color-ink, #1a1a2e);
}
.aksrt-gateway__stat p {
margin: 4px 0 0;
font-size: 12px;
color: var(--color-ink-4, #8a909c);
}
.aksrt-text-warning {
color: var(--color-warning, #f59e0b);
}
.aksrt-text-info {
color: var(--color-info, #3b82f6);
.aksrt-gateway__help {
margin: 0;
font-size: 13px;
color: var(--color-ink-3, #5c6370);
line-height: 1.6;
}
</style>
@@ -2,25 +2,15 @@ export const gatewaySchema = {
key: 'payment.gateway',
title: '收款接口',
icon: 'fas fa-credit-card',
description: '配置支付网关凭据、回调校验口令与运行模式。开启渠道后展开填写配置,可随时收起。',
description: '管理已支持的收款通道:余额、支付宝、微信、PayPal、Stripe、易支付与卡密兑换。开启渠道后展开填写凭据。',
defaults: {
payment_gateway_mode: 'manual_review',
payment_gateway_test_enabled: true,
payment_gateway_manual_review: true,
payment_gateway_callback_secret: '',
payment_gateway_help: '未接真实支付网关时,建议使用测试网关或人工确认模式完成联调。',
payment_wechat_enabled: false,
payment_alipay_enabled: false,
payment_paypal_enabled: false,
payment_stripe_enabled: false,
payment_epay_enabled: false,
payment_balance_enabled: true,
payment_card_pass_enabled: false,
payment_xunhupay_enabled: false,
payment_xhpay_enabled: false,
payment_payjs_enabled: false,
payment_epay_enabled: false,
payment_vmq_enabled: false,
payment_codepay_enabled: false,
payment_stripe_enabled: false,
wechat_merchantid: '',
wechat_appid: '',
wechat_payment_type: 'native',
@@ -37,53 +27,46 @@ export const gatewaySchema = {
paypal_client_secret: '',
paypal_currency: 'USD',
paypal_sandbox: true,
xunhupay_appid: '',
xunhupay_appsecret: '',
xhpay_mchid: '',
xhpay_alipay_v2: '',
xhpay_wx_h5: '',
payjs_mchid: '',
payjs_key: '',
epay_apiurl: '',
epay_pid: '',
epay_key: '',
stripe_secret_key: '',
stripe_publishable_key: '',
stripe_webhook_secret: '',
stripe_currency: 'usd',
stripe_test_mode: true,
epay_apiurl: '',
epay_pid: '',
epay_key: '',
epay_channel_alipay: true,
epay_channel_wxpay: true,
epay_device: 'auto',
},
groups: [
{
key: 'adapter',
title: '网关适配器',
icon: 'fas fa-plug',
key: 'notice',
title: '说明',
icon: 'fas fa-info-circle',
fields: [
{ key: 'payment_gateway_mode', label: '运行模式', type: 'radio', default: 'manual_review', options: [{ label: '人工确认', value: 'manual_review' }, { label: '测试网关', value: 'test_gateway' }, { label: '适配器就绪', value: 'adapter_ready' }] },
{ key: 'payment_gateway_test_enabled', label: '启用测试网关', type: 'switch', default: true },
{ key: 'payment_gateway_manual_review', label: '保留人工复核', type: 'switch', default: true },
{ key: 'payment_gateway_callback_secret', label: '回调校验口令', type: 'input', default: '', help: '真实网关接入时可直接复用该口令字段。' },
{ key: 'payment_gateway_help', label: '运维说明', type: 'textarea', default: '未接真实支付网关时,建议使用测试网关或人工确认模式完成联调。' },
{
key: 'payment_gateway_notice',
label: '通道说明',
type: 'notice',
alertType: 'info',
alertContent: '以渠道开关 + 凭据是否齐全为准。异步回调由各网关验签(支付宝/微信/Stripe/易支付等),无需额外口令。卡密为兑换入口,不出现在收银台支付方式列表。',
},
],
},
{
key: 'gateways',
title: '支付渠道开关',
icon: 'fas fa-toggle-on',
description: '先开启渠道,下方对应配置卡片会展开;填写凭据后网关状态变为可用。无凭据的内置渠道(余额/卡密)仅开关即可。',
description: '先开启渠道,下方对应配置卡片会展开;填写凭据后网关状态变为可用。余额与卡密无第三方凭据,仅开关即可。',
fields: [
{ key: 'payment_wechat_enabled', label: '微信支付', type: 'switch', default: false },
{ key: 'payment_balance_enabled', label: '余额支付', type: 'switch', default: true, help: '需同时在「余额充值」启用余额功能。关闭后用户中心仍可有钱包,但结账不可用余额支付。' },
{ key: 'payment_alipay_enabled', label: '支付宝', type: 'switch', default: false },
{ key: 'payment_wechat_enabled', label: '微信支付', type: 'switch', default: false },
{ key: 'payment_paypal_enabled', label: 'PayPal', type: 'switch', default: false },
{ key: 'payment_stripe_enabled', label: 'Stripe', type: 'switch', default: false },
{ key: 'payment_xunhupay_enabled', label: '虎皮椒', type: 'switch', default: false },
{ key: 'payment_xhpay_enabled', label: '小火付', type: 'switch', default: false },
{ key: 'payment_payjs_enabled', label: 'PayJS', type: 'switch', default: false },
{ key: 'payment_epay_enabled', label: '易支付', type: 'switch', default: false },
{ key: 'payment_vmq_enabled', label: 'V免签', type: 'switch', default: false, help: '当前仅开关占位,凭据接入后补齐。' },
{ key: 'payment_codepay_enabled', label: 'Codepay', type: 'switch', default: false, help: '当前仅开关占位,凭据接入后补齐。' },
{ key: 'payment_balance_enabled', label: '余额支付', type: 'switch', default: true, help: '需同时在「余额充值」启用余额功能。关闭后用户中心仍可有钱包,但结账不可用余额支付。' },
{ key: 'payment_card_pass_enabled', label: '卡密充值', type: 'switch', default: false, help: '卡密规则在「卡密设置」;此处仅控制是否作为收款渠道展示。' },
{ key: 'payment_epay_enabled', label: '易支付', type: 'switch', default: false, help: '兼容彩虹易支付协议的聚合收银台,可同时开启支付宝/微信子通道。' },
{ key: 'payment_card_pass_enabled', label: '卡密充值', type: 'switch', default: false, help: '仅控制卡密兑换入口是否开放;卡密规则与批量发卡在「充值卡」页。不进入结账支付方式列表。' },
],
},
{
@@ -110,7 +93,7 @@ export const gatewaySchema = {
key: 'alipay',
title: '支付宝配置',
icon: 'fab fa-alipay',
description: '开启「支付宝」后填写应用与回调地址。',
description: '开启「支付宝」后填写应用与证书。',
collapsible: true,
expandWhen: 'payment_alipay_enabled',
dependsOn: { key: 'payment_alipay_enabled', value: true },
@@ -158,58 +141,26 @@ export const gatewaySchema = {
{ key: 'stripe_test_mode', label: '测试模式', type: 'switch', default: true },
],
},
{
key: 'xunhupay',
title: '虎皮椒配置',
icon: 'fas fa-pepper-hot',
description: '开启「虎皮椒」后填写 App 凭据。',
collapsible: true,
expandWhen: 'payment_xunhupay_enabled',
dependsOn: { key: 'payment_xunhupay_enabled', value: true },
fields: [
{ key: 'xunhupay_appid', label: 'AppID', type: 'input', default: '' },
{ key: 'xunhupay_appsecret', label: 'AppSecret', type: 'input', default: '' },
],
},
{
key: 'xhpay',
title: '小火付配置',
icon: 'fas fa-fire',
description: '开启「小火付」后填写商户与通道参数。',
collapsible: true,
expandWhen: 'payment_xhpay_enabled',
dependsOn: { key: 'payment_xhpay_enabled', value: true },
fields: [
{ key: 'xhpay_mchid', label: '商户号', type: 'input', default: '' },
{ key: 'xhpay_alipay_v2', label: '支付宝V2', type: 'input', default: '' },
{ key: 'xhpay_wx_h5', label: '微信H5', type: 'input', default: '' },
],
},
{
key: 'payjs',
title: 'PayJS 配置',
icon: 'fab fa-js',
description: '开启「PayJS」后填写商户号与通信密钥。',
collapsible: true,
expandWhen: 'payment_payjs_enabled',
dependsOn: { key: 'payment_payjs_enabled', value: true },
fields: [
{ key: 'payjs_mchid', label: '商户号', type: 'input', default: '' },
{ key: 'payjs_key', label: '通信密钥', type: 'input', default: '' },
],
},
{
key: 'epay',
title: '易支付配置',
icon: 'fas fa-university',
description: '开启「易支付」后填写接口与商户信息。',
description: '开启「易支付」后填写接口地址与商户信息,并选择支付宝/微信子通道。',
collapsible: true,
expandWhen: 'payment_epay_enabled',
dependsOn: { key: 'payment_epay_enabled', value: true },
fields: [
{ key: 'epay_apiurl', label: '接口地址', type: 'input', default: '' },
{ key: 'epay_apiurl', label: '接口地址', type: 'input', default: '', help: '易支付站点根地址,如 https://pay.example.com/(末尾斜杠可选)。' },
{ key: 'epay_pid', label: '商户ID', type: 'input', default: '' },
{ key: 'epay_key', label: '商户密钥', type: 'input', default: '' },
{ key: 'epay_key', label: '商户密钥', type: 'input', default: '', help: 'MD5 通信密钥。' },
{ key: 'epay_channel_alipay', label: '启用支付宝通道', type: 'switch', default: true },
{ key: 'epay_channel_wxpay', label: '启用微信通道', type: 'switch', default: true },
{ key: 'epay_device', label: '下单终端', type: 'select', default: 'auto', options: [
{ label: '自动(按 UA)', value: 'auto' },
{ label: '电脑端 submit', value: 'pc' },
{ label: '移动端 mapi', value: 'mobile' },
] },
{ key: 'epay_notify_help', label: '回调地址说明', type: 'notice', alertType: 'info', alertContent: '异步/同步回调固定为 /wp-json/aksrt/v1/payment/epay_alipay/{notify|return} 与 /payment/epay_wxpay/{notify|return},请在易支付后台放行对应 URL。' },
],
},
],
+4
View File
@@ -345,6 +345,10 @@ if (class_exists('\AKSRT\Payment\Gateway')) {
if (class_exists('\AKSRT\Payment\Gateways\StripeGateway')) {
$gw->register('stripe', new \AKSRT\Payment\Gateways\StripeGateway());
}
if (class_exists('\AKSRT\Payment\Gateways\EpayGateway')) {
$gw->register('epay_alipay', new \AKSRT\Payment\Gateways\EpayGateway('alipay'));
$gw->register('epay_wxpay', new \AKSRT\Payment\Gateways\EpayGateway('wxpay'));
}
do_action('aksrt_register_payment_gateways', $gw);
}
+2 -2
View File
@@ -82,8 +82,8 @@ function aksrt_get_pay_types(): array {
if (!function_exists('aksrt_list_cash_payment_methods')) {
function aksrt_list_cash_payment_methods(bool $logged_in = true): array {
$ids = $logged_in
? ['balance', 'alipay', 'wechatpay', 'paypal', 'stripe']
: ['alipay', 'wechatpay', 'paypal', 'stripe'];
? ['balance', 'alipay', 'wechatpay', 'paypal', 'stripe', 'epay_alipay', 'epay_wxpay']
: ['alipay', 'wechatpay', 'paypal', 'stripe', 'epay_alipay', 'epay_wxpay'];
if (!class_exists('\AKSRT\Payment\Gateway')) {
// No gateway manager → no real methods (avoid dead balance/alipay stubs).
+17 -31
View File
@@ -219,29 +219,20 @@ class SchemaPayment {
'gateway' => [
'key' => 'gateway',
'title' => '收款接口',
'version' => '1.1.0',
'version' => '1.2.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'adapter', 'title' => '网关适配器', 'fields' => [
['key' => 'payment_gateway_mode', 'label' => '运行模式', 'type' => 'string', 'default' => 'manual_review'],
['key' => 'payment_gateway_test_enabled', 'label' => '启用测试网关', 'type' => 'bool', 'default' => true],
['key' => 'payment_gateway_manual_review', 'label' => '保留人工复核', 'type' => 'bool', 'default' => true],
['key' => 'payment_gateway_callback_secret', 'label' => '回调校验口令', 'type' => 'string', 'default' => ''],
['key' => 'payment_gateway_help', 'label' => '运维说明', 'type' => 'string', 'default' => '未接真实支付网关时,建议使用测试网关或人工确认模式完成联调。'],
['key' => 'notice', 'title' => '说明', 'fields' => [
['key' => 'payment_gateway_notice', 'label' => '通道说明', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '以渠道开关 + 凭据是否齐全为准。异步回调由各网关验签(支付宝/微信/Stripe/易支付等),无需额外口令。卡密为兑换入口,不出现在收银台支付方式列表。'],
]],
['key' => 'gateways', 'title' => '支付渠道开关', 'fields' => [
['key' => 'payment_wechat_enabled', 'label' => '微信支付', 'type' => 'switch', 'default' => false],
['key' => 'payment_balance_enabled', 'label' => '余额支付', 'type' => 'switch', 'default' => true, 'help' => '需同时在「余额充值」启用余额功能。关闭后用户中心仍可有钱包,但结账不可用余额支付。'],
['key' => 'payment_alipay_enabled', 'label' => '支付宝', 'type' => 'switch', 'default' => false],
['key' => 'payment_wechat_enabled', 'label' => '微信支付', 'type' => 'switch', 'default' => false],
['key' => 'payment_paypal_enabled', 'label' => 'PayPal', 'type' => 'switch', 'default' => false],
['key' => 'payment_stripe_enabled', 'label' => 'Stripe', 'type' => 'switch', 'default' => false],
['key' => 'payment_xunhupay_enabled', 'label' => '虎皮椒', 'type' => 'switch', 'default' => false],
['key' => 'payment_xhpay_enabled', 'label' => '小火付', 'type' => 'switch', 'default' => false],
['key' => 'payment_payjs_enabled', 'label' => 'PayJS', 'type' => 'switch', 'default' => false],
['key' => 'payment_epay_enabled', 'label' => '易支付', 'type' => 'switch', 'default' => false],
['key' => 'payment_vmq_enabled', 'label' => 'V免签', 'type' => 'switch', 'default' => false],
['key' => 'payment_codepay_enabled', 'label' => 'Codepay', 'type' => 'switch', 'default' => false],
['key' => 'payment_balance_enabled', 'label' => '余额支付', 'type' => 'switch', 'default' => true],
['key' => 'payment_card_pass_enabled', 'label' => '卡密充值', 'type' => 'switch', 'default' => false],
['key' => 'payment_epay_enabled', 'label' => '易支付', 'type' => 'switch', 'default' => false, 'help' => '兼容彩虹易支付协议的聚合收银台,可同时开启支付宝/微信子通道。'],
['key' => 'payment_card_pass_enabled', 'label' => '卡密充值', 'type' => 'switch', 'default' => false, 'help' => '仅控制卡密兑换入口是否开放;卡密规则与批量发卡在「充值卡」页。不进入结账支付方式列表。'],
]],
// Channel credential groups: SPA uses collapsible + expandWhen + group dependsOn.
// Field-level dependsOn kept for contract audit / non-SPA consumers.
@@ -281,23 +272,18 @@ class SchemaPayment {
['key' => 'stripe_currency', 'label' => '货币', 'type' => 'input', 'default' => 'usd', 'dependsOn' => ['key' => 'payment_stripe_enabled', 'value' => true]],
['key' => 'stripe_test_mode', 'label' => '测试模式', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'payment_stripe_enabled', 'value' => true]],
]],
['key' => 'xunhupay', 'title' => '虎皮椒配置', 'collapsible' => true, 'expandWhen' => 'payment_xunhupay_enabled', 'dependsOn' => ['key' => 'payment_xunhupay_enabled', 'value' => true], 'fields' => [
['key' => 'xunhupay_appid', 'label' => 'AppID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_xunhupay_enabled', 'value' => true]],
['key' => 'xunhupay_appsecret', 'label' => 'AppSecret', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_xunhupay_enabled', 'value' => true]],
]],
['key' => 'xhpay', 'title' => '小火付配置', 'collapsible' => true, 'expandWhen' => 'payment_xhpay_enabled', 'dependsOn' => ['key' => 'payment_xhpay_enabled', 'value' => true], 'fields' => [
['key' => 'xhpay_mchid', 'label' => '商户号', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_xhpay_enabled', 'value' => true]],
['key' => 'xhpay_alipay_v2', 'label' => '支付宝V2', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_xhpay_enabled', 'value' => true]],
['key' => 'xhpay_wx_h5', 'label' => '微信H5', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_xhpay_enabled', 'value' => true]],
]],
['key' => 'payjs', 'title' => 'PayJS配置', 'collapsible' => true, 'expandWhen' => 'payment_payjs_enabled', 'dependsOn' => ['key' => 'payment_payjs_enabled', 'value' => true], 'fields' => [
['key' => 'payjs_mchid', 'label' => '商户号', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_payjs_enabled', 'value' => true]],
['key' => 'payjs_key', 'label' => '通信密钥', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_payjs_enabled', 'value' => true]],
]],
['key' => 'epay', 'title' => '易支付配置', 'collapsible' => true, 'expandWhen' => 'payment_epay_enabled', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true], 'fields' => [
['key' => 'epay_apiurl', 'label' => '接口地址', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
['key' => 'epay_apiurl', 'label' => '接口地址', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true], 'help' => '易支付站点根地址,如 https://pay.example.com/(末尾斜杠可选)。'],
['key' => 'epay_pid', 'label' => '商户ID', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
['key' => 'epay_key', 'label' => '商户密钥', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
['key' => 'epay_key', 'label' => '商户密钥', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true], 'help' => 'MD5 通信密钥。'],
['key' => 'epay_channel_alipay', 'label' => '启用支付宝通道', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
['key' => 'epay_channel_wxpay', 'label' => '启用微信通道', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
['key' => 'epay_device', 'label' => '下单终端', 'type' => 'select', 'default' => 'auto', 'options' => [
['value' => 'auto', 'label' => '自动(按 UA)'],
['value' => 'pc', 'label' => '电脑端 submit'],
['value' => 'mobile', 'label' => '移动端 mapi'],
], 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
['key' => 'epay_notify_help', 'label' => '回调地址说明', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '异步/同步回调固定为 /wp-json/aksrt/v1/payment/epay_alipay/{notify|return} 与 /payment/epay_wxpay/{notify|return},请在易支付后台放行对应 URL。', 'dependsOn' => ['key' => 'payment_epay_enabled', 'value' => true]],
]],
],
],
+25 -10
View File
@@ -111,6 +111,30 @@ class ChargeCard {
* @return array<string,mixed>
*/
public function get_public_options(): array {
$labels = [
'title' => __('卡密兑换', 'aksrt'),
'password' => __('卡密', 'aksrt'),
'card' => __('卡号', 'aksrt'),
'submit_balance' => __('兑换余额', 'aksrt'),
'submit_vip' => __('兑换会员', 'aksrt'),
'submit_credits' => __('兑换积分', 'aksrt'),
'placeholder_pw' => __('请输入卡密', 'aksrt'),
'placeholder_card' => __('请输入卡号', 'aksrt'),
];
// Master kill-switch on 收款接口 → 卡密充值.
$channel_on = !class_exists('\AKSRT\Settings\PaymentRuntime')
|| \AKSRT\Settings\PaymentRuntime::is_gateway_enabled('card_pass');
if (!$channel_on) {
$off = ['enabled' => false, 'desc' => '', 'only_password' => true];
return [
'balance' => $off,
'vip' => $off,
'credits' => $off,
'labels' => $labels,
];
}
$balance = ['enabled' => false, 'desc' => '', 'only_password' => true];
$vip = ['enabled' => false, 'desc' => '', 'only_password' => true];
@@ -154,16 +178,7 @@ class ChargeCard {
'balance' => $balance,
'vip' => $vip,
'credits' => $credits,
'labels' => [
'title' => __('卡密兑换', 'aksrt'),
'password' => __('卡密', 'aksrt'),
'card' => __('卡号', 'aksrt'),
'submit_balance' => __('兑换余额', 'aksrt'),
'submit_vip' => __('兑换会员', 'aksrt'),
'submit_credits' => __('兑换积分', 'aksrt'),
'placeholder_pw' => __('请输入卡密', 'aksrt'),
'placeholder_card' => __('请输入卡号', 'aksrt'),
],
'labels' => $labels,
];
}
+5 -2
View File
@@ -128,8 +128,11 @@ class Gateway {
if ($gateway_id === 'wechatpay' && method_exists($gateway, 'output_callback_response')) {
$gateway->output_callback_response(['success' => $processed]); // exit
}
if ($gateway_id === 'alipay') {
// 支付宝异步通知要求返回纯文本 success,否则会重复通知
if ($gateway_id === 'alipay' || str_starts_with($gateway_id, 'epay')) {
// 支付宝 / 易支付异步通知要求返回纯文本 success,否则会重复通知
if (method_exists($gateway, 'output_callback_response')) {
$gateway->output_callback_response(['success' => $processed]); // exit
}
header('Content-Type: text/plain; charset=utf-8');
echo $processed ? 'success' : 'fail';
exit;
+312
View File
@@ -0,0 +1,312 @@
<?php
/**
* Compatible Epay (彩虹易支付) gateway — alipay / wxpay channels.
*
* @package AKSRT
*/
namespace AKSRT\Payment\Gateways;
defined('ABSPATH') || exit;
class EpayGateway implements \AKSRT\Payment\GatewayInterface {
/** @var string alipay|wxpay */
private string $channel;
public function __construct(string $channel = 'alipay') {
$channel = sanitize_key($channel);
$this->channel = in_array($channel, ['alipay', 'wxpay'], true) ? $channel : 'alipay';
}
public function get_id(): string {
return $this->channel === 'wxpay' ? 'epay_wxpay' : 'epay_alipay';
}
public function get_name(): string {
return $this->channel === 'wxpay'
? __('易支付·微信', 'aksrt')
: __('易支付·支付宝', 'aksrt');
}
public function is_configured(): bool {
if (class_exists('\AKSRT\Settings\PaymentRuntime')
&& !\AKSRT\Settings\PaymentRuntime::is_gateway_enabled($this->get_id())) {
return false;
}
$c = $this->get_config();
return $c['apiurl'] !== '' && $c['pid'] !== '' && $c['key'] !== '';
}
public function create_payment(array $order): array {
if (!$this->is_configured()) {
return ['success' => false, 'message' => __('易支付未配置', 'aksrt')];
}
$order_number = (string) ($order['order_number'] ?? '');
$amount = (float) ($order['amount'] ?? 0);
$subject = (string) ($order['subject'] ?? __('订单支付', 'aksrt'));
if ($order_number === '' || $amount <= 0) {
return ['success' => false, 'message' => __('订单参数无效', 'aksrt')];
}
$config = $this->get_config();
$params = [
'pid' => $config['pid'],
'type' => $this->channel,
'out_trade_no' => $order_number,
'notify_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/notify'),
'return_url' => rest_url('aksrt/v1/payment/' . $this->get_id() . '/return'),
'name' => mb_substr(wp_strip_all_tags($subject), 0, 127),
'money' => number_format($amount, 2, '.', ''),
];
$params['sign'] = $this->sign($params, $config['key']);
$params['sign_type'] = 'MD5';
$use_mobile = $this->prefer_mobile($config['device']);
if ($use_mobile) {
// mapi.php returns JSON with payurl / qrcode / urlscheme.
$api = $this->endpoint($config['apiurl'], 'mapi.php');
$response = wp_remote_post($api, [
'timeout' => 30,
'body' => $params,
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8'],
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
$body = json_decode((string) wp_remote_retrieve_body($response), true);
if (!is_array($body)) {
// Fall back to browser submit if mapi is unavailable.
return $this->browser_submit_result($config['apiurl'], $params, $order_number);
}
$code = (int) ($body['code'] ?? 0);
if ($code !== 1) {
$msg = (string) ($body['msg'] ?? $body['message'] ?? __('易支付下单失败', 'aksrt'));
return ['success' => false, 'message' => $msg];
}
$pay_url = (string) ($body['payurl'] ?? $body['urlscheme'] ?? '');
$qrcode = (string) ($body['qrcode'] ?? '');
if ($pay_url === '' && $qrcode === '') {
return ['success' => false, 'message' => __('易支付未返回支付链接', 'aksrt')];
}
$result = [
'success' => true,
'payment_data' => [
'order_number' => $order_number,
'code_url' => $qrcode,
'channel' => $this->channel,
],
'message' => __('易支付订单已创建', 'aksrt'),
];
if ($pay_url !== '') {
$result['payment_url'] = $pay_url;
} elseif ($qrcode !== '') {
$result['payment_url'] = $qrcode;
}
return $result;
}
return $this->browser_submit_result($config['apiurl'], $params, $order_number);
}
public function verify_callback(\WP_REST_Request $request): array {
if (!$this->is_configured()) {
return ['success' => false, 'message' => __('易支付未配置', 'aksrt')];
}
$params = strtoupper($request->get_method()) === 'POST'
? $request->get_body_params()
: $request->get_query_params();
if (!is_array($params) || $params === []) {
// Some panels POST raw query string.
$params = $request->get_params();
}
if (!is_array($params)) {
$params = [];
}
unset($params['gateway'], $params['rest_route']);
$config = $this->get_config();
$sign = (string) ($params['sign'] ?? '');
if ($sign === '' || !$this->verify_sign($params, $config['key'], $sign)) {
return ['success' => false, 'message' => __('易支付签名校验失败', 'aksrt')];
}
$pid = (string) ($params['pid'] ?? '');
if ($pid !== '' && !hash_equals($config['pid'], $pid)) {
return ['success' => false, 'message' => __('易支付商户号不匹配', 'aksrt')];
}
$order_number = sanitize_text_field((string) ($params['out_trade_no'] ?? ''));
$transaction_id = sanitize_text_field((string) ($params['trade_no'] ?? ''));
$amount = (float) ($params['money'] ?? 0);
$trade_status = (string) ($params['trade_status'] ?? '');
$type = sanitize_key((string) ($params['type'] ?? ''));
// When type is present, require channel match.
if ($type !== '' && $type !== $this->channel) {
return ['success' => false, 'message' => __('易支付通道类型不匹配', 'aksrt')];
}
$paid = ($trade_status === '' || $trade_status === 'TRADE_SUCCESS');
if ($order_number === '' || ($paid && ($transaction_id === '' || $amount <= 0))) {
return ['success' => false, 'message' => __('易支付回调数据无效', 'aksrt')];
}
return [
'success' => true,
'order_number' => $order_number,
'amount' => $amount,
'transaction_id' => $transaction_id,
'status' => $paid ? 'paid' : 'pending',
];
}
/**
* Epay expects plain-text success/fail (handled also by Gateway for epay_* ids).
*/
public function output_callback_response(array $result): void {
header('Content-Type: text/plain; charset=utf-8');
echo !empty($result['success']) ? 'success' : 'fail';
exit;
}
public function query_status(string $order_number): array {
if (!$this->is_configured() || $order_number === '') {
return ['success' => false, 'message' => __('易支付未配置', 'aksrt')];
}
$config = $this->get_config();
// Common compatible query: api.php?act=order&pid&key&out_trade_no
$url = add_query_arg([
'act' => 'order',
'pid' => $config['pid'],
'key' => $config['key'],
'out_trade_no' => $order_number,
], $this->endpoint($config['apiurl'], 'api.php'));
$response = wp_remote_get($url, ['timeout' => 20]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
$body = json_decode((string) wp_remote_retrieve_body($response), true);
if (!is_array($body)) {
return ['success' => false, 'message' => __('易支付查询响应无效', 'aksrt')];
}
// code=1 success in most forks; status 1 = paid.
$code = (int) ($body['code'] ?? 0);
$status = (int) ($body['status'] ?? $body['trade_status'] ?? 0);
$paid = $code === 1 && ((string) $status === '1' || (string) ($body['trade_status'] ?? '') === 'TRADE_SUCCESS');
$amount = (float) ($body['money'] ?? $body['amount'] ?? 0);
$tx = (string) ($body['trade_no'] ?? $body['api_trade_no'] ?? '');
return [
'success' => true,
'order_number' => (string) ($body['out_trade_no'] ?? $order_number),
'amount' => $amount,
'transaction_id' => $tx,
'status' => $paid ? 'paid' : 'pending',
];
}
public function refund(string $order_number, float $amount, string $request_id = ''): array {
return [
'success' => false,
'status' => 'failed',
'message' => __('易支付退款请在商户后台人工处理', 'aksrt'),
];
}
/**
* @return array{apiurl:string,pid:string,key:string,device:string}
*/
private function get_config(): array {
$all = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_all()
: [];
$device = sanitize_key((string) ($all['epay_device'] ?? 'auto'));
if (!in_array($device, ['auto', 'pc', 'mobile'], true)) {
$device = 'auto';
}
$apiurl = (string) ($all['epay_apiurl'] ?? '');
$apiurl = rtrim(trim($apiurl), '/') . '/';
if ($apiurl === '/') {
$apiurl = '';
}
return [
'apiurl' => $apiurl,
'pid' => (string) ($all['epay_pid'] ?? ''),
'key' => (string) ($all['epay_key'] ?? ''),
'device' => $device,
];
}
/**
* @param array<string,mixed> $params
*/
private function sign(array $params, string $key): string {
$filtered = [];
foreach ($params as $k => $v) {
if ($k === 'sign' || $k === 'sign_type') {
continue;
}
if ($v === '' || $v === null) {
continue;
}
$filtered[(string) $k] = is_scalar($v) ? (string) $v : '';
}
ksort($filtered);
$parts = [];
foreach ($filtered as $k => $v) {
$parts[] = $k . '=' . $v;
}
return md5(implode('&', $parts) . $key);
}
/**
* @param array<string,mixed> $params
*/
private function verify_sign(array $params, string $key, string $sign): bool {
$expected = $this->sign($params, $key);
return hash_equals(strtolower($expected), strtolower($sign));
}
private function endpoint(string $apiurl, string $file): string {
return rtrim($apiurl, '/') . '/' . ltrim($file, '/');
}
private function prefer_mobile(string $device): bool {
if ($device === 'mobile') {
return true;
}
if ($device === 'pc') {
return false;
}
// auto
if (!empty($_SERVER['HTTP_USER_AGENT'])) {
$ua = (string) $_SERVER['HTTP_USER_AGENT'];
return (bool) preg_match('/Mobile|Android|iPhone|iPad|iPod|MicroMessenger/i', $ua);
}
return false;
}
/**
* @param array<string,string> $params
* @return array{success:bool,payment_url?:string,payment_data?:array,message?:string}
*/
private function browser_submit_result(string $apiurl, array $params, string $order_number): array {
$url = add_query_arg($params, $this->endpoint($apiurl, 'submit.php'));
return [
'success' => true,
'payment_url' => $url,
'payment_data' => [
'order_number' => $order_number,
'channel' => $this->channel,
],
'message' => __('易支付支付链接已生成', 'aksrt'),
];
}
}
+9 -7
View File
@@ -504,13 +504,15 @@ class UserOrders {
private function payment_method_label(string $method): string {
$method = sanitize_key($method);
$map = [
'balance' => __('站内余额', 'aksrt'),
'points' => __('积分支付', 'aksrt'),
'alipay' => __('支付宝', 'aksrt'),
'wechatpay' => __('微信支付', 'aksrt'),
'wechat' => __('微信支付', 'aksrt'),
'paypal' => 'PayPal',
'stripe' => 'Stripe',
'balance' => __('站内余额', 'aksrt'),
'points' => __('积分支付', 'aksrt'),
'alipay' => __('支付宝', 'aksrt'),
'wechatpay' => __('微信支付', 'aksrt'),
'wechat' => __('微信支付', 'aksrt'),
'paypal' => 'PayPal',
'stripe' => 'Stripe',
'epay_alipay' => __('易支付·支付宝', 'aksrt'),
'epay_wxpay' => __('易支付·微信', 'aksrt'),
];
return $map[$method] ?? ($method !== '' ? $method : __('—', 'aksrt'));
}
+31 -43
View File
@@ -299,42 +299,22 @@ class PaymentRuntime {
*/
public static function get_payment_gateway_settings(): array {
$settings = Settings::get_all();
$mode = sanitize_key((string) ($settings['payment_gateway_mode'] ?? 'manual_review'));
if (!in_array($mode, ['manual_review', 'test_gateway', 'adapter_ready'], true)) {
$mode = 'manual_review';
}
$help_text = trim((string) ($settings['payment_gateway_help'] ?? ''));
$gateway_keys = ['wechat', 'alipay', 'paypal', 'stripe', 'balance', 'card_pass', 'xunhupay', 'xhpay', 'payjs', 'epay', 'vmq', 'codepay'];
$gateway_keys = ['wechat', 'alipay', 'paypal', 'stripe', 'balance', 'card_pass', 'epay'];
$gateways = [];
foreach ($gateway_keys as $gw) {
$gateways[$gw] = !empty($settings["payment_{$gw}_enabled"]);
}
return [
'mode' => $mode,
'test_enabled' => !isset($settings['payment_gateway_test_enabled']) || !empty($settings['payment_gateway_test_enabled']),
'manual_review' => !isset($settings['payment_gateway_manual_review']) || !empty($settings['payment_gateway_manual_review']),
'callback_secret' => sanitize_text_field((string) ($settings['payment_gateway_callback_secret'] ?? '')),
'help_text' => $help_text !== '' ? $help_text : __('未接真实支付网关时,建议使用测试网关或人工确认模式完成联调。', 'aksrt'),
'gateways' => $gateways,
'xunhupay' => [
'appid' => sanitize_text_field((string) ($settings['xunhupay_appid'] ?? '')),
'appsecret' => sanitize_text_field((string) ($settings['xunhupay_appsecret'] ?? '')),
],
'xhpay' => [
'mchid' => sanitize_text_field((string) ($settings['xhpay_mchid'] ?? '')),
'alipay_v2' => sanitize_text_field((string) ($settings['xhpay_alipay_v2'] ?? '')),
'wx_h5' => sanitize_text_field((string) ($settings['xhpay_wx_h5'] ?? '')),
],
'payjs' => [
'mchid' => sanitize_text_field((string) ($settings['payjs_mchid'] ?? '')),
'key' => sanitize_text_field((string) ($settings['payjs_key'] ?? '')),
],
'epay' => [
'apiurl' => esc_url_raw((string) ($settings['epay_apiurl'] ?? '')),
'pid' => sanitize_text_field((string) ($settings['epay_pid'] ?? '')),
'key' => sanitize_text_field((string) ($settings['epay_key'] ?? '')),
'help_text' => __('以渠道开关与凭据是否齐全为准;异步回调由各网关验签。卡密为兑换入口,不进入收银台。', 'aksrt'),
'gateways' => $gateways,
'epay' => [
'apiurl' => esc_url_raw((string) ($settings['epay_apiurl'] ?? '')),
'pid' => sanitize_text_field((string) ($settings['epay_pid'] ?? '')),
'key' => sanitize_text_field((string) ($settings['epay_key'] ?? '')),
'channel_alipay' => !array_key_exists('epay_channel_alipay', $settings) || !empty($settings['epay_channel_alipay']),
'channel_wxpay' => !array_key_exists('epay_channel_wxpay', $settings) || !empty($settings['epay_channel_wxpay']),
'device' => sanitize_key((string) ($settings['epay_device'] ?? 'auto')),
],
];
}
@@ -444,19 +424,17 @@ class PaymentRuntime {
*/
public static function is_gateway_enabled(string $gateway_id): bool {
$settings = Settings::get_all();
$gateway_id = sanitize_key($gateway_id);
$map = [
'alipay' => 'payment_alipay_enabled',
'wechatpay' => 'payment_wechat_enabled',
'paypal' => 'payment_paypal_enabled',
'stripe' => 'payment_stripe_enabled',
'balance' => 'payment_balance_enabled',
'card_pass' => 'payment_card_pass_enabled',
'xunhupay' => 'payment_xunhupay_enabled',
'xhpay' => 'payment_xhpay_enabled',
'payjs' => 'payment_payjs_enabled',
'epay' => 'payment_epay_enabled',
'vmq' => 'payment_vmq_enabled',
'codepay' => 'payment_codepay_enabled',
'alipay' => 'payment_alipay_enabled',
'wechatpay' => 'payment_wechat_enabled',
'paypal' => 'payment_paypal_enabled',
'stripe' => 'payment_stripe_enabled',
'balance' => 'payment_balance_enabled',
'card_pass' => 'payment_card_pass_enabled',
'epay' => 'payment_epay_enabled',
'epay_alipay' => 'payment_epay_enabled',
'epay_wxpay' => 'payment_epay_enabled',
];
$key = $map[$gateway_id] ?? '';
if ($key === '') {
@@ -473,7 +451,17 @@ class PaymentRuntime {
}
return true;
}
return !empty($settings[$key]);
if (empty($settings[$key])) {
return false;
}
// Epay sub-channels require the matching channel switch (default on when key missing).
if ($gateway_id === 'epay_alipay') {
return !array_key_exists('epay_channel_alipay', $settings) || !empty($settings['epay_channel_alipay']);
}
if ($gateway_id === 'epay_wxpay') {
return !array_key_exists('epay_channel_wxpay', $settings) || !empty($settings['epay_channel_wxpay']);
}
return true;
}
/**
+6 -14
View File
@@ -95,24 +95,12 @@ class ProductService {
];
}
$manual = true;
if (class_exists('\AKSRT\Settings\Settings')) {
$gw = \AKSRT\Settings\Settings::get_payment_gateway_settings();
$manual = !empty($gw['manual_review']) || ($gw['mode'] ?? '') === 'manual_review';
}
if ($manual) {
$methods['manual_review'] = [
'value' => 'manual_review',
'label' => __('人工确认', 'aksrt'),
'description' => __('保留待支付订单,后续由管理员确认。', 'aksrt'),
];
}
// Fallback only when no configured gateways (and no points).
if (empty($methods)) {
$methods['manual_review'] = [
'value' => 'manual_review',
'label' => __('人工确认', 'aksrt'),
'description' => __('保留待支付订单,后续由管理员确认。', 'aksrt'),
'description' => __('暂无可用支付网关,保留待支付订单由管理员确认。', 'aksrt'),
];
}
@@ -131,6 +119,10 @@ class ProductService {
return __('跳转 PayPal 完成支付。', 'aksrt');
case 'stripe':
return __('跳转 Stripe Checkout 完成支付。', 'aksrt');
case 'epay_alipay':
return __('经易支付跳转支付宝完成支付。', 'aksrt');
case 'epay_wxpay':
return __('经易支付跳转微信完成支付。', 'aksrt');
default:
return '';
}