diff --git a/lib/utils/device.ts b/lib/utils/device.ts new file mode 100644 index 0000000..c2de436 --- /dev/null +++ b/lib/utils/device.ts @@ -0,0 +1,4 @@ +export function isMobile(): boolean { + if (typeof navigator === "undefined") return false; + return /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent); +} \ No newline at end of file diff --git a/modules/order/service.ts b/modules/order/service.ts index 51858ad..790c2a5 100644 --- a/modules/order/service.ts +++ b/modules/order/service.ts @@ -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, diff --git a/modules/payment/alipay.ts b/modules/payment/alipay.ts index e329ee5..c111237 100644 --- a/modules/payment/alipay.ts +++ b/modules/payment/alipay.ts @@ -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) { 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 = { - 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 = {}; + 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 { + 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 = { + 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, + }; } \ No newline at end of file diff --git a/modules/payment/service.ts b/modules/payment/service.ts index 0bf91ef..c7cc972 100644 --- a/modules/payment/service.ts +++ b/modules/payment/service.ts @@ -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 = { 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, diff --git a/pages/admin/payments/PaymentConfigCard.vue b/pages/admin/payments/PaymentConfigCard.vue index 37e59f1..c5cebb1 100644 --- a/pages/admin/payments/PaymentConfigCard.vue +++ b/pages/admin/payments/PaymentConfigCard.vue @@ -23,7 +23,7 @@ - +