route.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import { NextRequest, NextResponse } from 'next/server';
  2. import { headers } from 'next/headers';
  3. import { db } from '@/lib/db';
  4. import { users, userActivities } from '@/lib/schema';
  5. import { addCredits } from '@/lib/credit-service';
  6. import { eq, and, like } from 'drizzle-orm';
  7. import { stripe } from '@/lib/stripe';
  8. import { CREDIT_CONFIG } from '@/lib/constants';
  9. import Stripe from 'stripe';
  10. const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
  11. export async function POST(request: NextRequest) {
  12. try {
  13. const body = await request.text();
  14. const sig = request.headers.get('stripe-signature');
  15. if (!sig) {
  16. return NextResponse.json({ error: 'No signature' }, { status: 400 });
  17. }
  18. let event: Stripe.Event;
  19. try {
  20. event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
  21. } catch (err: any) {
  22. console.error('Webhook signature verification failed:', err.message);
  23. return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
  24. }
  25. console.log('Received webhook event:', event.type);
  26. // 处理支付成功事件
  27. if (event.type === 'checkout.session.completed') {
  28. const session = event.data.object as Stripe.Checkout.Session;
  29. console.log('Checkout session completed:', session.id);
  30. console.log('Session metadata:', session.metadata);
  31. if (session.metadata) {
  32. const { userId, planId, credits, planType } = session.metadata;
  33. // 检查是否已经处理过此会话(避免与verify-payment API重复)
  34. const existingActivity = await db.query.userActivities.findFirst({
  35. where: and(
  36. eq(userActivities.userId, userId),
  37. like(userActivities.metadata, `%"sessionId":"${session.id}"%`)
  38. ),
  39. });
  40. if (!existingActivity) {
  41. try {
  42. // 为用户添加积分,订阅类型使用subscription积分
  43. const creditType = planType === 'subscription' ? 'subscription' : 'permanent';
  44. await addCredits(
  45. userId,
  46. parseInt(credits),
  47. planType === 'subscription' ? 'credit_description.subscription_activated' : 'credit_description.credit_purchase',
  48. {
  49. type: 'payment',
  50. planId: planId,
  51. sessionId: session.id,
  52. amount: session.amount_total ? session.amount_total / 100 : 0,
  53. currency: session.currency || 'usd',
  54. source: 'stripe-webhook', // 标识来源
  55. timestamp: new Date().toISOString()
  56. },
  57. creditType
  58. );
  59. // 如果是订阅类型,更新用户的订阅状态
  60. if (planType === 'subscription') {
  61. // 获取订阅详情
  62. if (session.subscription && typeof session.subscription === 'string') {
  63. try {
  64. const subscription = await stripe.subscriptions.retrieve(session.subscription);
  65. // 更新用户订阅状态
  66. await db.update(users)
  67. .set({
  68. subscriptionStatus: 'active',
  69. subscriptionPlan: planId,
  70. subscriptionStartDate: new Date((subscription as any).current_period_start * 1000),
  71. subscriptionEndDate: new Date((subscription as any).current_period_end * 1000),
  72. })
  73. .where(eq(users.id, userId));
  74. console.log(`Successfully updated subscription status for user ${userId}`);
  75. } catch (subscriptionError) {
  76. console.error('Error retrieving subscription details:', subscriptionError);
  77. // 如果无法获取订阅详情,至少设置基本的订阅状态
  78. await db.update(users)
  79. .set({
  80. subscriptionStatus: 'active',
  81. subscriptionPlan: planId,
  82. subscriptionStartDate: new Date(),
  83. // 默认设置为30天后过期,实际会通过其他webhook事件更新
  84. subscriptionEndDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
  85. })
  86. .where(eq(users.id, userId));
  87. console.log(`Updated basic subscription status for user ${userId} without detailed subscription info`);
  88. }
  89. }
  90. }
  91. console.log(`Successfully added ${credits} ${creditType} credits to user ${userId} via webhook`);
  92. } catch (error) {
  93. console.error('Error adding credits via webhook:', error);
  94. }
  95. } else {
  96. console.log(`Session ${session.id} has already been processed, skipping webhook processing`);
  97. }
  98. }
  99. }
  100. // 处理订阅相关事件
  101. if (event.type === 'invoice.payment_succeeded') {
  102. const invoice = event.data.object as Stripe.Invoice;
  103. if ((invoice as any).subscription && invoice.billing_reason === 'subscription_cycle') {
  104. // 这是订阅续费,为用户添加每月积分
  105. console.log('Subscription renewal payment succeeded:', invoice.id);
  106. // 从 subscription 中获取用户信息
  107. if (invoice.customer && typeof invoice.customer === 'string') {
  108. try {
  109. const customer = await stripe.customers.retrieve(invoice.customer);
  110. if (customer && !customer.deleted && customer.email) {
  111. const user = await db.query.users.findFirst({
  112. where: eq(users.email, customer.email),
  113. });
  114. if (user) {
  115. // 为订阅用户每月添加订阅积分
  116. await addCredits(
  117. user.id,
  118. CREDIT_CONFIG.SUBSCRIPTION.PRO_MONTHLY_CREDITS,
  119. 'credit_description.subscription_renewal',
  120. {
  121. type: 'subscription_renewal',
  122. invoiceId: invoice.id,
  123. amount: invoice.amount_paid ? invoice.amount_paid / 100 : 0,
  124. currency: invoice.currency || 'usd',
  125. },
  126. 'subscription'
  127. );
  128. console.log(`Successfully added ${CREDIT_CONFIG.SUBSCRIPTION.PRO_MONTHLY_CREDITS} subscription credits to user ${user.id} for subscription renewal`);
  129. }
  130. }
  131. } catch (error) {
  132. console.error('Error handling subscription renewal:', error);
  133. }
  134. }
  135. }
  136. }
  137. // 处理订阅取消事件
  138. if (event.type === 'customer.subscription.deleted') {
  139. const subscription = event.data.object as Stripe.Subscription;
  140. console.log('Subscription deleted:', subscription.id);
  141. if (subscription.customer && typeof subscription.customer === 'string') {
  142. try {
  143. const customer = await stripe.customers.retrieve(subscription.customer);
  144. if (customer && !customer.deleted && customer.email) {
  145. const user = await db.query.users.findFirst({
  146. where: eq(users.email, customer.email),
  147. });
  148. if (user) {
  149. // 清零订阅积分
  150. await addCredits(user.id, 0, 'credit_description.subscription_expired', {
  151. type: 'subscription_expired',
  152. amount: 0,
  153. currency: 'usd',
  154. source: 'stripe-webhook',
  155. timestamp: new Date().toISOString()
  156. }, 'subscription');
  157. // 更新订阅状态为取消
  158. await db.update(users)
  159. .set({
  160. subscriptionStatus: 'canceled',
  161. subscriptionEndDate: new Date(), // 设置为当前时间表示已结束
  162. })
  163. .where(eq(users.id, user.id));
  164. console.log(`Cleared subscription credits and updated status for user ${user.id} due to subscription cancellation`);
  165. }
  166. }
  167. } catch (error) {
  168. console.error('Error handling subscription deletion:', error);
  169. }
  170. }
  171. }
  172. // 处理订阅过期事件
  173. if (event.type === 'invoice.payment_failed') {
  174. const invoice = event.data.object as Stripe.Invoice;
  175. if ((invoice as any).subscription && invoice.attempt_count >= 3) {
  176. // 连续3次支付失败,认为订阅过期
  177. console.log('Subscription payment failed 3 times:', invoice.id);
  178. if (invoice.customer && typeof invoice.customer === 'string') {
  179. try {
  180. const customer = await stripe.customers.retrieve(invoice.customer);
  181. if (customer && !customer.deleted && customer.email) {
  182. const user = await db.query.users.findFirst({
  183. where: eq(users.email, customer.email),
  184. });
  185. if (user) {
  186. // 清零订阅积分
  187. await addCredits(user.id, 0, 'credit_description.subscription_expired', {
  188. type: 'subscription_expired',
  189. amount: 0,
  190. currency: 'usd',
  191. source: 'stripe-webhook',
  192. timestamp: new Date().toISOString()
  193. }, 'subscription');
  194. // 更新订阅状态为过期
  195. await db.update(users)
  196. .set({
  197. subscriptionStatus: 'expired',
  198. subscriptionEndDate: new Date(), // 设置为当前时间表示已过期
  199. })
  200. .where(eq(users.id, user.id));
  201. console.log(`Cleared subscription credits and updated status for user ${user.id} due to payment failure`);
  202. }
  203. }
  204. } catch (error) {
  205. console.error('Error handling subscription payment failure:', error);
  206. }
  207. }
  208. }
  209. }
  210. return NextResponse.json({ received: true });
  211. } catch (error: any) {
  212. console.error('Webhook error:', error);
  213. return NextResponse.json(
  214. { error: error.message || 'Webhook处理失败' },
  215. { status: 500 }
  216. );
  217. }
  218. }