mirror of
https://github.com/34892002/edgeKey.git
synced 2026-06-06 03:30:45 +08:00
feat: 支付宝官方支付
This commit is contained in:
4
lib/utils/device.ts
Normal file
4
lib/utils/device.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function isMobile(): boolean {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
return /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent);
|
||||
}
|
||||
@@ -56,12 +56,12 @@ export async function createOrder(input: {
|
||||
const quantity = Math.max(product.minBuy, Math.min(product.maxBuy, Math.floor(input.quantity)));
|
||||
const orderNo = generateOrderNo();
|
||||
const queryToken = generateQueryToken();
|
||||
const paymentChannel =
|
||||
input.paymentProvider === "EPAY"
|
||||
? input.paymentChannel === "wxpay"
|
||||
? "wxpay"
|
||||
: "alipay"
|
||||
: null;
|
||||
let paymentChannel: string | null = null;
|
||||
if (input.paymentProvider === "EPAY") {
|
||||
paymentChannel = input.paymentChannel === "wxpay" ? "wxpay" : "alipay";
|
||||
} else if (input.paymentProvider === "ALIPAY") {
|
||||
paymentChannel = input.paymentChannel ?? "alipay_h5";
|
||||
}
|
||||
|
||||
const order = await createOrderRecord(prisma, {
|
||||
orderNo,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { badRequestError } from "../../lib/app-error";
|
||||
import { badRequestError, externalServiceError } from "../../lib/app-error";
|
||||
import type { PaymentProviderAdapter } from "./provider";
|
||||
|
||||
interface AlipayConfig {
|
||||
export interface AlipayTradeQueryResult {
|
||||
isPaid: boolean;
|
||||
tradeNo?: string;
|
||||
amount?: number;
|
||||
}
|
||||
|
||||
export interface AlipayConfig {
|
||||
baseUrl?: string;
|
||||
alipayAppId?: string;
|
||||
alipayPrivateKey?: string;
|
||||
alipayPublicKey?: string;
|
||||
@@ -9,7 +16,7 @@ interface AlipayConfig {
|
||||
returnUrl?: string;
|
||||
}
|
||||
|
||||
const GATEWAY = "https://openapi.alipay.com/gateway.do";
|
||||
const DEFAULT_GATEWAY = "https://openapi.alipay.com/gateway.do";
|
||||
|
||||
function pemToBase64(pem: string) {
|
||||
return pem.replace(/-----[^-]+-----/g, "").replace(/\s+/g, "");
|
||||
@@ -58,38 +65,39 @@ function buildSignString(params: Record<string, string>) {
|
||||
export function createAlipayAdapter(config: AlipayConfig): PaymentProviderAdapter {
|
||||
return {
|
||||
async createPayment(input) {
|
||||
const gateway = config.baseUrl?.trim().replace(/\/+$/, "") || DEFAULT_GATEWAY;
|
||||
|
||||
if (!config.alipayAppId || !config.alipayPrivateKey) {
|
||||
throw badRequestError("支付宝配置不完整", "ALIPAY_CONFIG_INCOMPLETE");
|
||||
}
|
||||
|
||||
const method = input.paymentChannel === "pc" ? "alipay.trade.page.pay" : "alipay.trade.wap.pay";
|
||||
const method = input.paymentChannel === "alipay_pc" ? "alipay.trade.page.pay" : "alipay.trade.wap.pay";
|
||||
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
||||
|
||||
const bizContent = JSON.stringify({
|
||||
out_trade_no: input.orderNo,
|
||||
total_amount: (input.amount / 100).toFixed(2),
|
||||
subject: input.productName,
|
||||
product_code: input.paymentChannel === "pc" ? "FAST_INSTANT_TRADE_PAY" : "QUICK_WAP_WAY",
|
||||
product_code: input.paymentChannel === "alipay_pc" ? "FAST_INSTANT_TRADE_PAY" : "QUICK_WAP_WAY",
|
||||
});
|
||||
|
||||
const params: Record<string, string> = {
|
||||
app_id: config.alipayAppId,
|
||||
method,
|
||||
charset: "utf-8",
|
||||
sign_type: "RSA2",
|
||||
timestamp,
|
||||
version: "1.0",
|
||||
notify_url: input.notifyUrl,
|
||||
return_url: input.returnUrl,
|
||||
biz_content: bizContent,
|
||||
};
|
||||
const params: Record<string, string> = {};
|
||||
params.app_id = config.alipayAppId;
|
||||
params.method = method;
|
||||
params.charset = "utf-8";
|
||||
params.sign_type = "RSA2";
|
||||
params.timestamp = timestamp;
|
||||
params.version = "1.0";
|
||||
if (input.notifyUrl) params.notify_url = input.notifyUrl;
|
||||
if (input.returnUrl) params.return_url = input.returnUrl;
|
||||
params.biz_content = bizContent;
|
||||
|
||||
const signStr = buildSignString(params);
|
||||
const sign = await rsaSign(signStr, config.alipayPrivateKey);
|
||||
|
||||
const query = new URLSearchParams({ ...params, sign }).toString();
|
||||
return {
|
||||
payUrl: `${GATEWAY}?${query}`,
|
||||
payUrl: `${gateway}?${query}`,
|
||||
paymentOrderNo: input.orderNo,
|
||||
raw: params,
|
||||
};
|
||||
@@ -121,4 +129,45 @@ export function createAlipayAdapter(config: AlipayConfig): PaymentProviderAdapte
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function queryAlipayTrade(config: AlipayConfig, outTradeNo: string): Promise<AlipayTradeQueryResult> {
|
||||
if (!config.alipayAppId || !config.alipayPrivateKey) {
|
||||
throw badRequestError("支付宝配置不完整", "ALIPAY_CONFIG_INCOMPLETE");
|
||||
}
|
||||
|
||||
const gateway = config.baseUrl?.trim().replace(/\/+$/, "") || DEFAULT_GATEWAY;
|
||||
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
||||
const params: Record<string, string> = {
|
||||
app_id: config.alipayAppId,
|
||||
method: "alipay.trade.query",
|
||||
charset: "utf-8",
|
||||
sign_type: "RSA2",
|
||||
timestamp,
|
||||
version: "1.0",
|
||||
biz_content: JSON.stringify({ out_trade_no: outTradeNo }),
|
||||
};
|
||||
|
||||
const sign = await rsaSign(buildSignString(params), config.alipayPrivateKey);
|
||||
const response = await fetch(`${gateway}?${new URLSearchParams({ ...params, sign }).toString()}`);
|
||||
const json = await response.json() as {
|
||||
alipay_trade_query_response?: {
|
||||
code?: string;
|
||||
trade_status?: string;
|
||||
trade_no?: string;
|
||||
total_amount?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const res = json.alipay_trade_query_response;
|
||||
if (!res || res.code !== "10000") {
|
||||
throw externalServiceError(`支付宝查询失败: ${res?.code}`, "ALIPAY_QUERY_FAILED");
|
||||
}
|
||||
|
||||
const isPaid = res.trade_status === "TRADE_SUCCESS" || res.trade_status === "TRADE_FINISHED";
|
||||
return {
|
||||
isPaid,
|
||||
tradeNo: res.trade_no,
|
||||
amount: res.total_amount ? Math.round(Number(res.total_amount) * 100) : undefined,
|
||||
};
|
||||
}
|
||||
@@ -11,9 +11,10 @@ import type { PaymentMethodItem, PaymentProvider } from "./types";
|
||||
import type { PaymentConfigValue } from "./types";
|
||||
import { createBepusdtAdapter } from "./bepusdt";
|
||||
import { createEpayAdapter } from "./epay";
|
||||
import { createAlipayAdapter } from "./alipay";
|
||||
import { createAlipayAdapter, queryAlipayTrade } from "./alipay";
|
||||
import { deliverOrder } from "../delivery/service";
|
||||
import { findOrderRecord, updateOrderPayment } from "../order/repository";
|
||||
import { notifyOrderPaid as _notifyOrderPaid } from "../email/service";
|
||||
|
||||
const defaultPaymentConfigs: Record<PaymentProvider, PaymentConfigValue> = {
|
||||
BEPUSDT: {
|
||||
@@ -336,6 +337,42 @@ function writePaymentNotifyDiagnostic(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function queryAlipayPayment(orderNo: string, prisma?: PrismaClient) {
|
||||
const client = prisma ?? getPaymentContext().prisma;
|
||||
const order = await findOrderRecord(client, orderNo);
|
||||
if (!order) throw notFoundError("订单不存在", "ORDER_NOT_FOUND");
|
||||
if (order.paymentStatus === "PAID") return { alreadyPaid: true };
|
||||
|
||||
const configs = await getPaymentConfigs(client);
|
||||
const config = configs["ALIPAY"];
|
||||
const result = await queryAlipayTrade(config, orderNo);
|
||||
|
||||
if (result.isPaid) {
|
||||
const updated = await updateOrderPayment(client, orderNo, {
|
||||
paymentOrderNo: result.tradeNo,
|
||||
status: "PAID",
|
||||
paymentStatus: "PAID",
|
||||
paidAt: new Date(),
|
||||
});
|
||||
if (updated) {
|
||||
await createPaymentLogRecord(client, {
|
||||
orderId: order.id,
|
||||
provider: "ALIPAY",
|
||||
orderNo,
|
||||
paymentOrderNo: result.tradeNo,
|
||||
eventType: "QUERY_PAID",
|
||||
rawPayload: JSON.stringify(result),verifyStatus: "VERIFIED",
|
||||
message: "paid via query",
|
||||
});
|
||||
try {
|
||||
await deliverOrder(client, orderNo);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return { alreadyPaid: false, isPaid: result.isPaid };
|
||||
}
|
||||
|
||||
export async function handlePaymentNotify(
|
||||
provider: PaymentProvider,
|
||||
payload: Record<string, string>,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<component :is="formMap[provider]" v-model="(extraFields as unknown as Record<string, string>)" />
|
||||
<component :is="formMap[provider]" v-model="extraFields" />
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="flex flex-col gap-1.5">
|
||||
@@ -54,7 +54,7 @@ import { onSavePaymentConfig } from "./savePaymentConfig.telefunc";
|
||||
import BEpusdtForm from "./forms/BEpusdtForm.vue";
|
||||
import EpayForm from "./forms/EpayForm.vue";
|
||||
import AlipayForm from "./forms/AlipayForm.vue";
|
||||
import type { PaymentProvider } from "../../../modules/payment/types";
|
||||
import type { PaymentProvider, PaymentConfigValue } from "../../../modules/payment/types";
|
||||
|
||||
const formMap = { BEPUSDT: BEpusdtForm, EPAY: EpayForm, ALIPAY: AlipayForm };
|
||||
|
||||
@@ -63,18 +63,7 @@ const emit = defineEmits<{ saved: [value: typeof props.initialValue] }>();
|
||||
const props = defineProps<{
|
||||
provider: PaymentProvider;
|
||||
title: string;
|
||||
initialValue: {
|
||||
provider: PaymentProvider;
|
||||
name: string;
|
||||
isEnabled: boolean;
|
||||
baseUrl: string;
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
pid?: string;
|
||||
key?: string;
|
||||
notifyUrl?: string;
|
||||
returnUrl?: string;
|
||||
} | null;
|
||||
initialValue: PaymentConfigValue | null;
|
||||
}>();
|
||||
|
||||
const form = reactive({
|
||||
@@ -89,7 +78,7 @@ const extraFields = reactive(
|
||||
props.provider === 'BEPUSDT'
|
||||
? { appId: props.initialValue?.appId ?? '', appSecret: props.initialValue?.appSecret ?? '' }
|
||||
: props.provider === 'ALIPAY'
|
||||
? { alipayAppId: (props.initialValue as any)?.alipayAppId ?? '', alipayPrivateKey: (props.initialValue as any)?.alipayPrivateKey ?? '', alipayPublicKey: (props.initialValue as any)?.alipayPublicKey ?? '' }
|
||||
? { alipayAppId: props.initialValue?.alipayAppId ?? '', alipayPrivateKey: props.initialValue?.alipayPrivateKey ?? '', alipayPublicKey: props.initialValue?.alipayPublicKey ?? '' }
|
||||
: { pid: props.initialValue?.pid ?? '', key: props.initialValue?.key ?? '' }
|
||||
);
|
||||
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ modelValue: Record<string, string> }>();
|
||||
defineProps<{ modelValue: Record<string, any> }>();
|
||||
</script>
|
||||
@@ -12,5 +12,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ modelValue: Record<string, string> }>();
|
||||
defineProps<{ modelValue: Record<string, any> }>();
|
||||
</script>
|
||||
@@ -15,5 +15,5 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ modelValue: Record<string, string> }>();
|
||||
defineProps<{ modelValue: Record<string, any> }>();
|
||||
</script>
|
||||
@@ -51,17 +51,28 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeTelefuncError } from "../../../lib/app-error";
|
||||
import { ref } from "vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useData } from "vike-vue/useData";
|
||||
import { formatCents } from "../../../lib/utils/money";
|
||||
import { getDeliveryStatusLabel, getOrderStatusLabel, getPaymentProviderLabel, getPaymentStatusLabel } from "../../../lib/utils/order-status";
|
||||
import { onCreatePayment } from "./createPayment.telefunc";
|
||||
import { onQueryAlipayPayment } from "./queryAlipayPayment.telefunc";
|
||||
import type { Data } from "./+data";
|
||||
|
||||
const { order } = useData<Data>();
|
||||
const paying = ref(false);
|
||||
const paymentError = ref("");
|
||||
|
||||
onMounted(async () => {
|
||||
if (!order || order.paymentStatus !== "UNPAID" || order.paymentProvider !== "ALIPAY") return;
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
if (!params.get("out_trade_no")) return;
|
||||
try {
|
||||
const result = await onQueryAlipayPayment({ orderNo: order.orderNo });
|
||||
if (result.isPaid || result.alreadyPaid) window.location.reload();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
async function handleContinuePay() {
|
||||
if (!order) return;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getOrderForQuery } from "../../../modules/order/service";
|
||||
|
||||
export type Data = ReturnType<typeof data>;
|
||||
export type Data = Awaited<ReturnType<typeof data>>;
|
||||
|
||||
export async function data(pageContext: {
|
||||
routeParams: { orderNo: string };
|
||||
|
||||
5
pages/order/@orderNo/queryAlipayPayment.telefunc.ts
Normal file
5
pages/order/@orderNo/queryAlipayPayment.telefunc.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { queryAlipayPayment } from "../../../modules/payment/service";
|
||||
|
||||
export async function onQueryAlipayPayment(input: { orderNo: string }) {
|
||||
return queryAlipayPayment(input.orderNo);
|
||||
}
|
||||
@@ -69,6 +69,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<button class="btn btn-primary" :disabled="submitting || !paymentMethods.length" @click="handleCreateOrder">
|
||||
{{ submitting ? '提交中...' : '提交订单' }}
|
||||
</button>
|
||||
@@ -88,6 +90,8 @@ import { isEmail } from "../../../lib/validators/email";
|
||||
import { formatCents } from "../../../lib/utils/money";
|
||||
import { onCreateOrder } from "./createOrder.telefunc";
|
||||
import type { PaymentProvider } from "../../../modules/payment/types";
|
||||
import { isMobile } from "../../../lib/utils/device";
|
||||
import { onMounted, watch } from "vue";
|
||||
import { saveLocalOrder } from "../../../lib/local-orders";
|
||||
import type { Data } from "./+data";
|
||||
|
||||
@@ -101,12 +105,26 @@ const epayChannels = [
|
||||
{ value: "wxpay", label: "微信支付" },
|
||||
] as const;
|
||||
|
||||
|
||||
|
||||
const form = reactive({
|
||||
quantity: product?.minBuy ?? 1,
|
||||
contactValue: "",
|
||||
buyerNote: "",
|
||||
paymentProvider: (paymentMethods[0]?.provider ?? "BEPUSDT") as PaymentProvider,
|
||||
paymentChannel: "alipay",
|
||||
paymentChannel: "alipay_h5",
|
||||
});
|
||||
|
||||
let mobile = false;
|
||||
onMounted(() => {
|
||||
mobile = isMobile();
|
||||
form.paymentChannel = mobile ? "alipay_h5" : "alipay_pc";
|
||||
});
|
||||
|
||||
watch(() => form.paymentProvider, (provider) => {
|
||||
if (provider === "EPAY") form.paymentChannel = "alipay";
|
||||
else if (provider === "ALIPAY") form.paymentChannel = mobile ? "alipay_h5" : "alipay_pc";
|
||||
else form.paymentChannel = "";
|
||||
});
|
||||
|
||||
const descriptionHtml = formatDescriptionHtml(product?.description || "");
|
||||
@@ -133,7 +151,7 @@ async function handleCreateOrder() {
|
||||
productId: product.id,
|
||||
quantity: form.quantity,
|
||||
paymentProvider: form.paymentProvider,
|
||||
paymentChannel: form.paymentProvider === "EPAY" ? form.paymentChannel : form.paymentProvider === "ALIPAY" ? "pc" : undefined,
|
||||
paymentChannel: form.paymentProvider === "EPAY" || form.paymentProvider === "ALIPAY" ? form.paymentChannel : undefined,
|
||||
contactType: "EMAIL",
|
||||
contactValue: contactEmail,
|
||||
buyerNote: form.buyerNote,
|
||||
|
||||
Reference in New Issue
Block a user