mirror of
https://github.com/34892002/edgeKey.git
synced 2026-05-06 15:22:43 +08:00
feat: 自动部署
1. 支付网关组件化 2. 支持cf绑定git自动化部署
This commit is contained in:
@@ -3,7 +3,7 @@ import { badRequestError } from "../app-error";
|
||||
export function validatePaymentConfigInput(input: {
|
||||
name?: string;
|
||||
baseUrl?: string;
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: string;
|
||||
isEnabled?: boolean;
|
||||
appSecret?: string;
|
||||
pid?: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ export function listPaymentConfigRecords(prisma: PrismaClient) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getPaymentConfigRecord(prisma: PrismaClient, provider: "BEPUSDT" | "EPAY") {
|
||||
export function getPaymentConfigRecord(prisma: PrismaClient, provider: string) {
|
||||
return prisma.paymentConfig.findUnique({
|
||||
where: { provider },
|
||||
});
|
||||
@@ -14,7 +14,7 @@ export function getPaymentConfigRecord(prisma: PrismaClient, provider: "BEPUSDT"
|
||||
|
||||
export function upsertPaymentConfigRecord(
|
||||
prisma: PrismaClient,
|
||||
provider: "BEPUSDT" | "EPAY",
|
||||
provider: string,
|
||||
input: {
|
||||
name: string;
|
||||
isEnabled: boolean;
|
||||
@@ -41,7 +41,7 @@ export function createPaymentLogRecord(
|
||||
prisma: PrismaClient,
|
||||
input: {
|
||||
orderId?: number;
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: string;
|
||||
orderNo?: string;
|
||||
paymentOrderNo?: string;
|
||||
eventType: string;
|
||||
|
||||
@@ -7,14 +7,14 @@ import { getAdminContext, logAdminOperation } from "../auth/service";
|
||||
import { notifyOrderPaid } from "../email/service";
|
||||
import { getSiteSetting } from "../site/service";
|
||||
import { createPaymentLogRecord, getPaymentConfigRecord, listPaymentConfigRecords, upsertPaymentConfigRecord } from "./repository";
|
||||
import type { PaymentMethodItem } from "./types";
|
||||
import type { PaymentMethodItem, PaymentProvider } from "./types";
|
||||
import type { PaymentConfigValue } from "./types";
|
||||
import { createBepusdtAdapter } from "./bepusdt";
|
||||
import { createEpayAdapter } from "./epay";
|
||||
import { deliverOrder } from "../delivery/service";
|
||||
import { findOrderRecord, updateOrderPayment } from "../order/repository";
|
||||
|
||||
const defaultPaymentConfigs: Record<"BEPUSDT" | "EPAY", PaymentConfigValue> = {
|
||||
const defaultPaymentConfigs: Record<PaymentProvider, PaymentConfigValue> = {
|
||||
BEPUSDT: {
|
||||
provider: "BEPUSDT",
|
||||
name: "USDT",
|
||||
@@ -41,7 +41,7 @@ function getPaymentContext() {
|
||||
return getContext<{ prisma: PrismaClient }>();
|
||||
}
|
||||
|
||||
function normalizePaymentConfig(record: Awaited<ReturnType<typeof getPaymentConfigRecord>>, provider: "BEPUSDT" | "EPAY"): PaymentConfigValue {
|
||||
function normalizePaymentConfig(record: Awaited<ReturnType<typeof getPaymentConfigRecord>>, provider: PaymentProvider): PaymentConfigValue {
|
||||
const defaults = defaultPaymentConfigs[provider];
|
||||
if (!record) {
|
||||
return defaults;
|
||||
@@ -69,7 +69,7 @@ export async function listEnabledPaymentMethods(prisma?: PrismaClient): Promise<
|
||||
const client = prisma ?? getPaymentContext().prisma;
|
||||
const records = await listPaymentConfigRecords(client);
|
||||
|
||||
return (["BEPUSDT", "EPAY"] as const).map((provider) => {
|
||||
return (Object.keys(defaultPaymentConfigs) as PaymentProvider[]).map((provider) => {
|
||||
const record = records.find((item) => item.provider === provider);
|
||||
const value = normalizePaymentConfig(record ?? null, provider);
|
||||
return {
|
||||
@@ -81,17 +81,15 @@ export async function listEnabledPaymentMethods(prisma?: PrismaClient): Promise<
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPaymentConfigs(prisma?: PrismaClient) {
|
||||
export async function getPaymentConfigs(prisma?: PrismaClient): Promise<Record<string, PaymentConfigValue>> {
|
||||
const client = prisma ?? getPaymentContext().prisma;
|
||||
const [bepusdt, epay] = await Promise.all([
|
||||
getPaymentConfigRecord(client, "BEPUSDT"),
|
||||
getPaymentConfigRecord(client, "EPAY"),
|
||||
]);
|
||||
|
||||
return {
|
||||
BEPUSDT: normalizePaymentConfig(bepusdt, "BEPUSDT"),
|
||||
EPAY: normalizePaymentConfig(epay, "EPAY"),
|
||||
};
|
||||
const records = await listPaymentConfigRecords(client);
|
||||
const result: Record<string, PaymentConfigValue> = {};
|
||||
for (const provider of Object.keys(defaultPaymentConfigs) as PaymentProvider[]) {
|
||||
const record = records.find((r) => r.provider === provider) ?? null;
|
||||
result[provider] = normalizePaymentConfig(record, provider);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function savePaymentConfig(input: PaymentConfigValue) {
|
||||
@@ -200,7 +198,7 @@ export async function createPaymentForOrder(orderNo: string, prisma?: PrismaClie
|
||||
const notifyUrl = resolveCallbackUrl(
|
||||
baseOrigin,
|
||||
config.notifyUrl,
|
||||
order.paymentProvider === "BEPUSDT" ? "/api/payments/bepusdt/notify" : "/api/payments/epay/notify",
|
||||
defaultPaymentConfigs[order.paymentProvider as PaymentProvider]?.notifyUrl ?? "/api/payments/notify",
|
||||
);
|
||||
const returnUrl = resolveCallbackUrl(
|
||||
baseOrigin,
|
||||
@@ -272,7 +270,7 @@ function sanitizePaymentPayload(payload?: Record<string, unknown>) {
|
||||
|
||||
async function createNotifyLog(prisma: PrismaClient, input: {
|
||||
orderId?: number;
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: PaymentProvider;
|
||||
orderNo?: string;
|
||||
paymentOrderNo?: string;
|
||||
eventType?: string;
|
||||
@@ -294,7 +292,7 @@ async function createNotifyLog(prisma: PrismaClient, input: {
|
||||
}
|
||||
|
||||
function writePaymentNotifyDiagnostic(input: {
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: PaymentProvider;
|
||||
source: string;
|
||||
reason: string;
|
||||
payload?: Record<string, unknown>;
|
||||
@@ -322,7 +320,7 @@ function writePaymentNotifyDiagnostic(input: {
|
||||
}
|
||||
|
||||
export async function handlePaymentNotify(
|
||||
provider: "BEPUSDT" | "EPAY",
|
||||
provider: PaymentProvider,
|
||||
payload: Record<string, string>,
|
||||
prisma: PrismaClient,
|
||||
source: string,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
export type PaymentProvider = "BEPUSDT" | "EPAY";
|
||||
|
||||
export interface PaymentMethodItem {
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: PaymentProvider;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface PaymentConfigValue {
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: PaymentProvider;
|
||||
name: string;
|
||||
isEnabled: boolean;
|
||||
baseUrl: string;
|
||||
@@ -16,4 +18,4 @@ export interface PaymentConfigValue {
|
||||
key?: string;
|
||||
notifyUrl?: string;
|
||||
returnUrl?: string;
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
"db:seed": "bunx wrangler d1 execute DB --local --file=./scripts/seed.sql -y",
|
||||
"db:generate": "prisma generate",
|
||||
"db:studio": "prisma studio",
|
||||
"db:migrations:remote": "wrangler d1 migrations apply DB --remote",
|
||||
"db:migrations:remote": "wrangler d1 migrations apply DB --remote ${D1_ID:+--database-id $D1_ID}",
|
||||
"db:migrations:local": "wrangler d1 migrations apply DB --local",
|
||||
"db:seed:remote": "wrangler d1 execute DB --remote --file=./scripts/seed.sql -y",
|
||||
"db:seed:remote": "wrangler d1 execute DB --remote --file=./scripts/seed.sql -y ${D1_ID:+--database-id $D1_ID}",
|
||||
"verify:payments": "bun run scripts/verify-payment-adapters.ts",
|
||||
"verify:payment-notify": "bun run scripts/verify-payment-notify.ts",
|
||||
"deploy": "bun run db:migrations:remote && bun run db:seed:remote && wrangler deploy",
|
||||
|
||||
@@ -392,7 +392,7 @@
|
||||
<th>#</th>
|
||||
<th>时间</th>
|
||||
<th>分类</th>
|
||||
<th>API服务商</th>
|
||||
<th>邮箱名称</th>
|
||||
<th>场景</th>
|
||||
<th>状态</th>
|
||||
<th>收件人</th>
|
||||
@@ -407,7 +407,7 @@
|
||||
<th>{{ index + 1 }}</th>
|
||||
<td class="whitespace-nowrap">{{ formatDate(log.createdAt) }}</td>
|
||||
<td class="whitespace-nowrap">{{ getChannelLabel(log.provider) }}</td>
|
||||
<td class="whitespace-nowrap">{{ log.apiProvider || '-' }}</td>
|
||||
<td class="whitespace-nowrap">{{ configs.find(c => c.provider === log.provider)?.name || '-' }}</td>
|
||||
<td class="whitespace-nowrap">{{ getSceneLabel(log.scene) }}</td>
|
||||
<td>
|
||||
<span class="badge whitespace-nowrap" :class="log.status === 'SUCCESS' ? 'badge-success' : 'badge-error'">
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<div class="alert alert-info">
|
||||
<span>启用支付前,请先前往“站点设置”配置网站地址,否则无法获取支付结果。</span>
|
||||
<span>启用支付前,请先前往"站点设置"配置网站地址,否则无法获取支付结果。</span>
|
||||
</div>
|
||||
<PaymentConfigCard provider="BEPUSDT" title="BEpusdt / USDT" :initial-value="configs.BEPUSDT" />
|
||||
<PaymentConfigCard provider="EPAY" title="Epay / 聚合支付" :initial-value="configs.EPAY" />
|
||||
<div role="tablist" class="tabs tabs-border">
|
||||
<a role="tab" class="tab" :class="{ 'tab-active': activeTab === 'BEPUSDT' }" @click="activeTab = 'BEPUSDT'">
|
||||
BEpusdt / USDT
|
||||
<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 / 聚合支付
|
||||
<span v-if="localConfigs.EPAY?.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" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from "vue";
|
||||
import { useData } from "vike-vue/useData";
|
||||
import PaymentConfigCard from "./PaymentConfigCard.vue";
|
||||
import type { Data } from "./+data";
|
||||
|
||||
const { configs } = useData<Data>();
|
||||
</script>
|
||||
const activeTab = ref("BEPUSDT");
|
||||
const localConfigs = reactive({ ...configs });
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getPaymentConfigs } from "../../../modules/payment/service";
|
||||
|
||||
export type Data = ReturnType<typeof data>;
|
||||
export type Data = Awaited<ReturnType<typeof data>>;
|
||||
|
||||
export async function data(pageContext: {
|
||||
prisma: import("../../../generated/prisma/client").PrismaClient;
|
||||
|
||||
@@ -23,27 +23,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="provider === 'BEPUSDT'" class="grid gap-4 md:grid-cols-2">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">App ID</span>
|
||||
<input v-model="form.appId" class="input input-bordered w-full" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">App Secret</span>
|
||||
<input v-model="form.appSecret" class="input input-bordered w-full" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-else class="grid gap-4 md:grid-cols-2">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">PID</span>
|
||||
<input v-model="form.pid" class="input input-bordered w-full" />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">Key</span>
|
||||
<input v-model="form.key" class="input input-bordered w-full" />
|
||||
</label>
|
||||
</div>
|
||||
<component :is="formMap[provider]" v-model="extraFields" />
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
@@ -56,10 +36,6 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p v-if="provider === 'EPAY'" class="text-xs text-base-content/60">
|
||||
`Notify URL` 和 `Return URL` 支持填写相对路径或完整 URL;`Return URL` 支持 `{orderNo}`、`{token}` 占位符。
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="btn btn-primary" :disabled="saving" @click="handleSave">
|
||||
{{ saving ? '保存中...' : '保存配置' }}
|
||||
@@ -72,15 +48,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeTelefuncError } from "../../../lib/app-error";
|
||||
import { reactive, ref } from "vue";
|
||||
import { normalizeTelefuncError } from "../../../lib/app-error";
|
||||
import { onSavePaymentConfig } from "./savePaymentConfig.telefunc";
|
||||
import BEpusdtForm from "./forms/BEpusdtForm.vue";
|
||||
import EpayForm from "./forms/EpayForm.vue";
|
||||
import type { PaymentProvider } from "../../../modules/payment/types";
|
||||
|
||||
const formMap = { BEPUSDT: BEpusdtForm, EPAY: EpayForm };
|
||||
|
||||
const emit = defineEmits<{ saved: [value: typeof props.initialValue] }>();
|
||||
|
||||
const props = defineProps<{
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: PaymentProvider;
|
||||
title: string;
|
||||
initialValue: {
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: PaymentProvider;
|
||||
name: string;
|
||||
isEnabled: boolean;
|
||||
baseUrl: string;
|
||||
@@ -94,18 +77,19 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const form = reactive({
|
||||
provider: props.provider,
|
||||
name: props.initialValue?.name ?? (props.provider === 'BEPUSDT' ? 'USDT' : '聚合支付'),
|
||||
isEnabled: props.initialValue?.isEnabled ?? false,
|
||||
baseUrl: props.initialValue?.baseUrl ?? '',
|
||||
appId: props.initialValue?.appId ?? '',
|
||||
appSecret: props.initialValue?.appSecret ?? '',
|
||||
pid: props.initialValue?.pid ?? '',
|
||||
key: props.initialValue?.key ?? '',
|
||||
notifyUrl: props.initialValue?.notifyUrl ?? '',
|
||||
returnUrl: props.initialValue?.returnUrl ?? '',
|
||||
});
|
||||
|
||||
const extraFields = reactive(
|
||||
props.provider === 'BEPUSDT'
|
||||
? { appId: props.initialValue?.appId ?? '', appSecret: props.initialValue?.appSecret ?? '' }
|
||||
: { pid: props.initialValue?.pid ?? '', key: props.initialValue?.key ?? '' }
|
||||
);
|
||||
|
||||
const saving = ref(false);
|
||||
const saved = ref(false);
|
||||
const errorMessage = ref('');
|
||||
@@ -114,23 +98,26 @@ async function handleSave() {
|
||||
saving.value = true;
|
||||
saved.value = false;
|
||||
errorMessage.value = '';
|
||||
|
||||
try {
|
||||
const result = await onSavePaymentConfig({ ...form });
|
||||
const result = await onSavePaymentConfig({ provider: props.provider, ...form, ...extraFields });
|
||||
form.name = result.name;
|
||||
form.isEnabled = result.isEnabled;
|
||||
form.baseUrl = result.baseUrl;
|
||||
form.appId = result.appId ?? '';
|
||||
form.appSecret = result.appSecret ?? '';
|
||||
form.pid = result.pid ?? '';
|
||||
form.key = result.key ?? '';
|
||||
form.notifyUrl = result.notifyUrl ?? '';
|
||||
form.returnUrl = result.returnUrl ?? '';
|
||||
if (props.provider === 'BEPUSDT') {
|
||||
(extraFields as any).appId = result.appId ?? '';
|
||||
(extraFields as any).appSecret = result.appSecret ?? '';
|
||||
} else {
|
||||
(extraFields as any).pid = result.pid ?? '';
|
||||
(extraFields as any).key = result.key ?? '';
|
||||
}
|
||||
saved.value = true;
|
||||
emit('saved', { provider: props.provider, ...form, ...extraFields });
|
||||
} catch (error) {
|
||||
errorMessage.value = normalizeTelefuncError(error, '保存失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
16
pages/admin/payments/forms/BEpusdtForm.vue
Normal file
16
pages/admin/payments/forms/BEpusdtForm.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">App ID</span>
|
||||
<input v-model="modelValue.appId" class="input input-bordered w-full" />
|
||||
</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" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ modelValue: Record<string, string> }>();
|
||||
</script>
|
||||
19
pages/admin/payments/forms/EpayForm.vue
Normal file
19
pages/admin/payments/forms/EpayForm.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
<span class="label-text font-medium">PID</span>
|
||||
<input v-model="modelValue.pid" class="input input-bordered w-full" />
|
||||
</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" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-base-content/60">
|
||||
`Notify URL` 和 `Return URL` 支持填写相对路径或完整 URL;`Return URL` 支持 `{orderNo}`、`{token}` 占位符。
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ modelValue: Record<string, string> }>();
|
||||
</script>
|
||||
@@ -1,8 +1,9 @@
|
||||
import { assertAdminAccess } from "../../../modules/auth/service";
|
||||
import { savePaymentConfig } from "../../../modules/payment/service";
|
||||
import type { PaymentProvider } from "../../../modules/payment/types";
|
||||
|
||||
export async function onSavePaymentConfig(input: {
|
||||
provider: "BEPUSDT" | "EPAY";
|
||||
provider: PaymentProvider;
|
||||
name: string;
|
||||
isEnabled: boolean;
|
||||
baseUrl: string;
|
||||
|
||||
@@ -48,10 +48,7 @@ enum ContactType {
|
||||
OTHER
|
||||
}
|
||||
|
||||
enum PaymentProvider {
|
||||
BEPUSDT
|
||||
EPAY
|
||||
}
|
||||
|
||||
|
||||
enum EmailChannel {
|
||||
API
|
||||
@@ -208,7 +205,7 @@ model Order {
|
||||
contactType ContactType @default(EMAIL)
|
||||
contactValue String?
|
||||
buyerNote String?
|
||||
paymentProvider PaymentProvider
|
||||
paymentProvider String
|
||||
paymentChannel String?
|
||||
paymentOrderNo String?
|
||||
status OrderStatus @default(PENDING)
|
||||
@@ -245,7 +242,7 @@ model OrderDelivery {
|
||||
|
||||
model PaymentConfig {
|
||||
id Int @id @default(autoincrement())
|
||||
provider PaymentProvider @unique
|
||||
provider String @unique
|
||||
name String
|
||||
isEnabled Boolean @default(false)
|
||||
configJson String
|
||||
@@ -256,7 +253,7 @@ model PaymentConfig {
|
||||
model PaymentLog {
|
||||
id Int @id @default(autoincrement())
|
||||
orderId Int?
|
||||
provider PaymentProvider
|
||||
provider String
|
||||
orderNo String?
|
||||
paymentOrderNo String?
|
||||
eventType String
|
||||
|
||||
Reference in New Issue
Block a user