237 lines
8.3 KiB
TypeScript
237 lines
8.3 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
||
import { postSalonApi } from "@/app/lib/salonApi";
|
||
import { getNowChinaStr } from "@/app/lib/dates";
|
||
import { getAdminToken } from "@/app/lib/pocketbase/admin";
|
||
import { getPocketBaseConfig } from "@/config/services.config";
|
||
import { COOKIE_NAME } from "@/app/lib/auth-cookie";
|
||
|
||
function getUserIdFromCookie(request: NextRequest): string | null {
|
||
const cookie = request.cookies.get(COOKIE_NAME)?.value;
|
||
if (!cookie) return null;
|
||
try {
|
||
const payload = JSON.parse(Buffer.from(cookie, "base64url").toString("utf-8"));
|
||
return payload?.userId ?? null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function generateAnonId(): string {
|
||
return "anon_" + crypto.randomUUID().replace(/-/g, "");
|
||
}
|
||
|
||
export async function POST(request: NextRequest) {
|
||
try {
|
||
const body = await request.json();
|
||
const formData = body && typeof body === "object" ? body : {};
|
||
|
||
const nickname = String(formData.nickname || "").trim();
|
||
const city = String(formData.city || "").trim();
|
||
const session = String(formData.session || "").trim();
|
||
const ageRange = String(formData.age_range || "").trim();
|
||
const wechatOrPhone = String(formData.wechat_or_phone || formData.wechat || "").trim();
|
||
const wechat = String(formData.wechat || "").trim();
|
||
const phone = String(formData.phone || "").trim();
|
||
const profession = String(formData.profession || "").trim();
|
||
const education = String(formData.education || "").trim();
|
||
const graduationYear = String(formData.graduation_year || formData.gradYear || "").trim();
|
||
const xiaohongshuUrl = String(formData.xiaohongshu_url || "").trim();
|
||
const xiaohongshuAvatarUrl = String(formData.xiaohongshu_avatar_url || formData.media || "").trim();
|
||
const gender = String(formData.gender || "").trim() || "\u65e0";
|
||
let intro = String(formData.intro || formData.reason || "").trim();
|
||
const firstTime = String(formData.first_time || "").trim();
|
||
const agreeRules = !!formData.agree_rules;
|
||
const status = String(formData.status || "").trim();
|
||
const activityType = String(formData.activity_type || "").trim();
|
||
const whyJoin = String(formData.why_join || "").trim();
|
||
const region = String(formData.region || "").trim();
|
||
const availableTime = String(formData.available_time || "").trim();
|
||
const meetupdate = String(formData.meetupdate || "").trim();
|
||
const isVolunteer = !!formData.is_volunteer;
|
||
const qualify = !!formData.qualify; // 女性且小红书帖子>10 时自动通过审核
|
||
/** reason 仅存自我介绍,不合并其他字段 */
|
||
const contact = wechatOrPhone || wechat || phone;
|
||
if (!nickname || !session || !agreeRules) {
|
||
return NextResponse.json(
|
||
{ ok: false, error: "请填写必填项" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
if (!contact) {
|
||
return NextResponse.json(
|
||
{ ok: false, error: "请填写微信号" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
if (!xiaohongshuUrl) {
|
||
return NextResponse.json(
|
||
{ ok: false, error: "请填写小红书主页链接或笔记链接" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
if (!intro) {
|
||
return NextResponse.json(
|
||
{ ok: false, error: "请填写自我介绍" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
if (intro.length < 20) {
|
||
return NextResponse.json(
|
||
{ ok: false, error: "自我介绍不少于20字" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
const userId = getUserIdFromCookie(request) || generateAnonId();
|
||
|
||
const ageRangeForRecord = ageRange || "";
|
||
/** occupation: 职业/城市,reason: 仅自我介绍 */
|
||
const occupationValue = profession || city;
|
||
|
||
const userInfo = String(formData.userInfo || "").trim();
|
||
const record: Record<string, unknown> = {
|
||
user_id: userId,
|
||
nickname,
|
||
occupation: occupationValue,
|
||
reason: intro,
|
||
wechatId: wechat || wechatOrPhone,
|
||
xiaohongshu_url: xiaohongshuUrl,
|
||
gender,
|
||
education: education,
|
||
gradYear: graduationYear,
|
||
single: "",
|
||
media: xiaohongshuAvatarUrl,
|
||
type: session,
|
||
order_id: "",
|
||
amount: 0,
|
||
age_range: ageRangeForRecord,
|
||
first_time: firstTime,
|
||
is_volunteer: isVolunteer,
|
||
qualify,
|
||
userInfo: userInfo || "",
|
||
meetupdate: meetupdate || "",
|
||
};
|
||
if (phone.trim()) {
|
||
record.phone = phone.trim();
|
||
}
|
||
try {
|
||
const { status, data } = await postSalonApi(request, "/api/salon/join", record);
|
||
if (status >= 500) {
|
||
const payload = typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
|
||
return NextResponse.json(
|
||
{ ok: false, error: (payload.detail as string) || (payload.error as string) || "提交失败" },
|
||
{ status: 503 }
|
||
);
|
||
}
|
||
if (status >= 400) {
|
||
const payload = typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
|
||
const errMsg = (payload.error as string) || (payload.detail as string) || "提交失败";
|
||
if (status === 401) {
|
||
return tryPocketBaseDirect(record, errMsg);
|
||
}
|
||
return NextResponse.json({ ok: false, error: errMsg }, { status });
|
||
}
|
||
return NextResponse.json({ ok: true });
|
||
} catch {
|
||
return tryPocketBaseDirect(record, "服务暂时不可用");
|
||
}
|
||
} catch (e) {
|
||
const msg = e instanceof Error ? e.message : String(e);
|
||
console.error("Join API error:", e);
|
||
return NextResponse.json(
|
||
{ ok: false, error: `提交失败: ${msg}` },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
|
||
async function tryPocketBaseDirect(
|
||
record: Record<string, unknown>,
|
||
fallbackError: string
|
||
): Promise<NextResponse> {
|
||
const token = await getAdminToken();
|
||
if (!token) {
|
||
return NextResponse.json({ ok: false, error: fallbackError }, { status: 503 });
|
||
}
|
||
|
||
const pb = getPocketBaseConfig();
|
||
const base = pb.url.replace(/\/$/, "");
|
||
const wechatId = String(record.wechatId ?? "").trim();
|
||
if (wechatId) {
|
||
const escaped = wechatId.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||
const filter = encodeURIComponent(`wechatId = "${escaped}"`);
|
||
const checkRes = await fetch(
|
||
`${base}/api/collections/${pb.joinCollection}/records?filter=${filter}&perPage=1`,
|
||
{
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
cache: "no-store",
|
||
}
|
||
);
|
||
if (checkRes.ok) {
|
||
const checkData = await checkRes.json();
|
||
if (Array.isArray((checkData as { items?: unknown[] }).items) && (checkData as { items: unknown[] }).items.length > 0) {
|
||
return NextResponse.json(
|
||
{ ok: false, error: "该微信号已报名,请勿重复提交" },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
const url = `${base}/api/collections/${pb.joinCollection}/records`;
|
||
|
||
const qualify = !!(record.qualify ?? false);
|
||
const body: Record<string, unknown> = {
|
||
user_id: record.user_id,
|
||
nickname: record.nickname,
|
||
occupation: record.occupation ?? "",
|
||
reason: record.reason ?? "",
|
||
wechatId: record.wechatId ?? "",
|
||
gender: record.gender ?? "",
|
||
education: record.education ?? "",
|
||
gradYear: record.gradYear ?? "",
|
||
single: record.single ?? "",
|
||
media: record.media ?? "",
|
||
type: record.type ?? "",
|
||
order_id: record.order_id ?? "",
|
||
amount: record.amount ?? 0,
|
||
xiaohongshu_url: record.xiaohongshu_url ?? "",
|
||
is_volunteer: record.is_volunteer ?? false,
|
||
is_approved: qualify,
|
||
age_range: record.age_range ?? "",
|
||
first_time: record.first_time ?? "",
|
||
userInfo: record.userInfo ?? "",
|
||
};
|
||
if (record.meetupdate != null && String(record.meetupdate).trim()) {
|
||
body.meetupdate = String(record.meetupdate).trim();
|
||
}
|
||
body.submitted_at_cn = getNowChinaStr();
|
||
if (record.phone != null && String(record.phone).trim()) {
|
||
body.phone = record.phone;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(url, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${token}`,
|
||
},
|
||
body: JSON.stringify(body),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errData = await res.json().catch(() => ({}));
|
||
const msg = (errData as { message?: string }).message || fallbackError;
|
||
return NextResponse.json({ ok: false, error: msg }, { status: res.status });
|
||
}
|
||
return NextResponse.json({ ok: true });
|
||
} catch (e) {
|
||
console.error("PocketBase direct write error:", e);
|
||
return NextResponse.json(
|
||
{ ok: false, error: fallbackError },
|
||
{ status: 503 }
|
||
);
|
||
}
|
||
}
|