import { and, eq } from "drizzle-orm"; import { NextResponse } from "next/server"; import { getMonthlyImageLimitByPlanId, getWechatPayConfig, isBillingPlanId, type BillingPlanId, } from "@/lib/billing/plans"; import { parseWechatNotifyPayload, verifyWechatPaySignature } from "@/lib/billing/wechat"; import { db } from "@/lib/db"; import { systemSettings, user } from "@/lib/db/schema"; type NotifyObject = { out_trade_no?: string; trade_state?: string; trade_state_desc?: string; transaction_id?: string; attach?: string; success_time?: string; }; function notifySuccess() { return NextResponse.json({ code: "SUCCESS", message: "成功" }); } function notifyFail(message: string, status = 400) { return NextResponse.json({ code: "FAIL", message }, { status }); } function getEventKey(id: string) { return `wechat:pay:event:${id}`; } async function lockEvent(id: string, type: string) { const key = getEventKey(id); const now = new Date(); const [inserted] = await db .insert(systemSettings) .values({ key, value: { id, type, status: "processing", occurredAt: now.toISOString(), attempts: 1, }, updatedAt: now, updatedBy: null, }) .onConflictDoNothing() .returning({ id: systemSettings.id }); if (inserted) { return { key, shouldProcess: true as const }; } const [existing] = await db .select({ value: systemSettings.value }) .from(systemSettings) .where(eq(systemSettings.key, key)) .limit(1); const value = existing?.value as Record | undefined; const status = typeof value?.status === "string" ? value.status : "unknown"; const attempts = typeof value?.attempts === "number" ? value.attempts : 1; if (status === "processed") { return { key, shouldProcess: false as const }; } await db .update(systemSettings) .set({ value: { ...(value || {}), status: "processing", attempts: attempts + 1, }, updatedAt: now, updatedBy: null, }) .where(eq(systemSettings.key, key)); return { key, shouldProcess: true as const }; } async function finalizeEvent({ key, status, payload, }: { key: string; status: "processed" | "failed"; payload: Record; }) { const now = new Date(); const [existing] = await db .select({ value: systemSettings.value }) .from(systemSettings) .where(eq(systemSettings.key, key)) .limit(1); const value = existing?.value as Record | undefined; await db .update(systemSettings) .set({ value: { ...(value || {}), ...payload, status, updatedAt: now.toISOString(), }, updatedAt: now, updatedBy: null, }) .where(eq(systemSettings.key, key)); } async function updateOrderRecord({ outTradeNo, payload, }: { outTradeNo: string; payload: Record; }) { const key = `wechat:pay:order:${outTradeNo}`; const now = new Date(); const [existing] = await db .select({ value: systemSettings.value }) .from(systemSettings) .where(eq(systemSettings.key, key)) .limit(1); const value = existing?.value as Record | undefined; if (!existing) { await db.insert(systemSettings).values({ key, value: { outTradeNo, ...payload, updatedAt: now.toISOString(), }, updatedAt: now, updatedBy: null, }); return; } await db .update(systemSettings) .set({ value: { ...(value || {}), ...payload, updatedAt: now.toISOString(), }, updatedAt: now, updatedBy: null, }) .where(eq(systemSettings.key, key)); } function parseAttach(attach: unknown) { if (typeof attach !== "string" || !attach) { return {} as { userId?: string; email?: string; planId?: string }; } try { const parsed = JSON.parse(attach) as Record; return { userId: typeof parsed.userId === "string" ? parsed.userId : undefined, email: typeof parsed.email === "string" ? parsed.email : undefined, planId: typeof parsed.planId === "string" ? parsed.planId : undefined, }; } catch { return {} as { userId?: string; email?: string; planId?: string }; } } async function applyPlanToUser({ userId, email, planId, }: { userId?: string; email?: string; planId: BillingPlanId; }) { const monthlyImageLimit = getMonthlyImageLimitByPlanId(planId); if (userId) { await db .update(user) .set({ monthlyImageLimit, updatedAt: new Date(), }) .where(eq(user.id, userId)); return; } if (email) { await db .update(user) .set({ monthlyImageLimit, updatedAt: new Date(), }) .where(and(eq(user.email, email), eq(user.isActive, true))); } } export async function POST(request: Request) { const config = getWechatPayConfig(); if (!config) { return notifyFail("wechat pay is not configured", 503); } const body = await request.text(); const timestamp = request.headers.get("wechatpay-timestamp") || ""; const nonce = request.headers.get("wechatpay-nonce") || ""; const signature = request.headers.get("wechatpay-signature") || ""; if (!timestamp || !nonce || !signature) { return notifyFail("missing wechat signature headers"); } const verified = verifyWechatPaySignature({ body, timestamp, nonce, signature, platformPublicKey: config.platformPublicKey, }); if (!verified) { return notifyFail("invalid signature", 401); } try { const { envelope, transaction } = parseWechatNotifyPayload({ body, apiV3Key: config.apiV3Key, }); const eventLock = await lockEvent(envelope.id, envelope.event_type); if (!eventLock.shouldProcess) { return notifySuccess(); } const order = transaction as NotifyObject; const outTradeNo = order.out_trade_no; if (!outTradeNo) { await finalizeEvent({ key: eventLock.key, status: "failed", payload: { id: envelope.id, type: envelope.event_type, detail: "missing_out_trade_no", }, }); return notifyFail("missing out_trade_no"); } const orderKey = `wechat:pay:order:${outTradeNo}`; const [orderSetting] = await db .select({ value: systemSettings.value }) .from(systemSettings) .where(eq(systemSettings.key, orderKey)) .limit(1); const orderValue = orderSetting?.value as Record | undefined; const attach = parseAttach(order.attach); const userId = attach.userId || (typeof orderValue?.userId === "string" ? orderValue.userId : undefined); const email = attach.email || (typeof orderValue?.email === "string" ? orderValue.email : undefined); const planId = attach.planId || (typeof orderValue?.planId === "string" ? orderValue.planId : undefined); const tradeState = order.trade_state || "UNKNOWN"; if (tradeState === "SUCCESS" && planId && isBillingPlanId(planId)) { await applyPlanToUser({ userId, email, planId, }); } await updateOrderRecord({ outTradeNo, payload: { status: tradeState === "SUCCESS" ? "paid" : tradeState.toLowerCase(), transactionId: order.transaction_id, paidAt: order.success_time, detail: order.trade_state_desc, userId, email, planId, }, }); await finalizeEvent({ key: eventLock.key, status: "processed", payload: { id: envelope.id, type: envelope.event_type, outTradeNo, tradeState, planId, userId, email, }, }); return notifySuccess(); } catch (error) { try { const parsed = JSON.parse(body) as { id?: string; event_type?: string }; if (parsed.id) { await finalizeEvent({ key: getEventKey(parsed.id), status: "failed", payload: { id: parsed.id, type: parsed.event_type || "unknown", detail: error instanceof Error ? error.message : "wechat_notify_error", }, }); } } catch { // ignore } console.error("[Billing WeChat Notify API] Error:", error); return notifyFail("notify process failed", 500); } }