import type { NextRequest } from "next/server"; import { z } from "zod"; import { rateLimit } from "@/lib/rate-limit"; import { getClientIp } from "@/lib/current-user"; import { storeOtp } from "@/lib/otp-store"; import { sendOtpSms } from "@/lib/sms"; import { ok, ERR } from "@/lib/api-response"; const Body = z.object({ phone: z .string() .regex(/^1[3-9]\d{9}$/, "请输入有效的中国大陆手机号"), }); /** * POST /api/auth/send-otp * 发送短信验证码 · 单手机号 60s 限频 / 单 IP 5 分钟 5 次 */ export async function POST(req: NextRequest) { try { const parsed = Body.safeParse(await req.json()); if (!parsed.success) { return ERR.VALIDATION(parsed.error.issues[0]?.message ?? "参数错误"); } const { phone } = parsed.data; // 限流 const phoneRl = await rateLimit(`otp:phone:${phone}`, 60, 1); if (!phoneRl.allowed) { return ERR.RATE_LIMITED(); } const ip = await getClientIp(); if (ip) { const ipRl = await rateLimit(`otp:ip:${ip}`, 300, 5); if (!ipRl.allowed) return ERR.RATE_LIMITED(); } // 生成 6 位验证码 + 存储 (Redis 可用走 Redis, 否则走进程内 Map; 始终 5min TTL) const code = String(Math.floor(100000 + Math.random() * 900000)); await storeOtp(phone, code); // 开发环境下把验证码打在终端, 方便本地 QA / 排错; 生产不打 if (process.env.NODE_ENV !== "production") { console.log(`[dev-otp] phone=${phone} code=${code} (dev 环境也接受 123456)`); } // 调阿里云短信发送 const sms = await sendOtpSms(phone, code); if (sms.ok) { console.log(`[sms] sent phone=${phone} bizId=${sms.bizId}`); return ok({ message: "验证码已发送", expiresIn: 300 }); } // 失败处理: // - SMS 未配置且非生产 → 控制台打 code, 仍返回成功(开发态联调) // - 阿里云明确返回参数 / 触发流控 → 422 // - 其它 → 500 if (sms.errorCode === "SMS_NOT_CONFIGURED") { if (process.env.NODE_ENV !== "production") { console.log(`[dev-otp] SMS 未配置, 验证码 ${phone}: ${code}(也可用万能码 123456)`); return ok({ message: "验证码已发送", expiresIn: 300 }); } console.error("[sms] 生产环境短信未配置, 检查 SMS_* 环境变量"); return ERR.INTERNAL("短信服务未配置"); } console.error( `[sms] 发送失败 phone=${phone} code=${sms.errorCode} message=${sms.errorMessage}`, ); if (sms.errorCode?.startsWith("isv.")) { return ERR.VALIDATION(sms.errorMessage ?? "短信发送失败"); } return ERR.INTERNAL("短信发送失败"); } catch (e) { console.error("[POST /api/auth/send-otp]", e); return ERR.INTERNAL(); } }