feat: 自动部署

1. 支付网关组件化
2. 支持cf绑定git自动化部署
This commit is contained in:
ggyy
2026-04-24 02:14:05 +08:00
parent b86b21da19
commit f89221b249
13 changed files with 115 additions and 82 deletions

View File

@@ -3,7 +3,7 @@ import { badRequestError } from "../app-error";
export function validatePaymentConfigInput(input: { export function validatePaymentConfigInput(input: {
name?: string; name?: string;
baseUrl?: string; baseUrl?: string;
provider: "BEPUSDT" | "EPAY"; provider: string;
isEnabled?: boolean; isEnabled?: boolean;
appSecret?: string; appSecret?: string;
pid?: string; pid?: string;

View File

@@ -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({ return prisma.paymentConfig.findUnique({
where: { provider }, where: { provider },
}); });
@@ -14,7 +14,7 @@ export function getPaymentConfigRecord(prisma: PrismaClient, provider: "BEPUSDT"
export function upsertPaymentConfigRecord( export function upsertPaymentConfigRecord(
prisma: PrismaClient, prisma: PrismaClient,
provider: "BEPUSDT" | "EPAY", provider: string,
input: { input: {
name: string; name: string;
isEnabled: boolean; isEnabled: boolean;
@@ -41,7 +41,7 @@ export function createPaymentLogRecord(
prisma: PrismaClient, prisma: PrismaClient,
input: { input: {
orderId?: number; orderId?: number;
provider: "BEPUSDT" | "EPAY"; provider: string;
orderNo?: string; orderNo?: string;
paymentOrderNo?: string; paymentOrderNo?: string;
eventType: string; eventType: string;

View File

@@ -7,14 +7,14 @@ import { getAdminContext, logAdminOperation } from "../auth/service";
import { notifyOrderPaid } from "../email/service"; import { notifyOrderPaid } from "../email/service";
import { getSiteSetting } from "../site/service"; import { getSiteSetting } from "../site/service";
import { createPaymentLogRecord, getPaymentConfigRecord, listPaymentConfigRecords, upsertPaymentConfigRecord } from "./repository"; import { createPaymentLogRecord, getPaymentConfigRecord, listPaymentConfigRecords, upsertPaymentConfigRecord } from "./repository";
import type { PaymentMethodItem } from "./types"; import type { PaymentMethodItem, PaymentProvider } from "./types";
import type { PaymentConfigValue } from "./types"; import type { PaymentConfigValue } from "./types";
import { createBepusdtAdapter } from "./bepusdt"; import { createBepusdtAdapter } from "./bepusdt";
import { createEpayAdapter } from "./epay"; import { createEpayAdapter } from "./epay";
import { deliverOrder } from "../delivery/service"; import { deliverOrder } from "../delivery/service";
import { findOrderRecord, updateOrderPayment } from "../order/repository"; import { findOrderRecord, updateOrderPayment } from "../order/repository";
const defaultPaymentConfigs: Record<"BEPUSDT" | "EPAY", PaymentConfigValue> = { const defaultPaymentConfigs: Record<PaymentProvider, PaymentConfigValue> = {
BEPUSDT: { BEPUSDT: {
provider: "BEPUSDT", provider: "BEPUSDT",
name: "USDT", name: "USDT",
@@ -41,7 +41,7 @@ function getPaymentContext() {
return getContext<{ prisma: PrismaClient }>(); 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]; const defaults = defaultPaymentConfigs[provider];
if (!record) { if (!record) {
return defaults; return defaults;
@@ -69,7 +69,7 @@ export async function listEnabledPaymentMethods(prisma?: PrismaClient): Promise<
const client = prisma ?? getPaymentContext().prisma; const client = prisma ?? getPaymentContext().prisma;
const records = await listPaymentConfigRecords(client); 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 record = records.find((item) => item.provider === provider);
const value = normalizePaymentConfig(record ?? null, provider); const value = normalizePaymentConfig(record ?? null, provider);
return { 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 client = prisma ?? getPaymentContext().prisma;
const [bepusdt, epay] = await Promise.all([ const records = await listPaymentConfigRecords(client);
getPaymentConfigRecord(client, "BEPUSDT"), const result: Record<string, PaymentConfigValue> = {};
getPaymentConfigRecord(client, "EPAY"), for (const provider of Object.keys(defaultPaymentConfigs) as PaymentProvider[]) {
]); const record = records.find((r) => r.provider === provider) ?? null;
result[provider] = normalizePaymentConfig(record, provider);
return { }
BEPUSDT: normalizePaymentConfig(bepusdt, "BEPUSDT"), return result;
EPAY: normalizePaymentConfig(epay, "EPAY"),
};
} }
export async function savePaymentConfig(input: PaymentConfigValue) { export async function savePaymentConfig(input: PaymentConfigValue) {
@@ -200,7 +198,7 @@ export async function createPaymentForOrder(orderNo: string, prisma?: PrismaClie
const notifyUrl = resolveCallbackUrl( const notifyUrl = resolveCallbackUrl(
baseOrigin, baseOrigin,
config.notifyUrl, 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( const returnUrl = resolveCallbackUrl(
baseOrigin, baseOrigin,
@@ -272,7 +270,7 @@ function sanitizePaymentPayload(payload?: Record<string, unknown>) {
async function createNotifyLog(prisma: PrismaClient, input: { async function createNotifyLog(prisma: PrismaClient, input: {
orderId?: number; orderId?: number;
provider: "BEPUSDT" | "EPAY"; provider: PaymentProvider;
orderNo?: string; orderNo?: string;
paymentOrderNo?: string; paymentOrderNo?: string;
eventType?: string; eventType?: string;
@@ -294,7 +292,7 @@ async function createNotifyLog(prisma: PrismaClient, input: {
} }
function writePaymentNotifyDiagnostic(input: { function writePaymentNotifyDiagnostic(input: {
provider: "BEPUSDT" | "EPAY"; provider: PaymentProvider;
source: string; source: string;
reason: string; reason: string;
payload?: Record<string, unknown>; payload?: Record<string, unknown>;
@@ -322,7 +320,7 @@ function writePaymentNotifyDiagnostic(input: {
} }
export async function handlePaymentNotify( export async function handlePaymentNotify(
provider: "BEPUSDT" | "EPAY", provider: PaymentProvider,
payload: Record<string, string>, payload: Record<string, string>,
prisma: PrismaClient, prisma: PrismaClient,
source: string, source: string,

View File

@@ -1,12 +1,14 @@
export type PaymentProvider = "BEPUSDT" | "EPAY";
export interface PaymentMethodItem { export interface PaymentMethodItem {
provider: "BEPUSDT" | "EPAY"; provider: PaymentProvider;
label: string; label: string;
enabled: boolean; enabled: boolean;
baseUrl?: string; baseUrl?: string;
} }
export interface PaymentConfigValue { export interface PaymentConfigValue {
provider: "BEPUSDT" | "EPAY"; provider: PaymentProvider;
name: string; name: string;
isEnabled: boolean; isEnabled: boolean;
baseUrl: string; baseUrl: string;
@@ -16,4 +18,4 @@ export interface PaymentConfigValue {
key?: string; key?: string;
notifyUrl?: string; notifyUrl?: string;
returnUrl?: string; returnUrl?: string;
} }

View File

@@ -7,9 +7,9 @@
"db:seed": "bunx wrangler d1 execute DB --local --file=./scripts/seed.sql -y", "db:seed": "bunx wrangler d1 execute DB --local --file=./scripts/seed.sql -y",
"db:generate": "prisma generate", "db:generate": "prisma generate",
"db:studio": "prisma studio", "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: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:payments": "bun run scripts/verify-payment-adapters.ts",
"verify:payment-notify": "bun run scripts/verify-payment-notify.ts", "verify:payment-notify": "bun run scripts/verify-payment-notify.ts",
"deploy": "bun run db:migrations:remote && bun run db:seed:remote && wrangler deploy", "deploy": "bun run db:migrations:remote && bun run db:seed:remote && wrangler deploy",

View File

@@ -392,7 +392,7 @@
<th>#</th> <th>#</th>
<th>时间</th> <th>时间</th>
<th>分类</th> <th>分类</th>
<th>API服务商</th> <th>邮箱名称</th>
<th>场景</th> <th>场景</th>
<th>状态</th> <th>状态</th>
<th>收件人</th> <th>收件人</th>
@@ -407,7 +407,7 @@
<th>{{ index + 1 }}</th> <th>{{ index + 1 }}</th>
<td class="whitespace-nowrap">{{ formatDate(log.createdAt) }}</td> <td class="whitespace-nowrap">{{ formatDate(log.createdAt) }}</td>
<td class="whitespace-nowrap">{{ getChannelLabel(log.provider) }}</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 class="whitespace-nowrap">{{ getSceneLabel(log.scene) }}</td>
<td> <td>
<span class="badge whitespace-nowrap" :class="log.status === 'SUCCESS' ? 'badge-success' : 'badge-error'"> <span class="badge whitespace-nowrap" :class="log.status === 'SUCCESS' ? 'badge-success' : 'badge-error'">

View File

@@ -1,17 +1,30 @@
<template> <template>
<section class="space-y-6"> <section class="space-y-6">
<div class="alert alert-info"> <div class="alert alert-info">
<span>启用支付前请先前往站点设置配置网站地址否则无法获取支付结果</span> <span>启用支付前请先前往"站点设置"配置网站地址否则无法获取支付结果</span>
</div> </div>
<PaymentConfigCard provider="BEPUSDT" title="BEpusdt / USDT" :initial-value="configs.BEPUSDT" /> <div role="tablist" class="tabs tabs-border">
<PaymentConfigCard provider="EPAY" title="Epay / 聚合支付" :initial-value="configs.EPAY" /> <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> </section>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from "vue";
import { useData } from "vike-vue/useData"; import { useData } from "vike-vue/useData";
import PaymentConfigCard from "./PaymentConfigCard.vue"; import PaymentConfigCard from "./PaymentConfigCard.vue";
import type { Data } from "./+data"; import type { Data } from "./+data";
const { configs } = useData<Data>(); const { configs } = useData<Data>();
</script> const activeTab = ref("BEPUSDT");
const localConfigs = reactive({ ...configs });
</script>

View File

@@ -1,6 +1,6 @@
import { getPaymentConfigs } from "../../../modules/payment/service"; import { getPaymentConfigs } from "../../../modules/payment/service";
export type Data = ReturnType<typeof data>; export type Data = Awaited<ReturnType<typeof data>>;
export async function data(pageContext: { export async function data(pageContext: {
prisma: import("../../../generated/prisma/client").PrismaClient; prisma: import("../../../generated/prisma/client").PrismaClient;

View File

@@ -23,27 +23,7 @@
</label> </label>
</div> </div>
<div v-if="provider === 'BEPUSDT'" class="grid gap-4 md:grid-cols-2"> <component :is="formMap[provider]" v-model="extraFields" />
<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>
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<label class="flex flex-col gap-1.5"> <label class="flex flex-col gap-1.5">
@@ -56,10 +36,6 @@
</label> </label>
</div> </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"> <div class="flex items-center gap-3">
<button class="btn btn-primary" :disabled="saving" @click="handleSave"> <button class="btn btn-primary" :disabled="saving" @click="handleSave">
{{ saving ? '保存中...' : '保存配置' }} {{ saving ? '保存中...' : '保存配置' }}
@@ -72,15 +48,22 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { normalizeTelefuncError } from "../../../lib/app-error";
import { reactive, ref } from "vue"; import { reactive, ref } from "vue";
import { normalizeTelefuncError } from "../../../lib/app-error";
import { onSavePaymentConfig } from "./savePaymentConfig.telefunc"; 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<{ const props = defineProps<{
provider: "BEPUSDT" | "EPAY"; provider: PaymentProvider;
title: string; title: string;
initialValue: { initialValue: {
provider: "BEPUSDT" | "EPAY"; provider: PaymentProvider;
name: string; name: string;
isEnabled: boolean; isEnabled: boolean;
baseUrl: string; baseUrl: string;
@@ -94,18 +77,19 @@ const props = defineProps<{
}>(); }>();
const form = reactive({ const form = reactive({
provider: props.provider,
name: props.initialValue?.name ?? (props.provider === 'BEPUSDT' ? 'USDT' : '聚合支付'), name: props.initialValue?.name ?? (props.provider === 'BEPUSDT' ? 'USDT' : '聚合支付'),
isEnabled: props.initialValue?.isEnabled ?? false, isEnabled: props.initialValue?.isEnabled ?? false,
baseUrl: props.initialValue?.baseUrl ?? '', baseUrl: props.initialValue?.baseUrl ?? '',
appId: props.initialValue?.appId ?? '',
appSecret: props.initialValue?.appSecret ?? '',
pid: props.initialValue?.pid ?? '',
key: props.initialValue?.key ?? '',
notifyUrl: props.initialValue?.notifyUrl ?? '', notifyUrl: props.initialValue?.notifyUrl ?? '',
returnUrl: props.initialValue?.returnUrl ?? '', 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 saving = ref(false);
const saved = ref(false); const saved = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
@@ -114,23 +98,26 @@ async function handleSave() {
saving.value = true; saving.value = true;
saved.value = false; saved.value = false;
errorMessage.value = ''; errorMessage.value = '';
try { try {
const result = await onSavePaymentConfig({ ...form }); const result = await onSavePaymentConfig({ provider: props.provider, ...form, ...extraFields });
form.name = result.name; form.name = result.name;
form.isEnabled = result.isEnabled; form.isEnabled = result.isEnabled;
form.baseUrl = result.baseUrl; 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.notifyUrl = result.notifyUrl ?? '';
form.returnUrl = result.returnUrl ?? ''; 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; saved.value = true;
emit('saved', { provider: props.provider, ...form, ...extraFields });
} catch (error) { } catch (error) {
errorMessage.value = normalizeTelefuncError(error, '保存失败'); errorMessage.value = normalizeTelefuncError(error, '保存失败');
} finally { } finally {
saving.value = false; saving.value = false;
} }
} }
</script> </script>

View 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>

View 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>

View File

@@ -1,8 +1,9 @@
import { assertAdminAccess } from "../../../modules/auth/service"; import { assertAdminAccess } from "../../../modules/auth/service";
import { savePaymentConfig } from "../../../modules/payment/service"; import { savePaymentConfig } from "../../../modules/payment/service";
import type { PaymentProvider } from "../../../modules/payment/types";
export async function onSavePaymentConfig(input: { export async function onSavePaymentConfig(input: {
provider: "BEPUSDT" | "EPAY"; provider: PaymentProvider;
name: string; name: string;
isEnabled: boolean; isEnabled: boolean;
baseUrl: string; baseUrl: string;

View File

@@ -48,10 +48,7 @@ enum ContactType {
OTHER OTHER
} }
enum PaymentProvider {
BEPUSDT
EPAY
}
enum EmailChannel { enum EmailChannel {
API API
@@ -208,7 +205,7 @@ model Order {
contactType ContactType @default(EMAIL) contactType ContactType @default(EMAIL)
contactValue String? contactValue String?
buyerNote String? buyerNote String?
paymentProvider PaymentProvider paymentProvider String
paymentChannel String? paymentChannel String?
paymentOrderNo String? paymentOrderNo String?
status OrderStatus @default(PENDING) status OrderStatus @default(PENDING)
@@ -245,7 +242,7 @@ model OrderDelivery {
model PaymentConfig { model PaymentConfig {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
provider PaymentProvider @unique provider String @unique
name String name String
isEnabled Boolean @default(false) isEnabled Boolean @default(false)
configJson String configJson String
@@ -256,7 +253,7 @@ model PaymentConfig {
model PaymentLog { model PaymentLog {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
orderId Int? orderId Int?
provider PaymentProvider provider String
orderNo String? orderNo String?
paymentOrderNo String? paymentOrderNo String?
eventType String eventType String