mirror of
https://github.com/34892002/edgeKey.git
synced 2026-05-06 15:22:43 +08:00
fix: payment
This commit is contained in:
@@ -300,5 +300,8 @@ bun dev
|
||||
4. 在搜索框输入关键词过滤日志,例如:
|
||||
- `email.notify_order_paid.config_failed` — 支付后邮件配置获取失败
|
||||
- `email.send.failed` — 邮件发送失败
|
||||
- `email.order_paid.failed` — 支付成功后发送邮件通知失败
|
||||
- `payment.notify.route_exception` — 支付回调路由异常
|
||||
- `payment.notify.context_missing` — 支付回调缺少数据库上下文
|
||||
- `payment.notify.context_missing` — 支付回调缺少数据库上下文
|
||||
- `payment.notify.diagnostic` — 支付回调校验异常诊断(签名错误、金额不匹配等)
|
||||
- `bepusdt.verify_notify` — BEpusdt 回调原始 payload(debug 级别)
|
||||
34
components/SecretInput.vue
Normal file
34
components/SecretInput.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="visible ? 'text' : 'password'"
|
||||
:value="modelValue"
|
||||
class="input input-bordered w-full pr-10"
|
||||
v-bind="$attrs"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/40 hover:text-base-content"
|
||||
@click="visible = !visible"
|
||||
>
|
||||
<svg v-if="visible" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" /><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M3.707 2.293a1 1 0 00-1.414 1.414l14 14a1 1 0 001.414-1.414l-1.473-1.473A10.014 10.014 0 0019.542 10C18.268 5.943 14.478 3 10 3a9.958 9.958 0 00-4.512 1.074l-1.78-1.781zm4.261 4.26l1.514 1.515a2.003 2.003 0 012.45 2.45l1.514 1.514a4 4 0 00-5.478-5.478z" clip-rule="evenodd" />
|
||||
<path d="M12.454 16.697L9.75 13.992a4 4 0 01-3.742-3.741L2.335 6.578A9.98 9.98 0 00.458 10c1.274 4.057 5.064 7 9.542 7 .847 0 1.669-.105 2.454-.303z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
defineProps<{ modelValue: string }>();
|
||||
defineEmits<{ "update:modelValue": [value: string] }>();
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const visible = ref(false);
|
||||
</script>
|
||||
@@ -1,5 +1,24 @@
|
||||
# 公共组件文档
|
||||
|
||||
## SecretInput
|
||||
|
||||
带显示/隐藏切换的密钥输入框,用于密码、API Secret 等敏感字段。
|
||||
|
||||
### Props
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `modelValue` | `string` | 输入值(v-model) |
|
||||
|
||||
支持透传所有原生 `input` 属性(如 `placeholder`、`disabled` 等)。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```components/SecretInput.vue#L1-3
|
||||
<SecretInput v-model="form.appSecret" placeholder="请输入 App Secret" />
|
||||
```
|
||||
|
||||
|
||||
## DataTable
|
||||
|
||||
通用带翻页的表格组件,基于 daisyUI `table` 样式。
|
||||
|
||||
@@ -46,7 +46,7 @@ export function getPaymentProviderLabel(provider: string) {
|
||||
case "EPAY":
|
||||
return "易支付";
|
||||
case "BEPUSDT":
|
||||
return "USDT";
|
||||
return "BEpusdt";
|
||||
default:
|
||||
return provider;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface AlipayConfig {
|
||||
returnUrl?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_GATEWAY = "https://openapi.alipay.com/gateway.do";
|
||||
|
||||
|
||||
function pemToBase64(pem: string) {
|
||||
return pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
||||
@@ -65,7 +65,7 @@ function buildSignString(params: Record<string, string>) {
|
||||
export function createAlipayAdapter(config: AlipayConfig): PaymentProviderAdapter {
|
||||
return {
|
||||
async createPayment(input) {
|
||||
const gateway = config.baseUrl?.trim().replace(/\/+$/, "") || DEFAULT_GATEWAY;
|
||||
const gateway = `${config.baseUrl?.trim().replace(/\/+$/, "")}/gateway.do`;
|
||||
|
||||
if (!config.alipayAppId || !config.alipayPrivateKey) {
|
||||
throw badRequestError("支付宝配置不完整", "ALIPAY_CONFIG_INCOMPLETE");
|
||||
@@ -136,7 +136,7 @@ export async function queryAlipayTrade(config: AlipayConfig, outTradeNo: string)
|
||||
throw badRequestError("支付宝配置不完整", "ALIPAY_CONFIG_INCOMPLETE");
|
||||
}
|
||||
|
||||
const gateway = config.baseUrl?.trim().replace(/\/+$/, "") || DEFAULT_GATEWAY;
|
||||
const gateway = `${config.baseUrl?.trim().replace(/\/+$/, "")}/gateway.do`;
|
||||
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
||||
const params: Record<string, string> = {
|
||||
app_id: config.alipayAppId,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { badRequestError, externalServiceError } from "../../lib/app-error";
|
||||
import { logger } from "../../lib/logger";
|
||||
import type { PaymentProviderAdapter } from "./provider";
|
||||
|
||||
interface BepusdtConfig {
|
||||
@@ -40,18 +41,8 @@ export function createBepusdtAdapter(config: BepusdtConfig): PaymentProviderAdap
|
||||
};
|
||||
|
||||
const signature = signBepusdt(payload, config.appSecret);
|
||||
const response = await fetch(`${normalizeBaseUrl(config.baseUrl)}/api/v1/order/create-transaction`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
signature,
|
||||
}),
|
||||
});
|
||||
|
||||
const json = (await response.json()) as {
|
||||
type BepusdtResponse = {
|
||||
status_code?: number;
|
||||
message?: string;
|
||||
data?: {
|
||||
@@ -60,7 +51,23 @@ export function createBepusdtAdapter(config: BepusdtConfig): PaymentProviderAdap
|
||||
};
|
||||
};
|
||||
|
||||
if (!response.ok || json.status_code !== 200 || !json.data?.payment_url) {
|
||||
let json: BepusdtResponse;
|
||||
try {
|
||||
const response = await fetch(`${normalizeBaseUrl(config.baseUrl)}/api/v1/order/create-order`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ ...payload, signature }),
|
||||
});
|
||||
const text = (await response.text()).replace(/^\uFEFF/, "");
|
||||
json = JSON.parse(text) as BepusdtResponse;
|
||||
} catch (err) {
|
||||
throw externalServiceError(
|
||||
`BEpusdt 请求失败: ${err instanceof Error ? err.message : String(err)}`,
|
||||
"BEPUSDT_INVALID_RESPONSE"
|
||||
);
|
||||
}
|
||||
|
||||
if (json.status_code !== 200 || !json.data?.payment_url) {
|
||||
throw externalServiceError(json.message || "BEpusdt 创建支付失败", "BEPUSDT_CREATE_PAYMENT_FAILED");
|
||||
}
|
||||
|
||||
@@ -80,11 +87,13 @@ export function createBepusdtAdapter(config: BepusdtConfig): PaymentProviderAdap
|
||||
};
|
||||
}
|
||||
|
||||
logger.debug("bepusdt.verify_notify", { payload });
|
||||
const signature = payload.signature || "";
|
||||
const unsignedPayload = { ...payload };
|
||||
delete unsignedPayload.signature;
|
||||
const expected = signBepusdt(unsignedPayload, config.appSecret);
|
||||
const status = payload.status === "2" ? "PAID" : payload.status === "3" ? "FAILED" : "PENDING";
|
||||
const statusVal = String(payload.status);
|
||||
const status = statusVal === "2" ? "PAID" : statusVal === "3" ? "FAILED" : "PENDING";
|
||||
|
||||
return {
|
||||
isValid: signature === expected,
|
||||
@@ -97,4 +106,4 @@ export function createBepusdtAdapter(config: BepusdtConfig): PaymentProviderAdap
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import { notifyOrderPaid as _notifyOrderPaid } from "../email/service";
|
||||
const defaultPaymentConfigs: Record<PaymentProvider, PaymentConfigValue> = {
|
||||
BEPUSDT: {
|
||||
provider: "BEPUSDT",
|
||||
name: "USDT",
|
||||
name: "BEpusdt",
|
||||
isEnabled: false,
|
||||
baseUrl: "",
|
||||
appId: "",
|
||||
@@ -41,7 +41,7 @@ const defaultPaymentConfigs: Record<PaymentProvider, PaymentConfigValue> = {
|
||||
provider: "ALIPAY",
|
||||
name: "支付宝",
|
||||
isEnabled: false,
|
||||
baseUrl: "",
|
||||
baseUrl: "https://openapi.alipay.com",
|
||||
alipayAppId: "",
|
||||
alipayPrivateKey: "",
|
||||
alipayPublicKey: "",
|
||||
|
||||
@@ -212,11 +212,11 @@
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">API Key</span>
|
||||
<input v-model="configForm.apiKey" class="input input-bordered w-full" />
|
||||
<SecretInput v-model="configForm.apiKey" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">Secret Key</span>
|
||||
<input v-model="configForm.secretKey" class="input input-bordered w-full" :disabled="configForm.apiProvider !== 'MAILJET'" :placeholder="configForm.apiProvider === 'MAILJET' ? 'Mailjet Secret Key' : 'Brevo 不需要该字段'" />
|
||||
<SecretInput v-model="configForm.secretKey" :disabled="configForm.apiProvider !== 'MAILJET'" :placeholder="configForm.apiProvider === 'MAILJET' ? 'Mailjet Secret Key' : 'Brevo 不需要该字段'" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">超时(ms)</span>
|
||||
@@ -253,7 +253,7 @@
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">SMTP 密码</span>
|
||||
<input v-model="configForm.smtpPassword" class="input input-bordered w-full" />
|
||||
<SecretInput v-model="configForm.smtpPassword" />
|
||||
</label><label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">认证方式</span>
|
||||
<select v-model="configForm.smtpAuthType" class="select select-bordered w-full">
|
||||
@@ -480,6 +480,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SecretInput from "../../../components/SecretInput.vue";
|
||||
import { normalizeTelefuncError } from "../../../lib/app-error";
|
||||
import { reactive, ref, computed } from "vue";
|
||||
import { useData } from "vike-vue/useData";
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<option value="">全部支付方式</option>
|
||||
<option value="EPAY">易支付</option>
|
||||
<option value="ALIPAY">支付宝</option>
|
||||
<option value="BEPUSDT">USDT</option>
|
||||
<option value="BEPUSDT">BEpusdt</option>
|
||||
</select>
|
||||
<select v-model="filter.status" class="select select-sm select-bordered w-32">
|
||||
<option value="">全部状态</option>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
</div>
|
||||
<div role="tablist" class="tabs tabs-border">
|
||||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'BEPUSDT' }" @click="activeTab = 'BEPUSDT'">
|
||||
BEpusdt / USDT
|
||||
BEpusdt
|
||||
<span v-if="localConfigs.BEPUSDT?.isEnabled" class="ml-1.5 inline-block w-2 h-2 rounded-full bg-success"></span>
|
||||
</a>
|
||||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'EPAY' }" @click="activeTab = 'EPAY'">
|
||||
Epay / 聚合支付
|
||||
Epay
|
||||
<span v-if="localConfigs.EPAY?.isEnabled" class="ml-1.5 inline-block w-2 h-2 rounded-full bg-success"></span>
|
||||
</a>
|
||||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'ALIPAY' }" @click="activeTab = 'ALIPAY'">
|
||||
@@ -17,9 +17,9 @@
|
||||
<span v-if="localConfigs.ALIPAY?.isEnabled" class="ml-1.5 inline-block w-2 h-2 rounded-full bg-success"></span>
|
||||
</a>
|
||||
</div>
|
||||
<PaymentConfigCard v-if="activeTab === 'BEPUSDT'" provider="BEPUSDT" title="BEpusdt / USDT" :initial-value="localConfigs.BEPUSDT" @saved="localConfigs.BEPUSDT = $event" />
|
||||
<PaymentConfigCard v-if="activeTab === 'EPAY'" provider="EPAY" title="Epay / 聚合支付" :initial-value="localConfigs.EPAY" @saved="localConfigs.EPAY = $event" />
|
||||
<PaymentConfigCard v-if="activeTab === 'ALIPAY'" provider="ALIPAY" title="支付宝" :initial-value="localConfigs.ALIPAY" @saved="localConfigs.ALIPAY = $event" />
|
||||
<PaymentConfigCard v-if="activeTab === 'BEPUSDT'" provider="BEPUSDT" title="Usdt/Usdc(数字货币)" :initial-value="localConfigs.BEPUSDT" @saved="localConfigs.BEPUSDT = $event" />
|
||||
<PaymentConfigCard v-if="activeTab === 'EPAY'" provider="EPAY" title="易支付(聚合支付)" :initial-value="localConfigs.EPAY" @saved="localConfigs.EPAY = $event" />
|
||||
<PaymentConfigCard v-if="activeTab === 'ALIPAY'" provider="ALIPAY" title="支付宝(官方)" :initial-value="localConfigs.ALIPAY" @saved="localConfigs.ALIPAY = $event" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
<input v-model="form.name" class="input input-bordered w-full" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">网关地址</span>
|
||||
<input v-model="form.baseUrl" class="input input-bordered w-full" placeholder="https://example.com" />
|
||||
<span class="label-text font-medium">网关地址(仅需填写主域名,系统处理接口)</span>
|
||||
<input v-model="form.baseUrl" class="input input-bordered w-full" placeholder="https://pay.example.com (末尾请勿加斜杠)" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +67,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const form = reactive({
|
||||
name: props.initialValue?.name ?? (props.provider === 'BEPUSDT' ? 'USDT' : '聚合支付'),
|
||||
name: props.initialValue?.name ?? (props.provider === 'BEPUSDT' ? 'BEpusdt' : '聚合支付'),
|
||||
isEnabled: props.initialValue?.isEnabled ?? false,
|
||||
baseUrl: props.initialValue?.baseUrl ?? '',
|
||||
notifyUrl: props.initialValue?.notifyUrl ?? '',
|
||||
|
||||
@@ -8,16 +8,17 @@
|
||||
</div>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">应用私钥(PKCS#8,去掉头尾和换行)</span>
|
||||
<textarea v-model="modelValue.alipayPrivateKey" class="textarea textarea-bordered w-full font-mono text-xs" rows="4" placeholder="MIIEvgIBADANBgkq..." />
|
||||
<SecretInput v-model="modelValue.alipayPrivateKey" placeholder="MIIEvgIBADANBgkq..." />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">支付宝公钥(用于验签,去掉头尾和换行)</span>
|
||||
<textarea v-model="modelValue.alipayPublicKey" class="textarea textarea-bordered w-full font-mono text-xs" rows="3" placeholder="MIIBIjANBgkq..." />
|
||||
<SecretInput v-model="modelValue.alipayPublicKey" placeholder="MIIBIjANBgkq..." />
|
||||
</label>
|
||||
<p class="text-xs text-base-content/60">密钥生成工具:<a href="https://opendocs.alipay.com/common/02kipl" target="_blank" class="link">支付宝密钥工具</a></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SecretInput from "../../../../components/SecretInput.vue";
|
||||
defineProps<{ modelValue: Record<string, any> }>();
|
||||
</script>
|
||||
@@ -6,11 +6,12 @@
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">App Secret</span>
|
||||
<input v-model="modelValue.appSecret" class="input input-bordered w-full" />
|
||||
<SecretInput v-model="modelValue.appSecret" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SecretInput from "../../../../components/SecretInput.vue";
|
||||
defineProps<{ modelValue: Record<string, any> }>();
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">Key</span>
|
||||
<input v-model="modelValue.key" class="input input-bordered w-full" />
|
||||
<SecretInput v-model="modelValue.key" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-base-content/60">
|
||||
@@ -15,5 +15,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SecretInput from "../../../../components/SecretInput.vue";
|
||||
defineProps<{ modelValue: Record<string, any> }>();
|
||||
</script>
|
||||
Reference in New Issue
Block a user